Browse Source

Merge branch 'master' into develop/3.6.4

pull/10406/head
ViacheslavKlimov 2 years ago
parent
commit
ca174d209f
  1. 1086
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 1083
      application/src/main/data/json/demo/dashboards/thermostats.json
  3. 6
      application/src/main/data/json/system/widget_bundles/charts.json
  4. 32
      application/src/main/data/json/system/widget_types/bar_chart.json
  5. 2
      application/src/main/data/json/system/widget_types/bar_chart_with_labels.json
  6. 32
      application/src/main/data/json/system/widget_types/line_chart.json
  7. 32
      application/src/main/data/json/system/widget_types/point_chart.json
  8. 5
      application/src/main/data/json/system/widget_types/range_chart.json
  9. 36
      application/src/main/data/json/system/widget_types/time_series_chart.json
  10. 5
      application/src/main/data/json/system/widget_types/timeseries_bar_chart.json
  11. 5
      application/src/main/data/json/system/widget_types/timeseries_line_chart.json
  12. 4
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  13. 9
      application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java
  14. 7
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java
  15. 16
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  16. 2
      application/src/main/resources/thingsboard.yml
  17. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
  18. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  19. 0
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java
  20. 151
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java
  21. 101
      common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java
  22. 26
      common/transport/lwm2m/src/test/resources/15.xml
  23. 26
      common/transport/lwm2m/src/test/resources/17.xml
  24. 25
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  25. 26
      dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
  26. 43
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  27. 30
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  28. 2
      msa/js-executor/docker/Dockerfile
  29. 2
      msa/js-executor/pom.xml
  30. 2
      msa/web-ui/docker/Dockerfile
  31. 2
      msa/web-ui/pom.xml
  32. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
  33. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  34. 2
      ui-ngx/package.json
  35. 2
      ui-ngx/patches/@angular+core+15.2.10.patch
  36. 426
      ui-ngx/patches/echarts+5.5.0.patch
  37. 4
      ui-ngx/pom.xml
  38. 17
      ui-ngx/src/app/core/api/data-aggregator.ts
  39. 8
      ui-ngx/src/app/core/api/widget-subscription.ts
  40. 37
      ui-ngx/src/app/core/services/dialog.service.ts
  41. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  42. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  43. 41
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  44. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  45. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts
  46. 10
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html
  47. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts
  48. 10
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html
  49. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts
  50. 259
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  51. 367
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts
  52. 170
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  53. 69
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  54. 284
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  55. 7
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  56. 118
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  57. 21
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  59. 27
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  60. 5
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts
  61. 15
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  62. 3
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  63. 6
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  64. 9
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  65. 12
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  66. 12
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  67. 1
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss
  68. 96
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  69. 6
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html
  70. 11
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss
  71. 105
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts
  72. 21
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts
  73. 1
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
  74. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
  75. 221
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  76. 10
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  77. 1
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  78. 166
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts
  79. 123
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html
  80. 172
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss
  81. 167
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts
  82. 54
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts
  83. 1323
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  84. 742
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  85. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  86. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
  87. 7
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts
  88. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html
  89. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts
  90. 67
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.html
  91. 148
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.ts
  92. 53
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.html
  93. 143
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.ts
  94. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html
  95. 116
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts
  96. 116
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html
  97. 192
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts
  98. 179
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html
  99. 222
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts
  100. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts

1086
application/src/main/data/json/demo/dashboards/rule_engine_statistics.json

File diff suppressed because it is too large

1083
application/src/main/data/json/demo/dashboards/thermostats.json

File diff suppressed because it is too large

6
application/src/main/data/json/system/widget_bundles/charts.json

File diff suppressed because one or more lines are too long

32
application/src/main/data/json/system/widget_types/bar_chart.json

File diff suppressed because one or more lines are too long

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

@ -20,7 +20,7 @@
"latestDataKeySettingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-bar-chart-with-labels-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":\"MONTH\",\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":\"MMMM y\",\"lastUpdateAgo\":false,\"custom\":true},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity\",\"color\":\"rgb(125, 142, 255)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 50) {\\n\\tvalue = 50;\\n} else if (value > 80) {\\n\\tvalue = 80;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Soil moisture\",\"color\":\"rgb(249, 111, 255)\",\"settings\":{},\"_hash\":0.9111685461089025,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 30) {\\n\\tvalue = 30;\\n} else if (value > 90) {\\n\\tvalue = 90;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Leaf wetness\",\"color\":\"rgb(255, 163, 137)\",\"settings\":{},\"_hash\":0.8487533373085416,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 40) {\\n\\tvalue = 40;\\n} else if (value > 70) {\\n\\tvalue = 70;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cloud cover\",\"color\":\"#FFED53\",\"settings\":{},\"_hash\":0.7690144858984289,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < 20) {\\n\\tvalue = 20;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":\"MONTH\",\"fixedTimewindow\":{\"startTimeMs\":1704293713163,\"endTimeMs\":1704380113163},\"quickInterval\":\"CURRENT_HALF_YEAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showBarLabel\":true,\"barLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"12px\"},\"barLabelColor\":\"rgba(0, 0, 0, 0.54)\",\"showBarValue\":true,\"barValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"700\",\"lineHeight\":\"12px\"},\"barValueColor\":\"rgba(0, 0, 0, 0.76)\",\"showLegend\":true,\"legendPosition\":\"top\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.76)\",\"showTooltip\":true,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipShowDate\":true,\"tooltipDateFormat\":{\"format\":null,\"lastUpdateAgo\":false,\"custom\":false,\"auto\":true,\"autoDateFormatSettings\":{}},\"tooltipDateFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"tooltipDateColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"tooltipDateInterval\":true},\"title\":\"Bar chart with labels\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"public\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"%\",\"decimals\":0,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"24px\"}"
},
"tags": [
"bar chart",

32
application/src/main/data/json/system/widget_types/line_chart.json

File diff suppressed because one or more lines are too long

32
application/src/main/data/json/system/widget_types/point_chart.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_types/range_chart.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_types/timeseries_bar_chart.json

@ -1,8 +1,8 @@
{
"fqn": "charts.timeseries_bars_flot",
"name": "Timeseries Bar Chart",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=",
"deprecated": true,
"image": "tb-image:dGltZXNlcmllc19iYXJfY2hhcnRfc3lzdGVtX3dpZGdldF9pbWFnZS5wbmc=:IlRpbWVzZXJpZXMgQmFyIENoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABOFBMVEUAAAA3oPR3d3d6enp8fHyBgYGDg4OGhoaNjY2RkZGSkpKTk5OUlJSVlZWWlpaXl5eYmJiZmZmampqcnJydnZ2enp6goKChoaGioqKjo6OkpKSlpaWmpqanp6eoqKipqamqqqqrq6usrKytra2urq6wsLCxsbGysrK0tLS1tbW2tra3t7e4uLi5ubm6urq8vLy9vb2+vr6/v7/AwMDBwcHDw8PExMTHx8fIyMjJycnLy8vNzc3Ozs7Pz8/S0tLT09PU1NTV1dXW1tbX19fZ2dna2trb29vc3Nzd3d3e3t7f39/h4eHi4uLj4+Pk5OTl5eXm5ubn5+fo6Ojq6urr6+vs7Ozt7e3u7u7v7+/w8PDx8fH09PT19fX29vb39/f4+Pj5+fn6+vr7+/v8/Pz9/f3+/v7/xx////8KXFhiAAAAAWJLR0RnW9PpswAAAvtJREFUeNrt3GtXElEUBuDpZlAqylVHCyMwyi4SpkaWAl4qk8wwUhJlmOn9//+gLzbKbc6AAm5991p82WvPOedZZw6zYM3aGupCQ4tYkZDSsKDrocNVvz8jHQKYY1ZiT/6OAJllRPSpsnzIqIG9SjYmHpKfAzI4CIuHjJeBtyHfZwCatiI1/m+BYV2Dw35NniOEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQUp/6Wx+DgbRbxOAhEb/fXJ98akiH1AIAHtSWlqRDSvdDGcOHnYR0SPV7zVsaQyHeO0jTJapEV5C9bUz9fIjsvHRIefhRxHqpjxzA3ftajXOudHGJOtHV+1rV04/wHenDc2QQkFbLJISQwUDaP90IIYQQQgghpAvIxRO9gFxG4lZ9XBFIx6sihJD+QVRjEEIIIYQQcoMgahkhhBDSe0iLX3ydQdSLIKR9ghCJkIYEIYQQIhPS8d+AhPQboppFw9HMxBrs/lqCIbObpgezP67YjnQ8iwagHDzrryUZUgsUz/prCYZY05vn+msJhiwM6XrR7q/V9OJU4xQuEpcxRsezNPbXEn3Yzz9Hulg3IYT0C3LxxI2CNCQIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQlpA7P5a0iF2fy3hkLP+WsIhVbu/lvRby+6vJR1i99eSG6ffu1XHxlRNcfWq3I0iIK4tJK2nXVyV0lOqEmvjGWAtvnCu+jMzkQWMiOlYdRSd3MAH/UnVPWR/ApFfSkcpiOBvRc3XtAeYW1ZUJbZMD5C8azhWLX4xvZVR692Se0huHm9ySogxkhs3lFVewPM4WlFUlUMoJEYUox1+jAFIbLuHrKaQWlMu8TicDp+4gdw7/qS4t2qBohk4UUF2nieBfLyDM/ItgXhBucT113i14QbixW7M+SRNb6EQ0u8kHavyZQxj2/kgNUCsYDRoqXfEF/Mdu4G8D43uOtakh3R9H1DsyKZvOmneDjt+D/0DTzolrPMHmggAAAAASUVORK5CYII=",
"description": "Displays changes to time-series data over time—for example, daily water consumption for the last month.",
"descriptor": {
"type": "timeseries",
@ -21,6 +21,5 @@
"basicModeDirective": "tb-flot-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":true,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":null,\"max\":null,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"\"},\"defaultBarWidth\":600,\"barAlignment\":\"left\",\"comparisonEnabled\":false,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"bottom\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false},\"title\":\"Timeseries Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}"
},
"externalId": null,
"tags": null
}

5
application/src/main/data/json/system/widget_types/timeseries_line_chart.json

File diff suppressed because one or more lines are too long

4
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -46,6 +46,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.subscription.SubscriptionErrorCode;
@ -232,6 +233,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
} catch (InvalidParameterException e) {
log.warn("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.BAD_DATA.withReason(e.getMessage()));
} catch (JwtExpiredTokenException e) {
log.trace("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.SERVER_ERROR.withReason(e.getMessage()));
} catch (Exception e) {
log.warn("[{}] Failed to start session", session.getId(), e);
session.close(CloseStatus.SERVER_ERROR.withReason(e.getMessage()));

9
application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.mail.MailOauth2Provider.OFFICE_365;
@ -46,7 +47,9 @@ public class RefreshTokenExpCheckService {
public static final int AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS = 90;
private final AdminSettingsService adminSettingsService;
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${mail.oauth2.refreshTokenCheckingInterval})}", fixedDelayString = "${mail.oauth2.refreshTokenCheckingInterval}")
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${mail.oauth2.refreshTokenCheckingInterval})}",
fixedDelayString = "${mail.oauth2.refreshTokenCheckingInterval}",
timeUnit = TimeUnit.SECONDS)
public void check() throws IOException {
AdminSettings settings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
if (settings != null && settings.getJsonValue().has("enableOauth2") && settings.getJsonValue().get("enableOauth2").asBoolean()) {
@ -65,8 +68,8 @@ public class RefreshTokenExpCheckService {
new GenericUrl(tokenUri), refreshToken)
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.execute();
((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode) jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
}
}

7
application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java

@ -22,6 +22,8 @@ import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
@ -71,6 +73,11 @@ public class DefaultFirebaseService implements FirebaseService {
.setAndroidConfig(AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setContentAvailable(true)
.build())
.build())
.putAllData(data)
.build();
firebaseContext.getMessaging().send(message);

16
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -191,6 +191,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private int telemetryTtl;
private ListeningExecutorService deviceStateExecutor;
private ListeningExecutorService deviceStateCallbackExecutor;
final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
@ -199,6 +200,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
super.init();
deviceStateExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
Math.max(4, Runtime.getRuntime().availableProcessors()), "device-state"));
deviceStateCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(
Math.max(4, Runtime.getRuntime().availableProcessors()), "device-state-callback"));
scheduledExecutor.scheduleWithFixedDelay(this::checkStates, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
scheduledExecutor.scheduleWithFixedDelay(this::reportActivityStats, defaultActivityStatsIntervalInSec, defaultActivityStatsIntervalInSec, TimeUnit.SECONDS);
}
@ -209,6 +212,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
if (deviceStateExecutor != null) {
deviceStateExecutor.shutdownNow();
}
if (deviceStateCallbackExecutor != null) {
deviceStateCallbackExecutor.shutdownNow();
}
}
@Override
@ -372,7 +378,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
log.warn("Failed to register device to the state service", t);
callback.onFailure(t);
}
}, deviceStateExecutor);
}, deviceStateCallbackExecutor);
} else if (proto.getUpdated()) {
DeviceStateData stateData = getOrFetchDeviceStateData(device.getId());
TbMsgMetaData md = new TbMsgMetaData();
@ -635,10 +641,10 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
ListenableFuture<DeviceStateData> future;
if (persistToTelemetry) {
ListenableFuture<List<TsKvEntry>> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES);
future = Futures.transform(tsData, extractDeviceStateData(device), deviceStateExecutor);
future = Futures.transform(tsData, extractDeviceStateData(device), MoreExecutors.directExecutor());
} else {
ListenableFuture<List<AttributeKvEntry>> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
future = Futures.transform(attrData, extractDeviceStateData(device), deviceStateExecutor);
future = Futures.transform(attrData, extractDeviceStateData(device), MoreExecutors.directExecutor());
}
return transformInactivityTimeout(future);
}
@ -656,8 +662,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
});
return deviceStateData;
}, deviceStateExecutor);
}, deviceStateExecutor);
}, MoreExecutors.directExecutor());
}, deviceStateCallbackExecutor);
}
private <T extends KvEntry> Function<List<T>, DeviceStateData> extractDeviceStateData(Device device) {

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

@ -193,7 +193,7 @@ ui:
# Help parameters
help:
# Base URL for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.2}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.3}"
# Database telemetry parameters
database:

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java

@ -146,7 +146,7 @@ public class LwM2MClientSerDes {
if (multiInstances) {
Map<Integer, Object> instances = new HashMap<>();
o.get("instances").asObject().forEach(entry -> {
instances.put(Integer.valueOf(entry.getName()), parseValue(type, entry.getValue()));
instances.put(Integer.valueOf(entry.getName()), parseValue(type, entry.getValue().asObject().get("value")));
});
return LwM2mMultipleResource.newResource(id, instances, type);
} else {

4
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java

@ -480,7 +480,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
CountDownLatch latch = new CountDownLatch(targetIds.size());
targetIds.forEach(versionedId -> sendReadRequest(lwM2MClient, versionedId,
new TbLwM2MLatchCallback<>(latch, new TbLwM2MReadCallback(this, logService, lwM2MClient, versionedId))));
latch.await();
latch.await(config.getTimeout(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("[{}] Failed to await Read requests!", lwM2MClient.getEndpoint(), e);
} catch (Exception e) {
@ -498,7 +498,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl
targetIds.forEach(targetId -> sendObserveRequest(lwM2MClient, targetId,
new TbLwM2MLatchCallback<>(latch, new TbLwM2MObserveCallback(this, logService, lwM2MClient, targetId))));
latch.await();
latch.await(config.getTimeout(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("[{}] Failed to await Observe requests!", lwM2MClient.getEndpoint(), e);
} catch (Exception e) {

0
common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java → common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java

151
common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java

@ -0,0 +1,151 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.store.util;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.TransportResourceCache;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class LwM2MClientSerDesTest {
@Test
public void serializeDeserialize() throws Exception {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
TransportDeviceInfo tdi = new TransportDeviceInfo();
tdi.setPowerMode(PowerMode.PSM);
tdi.setPsmActivityTimer(10000L);
tdi.setPagingTransmissionWindow(2000L);
tdi.setEdrxCycle(3000L);
tdi.setTenantId(TenantId.fromUUID(UUID.randomUUID()));
tdi.setCustomerId(new CustomerId(UUID.randomUUID()));
tdi.setDeviceId(new DeviceId(UUID.randomUUID()));
tdi.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID()));
tdi.setDeviceName("testDevice");
tdi.setDeviceType("testType");
ValidateDeviceCredentialsResponse credentialsResponse = ValidateDeviceCredentialsResponse.builder()
.deviceInfo(tdi)
.build();
client.init(credentialsResponse, UUID.randomUUID());
Registration registration =
new Registration.Builder("test", "testEndpoint", Identity
.unsecure(new InetSocketAddress(1000)))
.supportedContentFormats()
.supportedObjects(Map.of(15, "1.0", 17, "1.0"))
.objectLinks(new Link[]{new Link("/")})
.build();
client.setRegistration(registration);
client.setState(LwM2MClientState.REGISTERED);
client.getSharedAttributes().put("key1", TransportProtos.TsKvProto.newBuilder().setTs(0).setKv(TransportProtos.KeyValueProto.newBuilder().setStringV("test").build()).build());
client.getSharedAttributes().put("key2", TransportProtos.TsKvProto.newBuilder().setTs(1).setKv(TransportProtos.KeyValueProto.newBuilder().setDoubleV(1.02).build()).build());
TransportResourceCache resourceCache = mock(TransportResourceCache.class);
LwM2mTransportContext context = mock(LwM2mTransportContext.class);
LwM2mClientContext clientContext = mock(LwM2mClientContext.class);
var provider = new LwM2mVersionedModelProvider(clientContext, new LwM2mTransportServerHelper(context), context);
TbResource resource15 = new TbResource();
resource15.setData(Files.readAllBytes(Path.of(this.getClass().getClassLoader().getResource("15.xml").toURI())));
TbResource resource17 = new TbResource();
resource17.setData(Files.readAllBytes(Path.of(this.getClass().getClassLoader().getResource("17.xml").toURI())));
when(resourceCache.get(any(), any(), eq("15_1.0"))).thenReturn(Optional.of(resource15));
when(resourceCache.get(any(), any(), eq("17_1.0"))).thenReturn(Optional.of(resource17));
when(context.getTransportResourceCache()).thenReturn(resourceCache);
when(clientContext.getClientByEndpoint(any())).thenReturn(client);
LwM2mResource singleResource = LwM2mSingleResource.newStringResource(15, "testValue");
LwM2mResource multipleResource = LwM2mMultipleResource.newStringResource(17, Map.of(0, "testValue", 1, "testValue"));
client.saveResourceValue("/15_1.0/0/0", singleResource, provider, WriteRequest.Mode.UPDATE);
client.saveResourceValue("/17_1.0/0/0", multipleResource, provider, WriteRequest.Mode.UPDATE);
byte[] bytes = LwM2MClientSerDes.serialize(client);
Assert.assertNotNull(bytes);
LwM2mClient desClient = LwM2MClientSerDes.deserialize(bytes);
assertEquals(client.getNodeId(), desClient.getNodeId());
assertEquals(client.getEndpoint(), desClient.getEndpoint());
assertEquals(client.getSharedAttributes(), desClient.getSharedAttributes());
assertEquals(client.getKeyTsLatestMap(), desClient.getKeyTsLatestMap());
assertEquals(client.getTenantId(), desClient.getTenantId());
assertEquals(client.getProfileId(), desClient.getProfileId());
assertEquals(client.getDeviceId(), desClient.getDeviceId());
assertEquals(client.getState(), desClient.getState());
assertEquals(client.getSession(), desClient.getSession());
assertEquals(client.getPowerMode(), desClient.getPowerMode());
assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer());
assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow());
assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle());
assertEquals(client.getRegistration(), desClient.getRegistration());
assertEquals(client.isAsleep(), desClient.isAsleep());
assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime());
assertEquals(client.getSleepTask(), desClient.getSleepTask());
assertEquals(client.getClientSupportContentFormats(), desClient.getClientSupportContentFormats());
assertEquals(client.getDefaultContentFormat(), desClient.getDefaultContentFormat());
assertEquals(client.getRetryAttempts().get(), desClient.getRetryAttempts().get());
assertEquals(client.getLastSentRpcId(), desClient.getLastSentRpcId());
Map<String, ResourceValue> expectedResources = client.getResources();
Map<String, ResourceValue> actualResources = desClient.getResources();
assertNotNull(actualResources);
assertEquals(expectedResources.size(), actualResources.size());
expectedResources.forEach((key, value) -> assertEquals(value.toString(), actualResources.get(key).toString()));
}
}

101
common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java

@ -1,101 +0,0 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.server.store.util;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import java.net.InetSocketAddress;
import java.util.UUID;
public class LwM2MClientSerDesTest {
@Test
public void serializeDeserialize() {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
TransportDeviceInfo tdi = new TransportDeviceInfo();
tdi.setPowerMode(PowerMode.PSM);
tdi.setPsmActivityTimer(10000L);
tdi.setPagingTransmissionWindow(2000L);
tdi.setEdrxCycle(3000L);
tdi.setTenantId(TenantId.fromUUID(UUID.randomUUID()));
tdi.setCustomerId(new CustomerId(UUID.randomUUID()));
tdi.setDeviceId(new DeviceId(UUID.randomUUID()));
tdi.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID()));
tdi.setDeviceName("testDevice");
tdi.setDeviceType("testType");
ValidateDeviceCredentialsResponse credentialsResponse = ValidateDeviceCredentialsResponse.builder()
.deviceInfo(tdi)
.build();
client.init(credentialsResponse, UUID.randomUUID());
Registration registration =
new Registration.Builder("test", "testEndpoint", Identity
.unsecure(new InetSocketAddress(1000)))
.supportedContentFormats()
.objectLinks(new Link[]{new Link("/")})
.build();
client.setRegistration(registration);
client.setState(LwM2MClientState.REGISTERED);
client.getSharedAttributes().put("key1", TransportProtos.TsKvProto.newBuilder().setTs(0).setKv(TransportProtos.KeyValueProto.newBuilder().setStringV("test").build()).build());
client.getSharedAttributes().put("key2", TransportProtos.TsKvProto.newBuilder().setTs(1).setKv(TransportProtos.KeyValueProto.newBuilder().setDoubleV(1.02).build()).build());
byte[] bytes = LwM2MClientSerDes.serialize(client);
Assert.assertNotNull(bytes);
LwM2mClient desClient = LwM2MClientSerDes.deserialize(bytes);
Assert.assertEquals(client.getNodeId(), desClient.getNodeId());
Assert.assertEquals(client.getEndpoint(), desClient.getEndpoint());
Assert.assertEquals(client.getResources(), desClient.getResources());
Assert.assertEquals(client.getSharedAttributes(), desClient.getSharedAttributes());
Assert.assertEquals(client.getKeyTsLatestMap(), desClient.getKeyTsLatestMap());
Assert.assertEquals(client.getTenantId(), desClient.getTenantId());
Assert.assertEquals(client.getProfileId(), desClient.getProfileId());
Assert.assertEquals(client.getDeviceId(), desClient.getDeviceId());
Assert.assertEquals(client.getState(), desClient.getState());
Assert.assertEquals(client.getSession(), desClient.getSession());
Assert.assertEquals(client.getPowerMode(), desClient.getPowerMode());
Assert.assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer());
Assert.assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow());
Assert.assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle());
Assert.assertEquals(client.getRegistration(), desClient.getRegistration());
Assert.assertEquals(client.isAsleep(), desClient.isAsleep());
Assert.assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime());
Assert.assertEquals(client.getSleepTask(), desClient.getSleepTask());
Assert.assertEquals(client.getClientSupportContentFormats(), desClient.getClientSupportContentFormats());
Assert.assertEquals(client.getDefaultContentFormat(), desClient.getDefaultContentFormat());
Assert.assertEquals(client.getRetryAttempts().get(), desClient.getRetryAttempts().get());
Assert.assertEquals(client.getLastSentRpcId(), desClient.getLastSentRpcId());
}
}

26
common/transport/lwm2m/src/test/resources/15.xml

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<LWM2M>
<Object ObjectType="MODefinition">
<Name>Test 15</Name>
<Description1></Description1>
<ObjectID>15</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:15:1.0</ObjectURN>
<LWM2MVersion>1.0</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Resources>
<Item ID="0">
<Name>Test</Name>
<Operations>R</Operations>
<MultipleInstances>Single</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description></Description>
</Item>
</Resources>
<Description2></Description2>
</Object>
</LWM2M>

26
common/transport/lwm2m/src/test/resources/17.xml

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<LWM2M>
<Object ObjectType="MODefinition">
<Name>Test 17</Name>
<Description1></Description1>
<ObjectID>17</ObjectID>
<ObjectURN>urn:oma:lwm2m:oma:17:1.0</ObjectURN>
<LWM2MVersion>1.0</LWM2MVersion>
<ObjectVersion>1.0</ObjectVersion>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Mandatory</Mandatory>
<Resources>
<Item ID="0">
<Name>Test</Name>
<Operations>R</Operations>
<MultipleInstances>Multiple</MultipleInstances>
<Mandatory>Optional</Mandatory>
<Type>String</Type>
<RangeEnumeration></RangeEnumeration>
<Units></Units>
<Description></Description>
</Item>
</Resources>
<Description2></Description2>
</Object>
</LWM2M>

25
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -113,15 +113,15 @@ public class CachedAttributesService implements AttributesService {
validate(entityId, scope);
Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey);
TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey);
if (cachedAttributeValue != null) {
hitCounter.increment();
AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get();
return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry));
} else {
missCounter.increment();
return cacheExecutor.submit(() -> {
return cacheExecutor.submit(() -> {
AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey);
TbCacheValueWrapper<AttributeKvEntry> cachedAttributeValue = cache.get(attributeCacheKey);
if (cachedAttributeValue != null) {
hitCounter.increment();
AttributeKvEntry cachedAttributeKvEntry = cachedAttributeValue.get();
return Optional.ofNullable(cachedAttributeKvEntry);
} else {
missCounter.increment();
var cacheTransaction = cache.newTransactionForKey(attributeCacheKey);
try {
Optional<AttributeKvEntry> result = attributesDao.find(tenantId, entityId, scope, attributeKey);
@ -133,8 +133,8 @@ public class CachedAttributesService implements AttributesService {
log.debug("Could not find attribute from cache: [{}] [{}] [{}]", entityId, scope, attributeKey, e);
throw e;
}
});
}
}
});
}
@Override
@ -207,7 +207,8 @@ public class CachedAttributesService implements AttributesService {
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(TenantId tenantId, EntityId entityId, String scope) {
validate(entityId, scope);
return Futures.immediateFuture(attributesDao.findAll(tenantId, entityId, scope));
// We can`t watch on cache because the keys are unknown.
return jpaExecutorService.submit(() -> attributesDao.findAll(tenantId, entityId, scope));
}
@Override

26
dao/src/main/java/org/thingsboard/server/dao/service/Validator.java

@ -27,6 +27,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Pattern;
public class Validator {
@ -46,6 +47,19 @@ public class Validator {
}
}
/**
* This method validate <code>EntityId</code> entity id. If entity id is invalid than throw
* <code>IncorrectParameterException</code> exception
*
* @param entityId the entityId
* @param errorMessageFunction the error message for exception that apply entityId
*/
public static void validateEntityId(EntityId entityId, Function<EntityId, String> errorMessageFunction) {
if (entityId == null || entityId.getId() == null) {
throw new IncorrectParameterException(errorMessageFunction.apply(entityId));
}
}
/**
* This method validate <code>String</code> string. If string is invalid than throw
* <code>IncorrectParameterException</code> exception
@ -59,6 +73,18 @@ public class Validator {
}
}
/*
* This method validate <code>String</code> string. If string is invalid than throw
* <code>IncorrectParameterException</code> exception
*
* @param val the value
* @param errorMessageFunction the error message function that apply value
*/
public static void validateString(String val, Function<String, String> errorMessageFunction) {
if (val == null || val.isEmpty()) {
throw new IncorrectParameterException(errorMessageFunction.apply(val));
}
}
/**
* This method validate <code>long</code> value. If value isn't positive than throw

43
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -151,12 +151,12 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override
public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) {
return Futures.immediateFuture(Optional.ofNullable(doFindLatest(entityId, key)));
return service.submit(() -> Optional.ofNullable(doFindLatest(entityId, key)));
}
@Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return Futures.immediateFuture(getLatestTsKvEntry(entityId, key));
return service.submit(() -> getLatestTsKvEntry(entityId, key));
}
@Override
@ -221,36 +221,29 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
}
protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
TsKvEntry latest = doFindLatest(entityId, query.getKey());
if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
}
long ts = latest.getTs();
ListenableFuture<Boolean> removedLatestFuture;
if (ts >= query.getStartTs() && ts < query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
removedLatestFuture = service.submit(() -> {
ListenableFuture<TsKvEntry> latestFuture = service.submit(() -> doFindLatest(entityId, query.getKey()));
return Futures.transformAsync(latestFuture, latest -> {
if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
}
boolean isRemoved = false;
long ts = latest.getTs();
if (ts >= query.getStartTs() && ts < query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
tsKvLatestRepository.delete(latestEntity);
return true;
});
} else {
removedLatestFuture = Futures.immediateFuture(false);
}
return Futures.transformAsync(removedLatestFuture, isRemoved -> {
if (isRemoved && query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query);
isRemoved = true;
if (query.getRewriteLatestIfDeleted()) {
return getNewLatestEntryFuture(tenantId, entityId, query);
}
}
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), isRemoved));
}, MoreExecutors.directExecutor());
}
protected ListenableFuture<List<TsKvEntry>> getFindAllLatestFuture(EntityId entityId) {
return Futures.immediateFuture(
return service.submit(() ->
DaoUtil.convertDataList(Lists.newArrayList(
searchTsKvLatestRepository.findAllByEntityId(entityId.getId()))));
}

30
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -16,7 +16,6 @@
package org.thingsboard.server.dao.timeseries;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -43,6 +42,7 @@ import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.Validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@ -126,18 +126,22 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<ListenableFuture<TsKvEntry>> futures = Lists.newArrayListWithExpectedSize(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
keys.forEach(key -> futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key)));
List<ListenableFuture<TsKvEntry>> futures = new ArrayList<>(keys.size());
keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k));
for (String key : keys) {
futures.add(timeseriesLatestDao.findLatest(tenantId, entityId, key));
}
return Futures.allAsList(futures);
}
@Override
public List<TsKvEntry> findLatestSync(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<TsKvEntry> latestEntries = Lists.newArrayListWithExpectedSize(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
keys.forEach(key -> latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key)));
List<TsKvEntry> latestEntries = new ArrayList<>(keys.size());
keys.forEach(key -> Validator.validateString(key, k -> "Incorrect key " + k));
for (String key : keys) {
latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key));
}
return latestEntries;
}
@ -165,7 +169,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {
validate(entityId);
List<ListenableFuture<Integer>> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
List<ListenableFuture<Integer>> futures = new ArrayList<>(INSERTS_PER_ENTRY);
saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, 0L);
return Futures.transform(Futures.allAsList(futures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor());
}
@ -182,7 +186,7 @@ public class BaseTimeseriesService implements TimeseriesService {
private ListenableFuture<Integer> doSave(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries, long ttl, boolean saveLatest) {
int inserts = saveLatest ? INSERTS_PER_ENTRY : INSERTS_PER_ENTRY_WITHOUT_LATEST;
List<ListenableFuture<Integer>> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * inserts);
List<ListenableFuture<Integer>> futures = new ArrayList<>(tsKvEntries.size() * inserts);
for (TsKvEntry tsKvEntry : tsKvEntries) {
if (saveLatest) {
saveAndRegisterFutures(tenantId, futures, entityId, tsKvEntry, ttl);
@ -195,7 +199,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<List<Void>> saveLatest(TenantId tenantId, EntityId entityId, List<TsKvEntry> tsKvEntries) {
List<ListenableFuture<Void>> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size());
List<ListenableFuture<Void>> futures = new ArrayList<>(tsKvEntries.size());
for (TsKvEntry tsKvEntry : tsKvEntries) {
futures.add(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry));
}
@ -242,7 +246,7 @@ public class BaseTimeseriesService implements TimeseriesService {
public ListenableFuture<List<TsKvLatestRemovingResult>> remove(TenantId tenantId, EntityId entityId, List<DeleteTsKvQuery> deleteTsKvQueries) {
validate(entityId);
deleteTsKvQueries.forEach(BaseTimeseriesService::validate);
List<ListenableFuture<TsKvLatestRemovingResult>> futures = Lists.newArrayListWithExpectedSize(deleteTsKvQueries.size() * DELETES_PER_ENTRY);
List<ListenableFuture<TsKvLatestRemovingResult>> futures = new ArrayList<>(deleteTsKvQueries.size() * DELETES_PER_ENTRY);
for (DeleteTsKvQuery tsKvQuery : deleteTsKvQueries) {
deleteAndRegisterFutures(tenantId, futures, entityId, tsKvQuery);
}
@ -252,7 +256,7 @@ public class BaseTimeseriesService implements TimeseriesService {
@Override
public ListenableFuture<List<TsKvLatestRemovingResult>> removeLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<ListenableFuture<TsKvLatestRemovingResult>> futures = Lists.newArrayListWithExpectedSize(keys.size());
List<ListenableFuture<TsKvLatestRemovingResult>> futures = new ArrayList<>(keys.size());
for (String key : keys) {
DeleteTsKvQuery query = new BaseDeleteTsKvQuery(key, 0, System.currentTimeMillis(), false);
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
@ -281,7 +285,7 @@ public class BaseTimeseriesService implements TimeseriesService {
}
private static void validate(EntityId entityId) {
Validator.validateEntityId(entityId, "Incorrect entityId " + entityId);
Validator.validateEntityId(entityId, id -> "Incorrect entityId " + id);
}
private void validate(ReadTsKvQuery query) {

2
msa/js-executor/docker/Dockerfile

@ -35,7 +35,7 @@ COPY src/server.js ./
RUN chmod a+x /tmp/*.sh \
&& mv /tmp/start-js-executor.sh /usr/bin \
&& chown -R node:node ${pkg.installFolder} \
&& yarn install --production && yarn cache clean --all
&& yarn install --production --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network && yarn cache clean --all
USER node

2
msa/js-executor/pom.xml

@ -81,7 +81,7 @@
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install</arguments>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration>
</execution>
<execution>

2
msa/web-ui/docker/Dockerfile

@ -34,7 +34,7 @@ COPY src/server.js ./
RUN chmod a+x /tmp/*.sh \
&& mv /tmp/start-web-ui.sh /usr/bin \
&& chown -R node:node ${pkg.installFolder} \
&& yarn install --production && yarn cache clean --all
&& yarn install --production --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network && yarn cache clean --all
USER node

2
msa/web-ui/pom.xml

@ -90,7 +90,7 @@
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install</arguments>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration>
</execution>
<execution>

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

@ -99,10 +99,10 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
private void safePutAttributes(TbContext ctx, TbMsg msg, ObjectNode msgDataNode, T entityId) {
Set<TbPair<String, List<String>>> failuresPairSet = ConcurrentHashMap.newKeySet();
var getKvEntryPairFutures = Futures.allAsList(
getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet)
getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet),
getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet)
);
withCallback(getKvEntryPairFutures, futuresList -> {
var msgMetaData = msg.getMetaData().copy();

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

File diff suppressed because one or more lines are too long

2
ui-ngx/package.json

@ -55,7 +55,7 @@
"core-js": "^3.29.1",
"date-fns": "2.0.0-alpha.27",
"dayjs": "1.11.4",
"echarts": "^5.4.3",
"echarts": "^5.5.0",
"flot": "https://github.com/thingsboard/flot.git#0.9-work",
"flot.curvedlines": "https://github.com/MichaelZinsmaier/CurvedLines.git#master",
"font-awesome": "^4.7.0",

2
ui-ngx/patches/@angular+core+15.2.9.patch → ui-ngx/patches/@angular+core+15.2.10.patch

@ -1,5 +1,5 @@
diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs
index 3e93015..9efcb96 100755
index e9a9b75..17044d9 100755
--- a/node_modules/@angular/core/fesm2020/core.mjs
+++ b/node_modules/@angular/core/fesm2020/core.mjs
@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) {

426
ui-ngx/patches/echarts+5.5.0.patch

@ -0,0 +1,426 @@
diff --git a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
index d6c05f3..aafb0b8 100644
--- a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
+++ b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
@@ -362,7 +362,10 @@ var DataZoomModel = /** @class */function (_super) {
return axisProxy.getDataValueWindow();
}
} else {
- return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
+ var axisProxy = this.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ return axisProxy.getDataValueWindow();
+ }
}
};
/**
@@ -381,10 +384,10 @@ var DataZoomModel = /** @class */function (_super) {
var axisInfo = this._targetAxisInfoMap.get(axisDim);
for (var j = 0; j < axisInfo.indexList.length; j++) {
var proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]);
- if (proxy.hostedBy(this)) {
+ if (proxy && proxy.hostedBy(this)) {
return proxy;
}
- if (!firstProxy) {
+ if (proxy && !firstProxy) {
firstProxy = proxy;
}
}
diff --git a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
index 06469b2..cf0b2ea 100644
--- a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
+++ b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
@@ -96,11 +96,14 @@ var getRangeHandlers = {
range[0] = (range[0] - percentPoint) * scale + percentPoint;
range[1] = (range[1] - percentPoint) * scale + percentPoint;
// Restrict range.
- var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
- sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
- this.range = range;
- if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
- return range;
+ var proxy = this.dataZoomModel.findRepresentativeAxisProxy();
+ if (proxy) {
+ var minMaxSpan = proxy.getMinMaxSpan();
+ sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
+ this.range = range;
+ if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
+ return range;
+ }
}
},
pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e) {
diff --git a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
index 98912e0..0cda6be 100644
--- a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
+++ b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
@@ -64,7 +64,7 @@ var DEFAULT_MOVE_HANDLE_SIZE = 7;
var HORIZONTAL = 'horizontal';
var VERTICAL = 'vertical';
var LABEL_GAP = 5;
-var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter'];
+var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter', 'custom'];
var REALTIME_ANIMATION_CONFIG = {
easing: 'cubicOut',
duration: 100,
@@ -359,30 +359,33 @@ var SliderZoomView = /** @class */function (_super) {
var result;
var ecModel = this.ecModel;
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- var seriesModels = dataZoomModel.getAxisProxy(axisDim, axisIndex).getTargetSeriesModels();
- each(seriesModels, function (seriesModel) {
- if (result) {
- return;
- }
- if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
- return;
- }
- var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
- var otherDim = getOtherDim(axisDim);
- var otherAxisInverse;
- var coordSys = seriesModel.coordinateSystem;
- if (otherDim != null && coordSys.getOtherAxis) {
- otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
- }
- otherDim = seriesModel.getData().mapDimension(otherDim);
- result = {
- thisAxis: thisAxis,
- series: seriesModel,
- thisDim: axisDim,
- otherDim: otherDim,
- otherAxisInverse: otherAxisInverse
- };
- }, this);
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ var seriesModels = axisProxy.getTargetSeriesModels();
+ each(seriesModels, function (seriesModel) {
+ if (result) {
+ return;
+ }
+ if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
+ return;
+ }
+ var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
+ var otherDim = getOtherDim(axisDim);
+ var otherAxisInverse;
+ var coordSys = seriesModel.coordinateSystem;
+ if (otherDim != null && coordSys.getOtherAxis) {
+ otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
+ }
+ otherDim = seriesModel.getData().mapDimension(otherDim);
+ result = {
+ thisAxis: thisAxis,
+ series: seriesModel,
+ thisDim: axisDim,
+ otherDim: otherDim,
+ otherAxisInverse: otherAxisInverse
+ };
+ }, this);
+ }
}, this);
return result;
};
@@ -530,12 +533,17 @@ var SliderZoomView = /** @class */function (_super) {
var dataZoomModel = this.dataZoomModel;
var handleEnds = this._handleEnds;
var viewExtend = this._getViewExtent();
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
- var percentExtent = [0, 100];
- sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
- var lastRange = this._range;
- var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
- return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
+ var proxy = dataZoomModel.findRepresentativeAxisProxy();
+ if (proxy) {
+ var minMaxSpan = proxy.getMinMaxSpan();
+ var percentExtent = [0, 100];
+ sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
+ var lastRange = this._range;
+ var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
+ return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
+ } else {
+ return false;
+ }
};
SliderZoomView.prototype._updateView = function (nonRealtime) {
var displaybles = this._displayables;
diff --git a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
index ce98fed..e154118 100644
--- a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
+++ b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
@@ -90,7 +90,10 @@ var dataZoomProcessor = {
// init stage and not after action dispatch handler, because
// reset should be called after seriesData.restoreData.
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel);
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ axisProxy.reset(dataZoomModel);
+ }
});
// Caution: data zoom filtering is order sensitive when using
// percent range and no min/max/scale set on axis.
@@ -107,7 +110,10 @@ var dataZoomProcessor = {
// So we should filter x-axis after reset x-axis immediately,
// and then reset y-axis and filter y-axis.
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- dataZoomModel.getAxisProxy(axisDim, axisIndex).filterData(dataZoomModel, api);
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ axisProxy.filterData(dataZoomModel, api);
+ }
});
});
ecModel.eachComponent('dataZoom', function (dataZoomModel) {
diff --git a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
index cf8d6bc..f9b9f90 100644
--- a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
+++ b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
@@ -109,9 +109,12 @@ var DataZoomFeature = /** @class */function (_super) {
var axisModel = axis.model;
var dataZoomModel = findDataZoom(dimName, axisModel, ecModel);
// Restrict range.
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
- if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
- minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
+ var proxy = dataZoomModel.findRepresentativeAxisProxy(axisModel);
+ if (proxy) {
+ var minMaxSpan = proxy.getMinMaxSpan();
+ if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
+ minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
+ }
}
dataZoomModel && (snapshot[dataZoomModel.id] = {
dataZoomId: dataZoomModel.id,
diff --git a/node_modules/echarts/lib/component/tooltip/TooltipView.js b/node_modules/echarts/lib/component/tooltip/TooltipView.js
index b8a9b95..11e49c0 100644
--- a/node_modules/echarts/lib/component/tooltip/TooltipView.js
+++ b/node_modules/echarts/lib/component/tooltip/TooltipView.js
@@ -360,7 +360,7 @@ var TooltipView = /** @class */function (_super) {
each(itemCoordSys.dataByAxis, function (axisItem) {
var axisModel = ecModel.getComponent(axisItem.axisDim + 'Axis', axisItem.axisIndex);
var axisValue = axisItem.value;
- if (!axisModel || axisValue == null) {
+ if (!axisModel || !axisModel.axis || axisValue == null) {
return;
}
var axisValueLabel = axisPointerViewHelper.getValueLabel(axisValue, axisModel.axis, ecModel, axisItem.seriesDataIndices, axisItem.valueLabelOpt);
diff --git a/node_modules/echarts/lib/coord/axisHelper.js b/node_modules/echarts/lib/coord/axisHelper.js
index a76c66b..e5b7ee5 100644
--- a/node_modules/echarts/lib/coord/axisHelper.js
+++ b/node_modules/echarts/lib/coord/axisHelper.js
@@ -187,7 +187,9 @@ export function createScaleByModel(model, axisType) {
});
default:
// case 'value'/'interval', 'log', or others.
- return new (Scale.getClass(axisType) || IntervalScale)();
+ return new (Scale.getClass(axisType) || IntervalScale)({
+ ticksGenerator: model.get('ticksGenerator')
+ });
}
}
}
diff --git a/node_modules/echarts/lib/coord/cartesian/Grid.js b/node_modules/echarts/lib/coord/cartesian/Grid.js
index 5b18f02..39a57f8 100644
--- a/node_modules/echarts/lib/coord/cartesian/Grid.js
+++ b/node_modules/echarts/lib/coord/cartesian/Grid.js
@@ -91,11 +91,11 @@ var Grid = /** @class */function () {
var scale = axis.scale;
if (
// Only value and log axis without interval support alignTicks.
- isIntervalOrLogScale(scale) && model.get('alignTicks') && model.get('interval') == null) {
+ isIntervalOrLogScale(scale) && model.get('alignTicks') && model.get('interval') == null && model.get('ticksGenerator') == null) {
axisNeedsAlign.push(axis);
} else {
niceScaleExtent(scale, model);
- if (isIntervalOrLogScale(scale)) {
+ if (isIntervalOrLogScale(scale) && !scale.isBlank()) {
// Can only align to interval or log axis.
alignTo = axis;
}
@@ -105,10 +105,15 @@ var Grid = /** @class */function () {
// All axes has set alignTicks. Pick the first one.
// PENDING. Should we find the axis that both set interval, min, max and align to this one?
if (axisNeedsAlign.length) {
- if (!alignTo) {
- alignTo = axisNeedsAlign.pop();
- niceScaleExtent(alignTo.scale, alignTo.model);
+ while (!alignTo && axisNeedsAlign.length) {
+ var axis = axisNeedsAlign.pop();
+ niceScaleExtent(axis.scale, axis.model);
+ if (!axis.scale.isBlank()) {
+ alignTo = axis;
+ }
}
+ }
+ if (axisNeedsAlign.length && alignTo) {
each(axisNeedsAlign, function (axis) {
alignScaleTicks(axis.scale, axis.model, alignTo.scale);
});
diff --git a/node_modules/echarts/lib/data/SeriesData.js b/node_modules/echarts/lib/data/SeriesData.js
index 98d5ce8..1c293a6 100644
--- a/node_modules/echarts/lib/data/SeriesData.js
+++ b/node_modules/echarts/lib/data/SeriesData.js
@@ -900,13 +900,16 @@ var SeriesData = /** @class */function () {
var dimInfo = data._dimInfos[dim];
// Currently, only dimensions that has ordinalMeta can create inverted indices.
var ordinalMeta = dimInfo.ordinalMeta;
+ var stack = dimInfo.stack;
var store = data._store;
- if (ordinalMeta) {
- invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array(ordinalMeta.categories.length);
- // The default value of TypedArray is 0. To avoid miss
- // mapping to 0, we should set it as INDEX_NOT_FOUND.
- for (var i = 0; i < invertedIndices.length; i++) {
- invertedIndices[i] = INDEX_NOT_FOUND;
+ if (ordinalMeta || stack) {
+ invertedIndices = invertedIndicesMap[dim] = stack ? new Array(store.count()) : new CtorInt32Array(ordinalMeta.categories.length);
+ if (ordinalMeta) {
+ // The default value of TypedArray is 0. To avoid miss
+ // mapping to 0, we should set it as INDEX_NOT_FOUND.
+ for (var i = 0; i < invertedIndices.length; i++) {
+ invertedIndices[i] = INDEX_NOT_FOUND;
+ }
}
for (var i = 0; i < store.count(); i++) {
// Only support the case that all values are distinct.
diff --git a/node_modules/echarts/lib/data/Source.js b/node_modules/echarts/lib/data/Source.js
index 7dda49b..2dd2b98 100644
--- a/node_modules/echarts/lib/data/Source.js
+++ b/node_modules/echarts/lib/data/Source.js
@@ -252,7 +252,8 @@ function normalizeDimensionsOption(dimensionsDefine) {
var item = {
name: rawItem.name,
displayName: rawItem.displayName,
- type: rawItem.type
+ type: rawItem.type,
+ stack: rawItem.stack
};
// User can set null in dimensions.
// We don't auto specify name, otherwise a given name may
diff --git a/node_modules/echarts/lib/data/helper/createDimensions.js b/node_modules/echarts/lib/data/helper/createDimensions.js
index 00d7eb7..dd514b1 100644
--- a/node_modules/echarts/lib/data/helper/createDimensions.js
+++ b/node_modules/echarts/lib/data/helper/createDimensions.js
@@ -110,6 +110,9 @@ source, opt) {
}
dimDefItem.type != null && (resultItem.type = dimDefItem.type);
dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName);
+ if (dimDefItem.stack) {
+ resultItem.stack = true;
+ }
var newIdx = resultList.length;
indicesMap[dimIdx] = newIdx;
resultItem.storeDimIndex = dimIdx;
diff --git a/node_modules/echarts/lib/data/helper/dataStackHelper.js b/node_modules/echarts/lib/data/helper/dataStackHelper.js
index c25de1b..ea8300d 100644
--- a/node_modules/echarts/lib/data/helper/dataStackHelper.js
+++ b/node_modules/echarts/lib/data/helper/dataStackHelper.js
@@ -91,7 +91,7 @@ export function enableDataStack(seriesModel, dimensionsInput, opt) {
}
if (mayStack && !dimensionInfo.isExtraCoord) {
// Find the first ordinal dimension as the stackedByDimInfo.
- if (!byIndex && !stackedByDimInfo && dimensionInfo.ordinalMeta) {
+ if (!byIndex && !stackedByDimInfo && (dimensionInfo.ordinalMeta || dimensionInfo.stack)) {
stackedByDimInfo = dimensionInfo;
}
// Find the first stackable dimension as the stackedDimInfo.
diff --git a/node_modules/echarts/lib/scale/Interval.js b/node_modules/echarts/lib/scale/Interval.js
index 1094662..363c0a5 100644
--- a/node_modules/echarts/lib/scale/Interval.js
+++ b/node_modules/echarts/lib/scale/Interval.js
@@ -46,12 +46,17 @@ import * as numberUtil from '../util/number.js';
import * as formatUtil from '../util/format.js';
import Scale from './Scale.js';
import * as helper from './helper.js';
+import { isFunction } from 'zrender/lib/core/util.js';
var roundNumber = numberUtil.round;
var IntervalScale = /** @class */function (_super) {
__extends(IntervalScale, _super);
- function IntervalScale() {
- var _this = _super !== null && _super.apply(this, arguments) || this;
+ function IntervalScale(setting) {
+ var _this = _super.call(this, setting) || this;
_this.type = 'interval';
+ var ticksGenerator = _this.getSetting('ticksGenerator');
+ if (isFunction(ticksGenerator)) {
+ _this._ticksGenerator = ticksGenerator;
+ }
// Step is calculated in adjustExtent.
_this._interval = 0;
_this._intervalPrecision = 2;
@@ -104,7 +109,17 @@ var IntervalScale = /** @class */function (_super) {
var extent = this._extent;
var niceTickExtent = this._niceExtent;
var intervalPrecision = this._intervalPrecision;
- var ticks = [];
+ var ticksGenerator = this._ticksGenerator;
+ var ticks;
+ if (ticksGenerator) {
+ try {
+ ticks = ticksGenerator(extent, interval, niceTickExtent, intervalPrecision);
+ if (ticks) {
+ return ticks;
+ }
+ } catch (_e) {}
+ }
+ ticks = [];
// If interval is 0, return [];
if (!interval) {
return ticks;
diff --git a/node_modules/echarts/types/dist/shared.d.ts b/node_modules/echarts/types/dist/shared.d.ts
index ca74097..ef41ce2 100644
--- a/node_modules/echarts/types/dist/shared.d.ts
+++ b/node_modules/echarts/types/dist/shared.d.ts
@@ -2422,6 +2422,9 @@ interface AxisBaseOptionCommon extends ComponentOption, AnimationOptionMixin {
max: number;
}) => ScaleDataValue);
}
+
+declare type NumericAxisTicksGenerator = (extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
+
interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
boundaryGap?: [number | string, number | string];
/**
@@ -2447,6 +2450,8 @@ interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
* Will be ignored if interval is set.
*/
alignTicks?: boolean;
+
+ ticksGenerator?: NumericAxisTicksGenerator;
}
interface CategoryAxisBaseOption extends AxisBaseOptionCommon {
type?: 'category';
@@ -6412,6 +6417,7 @@ declare type DimensionDefinition = {
type?: DataStoreDimensionType;
name?: DimensionName;
displayName?: string;
+ stack?: boolean;
};
declare type DimensionDefinitionLoose = DimensionDefinition['name'] | DimensionDefinition;
declare const SOURCE_FORMAT_ORIGINAL: "original";
diff --git a/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts b/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
index c5c2792..d524b70 100644
--- a/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
+++ b/node_modules/echarts/types/src/coord/axisCommonTypes.d.ts
@@ -56,6 +56,9 @@ export interface AxisBaseOptionCommon extends ComponentOption, AnimationOptionMi
max: number;
}) => ScaleDataValue);
}
+
+export declare type NumericAxisTicksGenerator = (extent?: number[], interval?: number, niceTickExtent?: number[], intervalPrecision?: number) => {value: number}[];
+
export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
boundaryGap?: [number | string, number | string];
/**
@@ -81,6 +84,8 @@ export interface NumericAxisBaseOptionCommon extends AxisBaseOptionCommon {
* Will be ignored if interval is set.
*/
alignTicks?: boolean;
+
+ ticksGenerator?: NumericAxisTicksGenerator;
}
export interface CategoryAxisBaseOption extends AxisBaseOptionCommon {
type?: 'category';

4
ui-ngx/pom.xml

@ -56,7 +56,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v16.20.2</nodeVersion>
<nodeVersion>v20.11.1</nodeVersion>
<yarnVersion>v1.22.17</yarnVersion>
</configuration>
</execution>
@ -66,7 +66,7 @@
<goal>yarn</goal>
</goals>
<configuration>
<arguments>install</arguments>
<arguments>install --non-interactive --network-concurrency 4 --network-timeout 100000 --mutex network</arguments>
</configuration>
</execution>
</executions>

17
ui-ngx/src/app/core/api/data-aggregator.ts

@ -14,24 +14,23 @@
/// limitations under the License.
///
import { AggKey, IndexedSubscriptionData, } from '@app/shared/models/telemetry/telemetry.models';
import {
AggKey,
IndexedSubscriptionData,
} from '@app/shared/models/telemetry/telemetry.models';
import {
AggregationType, calculateAggIntervalWithSubscriptionTimeWindow,
AggregationType,
calculateAggIntervalWithSubscriptionTimeWindow,
calculateIntervalComparisonEndTime,
calculateIntervalEndTime,
calculateIntervalStartEndTime,
getCurrentTime,
getTime, IntervalMath,
getTime,
IntervalMath,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import Timeout = NodeJS.Timeout;
import { DataEntry, DataSet, IndexedData } from '@shared/models/widget.models';
import BTree from 'sorted-btree';
import Timeout = NodeJS.Timeout;
export declare type onAggregatedData = (data: IndexedData, detectChanges: boolean) => void;
@ -318,7 +317,9 @@ export class DataAggregator {
this.startTs += tickTs;
this.endTs += tickTs;
}
this.updateLastInterval();
if (this.subsTw.aggregation.type !== AggregationType.NONE) {
this.updateLastInterval();
}
this.data = this.updateData();
this.elapsed = this.elapsed - delta * this.aggregationTimeout;
}

8
ui-ngx/src/app/core/api/widget-subscription.ts

@ -61,9 +61,8 @@ import {
deepClone,
flatFormattedData,
formattedDataFormDatasourceData,
isDefined,
isDefinedAndNotNull,
isEqual,
isEqual, isUndefined,
parseHttpErrorMessage
} from '@core/utils';
import { EntityId } from '@app/shared/models/id/entity-id';
@ -355,7 +354,7 @@ export class WidgetSubscription implements IWidgetSubscription {
}
this.units = options.units || '';
this.decimals = isDefined(options.decimals) ? options.decimals : 2;
this.decimals = isDefinedAndNotNull(options.decimals) ? options.decimals : 2;
this.loadingData = false;
@ -1491,7 +1490,8 @@ export class WidgetSubscription implements IWidgetSubscription {
let datasourceDataArray: Array<DatasourceData> = [];
datasourceDataArray = datasourceDataArray.concat(datasource.dataKeys.map((dataKey, keyIndex) => {
dataKey.hidden = !!dataKey.settings.hideDataByDefault;
dataKey.inLegend = !dataKey.settings.removeFromLegend;
dataKey.inLegend = dataKey.settings.showInLegend ||
(isUndefined(dataKey.settings.showInLegend) && !dataKey.settings.removeFromLegend);
dataKey.label = this.ctx.utils.customTranslation(dataKey.label, dataKey.label);
const datasourceData: DatasourceData = {
datasource,

37
ui-ngx/src/app/core/services/dialog.service.ts

@ -21,22 +21,25 @@ import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import {
ColorPickerDialogComponent,
ColorPickerDialogData, ColorPickerDialogResult
ColorPickerDialogData,
ColorPickerDialogResult
} from '@shared/components/dialog/color-picker-dialog.component';
import {
MaterialIconsDialogComponent,
MaterialIconsDialogData, MaterialIconsDialogResult
MaterialIconsDialogData,
MaterialIconsDialogResult
} from '@shared/components/dialog/material-icons-dialog.component';
import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component';
import { ConfirmDialogComponent, ConfirmDialogData } from '@shared/components/dialog/confirm-dialog.component';
import { AlertDialogComponent, AlertDialogData } from '@shared/components/dialog/alert-dialog.component';
import {
ErrorAlertDialogComponent,
ErrorAlertDialogData
} from '@shared/components/dialog/error-alert-dialog.component';
import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component';
@Injectable(
{
providedIn: 'root'
}
)
@Injectable({
providedIn: 'root'
})
export class DialogService {
constructor(
@ -47,7 +50,7 @@ export class DialogService {
}
confirm(title: string, message: string, cancel: string = null, ok: string = null, fullscreen: boolean = false): Observable<boolean> {
const dialogConfig: MatDialogConfig = {
const dialogConfig: MatDialogConfig<ConfirmDialogData> = {
disableClose: true,
data: {
title,
@ -59,12 +62,12 @@ export class DialogService {
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(ConfirmDialogComponent, dialogConfig);
const dialogRef = this.dialog.open<ConfirmDialogComponent, ConfirmDialogData, boolean>(ConfirmDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
alert(title: string, message: string, ok: string = null, fullscreen: boolean = false): Observable<boolean> {
const dialogConfig: MatDialogConfig = {
const dialogConfig: MatDialogConfig<AlertDialogData> = {
disableClose: true,
data: {
title,
@ -75,12 +78,12 @@ export class DialogService {
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(AlertDialogComponent, dialogConfig);
const dialogRef = this.dialog.open<AlertDialogComponent, AlertDialogData, boolean>(AlertDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable<any> {
const dialogConfig: MatDialogConfig = {
errorAlert(title: string, message: string, error: any, ok: string = null, fullscreen: boolean = false): Observable<boolean> {
const dialogConfig: MatDialogConfig<ErrorAlertDialogData> = {
disableClose: true,
data: {
title,
@ -92,7 +95,7 @@ export class DialogService {
if (fullscreen) {
dialogConfig.panelClass = ['tb-fullscreen-dialog'];
}
const dialogRef = this.dialog.open(ErrorAlertDialogComponent, dialogConfig);
const dialogRef = this.dialog.open<ErrorAlertDialogComponent, ErrorAlertDialogData, boolean>(ErrorAlertDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}

3
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -29,6 +29,7 @@ import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-compo
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
export interface AddWidgetDialogData {
dashboard: Dashboard;
@ -97,6 +98,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const rawLatestDataKeySettingsSchema = widgetInfo.typeLatestDataKeySettingsSchema || widgetInfo.latestDataKeySettingsSchema;
const typeParameters = widgetInfo.typeParameters;
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsSchema;
@ -129,6 +131,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,
dataKeySettingsFunction,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,

3
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -27,6 +27,7 @@ import { WidgetConfigComponentData } from '../../models/widget-component.models'
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
@Component({
selector: 'tb-edit-widget',
@ -133,6 +134,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const rawLatestDataKeySettingsSchema = widgetInfo.typeLatestDataKeySettingsSchema || widgetInfo.latestDataKeySettingsSchema;
const typeParameters = widgetInfo.typeParameters;
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsSchema;
@ -165,6 +167,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,
dataKeySettingsFunction,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,

41
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -20,11 +20,11 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidatorFn,
Validators
} from '@angular/forms';
@ -32,13 +32,15 @@ import { Subject } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
toWidgetActionDescriptor,
WidgetActionCallbacks,
WidgetActionDescriptorInfo,
WidgetActionsData
} from '@home/components/widget/action/manage-widget-actions.component.models';
import { UtilsService } from '@core/services/utils.service';
import {
actionDescriptorToAction, defaultWidgetAction,
actionDescriptorToAction,
defaultWidgetAction,
WidgetActionSource,
widgetType
} from '@shared/models/widget.models';
@ -65,7 +67,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
private destroy$ = new Subject<void>();
widgetActionFormGroup: UntypedFormGroup;
widgetActionFormGroup: FormGroup;
isAdd: boolean;
action: WidgetActionDescriptorInfo;
@ -83,7 +85,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
@Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>,
public fb: UntypedFormBuilder) {
public fb: FormBuilder) {
super(store, router, dialogRef);
this.isAdd = data.isAdd;
if (this.isAdd) {
@ -100,19 +102,14 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
}
ngOnInit(): void {
this.widgetActionFormGroup = this.fb.group({});
this.widgetActionFormGroup.addControl('actionSourceId',
this.fb.control(this.action.actionSourceId, [Validators.required]));
this.widgetActionFormGroup.addControl('name',
this.fb.control(this.action.name, [this.validateActionName(), Validators.required]));
this.widgetActionFormGroup.addControl('icon',
this.fb.control(this.action.icon, [Validators.required]));
this.widgetActionFormGroup.addControl('useShowWidgetActionFunction',
this.fb.control(this.action.useShowWidgetActionFunction, []));
this.widgetActionFormGroup.addControl('showWidgetActionFunction',
this.fb.control(this.action.showWidgetActionFunction || 'return true;', []));
this.widgetActionFormGroup.addControl('widgetAction',
this.fb.control(actionDescriptorToAction(this.action), [Validators.required]));
this.widgetActionFormGroup = this.fb.group({
actionSourceId: [this.action.actionSourceId, Validators.required],
name: [this.action.name, [this.validateActionName(), Validators.required]],
icon: [this.action.icon, Validators.required],
useShowWidgetActionFunction: [this.action.useShowWidgetActionFunction],
showWidgetActionFunction: [this.action.showWidgetActionFunction || 'return true;'],
widgetAction: [actionDescriptorToAction(toWidgetActionDescriptor(this.action)), Validators.required]
});
this.updateShowWidgetActionForm();
this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe(
takeUntil(this.destroy$)
@ -159,9 +156,9 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
}
private validateActionName(): ValidatorFn {
return (c: UntypedFormControl) => {
return (c: FormControl) => {
const newName = c.value;
const valid = this.checkActionName(newName, this.widgetActionFormGroup.get('actionSourceId').value);
const valid = this.checkActionName(newName, c.parent?.get('actionSourceId').value);
return !valid ? {
actionNameNotUnique: true
} : null;
@ -182,7 +179,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
return actionNameIsUnique;
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;

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

@ -107,6 +107,9 @@ import { SliderBasicConfigComponent } from '@home/components/widget/config/basic
import {
ToggleButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/toggle-button-basic-config.component';
import {
TimeSeriesChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/time-series-chart-basic-config.component';
@NgModule({
declarations: [
@ -141,7 +144,8 @@ import {
CommandButtonBasicConfigComponent,
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent
],
imports: [
CommonModule,
@ -180,7 +184,8 @@ import {
CommandButtonBasicConfigComponent,
PowerButtonBasicConfigComponent,
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -213,5 +218,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-command-button-basic-config': CommandButtonBasicConfigComponent,
'tb-power-button-basic-config': PowerButtonBasicConfigComponent,
'tb-slider-basic-config': SliderBasicConfigComponent,
'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent
'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent,
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent
};

3
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts

@ -152,7 +152,8 @@ export class AggregatedDataKeysPanelComponent implements ControlValueAccessor, O
}
addKey() {
const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null);
const dataKey = this.callbacks.generateDataKey(this.keyName, this.dataKeyType, null,
true,null);
dataKey.decimals = 0;
dataKey.settings = {...aggregatedValueCardDefaultKeySettings};
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray;

10
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html

@ -195,7 +195,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>
@ -205,6 +206,13 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput

6
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts

@ -127,6 +127,8 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
@ -178,6 +180,7 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom
this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat;
this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont;
this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor;
this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval;
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
@ -266,10 +269,12 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom
this.barChartWidgetConfigForm.get('tooltipDateFormat').enable();
this.barChartWidgetConfigForm.get('tooltipDateFont').enable();
this.barChartWidgetConfigForm.get('tooltipDateColor').enable();
this.barChartWidgetConfigForm.get('tooltipDateInterval').enable();
} else {
this.barChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.barChartWidgetConfigForm.get('tooltipDateFont').disable();
this.barChartWidgetConfigForm.get('tooltipDateColor').disable();
this.barChartWidgetConfigForm.get('tooltipDateInterval').disable();
}
} else {
this.barChartWidgetConfigForm.get('tooltipValueFont').disable();
@ -278,6 +283,7 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom
this.barChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.barChartWidgetConfigForm.get('tooltipDateFont').disable();
this.barChartWidgetConfigForm.get('tooltipDateColor').disable();
this.barChartWidgetConfigForm.get('tooltipDateInterval').disable();
this.barChartWidgetConfigForm.get('tooltipBackgroundColor').disable();
this.barChartWidgetConfigForm.get('tooltipBackgroundBlur').disable();
}

10
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html

@ -169,7 +169,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>
@ -179,6 +180,13 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput

6
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts

@ -108,6 +108,8 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
@ -155,6 +157,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat;
this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont;
this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor;
this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval;
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
@ -225,10 +228,12 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
this.rangeChartWidgetConfigForm.get('tooltipDateFormat').enable();
this.rangeChartWidgetConfigForm.get('tooltipDateFont').enable();
this.rangeChartWidgetConfigForm.get('tooltipDateColor').enable();
this.rangeChartWidgetConfigForm.get('tooltipDateInterval').enable();
} else {
this.rangeChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.rangeChartWidgetConfigForm.get('tooltipDateFont').disable();
this.rangeChartWidgetConfigForm.get('tooltipDateColor').disable();
this.rangeChartWidgetConfigForm.get('tooltipDateInterval').disable();
}
} else {
this.rangeChartWidgetConfigForm.get('tooltipValueFont').disable();
@ -237,6 +242,7 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent {
this.rangeChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.rangeChartWidgetConfigForm.get('tooltipDateFont').disable();
this.rangeChartWidgetConfigForm.get('tooltipDateColor').disable();
this.rangeChartWidgetConfigForm.get('tooltipDateInterval').disable();
this.rangeChartWidgetConfigForm.get('tooltipBackgroundColor').disable();
this.rangeChartWidgetConfigForm.get('tooltipBackgroundBlur').disable();
}

259
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html

@ -0,0 +1,259 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="timeSeriesChartWidgetConfigForm">
<tb-timewindow-config-panel formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDatasourceLabel
hideDataKeys
forceSingleDatasource
formControlName="datasources">
</tb-datasources>
<tb-data-keys-panel
panelTitle="{{ 'widgets.chart.series' | translate }}"
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}"
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
timeSeriesChart
[yAxisIds]="yAxisIds"
[showTimeSeriesType]="chartType === TimeSeriesChartType.default"
hideSourceSelection
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<tb-time-series-chart-y-axes-panel
formControlName="yAxes"
(axisRemoved)="yAxisRemoved($event)">
</tb-time-series-chart-y-axes-panel>
<tb-time-series-chart-thresholds-panel
formControlName="thresholds"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[datasource]="datasource"
[widgetConfig]="widgetConfig?.config"
[yAxisIds]="yAxisIds">
</tb-time-series-chart-thresholds-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="timeSeriesChartWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
[color]="timeSeriesChartWidgetConfigForm.get('iconColor').value"
formControlName="icon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.chart</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="dataZoom">
{{ 'widgets.time-series-chart.data-zoom' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="stack">
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.stack-mode-hint' | translate}}">
{{ 'widgets.time-series-chart.stack-mode' | translate }}
</div>
</mat-slide-toggle>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div>
<tb-time-series-chart-axis-settings
formControlName="xAxis"
axisType="xAxis">
</tb-time-series-chart-axis-settings>
</div>
<tb-time-series-no-aggregation-bar-width-settings
*ngIf="chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar"
formControlName="noAggregationBarWidthSettings">
</tb-time-series-no-aggregation-bar-width-settings>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetConfigForm.get('showLegend').value"
[disabled]="!timeSeriesChartWidgetConfigForm.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'legend.label' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendLabelFont"
previewText="Temperature">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendLabelColor">
</tb-color-input>
</div>
</div>
<tb-legend-config hideDirection
formControlName="legendConfig">
</tb-legend-config>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetConfigForm.get('showTooltip').value"
[disabled]="!timeSeriesChartWidgetConfigForm.get('showTooltip').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.tooltip' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.trigger' | translate }}</div>
<mat-chip-listbox class="center-stretch" formControlName="tooltipTrigger">
<mat-chip-option [selectable]="timeSeriesChartWidgetConfigForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.point"
[value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="timeSeriesChartWidgetConfigForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.axis"
[value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="tooltipValueFont"
[previewText]="tooltipValuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="tooltipShowDate">
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipDateColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipBackgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-blur' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
<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>

367
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts

@ -0,0 +1,367 @@
///
/// 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, Injector } 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 {
DataKey,
Datasource,
legendPositions,
legendPositionTranslationMap,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isDefinedAndNotNull, isUndefined, mergeDeep } from '@core/utils';
import {
cssSizeToStrSize,
DateFormatProcessor,
DateFormatSettings,
resolveCssSize
} from '@shared/models/widget-settings.models';
import {
timeSeriesChartWidgetDefaultSettings,
TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models';
import {
TimeSeriesChartKeySettings, TimeSeriesChartThreshold,
TimeSeriesChartType, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
@Component({
selector: 'tb-time-series-chart-basic-config',
templateUrl: './time-series-chart-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.timeSeriesChartWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get yAxisIds(): TimeSeriesChartYAxisId[] {
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetConfigForm.get('yAxes').value;
return Object.keys(yAxes);
}
TimeSeriesChartType = TimeSeriesChartType;
EChartsTooltipTrigger = EChartsTooltipTrigger;
legendPositions = legendPositions;
legendPositionTranslationMap = legendPositionTranslationMap;
timeSeriesChartWidgetConfigForm: UntypedFormGroup;
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this);
chartType: TimeSeriesChartType = TimeSeriesChartType.default;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
public yAxisRemoved(yAxisId: TimeSeriesChartYAxisId): void {
if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources.length > 1) {
for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) {
const datasource = this.widgetConfig.config.datasources[i];
this.removeYaxisId(datasource.dataKeys, yAxisId);
}
}
}
protected configForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetConfigForm;
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
if (isDefinedAndNotNull(params.chartType)) {
this.chartType = params.chartType;
}
super.setupConfig(widgetConfig);
}
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] {
return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }];
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: TimeSeriesChartWidgetSettings = mergeDeep<TimeSeriesChartWidgetSettings>({} as TimeSeriesChartWidgetSettings,
timeSeriesChartWidgetDefaultSettings, configData.config.settings as TimeSeriesChartWidgetSettings);
const iconSize = resolveCssSize(configData.config.iconSize);
this.timeSeriesChartWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
yAxes: [settings.yAxes, []],
series: [this.getSeries(configData.config.datasources), []],
thresholds: [settings.thresholds, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []],
xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
showLegend: [settings.showLegend, []],
legendLabelFont: [settings.legendLabelFont, []],
legendLabelColor: [settings.legendLabelColor, []],
legendConfig: [settings.legendConfig, []],
showTooltip: [settings.showTooltip, []],
tooltipTrigger: [settings.tooltipTrigger, []],
tooltipValueFont: [settings.tooltipValueFont, []],
tooltipValueColor: [settings.tooltipValueColor, []],
tooltipShowDate: [settings.tooltipShowDate, []],
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources);
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.thresholds = config.thresholds;
this.widgetConfig.config.settings.dataZoom = config.dataZoom;
this.widgetConfig.config.settings.stack = config.stack;
this.widgetConfig.config.settings.yAxes = config.yAxes;
this.widgetConfig.config.settings.xAxis = config.xAxis;
this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings;
this.widgetConfig.config.settings.showLegend = config.showLegend;
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont;
this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor;
this.widgetConfig.config.settings.legendConfig = config.legendConfig;
this.widgetConfig.config.settings.showTooltip = config.showTooltip;
this.widgetConfig.config.settings.tooltipTrigger = config.tooltipTrigger;
this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont;
this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor;
this.widgetConfig.config.settings.tooltipShowDate = config.tooltipShowDate;
this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat;
this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont;
this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor;
this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval;
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
this.widgetConfig.config.settings.animation = config.animation;
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 ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value;
const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value;
if (showTitle) {
this.timeSeriesChartWidgetConfigForm.get('title').enable();
this.timeSeriesChartWidgetConfigForm.get('titleFont').enable();
this.timeSeriesChartWidgetConfigForm.get('titleColor').enable();
this.timeSeriesChartWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.timeSeriesChartWidgetConfigForm.get('iconSize').enable();
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').enable();
this.timeSeriesChartWidgetConfigForm.get('icon').enable();
this.timeSeriesChartWidgetConfigForm.get('iconColor').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('iconSize').disable();
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable();
this.timeSeriesChartWidgetConfigForm.get('icon').disable();
this.timeSeriesChartWidgetConfigForm.get('iconColor').disable();
}
} else {
this.timeSeriesChartWidgetConfigForm.get('title').disable();
this.timeSeriesChartWidgetConfigForm.get('titleFont').disable();
this.timeSeriesChartWidgetConfigForm.get('titleColor').disable();
this.timeSeriesChartWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.timeSeriesChartWidgetConfigForm.get('iconSize').disable();
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable();
this.timeSeriesChartWidgetConfigForm.get('icon').disable();
this.timeSeriesChartWidgetConfigForm.get('iconColor').disable();
}
if (showLegend) {
this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').enable();
this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').enable();
this.timeSeriesChartWidgetConfigForm.get('legendConfig').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').disable();
this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').disable();
this.timeSeriesChartWidgetConfigForm.get('legendConfig').disable();
}
if (showTooltip) {
this.timeSeriesChartWidgetConfigForm.get('tooltipTrigger').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false});
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').enable();
if (tooltipShowDate) {
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable();
}
} else {
this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').disable({emitEvent: false});
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').disable();
}
}
private getSeries(datasources?: Datasource[]): DataKey[] {
if (datasources && datasources.length) {
return datasources[0].dataKeys || [];
}
return [];
}
private setSeries(series: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
datasources[0].dataKeys = series;
}
}
private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean {
let changed = false;
if (series) {
series.forEach(key => {
const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings);
if (keySettings.yAxisId === yAxisId) {
keySettings.yAxisId = 'default';
changed = true;
}
});
}
return changed;
}
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 _tooltipValuePreviewFn(): string {
return formatValue(22, 0, '°C', false);
}
private _tooltipDatePreviewFn(): string {
const dateFormat: DateFormatSettings = this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

170
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -22,126 +22,46 @@
<mat-option [value]="true">{{ 'datakey.latest' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList [formControl]="keysFormControl">
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type"
(removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels">
<div class="tb-chip-label tb-chip-text tb-chip-icon">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
</div>
<div class="tb-chip-label tb-chip-text">
<strong>
<ng-container *ngTemplateOutlet="keyName"></ng-container>
</strong>
</div>
</div>
<button type="button"
(click)="editKey()" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">edit</mat-icon>
</button>
<button matChipRemove
type="button"
mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">close</mat-icon>
</button>
</div>
</mat-chip-row>
<input matInput
type="text"
placeholder="{{ 'widget-config.set' | translate }}"
#keyInput
[formControl]="keyFormControl"
matAutocompleteOrigin
[fxHide]="!!modelValue?.type"
[readonly]="!!modelValue?.type"
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
(focusin)="onKeyInputFocus()"
(drop)="$event.preventDefault();"
[matAutocomplete]="keyAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addKey($event)"
/>
</mat-chip-grid>
<mat-autocomplete #keyAutocomplete="matAutocomplete"
class="tb-autocomplete"
panelWidth="fit-content"
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
<span [innerHTML]="key.name | highlight:keySearchText"></span>
</span>
</mat-option>
<mat-option *ngIf="!(filteredKeys | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(keySearchText); else searchNotEmpty">
<span translate>entity.no-keys-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(keySearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(keySearchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>
<span>{{'entity.create-new-key' | translate }} </span>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above"
(click)="createKey(keySearchText, dataKeyTypes.alarm)">notifications</mat-icon>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"
(click)="createKey(keySearchText, dataKeyTypes.attribute)"></mat-icon>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"
(click)="createKey(keySearchText, dataKeyTypes.entityField)"></mat-icon>
<mat-icon class="tb-datakey-icon new-key"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above"
(click)="createKey(keySearchText, dataKeyTypes.timeseries)">timeline</mat-icon>
</ng-template>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<tb-data-key-input
[required]="required"
[widgetType]="widgetType"
[datasourceType]="datasourceType"
[entityAliasId]="entityAliasId"
[deviceId]="deviceId"
[dataKeyType]="dataKeyType"
[isLatestDataKeys]="isLatestDataKeys"
[callbacks]="callbacks"
[generateKey]="generateDataKey"
(keyEdit)="editKey()"
[formControl]="keyFormControl">
</tb-data-key-input>
<mat-form-field *ngIf="!hideDataKeyLabel" class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div *ngIf="timeSeriesChart && showTimeSeriesType" class="tb-time-series-type-field">
<mat-form-field *ngIf="displayUnitsOrDigits" class="tb-inline-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeSeriesType" panelClass="tb-time-series-type-field">
<mat-select-trigger>
<tb-icon *ngIf="keyRowFormGroup.get('timeSeriesType').value">
{{ timeSeriesChartSeriesTypeIcons.get(keyRowFormGroup.get('timeSeriesType').value) }}
</tb-icon>
</mat-select-trigger>
<mat-option *ngFor="let type of timeSeriesChartSeriesTypes" [value]="type" class="flex">
<tb-icon>{{ timeSeriesChartSeriesTypeIcons.get(type) }}</tb-icon>
<span>{{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="timeSeriesChart" class="tb-y-axis-field">
<mat-form-field *ngIf="displayUnitsOrDigits" class="tb-inline-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="yAxis">
<mat-option *ngFor="let yAxis of yAxisIds" [value]="yAxis">
{{ yAxis }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!hideDataKeyColor" class="tb-color-field">
<tb-color-input asBoxInput
formControlName="color">
@ -178,19 +98,3 @@
</button>
</div>
</div>
<ng-template #keyName>
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName">
<span>f(</span><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><span>)</span>
</ng-container>
<ng-template #keyName>
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container>
</ng-template>
</ng-template>
<ng-template #keyNameTemplate>
<ng-container *ngIf="dataKeyHasAggregation(); else keyName;">
<span class="tb-agg-func">{{ modelValue?.aggregationType }}</span><span>({{ modelValue?.name }})</span>
</ng-container>
<ng-template #keyName>
<span>{{modelValue?.name}}</span>
</ng-template>
</ng-template>

69
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -16,37 +16,13 @@
@import '../../../../../../../../scss/constants';
.tb-data-key-row {
.mat-mdc-form-field.tb-inline-field.tb-key-field {
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
padding-left: 8px;
padding-right: 0;
.mat-mdc-form-field-infix {
padding-top: 0;
padding-bottom: 6px;
.mdc-evolution-chip-set .mdc-evolution-chip {
margin: 0;
}
input.mat-mdc-chip-input {
height: 32px;
margin-left: 0;
}
}
}
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip {
.tb-attribute-chip {
.tb-chip-labels {
background: transparent;
}
}
}
}
.tb-source-field {
width: 108px;
min-width: 108px;
}
.tb-key-field {
.tb-data-key-input {
flex: 1;
min-width: 150px;
@media #{$mat-gt-sm} {
@ -59,7 +35,7 @@
min-width: 100px;
}
.tb-color-field, .tb-units-field, .tb-decimals-field {
.tb-color-field, .tb-units-field, .tb-decimals-field, .tb-time-series-type-field {
display: flex;
flex-direction: row;
place-content: center;
@ -81,10 +57,41 @@
min-width: 60px;
}
.tb-y-axis-field {
width: 100px;
min-width: 100px;
.mat-mdc-form-field {
width: 100%;
}
}
.tb-time-series-type-field {
width: 60px;
min-width: 60px;
.mat-icon {
color: rgba(0,0,0,0.76);
}
}
.tb-label-field, .tb-units-field, .tb-color-field, .tb-decimals-field {
display: none;
@media #{$mat-gt-sm} {
display: block;
display: flex;
}
}
.tb-y-axis-field {
@media #{$mat-md} {
display: none;
}
}
.tb-time-series-type-field {
@media #{$mat-xs} {
display: none;
}
@media #{$mat-md} {
display: none;
}
}
@ -113,3 +120,11 @@
}
}
}
.tb-time-series-type-field {
.mat-mdc-option:not(.mdc-list-item--selected) {
.mat-icon {
color: rgba(0,0,0,0.54);
}
}
}

284
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -17,15 +17,11 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnChanges,
Input, OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
Output, SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {
@ -49,15 +45,8 @@ import {
widgetType
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { merge } from 'rxjs';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
@ -66,8 +55,13 @@ import { deepClone } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import {
TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypeIcons,
timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name;
@ -96,15 +90,9 @@ export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors
})
export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges {
dataKeyTypes = DataKeyType;
widgetTypes = widgetType;
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
@ViewChild('keyInput') keyInput: ElementRef<HTMLInputElement>;
@ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
@ViewChild('chipList') chipList: MatChipGrid;
timeSeriesChartSeriesTypes = timeSeriesChartSeriesTypes;
timeSeriesChartSeriesTypeTranslations = timeSeriesChartSeriesTypeTranslations;
timeSeriesChartSeriesTypeIcons = timeSeriesChartSeriesTypeIcons;
@Input()
disabled: boolean;
@ -150,6 +138,17 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@coerceBoolean()
hideDecimals = false;
@Input()
@coerceBoolean()
timeSeriesChart = false;
@Input()
@coerceBoolean()
showTimeSeriesType = false;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
@Input()
@coerceBoolean()
singleRow = true;
@ -166,23 +165,13 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
@Output()
keyRemoved = new EventEmitter();
keysFormControl: UntypedFormControl;
keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup;
modelValue: DataKey;
filteredKeys: Observable<Array<DataKey>>;
keySearchText = '';
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
private latestKeySearchTextResult: Array<DataKey> = null;
private keyFetchObservable$: Observable<Array<DataKey>> = null;
generateDataKey = this._generateDataKey.bind(this);
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
@ -220,6 +209,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective;
}
get dataKeySettingsFunction(): DataKeySettingsFunction {
return this.widgetConfigComponent.modelValue?.dataKeySettingsFunction;
}
get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
}
@ -237,29 +230,11 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef,
public translate: TranslateService,
public truncate: TruncatePipe,
private utils: UtilsService,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({
name,
type: DataKeyType.alarm
});
}
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
this.keyFormControl = this.fb.control('');
this.keysFormControl = this.fb.control([], this.required ? [Validators.required] : []);
this.keyFormControl = this.fb.control(null, this.required ? [Validators.required] : []);
this.keyRowFormGroup = this.fb.group({
label: [null, []],
color: [null, []],
@ -268,52 +243,24 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
});
if (this.hasAdditionalLatestDataKeys) {
this.keyRowFormGroup.addControl('latest', this.fb.control(false));
this.keyRowFormGroup.valueChanges.subscribe(
() => this.clearKeySearchCache()
);
}
this.keyRowFormGroup.valueChanges.subscribe(
if (this.timeSeriesChart) {
this.keyRowFormGroup.addControl('yAxis', this.fb.control(null));
this.keyRowFormGroup.addControl('timeSeriesType', this.fb.control(null));
}
merge(this.keyFormControl.valueChanges, this.keyRowFormGroup.valueChanges).subscribe(
() => this.updateModel()
);
this.filteredKeys = this.keyFormControl.valueChanges
.pipe(
tap((value: string | DataKey) => {
if (value && typeof value !== 'string') {
this.addKeyFromChipValue(value);
} else if (value === null) {
this.clearKeyChip(this.keyInput.nativeElement.value);
}
}),
filter((value) => typeof value === 'string'),
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchKeys(name) ),
share()
);
}
private reset() {
if (this.keyInput) {
this.keyInput.nativeElement.value = '';
}
this.keyFormControl.patchValue('', {emitEvent: false});
this.latestKeySearchTextResult = null;
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['deviceId', 'entityAliasId'].includes(propName)) {
this.clearKeySearchCache();
} else if (['datasourceType'].includes(propName)) {
if ([DatasourceType.device, DatasourceType.entity].includes(change.previousValue) &&
[DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) {
this.clearKeySearchCache();
} else {
this.clearKeySearchCache();
setTimeout(() => {
this.reset();
}, 1);
if (this.timeSeriesChart && ['yAxisIds'].includes(propName)) {
if (this.modelValue?.settings?.yAxisId &&
!this.yAxisIds.includes(this.modelValue.settings.yAxisId)) {
this.keyRowFormGroup.patchValue({yAxis: 'default'}, {emitEvent: true});
}
}
}
@ -330,10 +277,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keysFormControl.disable({emitEvent: false});
this.keyFormControl.disable({emitEvent: false});
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keysFormControl.enable({emitEvent: false});
this.keyFormControl.enable({emitEvent: false});
this.keyRowFormGroup.enable({emitEvent: false});
}
}
@ -353,34 +300,17 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
latest: (value as any)?.latest
}, {emitEvent: false});
}
this.keysFormControl.patchValue(this.modelValue ? [this.modelValue] : [], {emitEvent: false});
this.cd.markForCheck();
}
dataKeyHasAggregation(): boolean {
return this.widgetConfigComponent.widgetType === widgetType.latest && this.modelValue?.type === DataKeyType.timeseries
&& this.modelValue?.aggregationType && this.modelValue?.aggregationType !== AggregationType.NONE;
}
dataKeyHasPostprocessing(): boolean {
return !!this.modelValue?.postFuncBody;
}
displayKeyFn(key?: DataKey): string | undefined {
return key ? key.name : undefined;
}
createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) {
this.addKeyFromChipValue({name: name ? name.trim() : '', type: dataKeyType});
}
addKey(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim() && this.dataKeyType) {
this.addKeyFromChipValue({name: value.trim(), type: this.dataKeyType});
} else {
this.clearKeyChip();
if (this.timeSeriesChart) {
const settings = value?.settings as TimeSeriesChartKeySettings;
const yAxis = settings?.yAxisId || 'default';
const timeSeriesType = settings?.type || TimeSeriesChartSeriesType.line;
this.keyRowFormGroup.patchValue({
yAxis,
timeSeriesType
}, {emitEvent: false});
}
this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false});
this.cd.markForCheck();
}
editKey(advanced = false) {
@ -413,109 +343,41 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false});
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
if (this.timeSeriesChart) {
this.keyRowFormGroup.get('yAxis').patchValue(this.modelValue.settings?.yAxisId, {emitEvent: false});
this.keyRowFormGroup.get('timeSeriesType').patchValue(this.modelValue.settings?.type, {emitEvent: false});
}
this.keyFormControl.patchValue(deepClone(this.modelValue), {emitEvent: false});
this.updateModel();
this.cd.markForCheck();
}
});
}
removeKey() {
this.modelValue = null;
this.updateModel();
this.clearKeyChip();
}
textIsNotEmpty(text: string): boolean {
return text && text.length > 0;
}
clearKeyChip(value: string = '', focus = true) {
this.autocomplete.closePanel();
this.keyInput.nativeElement.value = value;
this.keyFormControl.patchValue(value, {emitEvent: focus});
if (focus) {
setTimeout(() => {
this.keyInput.nativeElement.blur();
this.keyInput.nativeElement.focus();
}, 0);
}
}
onKeyInputFocus() {
if (!this.modelValue?.type) {
this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
}
}
private fetchKeys(searchText?: string): Observable<Array<DataKey>> {
if (this.keySearchText !== searchText || this.latestKeySearchTextResult === null) {
this.keySearchText = searchText;
const dataKeyFilter = this.createDataKeyFilter(this.keySearchText);
return this.getKeys().pipe(
map(name => name.filter(dataKeyFilter)),
tap(res => this.latestKeySearchTextResult = res)
);
}
return of(this.latestKeySearchTextResult);
}
private getKeys(): Observable<Array<DataKey>> {
if (this.keyFetchObservable$ === null) {
let fetchObservable: Observable<Array<DataKey>>;
if (this.datasourceType === DatasourceType.function) {
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList);
} else if (this.datasourceType === DatasourceType.entity && this.entityAliasId ||
this.datasourceType === DatasourceType.device && this.deviceId) {
const dataKeyTypes = [DataKeyType.timeseries];
if (this.isLatestDataKeys || this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute);
dataKeyTypes.push(DataKeyType.entityField);
if (this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.alarm);
}
}
if (this.datasourceType === DatasourceType.device) {
fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes);
} else {
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
}
} else {
fetchObservable = of([]);
}
this.keyFetchObservable$ = fetchObservable.pipe(
publishReplay(1),
refCount()
);
}
return this.keyFetchObservable$;
}
private createDataKeyFilter(query: string): (key: DataKey) => boolean {
const lowercaseQuery = query.toLowerCase();
return key => key.name.toLowerCase().startsWith(lowercaseQuery);
}
private addKeyFromChipValue(chip: DataKey) {
this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsSchema);
private _generateDataKey(key: DataKey): DataKey {
key = this.callbacks.generateDataKey(key.name, key.type, this.dataKeySettingsSchema, this.isLatestDataKeys,
this.dataKeySettingsFunction);
if (!this.keyRowFormGroup.get('label').value) {
this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false});
this.keyRowFormGroup.get('label').patchValue(key.label, {emitEvent: false});
}
this.updateModel();
this.clearKeyChip('', false);
}
private clearKeySearchCache() {
this.keySearchText = '';
this.keyFetchObservable$ = null;
this.latestKeySearchTextResult = null;
if (this.timeSeriesChart) {
this.keyRowFormGroup.get('yAxis').patchValue(key.settings?.yAxisId, {emitEvent: false});
this.keyRowFormGroup.get('timeSeriesType').patchValue(key.settings?.type, {emitEvent: false});
}
return key;
}
private updateModel() {
this.keysFormControl.patchValue(this.modelValue ? [this.modelValue] : [], {emitEvent: false});
this.modelValue = this.keyFormControl.value;
if (this.modelValue !== null) {
const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
if (this.timeSeriesChart) {
this.modelValue.settings.yAxisId = (this.modelValue as any).yAxis;
this.modelValue.settings.type = (this.modelValue as any).timeSeriesType;
delete (this.modelValue as any).yAxis;
delete (this.modelValue as any).timeSeriesType;
}
}
this.propagateChange(this.modelValue);
}

7
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -15,13 +15,15 @@
limitations under the License.
-->
<div class="tb-form-panel">
<div class="tb-form-panel tb-data-keys-panel">
<div class="tb-form-panel-title">{{ panelTitle }}</div>
<div class="tb-form-table">
<div class="tb-form-table-header no-padding-right">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-form-table-header-cell tb-key-header" translate>datakey.key</div>
<div class="tb-form-table-header-cell tb-label-header" translate>datakey.label</div>
<div *ngIf="timeSeriesChart && showTimeSeriesType" class="tb-form-table-header-cell tb-time-series-type-header" translate>widgets.time-series-chart.series.type</div>
<div *ngIf="timeSeriesChart" class="tb-form-table-header-cell tb-y-axis-header" translate>widgets.time-series-chart.axis.y-axis</div>
<div *ngIf="!hideDataKeyColor" class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div>
<div *ngIf="!hideUnits" class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div *ngIf="!hideDecimals" class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
@ -43,6 +45,9 @@
[hasAdditionalLatestDataKeys]="hasAdditionalLatestDataKeys"
[hideDataKeyColor]="hideDataKeyColor"
[hideDecimals]="hideDecimals"
[timeSeriesChart]="timeSeriesChart"
[showTimeSeriesType]="showTimeSeriesType"
[yAxisIds]="yAxisIds"
[hideUnits]="hideUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals"
[hideDataKeyUnits]="hideDataKeyUnits"

118
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss

@ -15,52 +15,86 @@
*/
@import '../../../../../../../../scss/constants';
.tb-form-table-header-cell {
&.tb-source-header {
width: 108px;
min-width: 108px;
}
&.tb-key-header {
flex: 1;
min-width: 150px;
@media #{$mat-gt-sm} {
flex: 1 1 60%;
.tb-data-keys-panel {
.tb-form-table-header-cell {
&.tb-source-header {
width: 108px;
min-width: 108px;
}
}
&.tb-label-header {
flex: 1 1 40%;
min-width: 100px;
}
&.tb-units-header {
width: 80px;
min-width: 80px;
}
&.tb-color-header {
width: 40px;
min-width: 40px;
}
&.tb-decimals-header {
width: 60px;
min-width: 60px;
}
&.tb-actions-header {
width: 40px;
min-width: 40px;
@media #{$mat-gt-md} {
width: 120px;
min-width: 120px;
&.tb-key-header {
flex: 1;
min-width: 150px;
@media #{$mat-gt-sm} {
flex: 1 1 60%;
}
}
}
&.tb-label-header, &.tb-units-header, &.tb-color-header, &.tb-decimals-header {
display: none;
@media #{$mat-gt-sm} {
display: block;
&.tb-label-header {
flex: 1 1 40%;
min-width: 100px;
}
&.tb-units-header {
width: 80px;
min-width: 80px;
}
&.tb-color-header {
width: 40px;
min-width: 40px;
}
&.tb-decimals-header {
width: 60px;
min-width: 60px;
}
&.tb-y-axis-header {
width: 100px;
min-width: 100px;
}
&.tb-time-series-type-header {
width: 60px;
min-width: 60px;
}
&.tb-actions-header {
width: 40px;
min-width: 40px;
@media #{$mat-gt-md} {
width: 120px;
min-width: 120px;
}
}
&.tb-label-header, &.tb-units-header, &.tb-color-header, &.tb-decimals-header {
display: none;
@media #{$mat-gt-sm} {
display: block;
}
}
&.tb-y-axis-header {
@media #{$mat-md} {
display: none;
}
}
&.tb-time-series-type-header {
@media #{$mat-xs} {
display: none;
}
@media #{$mat-md} {
display: none;
}
}
}
}
.tb-form-table-body {
tb-data-key-row {
overflow: hidden;
.tb-form-table-body {
tb-data-key-row {
overflow: hidden;
}
}
}

21
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -42,8 +42,9 @@ import { dataKeyRowValidator, dataKeyValid } from '@home/components/widget/confi
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models';
@Component({
selector: 'tb-data-keys-panel',
@ -119,6 +120,17 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean()
hideSourceSelection = false;
@Input()
@coerceBoolean()
timeSeriesChart = false;
@Input()
@coerceBoolean()
showTimeSeriesType = false;
@Input()
yAxisIds: TimeSeriesChartYAxisId[];
dataKeyType: DataKeyType;
keysListFormGroup: UntypedFormGroup;
@ -142,6 +154,10 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
get dataKeySettingsFunction(): DataKeySettingsFunction {
return this.widgetConfigComponent.modelValue?.dataKeySettingsFunction;
}
get dragEnabled(): boolean {
return this.keysFormArray().controls.length > 1;
}
@ -256,7 +272,8 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
}
addKey() {
const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema);
const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema,
false, this.dataKeySettingsFunction);
dataKey.label = '';
dataKey.decimals = 0;
if (this.hasAdditionalLatestDataKeys) {

2
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -196,7 +196,9 @@
<tb-widget-settings
[dashboard]="dashboard"
[aliasController]="aliasController"
[dataKeyCallbacks]="callbacks"
[widget]="widget"
[widgetConfig]="widgetConfig"
formControlName="settings">
</tb-widget-settings>
</div>

27
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -40,7 +40,7 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of } from 'rxjs';
import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
@ -51,8 +51,10 @@ import { WidgetService } from '@core/http/widget.service';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models';
import { genNextLabel } from '@core/utils';
import { genNextLabel, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
@Component({
selector: 'tb-data-key-config',
@ -155,6 +157,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
modelValue: DataKey;
widgetConfig: WidgetConfigComponentData;
private propagateChange = null;
public dataKeyFormGroup: UntypedFormGroup;
@ -180,12 +184,31 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
private dialog: MatDialog,
private translate: TranslateService,
private widgetService: WidgetService,
private widgetComponentService: WidgetComponentService,
private fb: UntypedFormBuilder) {
super(store);
this.functionScopeVariables = this.widgetService.getWidgetScopeVariables();
}
ngOnInit(): void {
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
const typeParameters = widgetInfo.typeParameters;
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
widgetType: this.widget.type,
typeParameters,
dataKeySettingsFunction,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
hasBasicMode: isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false,
basicModeDirective: widgetInfo.basicModeDirective
} as WidgetConfigComponentData;
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({

5
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts

@ -18,8 +18,11 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, JsonSettingsSchema } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
export type DataKeySettingsFunction = (key: DataKey, isLatestDataKey: boolean) => any;
export interface DataKeysCallbacks {
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey;
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema,
isLatestDataKey: boolean, dataKeySettingsFunction: DataKeySettingsFunction) => DataKey;
fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
fetchEntityKeysForDevice: (deviceId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
}

15
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts

@ -52,7 +52,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { ErrorStateMatcher } from '@angular/material/core';
@ -139,6 +139,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input()
optDataKeys: boolean;
@Input()
@coerceBoolean()
latestDataKeys = false;
@Input()
@coerceBoolean()
simpleDataKeysLabel = false;
@ -149,6 +153,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input()
datakeySettingsSchema: JsonSettingsSchema;
@Input()
datakeySettingsFunction: DataKeySettingsFunction;
@Input()
dataKeySettingsDirective: string;
@ -361,7 +368,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
if (this.widgetType === widgetType.alarm) {
this.keys = this.utils.getDefaultAlarmDataKeys();
} else if (this.isCountDatasource) {
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)];
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema,
this.latestDataKeys, this.datakeySettingsFunction)];
} else {
this.keys = [];
}
@ -447,7 +455,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
private addFromChipValue(chip: DataKey) {
const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema);
const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema, this.latestDataKeys,
this.datakeySettingsFunction);
this.addKey(key);
}

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

@ -72,6 +72,7 @@
[aliasController]="aliasController"
[datakeySettingsSchema]="dataKeySettingsSchema"
[dataKeySettingsDirective]="dataKeySettingsDirective"
[datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="dataKeysCallbacks"
@ -82,10 +83,12 @@
<tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex
[widgetType]="widgetTypes.latest"
[datasourceType]="datasourceFormGroup.get('type').value"
latestDataKeys
[optDataKeys]="true"
[aliasController]="aliasController"
[datakeySettingsSchema]="latestDataKeySettingsSchema"
[dataKeySettingsDirective]="latestDataKeySettingsDirective"
[datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="dataKeysCallbacks"

6
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -39,7 +39,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
@ -119,6 +119,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective;
}
public get dataKeySettingsFunction(): DataKeySettingsFunction {
return this.widgetConfigComponent.modelValue?.dataKeySettingsFunction;
}
public get dashboard(): Dashboard {
return this.widgetConfigComponent.dashboard;
}

9
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

@ -39,7 +39,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { deepClone } from '@core/utils';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
@ -337,7 +337,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
let newDatasource: Datasource;
if (this.widgetConfigComponent.functionsOnly) {
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)];
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema,
false, this.dataKeySettingsFunction)];
} else {
const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity;
newDatasource = { type,
@ -354,6 +355,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
private get dataKeySettingsFunction(): DataKeySettingsFunction {
return this.widgetConfigComponent.modelValue?.dataKeySettingsFunction;
}
private get dataKeysCallbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}

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

@ -23,9 +23,8 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode, widgetType } from '@shared/models/widget.models';
import { DataKey, DatasourceType, WidgetConfigMode, widgetType } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models';
@ -187,22 +186,23 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
if (keys && keys.length) {
dataKeys.length = 0;
keys.forEach(key => {
const dataKey = this.constructDataKey(configData, key);
const dataKey = this.constructDataKey(configData, key, false);
dataKeys.push(dataKey);
});
}
if (latestKeys && latestKeys.length) {
latestDataKeys.length = 0;
latestKeys.forEach(key => {
const dataKey = this.constructDataKey(configData, key);
const dataKey = this.constructDataKey(configData, key, true);
latestDataKeys.push(dataKey);
});
}
}
protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey): DataKey {
protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey, isLatestKey: boolean): DataKey {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema);
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type,
configData.dataKeySettingsSchema, isLatestKey, configData.dataKeySettingsFunction);
if (key.label) {
dataKey.label = key.label;
}

12
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts

@ -49,6 +49,7 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
@Component({
selector: 'tb-widget-settings',
@ -77,6 +78,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
@Input()
aliasController: IAliasController;
@Input()
dataKeyCallbacks: DataKeysCallbacks;
@Input()
dashboard: Dashboard;
@ -98,7 +102,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
private definedSettingsComponent: IWidgetSettingsComponent;
private widgetSettingsFormData: JsonFormComponentData;
private propagateChange = (v: any) => { };
private propagateChange = (_v: any) => { };
constructor(private translate: TranslateService,
private cfr: ComponentFactoryResolver,
@ -138,6 +142,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponent.aliasController = this.aliasController;
}
}
if (propName === 'dataKeyCallbacks') {
if (this.definedSettingsComponent) {
this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks;
}
}
if (propName === 'widgetConfig') {
if (this.definedSettingsComponent) {
this.definedSettingsComponent.widgetConfig = this.widgetConfig;
@ -225,6 +234,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, On
this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory);
this.definedSettingsComponent = this.definedSettingsComponentRef.instance;
this.definedSettingsComponent.aliasController = this.aliasController;
this.definedSettingsComponent.dataKeyCallbacks = this.dataKeyCallbacks;
this.definedSettingsComponent.dashboard = this.dashboard;
this.definedSettingsComponent.widget = this.widget;
this.definedSettingsComponent.widgetConfig = this.widgetConfig;

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

@ -92,6 +92,7 @@
.tb-aggregated-value-card-chart-container {
position: relative;
flex: 1;
min-width: 0;
margin-top: 8px;
margin-bottom: 8px;
.tb-aggregated-value-card-chart-element {

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

@ -37,6 +37,7 @@ import {
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import {
autoDateFormat,
backgroundStyle,
ComponentStyle,
DateFormatProcessor,
@ -45,14 +46,19 @@ import {
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models';
import { DataKey } from '@shared/models/widget.models';
import { formatNumberValue, formatValue, isDefined, isDefinedAndNotNull, isNumeric } from '@core/utils';
import { map } from 'rxjs/operators';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import {
TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType,
TimeSeriesChartSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { DeepPartial } from '@shared/models/common';
const valuesLayoutHeight = 66;
const valuesLayoutVerticalPadding = 16;
@ -101,8 +107,8 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
private flot: TbFlot;
private flotDataKey: DataKey;
private lineChart: TbTimeSeriesChart;
private lineChartDataKey: DataKey;
private lastUpdateTs: number;
@ -141,12 +147,18 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
this.showChart = this.settings.showChart;
if (this.showChart) {
if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) {
this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0];
this.flotDataKey.settings = {
fillLines: false,
showLines: true,
lineWidth: 2
} as TbFlotKeySettings;
this.lineChartDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0];
this.lineChartDataKey.settings = {
type: TimeSeriesChartSeriesType.line,
lineSettings: {
showLine: true,
step: false,
smooth: false,
lineWidth: 2,
showPoints: false,
showPointLabel: false
}
} as TimeSeriesChartKeySettings;
}
}
@ -161,33 +173,35 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
ngAfterViewInit(): void {
if (this.showChart && this.ctx.datasources?.length) {
const settings = {
shadowSize: 0,
enableSelection: false,
smoothLines: false,
grid: {
tickColor: 'rgba(0,0,0,0.12)',
horizontalLines: true,
verticalLines: false,
outlineWidth: 0,
minBorderMargin: 0,
margin: 0
},
yaxis: {
showLabels: false,
tickGenerator: 'return [(axis.max + axis.min) / 2];'
},
xaxis: {
showLabels: false
}
} as TbFlotSettings;
this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings);
this.tickMin$ = this.flot.yMin$.pipe(
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals))
const settings: DeepPartial<TimeSeriesChartSettings> = {
dataZoom: false,
xAxis: {
show: false
},
yAxes: {
default: {
show: true,
showLine: false,
showTicks: false,
showTickLabels: false,
showSplitLines: true,
min: 'dataMin',
max: 'dataMax',
ticksGenerator: (extent?: number[]) => (extent ? [{ value: (extent[0] + extent[1]) / 2}] : [])
}
},
tooltipDateInterval: false,
tooltipDateFormat: autoDateFormat()
};
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true);
this.tickMin$ =this.lineChart.yMin$.pipe(
map((value) => formatValue(value, (this.lineChartDataKey?.decimals || this.ctx.decimals))
));
this.tickMax$ = this.lineChart.yMax$.pipe(
map((value) => formatValue(value, (this.lineChartDataKey?.decimals || this.ctx.decimals))
));
this.tickMax$ = this.flot.yMax$.pipe(
map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals))
));
}
if (this.settings.autoScale && this.showValues) {
this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'height', valuesLayoutHeight + 'px');
@ -221,7 +235,7 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
}
if (this.showChart) {
this.flot.update();
this.lineChart.update();
}
this.updateLastUpdateTs(ts);
@ -256,20 +270,14 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
}
public onResize() {
if (this.showChart) {
this.flot.resize();
}
}
public onEditModeChanged() {
if (this.showChart) {
this.flot.checkMouseEvents();
}
}
public onDestroy() {
if (this.showChart) {
this.flot.destroy();
this.lineChart.destroy();
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html

@ -18,8 +18,10 @@
<div class="tb-value-chart-card-panel" [style]="backgroundStyle$ | async">
<div class="tb-value-chart-card-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #valueChartCardContent class="tb-value-chart-card-content" [class]="layout">
<div *ngIf="showValue" class="tb-value-chart-card-value" #valueChartCardValue [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
<div #valueChartCardContent class="tb-value-chart-card-content" [class]="layout" [class.auto-scale]="autoScale && showValue">
<div *ngIf="showValue" class="tb-value-chart-card-value-container">
<div class="tb-value-chart-card-value" #valueChartCardValue [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</div>
<div class="tb-value-chart-card-chart" #chartElement></div>
</div>
</div>

11
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss

@ -52,7 +52,18 @@
}
.tb-value-chart-card-chart {
flex: 1;
min-width: 0;
height: 100%;
}
&.auto-scale {
.tb-value-chart-card-value-container {
width: 35%;
display: flex;
justify-content: flex-start;
}
.tb-value-chart-card-chart {
width: 65%;
}
}
}
}

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

@ -30,11 +30,14 @@ import {
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import {
autoDateFormat,
backgroundStyle,
ColorProcessor,
ComponentStyle,
getDataKey,
overlayStyle, resolveCssSize,
overlayStyle,
resolveCssSize,
simpleDateFormat,
textStyle
} from '@shared/models/widget-settings.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
@ -44,16 +47,21 @@ import {
ValueChartCardLayout,
ValueChartCardWidgetSettings
} from '@home/components/widget/lib/cards/value-chart-card-widget.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { DataKey } from '@shared/models/widget.models';
import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models';
import { getTsValueByLatestDataKey } from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import {
TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType,
TimeSeriesChartSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { DeepPartial } from '@shared/models/common';
const layoutHeight = 56;
const valueRelativeMaxWidth = 0.5;
const valueRelativeWidth = 0.35;
@Component({
selector: 'tb-value-chart-card-widget',
@ -81,6 +89,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
widgetTitlePanel: TemplateRef<any>;
layout: ValueChartCardLayout;
autoScale: boolean;
showValue = true;
valueText = 'N/A';
@ -90,8 +99,8 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
private flot: TbFlot;
private flotDataKey: DataKey;
private lineChart: TbTimeSeriesChart;
private lineChartDataKey: DataKey;
private valueKey: DataKey;
private contentResize$: ResizeObserver;
@ -129,6 +138,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
}
this.layout = this.settings.layout;
this.autoScale = this.settings.autoScale;
this.showValue = this.settings.showValue;
this.valueStyle = textStyle(this.settings.valueFont);
@ -138,37 +148,37 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
this.overlayStyle = overlayStyle(this.settings.background.overlay);
if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) {
this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0];
this.flotDataKey.settings = {
fillLines: false,
showLines: true,
lineWidth: 2
} as TbFlotKeySettings;
this.lineChartDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0];
this.lineChartDataKey.settings = {
type: TimeSeriesChartSeriesType.line,
lineSettings: {
showLine: true,
step: false,
smooth: true,
lineWidth: 2,
showPoints: false,
showPointLabel: false
}
} as TimeSeriesChartKeySettings;
}
}
public ngAfterViewInit() {
const settings = {
shadowSize: 0,
enableSelection: false,
smoothLines: true,
grid: {
tickColor: 'rgba(0,0,0,0.12)',
horizontalLines: false,
verticalLines: false,
outlineWidth: 0,
minBorderMargin: 0,
margin: 0
const settings: DeepPartial<TimeSeriesChartSettings> = {
dataZoom: false,
xAxis: {
show: false
},
yaxis: {
showLabels: false,
tickGenerator: 'return [(axis.max + axis.min) / 2];'
yAxes: {
default: {
show: false,
}
},
xaxis: {
showLabels: false
}
} as TbFlotSettings;
this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings);
tooltipDateInterval: false,
tooltipDateFormat: autoDateFormat()
};
this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false);
this.contentResize$ = new ResizeObserver(() => {
this.onResize();
@ -190,7 +200,9 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
}
public onDataUpdated() {
this.flot.update();
if (this.lineChart) {
this.lineChart.update();
}
}
public onLatestDataUpdated() {
@ -206,20 +218,21 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
this.valueColor.update(value);
this.cd.detectChanges();
setTimeout(() => {
this.onResize(false);
this.onResize();
}, 0);
}
}
public onEditModeChanged() {
this.flot.checkMouseEvents();
}
public onDestroy() {
this.flot.destroy();
if (this.lineChart) {
this.lineChart.destroy();
}
}
private onResize(fitTargetHeight = true) {
private onResize(fitTargetWidth = true) {
if (this.settings.autoScale && this.showValue) {
const contentWidth = this.valueChartCardContent.nativeElement.getBoundingClientRect().width;
const contentHeight = this.valueChartCardContent.nativeElement.getBoundingClientRect().height;
@ -228,26 +241,26 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD
this.valueFontSize = resolveCssSize(fontSize)[0];
}
const valueRelativeHeight = Math.min(this.valueFontSize / layoutHeight, 1);
const targetValueHeight = contentHeight * valueRelativeHeight;
const maxValueWidth = contentWidth * valueRelativeMaxWidth;
this.setValueFontSize(targetValueHeight, maxValueWidth, fitTargetHeight);
const targetValueWidth = contentWidth * valueRelativeWidth;
const maxValueHeight = contentHeight * valueRelativeHeight;
this.setValueFontSize(targetValueWidth, maxValueHeight, fitTargetWidth);
}
this.flot.resize();
this.lineChart.resize();
}
private setValueFontSize(targetHeight: number, maxWidth: number, fitTargetHeight = true) {
private setValueFontSize(targetWidth: number, maxHeight: number, fitTargetWidth = true) {
const fontSize = getComputedStyle(this.valueChartCardValue.nativeElement).fontSize;
let valueFontSize = resolveCssSize(fontSize)[0];
this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px');
this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'lineHeight', '1');
let valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height;
while (fitTargetHeight && valueHeight < targetHeight) {
let valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width;
while (fitTargetWidth && valueWidth < targetWidth) {
valueFontSize++;
this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px');
valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height;
valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width;
}
let valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width;
while ((valueHeight > targetHeight || valueWidth > maxWidth) && valueFontSize > 6) {
let valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height;
while ((valueWidth > targetWidth || valueHeight > maxHeight) && valueFontSize > 6) {
valueFontSize--;
this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px');
valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width;

21
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts

@ -37,7 +37,6 @@ import {
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { formatValue } from '@core/utils';
import { DataKey } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
@ -54,18 +53,13 @@ import {
ECharts,
echartsModule,
EChartsOption,
EChartsSeriesItem,
echartsTooltipFormatter,
NamedDataSet,
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { IntervalMath } from '@shared/models/time/time.models';
import { AggregationType, IntervalMath } from '@shared/models/time/time.models';
interface BarChartDataItem {
id: string;
dataKey: DataKey;
data: NamedDataSet;
enabled: boolean;
}
type BarChartDataItem = EChartsSeriesItem;
interface BarChartLegendItem {
id: string;
@ -103,6 +97,10 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
legendLabelStyle: ComponentStyle;
disabledLegendLabelStyle: ComponentStyle;
private get noAggregation(): boolean {
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE;
}
private shapeResize$: ResizeObserver;
private decimals = 0;
@ -386,7 +384,7 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
appendTo: 'body',
axisPointer: {
type: 'shadow'
},
@ -394,7 +392,8 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
if (this.settings.showTooltip) {
const focusedSeriesIndex = this.focusedSeriesIndex();
return echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.decimals, this.units, focusedSeriesIndex);
this.settings, params, this.decimals, this.units, focusedSeriesIndex, null,
this.noAggregation ? null : this.ctx.timeWindow.interval);
} else {
return undefined;
}

1
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts

@ -75,6 +75,7 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings
},
tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
tooltipShowDate: true,
tooltipDateInterval: true,
tooltipDateFormat: customDateFormat('MMMM y'),
tooltipDateFont: {
family: 'Roboto',

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts

@ -387,7 +387,7 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit
tooltip: {
trigger: this.settings.showTooltip ? 'item' : 'none',
confine: false,
appendToBody: true
appendTo: 'body',
},
series: [
{

221
ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts

@ -17,26 +17,34 @@
import * as echarts from 'echarts/core';
import { Axis } from 'echarts';
import AxisModel from 'echarts/types/src/coord/cartesian/AxisModel';
import { formatValue, isDefinedAndNotNull, isNumber, isString } from '@core/utils';
import { estimateLabelUnionRect } from 'echarts/lib/coord/axisHelper';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import TimeScale from 'echarts/types/src/scale/Time';
import {
DataZoomComponent, DataZoomComponentOption,
GridComponent, GridComponentOption,
MarkLineComponent, MarkLineComponentOption,
TooltipComponent, TooltipComponentOption,
VisualMapComponent, VisualMapComponentOption
DataZoomComponent,
DataZoomComponentOption,
GridComponent,
GridComponentOption,
MarkLineComponent,
MarkLineComponentOption,
TooltipComponent,
TooltipComponentOption,
VisualMapComponent,
VisualMapComponentOption
} from 'echarts/components';
import {
BarChart,
LineChart,
BarSeriesOption,
CustomChart,
CustomSeriesOption,
LineChart,
LineSeriesOption,
BarSeriesOption, PieSeriesOption, PieChart
PieChart,
PieSeriesOption
} from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer, SVGRenderer } from 'echarts/renderers';
import { DataEntry, DataSet } from '@shared/models/widget.models';
import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models';
import {
calculateAggIntervalWithWidgetTimeWindow,
Interval,
@ -46,6 +54,9 @@ import {
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { Renderer2 } from '@angular/core';
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models';
import GlobalModel from 'echarts/types/src/model/Global';
import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D';
import SeriesModel from 'echarts/types/src/model/Series';
class EChartsModule {
private initialized = false;
@ -139,6 +150,134 @@ export type EChartsDataItem = [number, any, number, number];
export type NamedDataSet = {name: string; value: EChartsDataItem}[];
export type EChartsSeriesItem = {
id: string;
dataKey: DataKey;
data: NamedDataSet;
dataSet?: DataSet;
enabled: boolean;
units?: string;
decimals?: number;
};
export const getXAxis = (chart: ECharts): Axis2D => {
const model: GlobalModel = (chart as any).getModel();
const models = model.queryComponents({mainType: 'xAxis'});
if (models?.length) {
const axisModel = models[0] as AxisModel;
return axisModel.axis;
}
return null;
};
export const getYAxis = (chart: ECharts, axisId: string): Axis2D => {
const model: GlobalModel = (chart as any).getModel();
const models = model.queryComponents({mainType: 'yAxis', id: axisId});
if (models?.length) {
const axisModel = models[0] as AxisModel;
return axisModel.axis;
}
return null;
};
export const calculateYAxisWidth = (chart: ECharts, axisId: string): number => {
const axis = getYAxis(chart, axisId);
return calculateAxisSize(axis);
};
export const calculateXAxisHeight = (chart: ECharts): number => {
const axis = getXAxis(chart);
return calculateAxisSize(axis);
};
const calculateAxisSize = (axis: Axis2D): number => {
let size = 0;
if (axis && axis.model.option.show) {
const labelUnionRect = estimateLabelUnionRect(axis);
if (labelUnionRect) {
const margin = axis.model.get(['axisLabel', 'margin']);
const dimension = axis.isHorizontal() ? 'height' : 'width';
size += labelUnionRect[dimension] + margin;
}
if (!axis.scale.isBlank() && axis.model.get(['axisTick', 'show'])) {
const tickLength = axis.model.get(['axisTick', 'length']);
size += tickLength;
}
}
return size;
};
export const measureYAxisNameWidth = (chart: ECharts, axisId: string, name: string): number => {
const axis = getYAxis(chart, axisId);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
export const measureXAxisNameHeight = (chart: ECharts, name: string): number => {
const axis = getXAxis(chart);
if (axis) {
return axis.model.getModel('nameTextStyle').getTextRect(name).height;
}
return 0;
};
export const measureThresholdLabelOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => {
const axis = getYAxis(chart, axisId);
if (axis && !axis.scale.isBlank()) {
const extent = axis.scale.getExtent();
const model: GlobalModel = (chart as any).getModel();
const models = model.queryComponents({mainType: 'series', id: thresholdId});
if (models?.length) {
const lineSeriesModel = models[0] as SeriesModel<LineSeriesOption>;
const markLineModel = lineSeriesModel.getModel('markLine');
const labelPosition = markLineModel.get(['label', 'position']);
if (labelPosition === 'start' || labelPosition === 'end') {
const labelModel = markLineModel.getModel('label');
const formatter = markLineModel.get(['label', 'formatter']);
let textWidth = 0;
if (Array.isArray(value)) {
for (const val of value) {
if (val >= extent[0] && val <= extent[1]) {
const textVal = typeof formatter === 'string' ? formatter : formatter({value: val} as CallbackDataParams);
textWidth = Math.max(textWidth, labelModel.getTextRect(textVal).width);
}
}
} else {
if (value >= extent[0] && value <= extent[1]) {
const textVal = typeof formatter === 'string' ? formatter : formatter({value} as CallbackDataParams);
textWidth = labelModel.getTextRect(textVal).width;
}
}
if (!textWidth) {
return [0,0];
}
const distanceOpt = markLineModel.get(['label', 'distance']);
let distance = 5;
if (distanceOpt) {
distance = typeof distanceOpt === 'number' ? distanceOpt : distanceOpt[0];
}
const offset = distance + textWidth;
if (labelPosition === 'start') {
return [offset, 0];
} else {
return [0, offset];
}
}
}
}
return [0,0];
};
export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => {
const axis = getYAxis(chart, axisId);
if (axis) {
return axis.scale.getExtent();
}
return [0,0];
};
export const toNamedData = (data: DataSet): NamedDataSet => {
if (!data?.length) {
return [];
@ -162,11 +301,25 @@ const toEChartsDataItem = (entry: DataEntry): EChartsDataItem => {
return item;
};
export enum EChartsTooltipTrigger {
point = 'point',
axis = 'axis'
}
export const tooltipTriggerTranslationMap = new Map<EChartsTooltipTrigger, string>(
[
[ EChartsTooltipTrigger.point, 'tooltip.trigger-point' ],
[ EChartsTooltipTrigger.axis, 'tooltip.trigger-axis' ]
]
);
export interface EChartsTooltipWidgetSettings {
showTooltip: boolean;
tooltipTrigger?: EChartsTooltipTrigger;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipShowDate: boolean;
tooltipDateInterval?: boolean;
tooltipDateFormat: DateFormatSettings;
tooltipDateFont: Font;
tooltipDateColor: string;
@ -177,11 +330,17 @@ export interface EChartsTooltipWidgetSettings {
export const echartsTooltipFormatter = (renderer: Renderer2,
tooltipDateFormat: DateFormatProcessor,
settings: EChartsTooltipWidgetSettings,
params: CallbackDataParams[],
params: CallbackDataParams[] | CallbackDataParams,
decimals: number,
units: string,
focusedSeriesIndex: number): null | HTMLElement => {
if (!params.length || !params[0]) {
focusedSeriesIndex: number,
series?: EChartsSeriesItem[],
interval?: Interval): null | HTMLElement => {
if (!params || Array.isArray(params) && !params[0]) {
return null;
}
const firstParam = Array.isArray(params) ? params[0] : params;
if (!firstParam.value) {
return null;
}
const tooltipElement: HTMLElement = renderer.createElement('div');
@ -192,19 +351,19 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
if (settings.tooltipShowDate) {
const dateElement: HTMLElement = renderer.createElement('div');
let dateText: string;
const startTs = params[0].value[2];
const endTs = params[0].value[3];
if (startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs);
const endDateText = tooltipDateFormat.update(endTs - 1);
const startTs = firstParam.value[2];
const endTs = firstParam.value[3];
if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) {
const startDateText = tooltipDateFormat.update(startTs, interval);
const endDateText = tooltipDateFormat.update(endTs - 1, interval);
if (startDateText === endDateText) {
dateText = startDateText;
} else {
dateText = startDateText + ' - ' + endDateText;
}
} else {
const ts = params[0].value[0];
dateText = tooltipDateFormat.update(ts);
const ts = firstParam.value[0];
dateText = tooltipDateFormat.update(ts, interval);
}
renderer.appendChild(dateElement, renderer.createText(dateText));
renderer.setStyle(dateElement, 'font-family', settings.tooltipDateFont.family);
@ -216,14 +375,16 @@ export const echartsTooltipFormatter = (renderer: Renderer2,
renderer.appendChild(tooltipElement, dateElement);
}
let seriesParams: CallbackDataParams = null;
if (focusedSeriesIndex > -1) {
if (Array.isArray(params) && focusedSeriesIndex > -1) {
seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex);
} else if (!Array.isArray(params)) {
seriesParams = params;
}
if (seriesParams) {
renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units));
} else {
renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series));
} else if (Array.isArray(params)) {
for (seriesParams of params) {
renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units));
renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series));
}
}
return tooltipElement;
@ -233,7 +394,8 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
settings: EChartsTooltipWidgetSettings,
seriesParams: CallbackDataParams,
decimals: number,
units: string): HTMLElement => {
units: string,
series?: EChartsSeriesItem[]): HTMLElement => {
const labelValueElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(labelValueElement, 'display', 'flex');
renderer.setStyle(labelValueElement, 'flex-direction', 'row');
@ -262,7 +424,16 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2,
renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)');
renderer.appendChild(labelElement, labelTextElement);
const valueElement: HTMLElement = renderer.createElement('div');
const value = formatValue(seriesParams.value[1], decimals, units, false);
let formatDecimals = decimals;
let formatUnits = units;
if (series) {
const item = series.find(s => s.id === seriesParams.seriesId);
if (item) {
formatDecimals = item.decimals;
formatUnits = item.units;
}
}
const value = formatValue(seriesParams.value[1], formatDecimals, formatUnits, false);
renderer.appendChild(valueElement, renderer.createText(value));
renderer.setStyle(valueElement, 'flex', '1');
renderer.setStyle(valueElement, 'text-align', 'end');

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

@ -54,6 +54,7 @@ import {
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { AggregationType } from '@shared/models/time/time.models';
interface VisualPiece {
lt?: number;
@ -197,6 +198,10 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
disabledLegendLabelStyle: ComponentStyle;
visibleRangeItems: RangeItem[];
private get noAggregation(): boolean {
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE;
}
private rangeItems: RangeItem[];
private shapeResize$: ResizeObserver;
@ -318,13 +323,14 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
appendTo: 'body',
axisPointer: {
type: 'shadow'
},
formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, this.decimals, this.units, 0) : undefined,
this.settings, params, this.decimals, this.units, 0, null,
this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined,
padding: [8, 12],
backgroundColor: this.settings.tooltipBackgroundColor,
borderWidth: 0,

1
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts

@ -71,6 +71,7 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = {
},
tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
tooltipShowDate: true,
tooltipDateInterval: true,
tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm'),
tooltipDateFont: {
family: 'Roboto',

166
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts

@ -0,0 +1,166 @@
///
/// 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 { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
import { Interval, IntervalMath } from '@shared/models/time/time.models';
import { LabelFormatterCallback, SeriesLabelOption } from 'echarts/types/src/util/types';
import {
TimeSeriesChartDataItem,
TimeSeriesChartNoAggregationBarWidthStrategy
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { CustomSeriesRenderItemParams } from 'echarts';
import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared';
import { isNumeric } from '@core/utils';
import * as echarts from 'echarts/core';
export interface BarVisualSettings {
color: string | LinearGradientObject;
borderColor: string;
borderWidth: number;
borderRadius: number;
}
export interface BarRenderSharedContext {
timeInterval: Interval;
noAggregationBarWidthStrategy: TimeSeriesChartNoAggregationBarWidthStrategy;
noAggregationWidthRelative: boolean;
noAggregationWidth: number;
}
export interface BarRenderContext {
shared: BarRenderSharedContext;
barsCount?: number;
barIndex?: number;
noAggregation?: boolean;
visualSettings?: BarVisualSettings;
labelOption?: SeriesLabelOption;
barStackIndex?: number;
currentStackItems?: TimeSeriesChartDataItem[];
}
export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI,
renderCtx: BarRenderContext): CustomSeriesRenderItemReturn => {
const time = api.value(0) as number;
let start = api.value(2) as number;
let end = api.value(3) as number;
let interval = end - start;
const ts = start ? start : time;
const separateBar = renderCtx.noAggregation &&
renderCtx.shared.noAggregationBarWidthStrategy === TimeSeriesChartNoAggregationBarWidthStrategy.separate;
if (renderCtx.noAggregation) {
if (renderCtx.shared.noAggregationWidthRelative) {
const scaleWidth = api.getWidth() / api.size([1,0])[0];
interval = scaleWidth * (renderCtx.shared.noAggregationWidth / 100);
} else {
interval = renderCtx.shared.noAggregationWidth;
}
start = time - interval / 2;
end = time + interval / 2;
}
if (!start || !end || !interval) {
interval = IntervalMath.numberValue(renderCtx.shared.timeInterval);
start = time - interval / 2;
}
const gap = 0.3;
const barInterval = separateBar ? interval : interval / (renderCtx.barsCount + gap * (renderCtx.barsCount + 3));
const intervalGap = barInterval * gap * 2;
const barGap = barInterval * gap;
const value = api.value(1);
const startTime = separateBar ? start : start + intervalGap + (barInterval + barGap) * renderCtx.barIndex;
const delta = barInterval;
let offset = 0;
if (renderCtx.currentStackItems?.length) {
for (let i = 0; i < renderCtx.barStackIndex; i++) {
const stackItem = renderCtx.currentStackItems[i];
const dataName = ts + '';
const data = stackItem.data.find(d => d.name === dataName);
if (data) {
const val = data.value[1];
if (isNumeric(val)) {
offset += Number(val);
}
}
}
}
let lowerLeft: number[];
if (offset !== 0 && isNumeric(value)) {
lowerLeft = api.coord([startTime, value >= 0 ? Number(value) + offset : offset]);
} else {
lowerLeft = api.coord([startTime, value >= 0 ? value : 0]);
}
const size = api.size([delta, value]);
const width = size[0];
const height = size[1];
const coordSys: {x: number; y: number; width: number; height: number} = params.coordSys as any;
const rectShape = echarts.graphic.clipRectByRect({
x: lowerLeft[0],
y: lowerLeft[1],
width,
height
}, {
x: coordSys.x,
y: coordSys.y,
width: coordSys.width,
height: coordSys.height
});
const zeroPos = api.coord([0, offset]);
const style: any = {
fill: renderCtx.visualSettings.color,
stroke: renderCtx.visualSettings.borderColor,
lineWidth: renderCtx.visualSettings.borderWidth
};
if (renderCtx.labelOption.show) {
let position = renderCtx.labelOption.position;
if (value < 0) {
if (position === 'top') {
position = 'bottom';
} else if (position === 'bottom') {
position = 'top';
}
}
style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)({value: [null, value]} as any);
style.textDistance = 5;
style.textPosition = position;
style.rich = renderCtx.labelOption.rich;
}
let borderRadius: number[];
if (value < 0) {
borderRadius = [0, 0, renderCtx.visualSettings.borderRadius, renderCtx.visualSettings.borderRadius];
} else {
borderRadius = [renderCtx.visualSettings.borderRadius, renderCtx.visualSettings.borderRadius, 0, 0];
}
return rectShape && {
type: 'rect',
id: time + '',
shape: {...rectShape, r: borderRadius},
style,
focus: 'series',
transition: 'all',
enterFrom: {
style: { opacity: 0 },
shape: { height: 0, y: zeroPos[1] }
}
};
};

123
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html

@ -0,0 +1,123 @@
<!--
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-time-series-chart-panel" [style.padding]="padding"
[class.overlay]="overlayEnabled" [style]="backgroundStyle$ | async">
<div class="tb-time-series-chart-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-time-series-chart-content" [class]="legendClass">
<div #chartShape class="tb-time-series-chart-shape">
</div>
<div *ngIf="showLegend" class="tb-time-series-chart-legend" [class.tb-simple-legend]="!displayLegendValues">
<ng-container *ngIf="displayLegendValues; else simpleLegend">
<ng-container *ngIf="horizontalLegendPosition; else verticalLegend">
<ng-container *ngTemplateOutlet="horizontalLegend"></ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #horizontalLegend>
<table class="tb-time-series-chart-legend-table">
<thead>
<tr>
<th></th>
<th *ngFor="let legendKey of legendKeys">
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey}"></ng-container>
</th>
</tr>
</thead>
<tbody>
<ng-container *ngIf="legendConfig.showMin === true">
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Min', type: 'min'}"></ng-container>
</ng-container>
<ng-container *ngIf="legendConfig.showMax === true">
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Max', type: 'max'}"></ng-container>
</ng-container>
<ng-container *ngIf="legendConfig.showAvg === true">
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Avg', type: 'avg'}"></ng-container>
</ng-container>
<ng-container *ngIf="legendConfig.showTotal === true">
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Total', type: 'total'}"></ng-container>
</ng-container>
<ng-container *ngIf="legendConfig.showLatest === true">
<ng-container *ngTemplateOutlet="legendDataRow; context:{label: 'legend.Latest', type: 'latest'}"></ng-container>
</ng-container>
</tbody>
</table>
</ng-template>
<ng-template #verticalLegend>
<table class="tb-time-series-chart-legend-table vertical">
<thead>
<tr>
<th></th>
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showMin === true">{{ 'legend.Min' | translate }}</th>
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showMax === true">{{ 'legend.Max' | translate }}</th>
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showAvg === true">{{ 'legend.Avg' | translate }}</th>
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showTotal === true">{{ 'legend.Total' | translate }}</th>
<th class="tb-time-series-chart-legend-type-label right" *ngIf="legendConfig.showLatest === true">{{ 'legend.Latest' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let legendKey of legendKeys">
<th>
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey, left: true}"></ng-container>
</th>
<td *ngIf="legendConfig.showMin === true" class="tb-time-series-chart-legend-value">
{{ legendData.data[legendKey.dataIndex].min }}
</td>
<td *ngIf="legendConfig.showMax === true" class="tb-time-series-chart-legend-value">
{{ legendData.data[legendKey.dataIndex].max }}
</td>
<td *ngIf="legendConfig.showAvg === true" class="tb-time-series-chart-legend-value">
{{ legendData.data[legendKey.dataIndex].avg }}
</td>
<td *ngIf="legendConfig.showTotal === true" class="tb-time-series-chart-legend-value">
{{ legendData.data[legendKey.dataIndex].total }}
</td>
<td *ngIf="legendConfig.showLatest === true" class="tb-time-series-chart-legend-value">
{{ legendData.data[legendKey.dataIndex].latest }}
</td>
</tr>
</tbody>
</table>
</ng-template>
<ng-template #simpleLegend>
<ng-container *ngFor="let legendKey of legendKeys">
<ng-container *ngTemplateOutlet="legendItem; context:{legendKey: legendKey}"></ng-container>
</ng-container>
</ng-template>
<ng-template #legendItem let-legendKey="legendKey" let-left="left">
<div class="tb-time-series-chart-legend-item"
[class.left]="left">
<div class="tb-time-series-chart-legend-item-label"
(mouseenter)="onLegendKeyEnter(legendKey)"
(mouseleave)="onLegendKeyLeave(legendKey)"
(click)="toggleLegendKey(legendKey)">
<div class="tb-time-series-chart-legend-item-label-circle" [style]="{background: !legendKey.dataKey.hidden ? legendKey.dataKey.color : null}"></div>
<div [style]="!legendKey.dataKey.hidden ? legendLabelStyle : disabledLegendLabelStyle">{{ legendKey.dataKey.label }}</div>
</div>
</div>
</ng-template>
<ng-template #legendDataRow let-label="label" let-type="type">
<tr>
<th class="tb-time-series-chart-legend-type-label">{{ label | translate }}</th>
<td *ngFor="let legendKey of legendKeys" class="tb-time-series-chart-legend-value">
{{ legendData.data[legendKey.dataIndex][type] }}
</td>
</tr>
</ng-template>
</div>
</div>

172
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss

@ -0,0 +1,172 @@
/**
* 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.
*/
$maxLegendWidth: 25%;
$maxLegendHeight: 35%;
.tb-time-series-chart-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
&.overlay {
padding: 20px 24px 24px 24px;
}
> div:not(.tb-time-series-chart-overlay) {
z-index: 1;
}
.tb-time-series-chart-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
div.tb-widget-title {
padding: 0;
}
.tb-time-series-chart-content {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
&.legend-top {
flex-direction: column-reverse;
}
&.legend-right {
flex-direction: row;
}
&.legend-left {
flex-direction: row-reverse;
}
.tb-time-series-chart-shape {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
}
&.legend-right, &.legend-left {
.tb-time-series-chart-legend {
display: inline-grid;
grid-auto-flow: column;
grid-template-rows: repeat(auto-fit, minmax(16px, min-content));
max-width: calc($maxLegendWidth - 8px);
height: fit-content;
max-height: 100%;
}
}
&.legend-top, &.legend-bottom {
.tb-time-series-chart-legend {
align-self: center;
&.tb-simple-legend {
justify-content: center;
}
&:not(.tb-simple-legend) {
width: 100%;
}
}
}
.tb-time-series-chart-legend {
display: flex;
align-items: flex-start;
align-self: stretch;
column-gap: 16px;
row-gap: 8px;
flex-wrap: wrap;
overflow: auto;
width: fit-content;
max-width: 100%;
max-height: calc($maxLegendHeight - 8px);
.tb-time-series-chart-legend-table {
border-spacing: 0;
table-layout: fixed;
&.vertical {
width: 100%;
table-layout: auto;
tbody {
th {
width: 95%;
}
}
}
th, td {
&:not(:last-child) {
padding-right: 16px;
}
}
thead tr, tbody tr:not(:last-child) {
th, td {
padding-bottom: 8px;
}
}
.tb-time-series-chart-legend-item {
align-items: flex-end;
&.left {
align-items: flex-start;
}
}
}
.tb-time-series-chart-legend-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
user-select: none;
.tb-time-series-chart-legend-item-label {
display: flex;
align-items: center;
gap: 4px;
color: #ccc;
white-space: nowrap;
cursor: pointer;
.tb-time-series-chart-legend-item-label-circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
}
}
}
.tb-time-series-chart-legend-type-label {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: rgba(0, 0, 0, 0.38);
white-space: nowrap;
text-align: left;
&.right {
text-align: right;
}
}
.tb-time-series-chart-legend-value {
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 16px;
color: rgba(0, 0, 0, 0.87);
white-space: nowrap;
text-align: right;
}
}
}
}

167
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts

@ -0,0 +1,167 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { timeSeriesChartKeyDefaultSettings, TimeSeriesChartKeySettings } from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { LegendConfig, LegendData, LegendKey, LegendPosition } from '@shared/models/widget.models';
import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart';
import {
timeSeriesChartWidgetDefaultSettings,
TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { mergeDeep } from '@core/utils';
@Component({
selector: 'tb-time-series-chart-widget',
templateUrl: './time-series-chart-widget.component.html',
styleUrls: ['./time-series-chart-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('chartShape', {static: false})
chartShape: ElementRef<HTMLElement>;
settings: TimeSeriesChartWidgetSettings;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
horizontalLegendPosition = false;
showLegend: boolean;
legendClass: string;
legendConfig: LegendConfig;
legendData: LegendData;
legendKeys: LegendKey[];
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
overlayEnabled: boolean;
padding: string;
legendLabelStyle: ComponentStyle;
disabledLegendLabelStyle: ComponentStyle;
displayLegendValues = false;
private timeSeriesChart: TbTimeSeriesChart;
constructor(private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.ctx.$scope.timeSeriesChartWidget = this;
this.settings = {...timeSeriesChartWidgetDefaultSettings, ...this.ctx.settings};
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.overlayEnabled = this.settings.background.overlay.enabled;
this.padding = this.overlayEnabled ? undefined : this.settings.padding;
this.showLegend = this.settings.showLegend;
if (this.showLegend) {
this.legendData = this.ctx.defaultSubscription.legendData;
this.legendConfig = this.settings.legendConfig;
this.legendKeys = this.legendData.keys;
if (this.legendConfig.sortDataKeys) {
this.legendKeys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label));
}
this.legendKeys.forEach(legendKey => {
legendKey.dataKey.settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings);
legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault;
});
this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend);
if (!this.legendKeys.length) {
this.showLegend = false;
}
}
if (this.showLegend) {
this.horizontalLegendPosition = [LegendPosition.left, LegendPosition.right].includes(this.legendConfig.position);
this.legendClass = `legend-${this.legendConfig.position}`;
this.legendLabelStyle = textStyle(this.settings.legendLabelFont);
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont);
this.legendLabelStyle.color = this.settings.legendLabelColor;
this.displayLegendValues = this.legendConfig.showMin || this.legendConfig.showMax ||
this.legendConfig.showAvg || this.legendConfig.showTotal || this.legendConfig.showLatest;
}
}
ngAfterViewInit() {
this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, this.settings, this.chartShape.nativeElement, this.renderer);
}
ngOnDestroy() {
if (this.timeSeriesChart) {
this.timeSeriesChart.destroy();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
if (this.timeSeriesChart) {
this.timeSeriesChart.update();
}
}
public onLatestDataUpdated() {
if (this.timeSeriesChart) {
this.timeSeriesChart.latestUpdated();
}
}
public onLegendKeyEnter(legendKey: LegendKey) {
this.timeSeriesChart.keyEnter(legendKey.dataKey);
}
public onLegendKeyLeave(legendKey: LegendKey) {
this.timeSeriesChart.keyLeave(legendKey.dataKey);
}
public toggleLegendKey(legendKey: LegendKey) {
this.timeSeriesChart.toggleKey(legendKey.dataKey);
}
}

54
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts

@ -0,0 +1,54 @@
///
/// 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 { timeSeriesChartDefaultSettings, TimeSeriesChartSettings } from '@home/components/widget/lib/chart/time-series-chart.models';
import { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models';
import { defaultLegendConfig, LegendConfig, LegendPosition, widgetType } from '@shared/models/widget.models';
import { mergeDeep } from '@core/utils';
export interface TimeSeriesChartWidgetSettings extends TimeSeriesChartSettings {
showLegend: boolean;
legendLabelFont: Font;
legendLabelColor: string;
legendConfig: LegendConfig;
background: BackgroundSettings;
padding: string;
}
export const timeSeriesChartWidgetDefaultSettings: TimeSeriesChartWidgetSettings =
mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartDefaultSettings as TimeSeriesChartWidgetSettings, {
showLegend: true,
legendLabelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '16px'
},
legendLabelColor: 'rgba(0, 0, 0, 0.76)',
legendConfig: {...defaultLegendConfig(widgetType.timeseries), position: LegendPosition.top},
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
},
padding: '12px'
} as TimeSeriesChartWidgetSettings);

1323
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

File diff suppressed because it is too large

742
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -0,0 +1,742 @@
///
/// 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 { WidgetContext } from '@home/models/widget-component.models';
import {
AxisPosition,
calculateThresholdsOffset,
createTimeSeriesXAxisOption,
createTimeSeriesYAxis,
defaultTimeSeriesChartYAxisSettings,
generateChartData,
parseThresholdData,
SeriesLabelPosition,
TimeSeriesChartDataItem,
timeSeriesChartDefaultSettings,
timeSeriesChartKeyDefaultSettings,
TimeSeriesChartKeySettings,
TimeSeriesChartNoAggregationBarWidthStrategy,
TimeSeriesChartSeriesType,
TimeSeriesChartSettings,
TimeSeriesChartShape,
TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings,
TimeSeriesChartThresholdItem,
TimeSeriesChartThresholdType,
TimeSeriesChartType,
TimeSeriesChartYAxis,
TimeSeriesChartYAxisId,
TimeSeriesChartYAxisSettings,
updateDarkMode
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { ResizeObserver } from '@juggle/resize-observer';
import {
calculateXAxisHeight,
calculateYAxisWidth,
ECharts,
echartsModule,
EChartsOption,
echartsTooltipFormatter,
EChartsTooltipTrigger,
getAxisExtent,
measureXAxisNameHeight,
measureYAxisNameWidth,
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { DateFormatProcessor } from '@shared/models/widget-settings.models';
import { isDefinedAndNotNull, isEqual, mergeDeep } from '@core/utils';
import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
import * as echarts from 'echarts/core';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { Renderer2 } from '@angular/core';
import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts';
import { BehaviorSubject } from 'rxjs';
import { AggregationType } from '@shared/models/time/time.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DeepPartial } from '@shared/models/common';
import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models';
export class TbTimeSeriesChart {
public static dataKeySettings(type = TimeSeriesChartType.default): DataKeySettingsFunction {
return (_key, isLatestDataKey) => {
if (!isLatestDataKey) {
const settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings);
if (type === TimeSeriesChartType.line) {
settings.type = TimeSeriesChartSeriesType.line;
} else if (type === TimeSeriesChartType.bar) {
settings.type = TimeSeriesChartSeriesType.bar;
} else if (type === TimeSeriesChartType.point) {
settings.type = TimeSeriesChartSeriesType.line;
settings.lineSettings.showLine = false;
settings.lineSettings.showPoints = true;
settings.lineSettings.pointShape = TimeSeriesChartShape.circle;
settings.lineSettings.pointSize = 8;
}
return settings;
}
return null;
};
}
private get noAggregation(): boolean {
return this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE;
}
private readonly shapeResize$: ResizeObserver;
private readonly settings: TimeSeriesChartSettings;
private yAxisList: TimeSeriesChartYAxis[] = [];
private dataItems: TimeSeriesChartDataItem[] = [];
private thresholdItems: TimeSeriesChartThresholdItem[] = [];
private timeSeriesChart: ECharts;
private timeSeriesChartOptions: EChartsOption;
private readonly tooltipDateFormat: DateFormatProcessor;
private yMinSubject = new BehaviorSubject(-1);
private yMaxSubject = new BehaviorSubject(1);
private darkMode = false;
private darkModeObserver: MutationObserver;
private topPointLabels = false;
private componentIndexCounter = 0;
private highlightedDataKey: DataKey;
private barRenderSharedContext: BarRenderSharedContext;
yMin$ = this.yMinSubject.asObservable();
yMax$ = this.yMaxSubject.asObservable();
constructor(private ctx: WidgetContext,
private readonly inputSettings: DeepPartial<TimeSeriesChartSettings>,
private chartElement: HTMLElement,
private renderer: Renderer2,
private autoResize = true) {
this.settings = mergeDeep({} as TimeSeriesChartSettings,
timeSeriesChartDefaultSettings,
this.inputSettings as TimeSeriesChartSettings);
const dashboardPageElement = this.ctx.$containerParent.parents('.tb-dashboard-page');
this.darkMode = this.settings.darkMode || dashboardPageElement.hasClass('dark');
this.setupYAxes();
this.setupData();
this.setupThresholds();
if (this.settings.showTooltip && this.settings.tooltipShowDate) {
this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat);
}
this.onResize();
if (this.autoResize) {
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.chartElement);
}
this.darkModeObserver = new MutationObserver(mutations => {
for(let mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const darkMode = dashboardPageElement.hasClass('dark');
this.setDarkMode(darkMode);
}
}
});
this.darkModeObserver.observe(dashboardPageElement[0], { attributes: true });
}
public update(): void {
for (const item of this.dataItems) {
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null;
if (!isEqual(item.dataSet, datasourceData?.data)) {
item.dataSet = datasourceData?.data;
item.data = datasourceData?.data ? toNamedData(datasourceData.data) : [];
}
}
this.onResize();
if (this.timeSeriesChart) {
this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime;
this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime;
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow;
if (this.noAggregation) {
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line';
} else {
this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow';
}
this.barRenderSharedContext.timeInterval = this.ctx.timeWindow.interval;
this.updateSeriesData(true);
if (this.highlightedDataKey) {
this.keyEnter(this.highlightedDataKey);
}
}
}
public latestUpdated() {
let update = false;
if (this.ctx.latestData) {
for (const item of this.thresholdItems) {
if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) {
const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey);
if (data.data[0]) {
item.value = parseThresholdData(data.data[0][1]);
update = true;
}
}
}
}
if (this.timeSeriesChart && update) {
this.updateSeriesData();
}
}
public keyEnter(dataKey: DataKey): void {
this.highlightedDataKey = dataKey;
const item = this.dataItems.find(d => d.dataKey === dataKey);
if (item) {
this.timeSeriesChart.dispatchAction({
type: 'highlight',
seriesId: item.id
});
}
}
public keyLeave(dataKey: DataKey): void {
this.highlightedDataKey = null;
const item = this.dataItems.find(d => d.dataKey === dataKey);
if (item) {
this.timeSeriesChart.dispatchAction({
type: 'downplay',
seriesId: item.id
});
}
}
public toggleKey(dataKey: DataKey): void {
const enable = dataKey.hidden;
const dataItem = this.dataItems.find(d => d.dataKey === dataKey);
if (dataItem) {
dataItem.enabled = enable;
if (!enable) {
this.timeSeriesChart.dispatchAction({
type: 'downplay',
seriesId: dataItem.id
});
}
this.timeSeriesChartOptions.series = this.updateSeries();
const mergeList = ['series'];
if (this.updateYAxisScale(this.yAxisList)) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
mergeList.push('yAxis');
}
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList});
this.updateAxes();
dataKey.hidden = !enable;
if (enable) {
this.timeSeriesChart.dispatchAction({
type: 'highlight',
seriesId: dataItem.id
});
}
}
}
public destroy(): void {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.timeSeriesChart) {
this.timeSeriesChart.dispose();
}
this.yMinSubject.complete();
this.yMaxSubject.complete();
this.darkModeObserver.disconnect();
}
public resize(): void {
this.onResize();
}
public setDarkMode(darkMode: boolean): void {
if (this.darkMode !== darkMode) {
this.darkMode = darkMode;
if (this.timeSeriesChart) {
this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions,
this.settings, this.yAxisList, this.dataItems,
this.thresholdItems, darkMode);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions);
}
}
}
public isDarkMode(): boolean {
return this.darkMode;
}
private setupData(): void {
const noAggregationBarWidthSettings = this.settings.noAggregationBarWidthSettings;
const targetBarWidth = noAggregationBarWidthSettings.strategy === TimeSeriesChartNoAggregationBarWidthStrategy.group ?
noAggregationBarWidthSettings.groupWidth : noAggregationBarWidthSettings.barWidth;
this.barRenderSharedContext = {
timeInterval: this.ctx.timeWindow?.interval,
noAggregationBarWidthStrategy: noAggregationBarWidthSettings.strategy,
noAggregationWidthRelative: targetBarWidth.relative,
noAggregationWidth: targetBarWidth.relative ? targetBarWidth.relativeWidth : targetBarWidth.absoluteWidth
};
if (this.ctx.datasources.length) {
for (const datasource of this.ctx.datasources) {
const dataKeys = datasource.dataKeys;
for (const dataKey of dataKeys) {
const keySettings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings, dataKey.settings);
if ((keySettings.type === TimeSeriesChartSeriesType.line && keySettings.lineSettings.showPointLabel &&
keySettings.lineSettings.pointLabelPosition === SeriesLabelPosition.top) ||
(keySettings.type === TimeSeriesChartSeriesType.bar && keySettings.barSettings.showLabel)) {
this.topPointLabels = true;
}
dataKey.settings = keySettings;
const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null;
const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : [];
const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units;
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals :
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2);
let yAxisId = keySettings.yAxisId;
if (!Object.keys(this.settings.yAxes).includes(yAxisId)) {
yAxisId = 'default';
}
this.dataItems.push({
id: this.nextComponentId(),
units,
decimals,
yAxisId,
yAxisIndex: this.getYAxisIndex(yAxisId),
dataKey,
data: namedData,
enabled: !keySettings.dataHiddenByDefault
});
}
}
}
}
private setupThresholds(): void {
const thresholdDatasources: Datasource[] = [];
for (const thresholdSettings of this.settings.thresholds) {
const threshold = mergeDeep<TimeSeriesChartThreshold>({} as TimeSeriesChartThreshold,
timeSeriesChartThresholdDefaultSettings, thresholdSettings);
let latestDataKey: DataKey = null;
let entityDataKey: DataKey = null;
let value = null;
if (threshold.type === TimeSeriesChartThresholdType.latestKey) {
if (this.ctx.datasources.length) {
for (const datasource of this.ctx.datasources) {
latestDataKey = datasource.latestDataKeys?.find(d =>
(d.type === DataKeyType.function && d.label === threshold.latestKey) ||
(d.type !== DataKeyType.function && d.name === threshold.latestKey &&
d.type === threshold.latestKeyType));
if (latestDataKey) {
break;
}
}
}
if (!latestDataKey) {
continue;
}
} else if (threshold.type === TimeSeriesChartThresholdType.entity) {
const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.entityAlias);
if (!entityAliasId) {
continue;
}
let datasource = thresholdDatasources.find(d => d.entityAliasId === entityAliasId);
entityDataKey = {
type: threshold.entityKeyType,
name: threshold.entityKey,
label: threshold.entityKey,
settings: {}
};
if (datasource) {
datasource.dataKeys.push(entityDataKey);
}
datasource = {
type: DatasourceType.entity,
name: threshold.entityAlias,
aliasName: threshold.entityAlias,
entityAliasId,
dataKeys: [ entityDataKey ]
};
thresholdDatasources.push(datasource);
} else { // constant
value = threshold.value;
}
const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units;
const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals :
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2);
let yAxisId = threshold.yAxisId;
if (!Object.keys(this.settings.yAxes).includes(yAxisId)) {
yAxisId = 'default';
}
const thresholdItem: TimeSeriesChartThresholdItem = {
id: this.nextComponentId(),
units,
decimals,
yAxisId,
yAxisIndex: this.getYAxisIndex(yAxisId),
value,
latestDataKey,
settings: threshold
};
if (entityDataKey) {
entityDataKey.settings.thresholdItemId = thresholdItem.id;
}
this.thresholdItems.push(thresholdItem);
}
this.subscribeForEntityThresholds(thresholdDatasources);
}
private setupYAxes(): void {
const yAxisSettingsList = Object.values(this.settings.yAxes);
yAxisSettingsList.sort((a1, a2) => a1.order - a2.order);
for (const yAxisSettings of yAxisSettingsList) {
const axisSettings = mergeDeep<TimeSeriesChartYAxisSettings>({} as TimeSeriesChartYAxisSettings,
defaultTimeSeriesChartYAxisSettings, yAxisSettings);
const units = axisSettings.units && axisSettings.units.length ? axisSettings.units : this.ctx.units;
const decimals = isDefinedAndNotNull(axisSettings.decimals) ? axisSettings.decimals :
(isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2);
const yAxis = createTimeSeriesYAxis(units, decimals, axisSettings, this.darkMode);
this.yAxisList.push(yAxis);
}
}
private nextComponentId(): string {
return (this.componentIndexCounter++) + '';
}
private getYAxisIndex(id: TimeSeriesChartYAxisId): number {
let yAxisIndex = this.yAxisList.findIndex(axis => axis.id === id);
if (yAxisIndex === -1) {
yAxisIndex = this.yAxisList.findIndex(axis => axis.id === 'default');
if (yAxisIndex === -1 && this.yAxisList.length) {
yAxisIndex = 0;
}
}
return yAxisIndex;
}
private subscribeForEntityThresholds(datasources: Datasource[]) {
if (datasources.length) {
const thresholdsSourcesSubscriptionOptions: WidgetSubscriptionOptions = {
datasources,
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {
let update = false;
if (subscription.data) {
for (const item of this.thresholdItems) {
if (item.settings.type === TimeSeriesChartThresholdType.entity) {
const data = subscription.data.find(d => d.dataKey.settings?.thresholdItemId === item.id);
if (data.data[0]) {
item.value = parseThresholdData(data.data[0][1]);
update = true;
}
}
}
}
if (this.timeSeriesChart && update) {
this.updateSeriesData();
}
}
}
};
this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe();
}
}
private drawChart() {
echartsModule.init();
this.timeSeriesChart = echarts.init(this.chartElement, null, {
renderer: 'canvas'
});
this.timeSeriesChartOptions = {
darkMode: this.darkMode,
backgroundColor: 'transparent',
tooltip: [{
trigger: this.settings.tooltipTrigger === EChartsTooltipTrigger.axis ? 'axis' : 'item',
confine: true,
appendTo: 'body',
axisPointer: {
type: this.noAggregation ? 'line' : 'shadow'
},
formatter: (params: CallbackDataParams[]) =>
this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat,
this.settings, params, 0, '', -1, this.dataItems,
this.noAggregation ? null : this.ctx.timeWindow.interval) : undefined,
padding: [8, 12],
backgroundColor: this.settings.tooltipBackgroundColor,
borderWidth: 0,
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);`
}],
grid: [{
top: this.minTopOffset(),
left: this.settings.dataZoom ? 5 : 0,
right: this.settings.dataZoom ? 5 : 0,
bottom: this.minBottomOffset()
}],
xAxis: [
createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime,
this.ctx.defaultSubscription.timeWindow.maxTime, this.ctx.date, this.darkMode)
],
yAxis: this.yAxisList.map(axis => axis.option),
dataZoom: [
{
type: 'inside',
disabled: !this.settings.dataZoom,
realtime: true
},
{
type: 'slider',
show: this.settings.dataZoom,
showDetail: false,
realtime: true,
bottom: 10
}
],
animation: this.settings.animation.animation,
animationThreshold: this.settings.animation.animationThreshold,
animationDuration: this.settings.animation.animationDuration,
animationEasing: this.settings.animation.animationEasing,
animationDelay: this.settings.animation.animationDelay,
animationDurationUpdate: this.settings.animation.animationDurationUpdate,
animationEasingUpdate: this.settings.animation.animationEasingUpdate,
animationDelayUpdate: this.settings.animation.animationDelayUpdate
};
this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow;
this.timeSeriesChartOptions.series = this.updateSeries();
if (this.updateYAxisScale(this.yAxisList)) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
}
this.timeSeriesChart.setOption(this.timeSeriesChartOptions);
this.updateAxes();
if (this.settings.dataZoom) {
this.timeSeriesChart.on('datazoom', () => {
this.updateAxes();
});
}
}
private updateSeriesData(updateScale = false): void {
this.timeSeriesChartOptions.series = this.updateSeries();
if (updateScale && this.updateYAxisScale(this.yAxisList)) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
}
this.timeSeriesChart.setOption(this.timeSeriesChartOptions);
this.updateAxes();
}
private updateSeries(): Array<LineSeriesOption | CustomSeriesOption> {
return generateChartData(this.dataItems, this.thresholdItems,
this.settings.stack,
this.noAggregation,
this.barRenderSharedContext, this.darkMode);
}
private updateAxes() {
const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left');
let res = this.updateYAxisOffset(leftAxisList);
let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0);
let changed = res.changed;
const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right');
res = this.updateYAxisOffset(rightAxisList);
let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0);
changed = changed || res.changed;
let bottomOffset = this.minBottomOffset();
const minTopOffset = this.minTopOffset();
let topOffset = minTopOffset;
if (this.timeSeriesChartOptions.xAxis[0].show) {
const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart);
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) {
bottomOffset += xAxisHeight;
} else {
topOffset = Math.max(minTopOffset, xAxisHeight);
}
if (this.settings.xAxis.label) {
const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name);
if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) {
bottomOffset += nameHeight;
} else {
topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight);
}
const nameGap = xAxisHeight;
if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) {
this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap;
changed = true;
}
}
}
const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList);
leftOffset = Math.max(leftOffset, thresholdsOffset[0]);
rightOffset = Math.max(rightOffset, thresholdsOffset[1]);
if (this.timeSeriesChartOptions.grid[0].left !== leftOffset ||
this.timeSeriesChartOptions.grid[0].right !== rightOffset ||
this.timeSeriesChartOptions.grid[0].bottom !== bottomOffset ||
this.timeSeriesChartOptions.grid[0].top !== topOffset) {
this.timeSeriesChartOptions.grid[0].left = leftOffset;
this.timeSeriesChartOptions.grid[0].right = rightOffset;
this.timeSeriesChartOptions.grid[0].bottom = bottomOffset;
this.timeSeriesChartOptions.grid[0].top = topOffset;
changed = true;
}
if (changed) {
this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option);
this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: true});
}
if (this.yAxisList.length) {
const extent = getAxisExtent(this.timeSeriesChart, this.yAxisList[0].id);
const min = extent[0];
const max = extent[1];
if (this.yMinSubject.value !== min) {
this.yMinSubject.next(min);
}
if (this.yMaxSubject.value !== max) {
this.yMaxSubject.next(max);
}
}
}
private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} {
const result = {offset: 0, changed: false};
let width = 0;
for (const yAxis of axisList) {
const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id);
if (width && newWidth) {
result.offset += 5;
}
width = newWidth;
const showLine = !!width && yAxis.settings.showLine;
if (yAxis.option.axisLine.show !== showLine) {
yAxis.option.axisLine.show = showLine;
result.changed = true;
}
if (yAxis.option.offset !== result.offset) {
yAxis.option.offset = result.offset;
result.changed = true;
}
if (yAxis.settings.label) {
if (!width) {
if (yAxis.option.name) {
yAxis.option.name = null;
result.changed = true;
}
} else {
if (!yAxis.option.name) {
yAxis.option.name = yAxis.settings.label;
result.changed = true;
}
const nameGap = width;
if (yAxis.option.nameGap !== nameGap) {
yAxis.option.nameGap = nameGap;
result.changed = true;
}
const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, yAxis.settings.label);
result.offset += nameWidth;
}
}
result.offset += width;
}
return result;
}
private updateYAxisScale(axisList: TimeSeriesChartYAxis[]): boolean {
let changed = false;
for (const yAxis of axisList) {
const scaleYAxis = this.scaleYAxis(yAxis);
if (yAxis.option.scale !== scaleYAxis) {
yAxis.option.scale = scaleYAxis;
changed = true;
}
}
return changed;
}
private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean {
const axisBarDataItems = this.dataItems.filter(d => d.yAxisId === yAxis.id && d.enabled &&
d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar);
return !axisBarDataItems.length;
}
private minTopOffset(): number {
const showTickLabels =
!!this.yAxisList.find(yAxis => yAxis.settings.show && yAxis.settings.showTickLabels);
return (this.topPointLabels) ? 20 :
(showTickLabels ? 10 : 5);
}
private minBottomOffset(): number {
return this.settings.dataZoom ? 45 : 5;
}
private onResize() {
const shapeWidth = this.chartElement.offsetWidth;
const shapeHeight = this.chartElement.offsetHeight;
if (shapeWidth && shapeHeight) {
if (!this.timeSeriesChart) {
this.drawChart();
} else {
const width = this.timeSeriesChart.getWidth();
const height = this.timeSeriesChart.getHeight();
if (width !== shapeWidth || height !== shapeHeight) {
let barItems: TimeSeriesChartDataItem[];
if (this.animationEnabled()) {
barItems =
this.dataItems.filter(d => d.enabled && d.data.length &&
d.dataKey.settings.type === TimeSeriesChartSeriesType.bar);
this.updateBarsAnimation(barItems, false);
}
this.timeSeriesChart.resize();
if (this.animationEnabled()) {
this.updateBarsAnimation(barItems, true);
}
}
}
}
}
private animationEnabled(): boolean {
return this.settings.animation.animation;
}
private updateBarsAnimation(barItems: TimeSeriesChartDataItem[], animation: boolean) {
if (barItems.length) {
barItems.forEach(item => {
item.option.animation = animation;
});
this.timeSeriesChart.setOption(this.timeSeriesChartOptions);
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -298,6 +298,6 @@ export class MapWidgetController implements MapWidgetInterface {
}
}
export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController;
export const TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController;

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html

@ -114,7 +114,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>
@ -124,6 +125,13 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput

7
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts

@ -33,7 +33,7 @@ import {
@Component({
selector: 'tb-bar-chart-with-labels-widget-settings',
templateUrl: './bar-chart-with-labels-widget-settings.component.html',
styleUrls: []
styleUrls: ['./../widget-settings.scss']
})
export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsComponent {
@ -83,6 +83,8 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
@ -137,10 +139,12 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom
this.barChartWidgetSettingsForm.get('tooltipDateFormat').enable();
this.barChartWidgetSettingsForm.get('tooltipDateFont').enable();
this.barChartWidgetSettingsForm.get('tooltipDateColor').enable();
this.barChartWidgetSettingsForm.get('tooltipDateInterval').enable();
} else {
this.barChartWidgetSettingsForm.get('tooltipDateFormat').disable();
this.barChartWidgetSettingsForm.get('tooltipDateFont').disable();
this.barChartWidgetSettingsForm.get('tooltipDateColor').disable();
this.barChartWidgetSettingsForm.get('tooltipDateInterval').disable();
}
} else {
this.barChartWidgetSettingsForm.get('tooltipValueFont').disable();
@ -149,6 +153,7 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom
this.barChartWidgetSettingsForm.get('tooltipDateFormat').disable();
this.barChartWidgetSettingsForm.get('tooltipDateFont').disable();
this.barChartWidgetSettingsForm.get('tooltipDateColor').disable();
this.barChartWidgetSettingsForm.get('tooltipDateInterval').disable();
this.barChartWidgetSettingsForm.get('tooltipBackgroundColor').disable();
this.barChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable();
}

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html

@ -104,7 +104,8 @@
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>
@ -114,6 +115,13 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts

@ -78,6 +78,8 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent {
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
@ -114,10 +116,12 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent {
this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').enable();
this.rangeChartWidgetSettingsForm.get('tooltipDateFont').enable();
this.rangeChartWidgetSettingsForm.get('tooltipDateColor').enable();
this.rangeChartWidgetSettingsForm.get('tooltipDateInterval').enable();
} else {
this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').disable();
this.rangeChartWidgetSettingsForm.get('tooltipDateFont').disable();
this.rangeChartWidgetSettingsForm.get('tooltipDateColor').disable();
this.rangeChartWidgetSettingsForm.get('tooltipDateInterval').disable();
}
} else {
this.rangeChartWidgetSettingsForm.get('tooltipValueFont').disable();
@ -126,6 +130,7 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent {
this.rangeChartWidgetSettingsForm.get('tooltipDateFormat').disable();
this.rangeChartWidgetSettingsForm.get('tooltipDateFont').disable();
this.rangeChartWidgetSettingsForm.get('tooltipDateColor').disable();
this.rangeChartWidgetSettingsForm.get('tooltipDateInterval').disable();
this.rangeChartWidgetSettingsForm.get('tooltipBackgroundColor').disable();
this.rangeChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable();
}

67
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.html

@ -0,0 +1,67 @@
<!--
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]="barSettingsFormGroup">
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="showBorder">
{{ 'widgets.time-series-chart.series.bar.show-border' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.series.bar.border-width</div>
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" formControlName="borderWidth" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.series.bar.border-radius</div>
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" formControlName="borderRadius" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-lt-md">
<mat-slide-toggle class="mat-slide" formControlName="showLabel">
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.bar.label-hint' | translate}}">
{{ 'widgets.time-series-chart.series.bar.label' | translate }}
</div>
</mat-slide-toggle>
<div fxLayout="row" fxFlex.lt-md fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="labelPosition">
<mat-option *ngFor="let position of seriesLabelPositions" [value]="position">
{{ seriesLabelPositionTranslations.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-font-settings formControlName="labelFont"
clearButton
disabledLineHeight
forceSizeUnit="px"
[previewText]="labelPreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="labelColor">
</tb-color-input>
</div>
</div>
<tb-time-series-chart-fill-settings
formControlName="backgroundSettings"
title="widgets.time-series-chart.series.background"
fillNoneTitle="widgets.time-series-chart.series.fill-type-solid">
</tb-time-series-chart-fill-settings>
</ng-container>

148
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-bar-settings.component.ts

@ -0,0 +1,148 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
BarSeriesSettings,
seriesLabelPositions,
seriesLabelPositionTranslations
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
@Component({
selector: 'tb-time-series-chart-bar-settings',
templateUrl: './time-series-chart-bar-settings.component.html',
styleUrls: ['./../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartBarSettingsComponent),
multi: true
}
]
})
export class TimeSeriesChartBarSettingsComponent implements OnInit, ControlValueAccessor {
seriesLabelPositions = seriesLabelPositions;
seriesLabelPositionTranslations = seriesLabelPositionTranslations;
labelPreviewFn = this._labelPreviewFn.bind(this);
@Input()
disabled: boolean;
private modelValue: BarSeriesSettings;
private propagateChange = null;
public barSettingsFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private dataKeyConfigComponent: DataKeyConfigComponent,
private fb: UntypedFormBuilder) {
}
ngOnInit(): void {
this.barSettingsFormGroup = this.fb.group({
showBorder: [null, []],
borderWidth: [null, [Validators.min(0)]],
borderRadius: [null, [Validators.min(0)]],
showLabel: [null, []],
labelPosition: [null, []],
labelFont: [null, []],
labelColor: [null, []],
backgroundSettings: [null, []]
});
this.barSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
merge(this.barSettingsFormGroup.get('showBorder').valueChanges,
this.barSettingsFormGroup.get('showLabel').valueChanges)
.subscribe(() => {
this.updateValidators();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.barSettingsFormGroup.disable({emitEvent: false});
} else {
this.barSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: BarSeriesSettings): void {
this.modelValue = value;
this.barSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateValidators() {
const showBorder: boolean = this.barSettingsFormGroup.get('showBorder').value;
const showLabel: boolean = this.barSettingsFormGroup.get('showLabel').value;
if (showBorder) {
this.barSettingsFormGroup.get('borderWidth').enable({emitEvent: false});
} else {
this.barSettingsFormGroup.get('borderWidth').disable({emitEvent: false});
}
if (showLabel) {
this.barSettingsFormGroup.get('labelPosition').enable({emitEvent: false});
this.barSettingsFormGroup.get('labelFont').enable({emitEvent: false});
this.barSettingsFormGroup.get('labelColor').enable({emitEvent: false});
} else {
this.barSettingsFormGroup.get('labelPosition').disable({emitEvent: false});
this.barSettingsFormGroup.get('labelFont').disable({emitEvent: false});
this.barSettingsFormGroup.get('labelColor').disable({emitEvent: false});
}
}
private updateModel() {
this.modelValue = this.barSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
private _labelPreviewFn(): string {
const dataKey = this.dataKeyConfigComponent.modelValue;
const widgetConfig = this.dataKeyConfigComponent.widgetConfig;
const units = dataKey.units && dataKey.units.length ? dataKey.units : widgetConfig.config.units;
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals :
(isDefinedAndNotNull(widgetConfig.config.decimals) ? widgetConfig.config.decimals : 2);
return formatValue(22, decimals, units, false);
}
}

53
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.html

@ -0,0 +1,53 @@
<!--
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]="fillSettingsFormGroup">
<div class="tb-form-row column">
<div class="tb-form-row no-border no-padding space-between">
<div>{{ title | translate }}</div>
<tb-toggle-select formControlName="type">
<tb-toggle-option *ngFor="let type of seriesFillTypes" [value]="type">{{ seriesFillTypeTranslationMap.get(type) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container *ngIf="fillSettingsFormGroup.get('type').value === SeriesFillType.opacity">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.series.opacity</div>
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" formControlName="opacity" min="0" max="100"
step="1" placeholder="{{ 'widget-config.set' | translate }}"/>
</mat-form-field>
</div>
</ng-container>
<ng-container *ngIf="fillSettingsFormGroup.get('type').value === SeriesFillType.gradient" formGroupName="gradient">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.series.gradient-stops</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div class="tb-small-label" translate>widgets.time-series-chart.series.gradient-start</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="start" min="0" max="100" step="1"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-small-label" translate>widgets.time-series-chart.series.gradient-end</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="end" min="0" max="100" step="1"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>
</div>
</ng-container>

143
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-fill-settings.component.ts

@ -0,0 +1,143 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
SeriesFillSettings,
SeriesFillType,
seriesFillTypes,
seriesFillTypeTranslations
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-time-series-chart-fill-settings',
templateUrl: './time-series-chart-fill-settings.component.html',
styleUrls: ['./../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartFillSettingsComponent),
multi: true
}
]
})
export class TimeSeriesChartFillSettingsComponent implements OnInit, ControlValueAccessor {
seriesFillTypes = seriesFillTypes;
seriesFillTypeTranslationMap: Map<SeriesFillType, string> = new Map<SeriesFillType, string>([]);
SeriesFillType = SeriesFillType;
@Input()
disabled: boolean;
@Input()
title = 'widgets.time-series-chart.series.fill';
@Input()
fillNoneTitle = 'widgets.time-series-chart.series.fill-type-none';
private modelValue: SeriesFillSettings;
private propagateChange = null;
public fillSettingsFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
}
ngOnInit(): void {
this.fillSettingsFormGroup = this.fb.group({
type: [null, []],
opacity: [null, [Validators.min(0), Validators.max(100)]],
gradient: this.fb.group({
start: [null, [Validators.min(0), Validators.max(100)]],
end: [null, [Validators.min(0), Validators.max(100)]]
})
});
this.fillSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.fillSettingsFormGroup.get('type').valueChanges.subscribe(() => {
this.updateValidators();
});
for (const type of seriesFillTypes) {
let translation: string;
if (type === SeriesFillType.none) {
translation = this.fillNoneTitle;
} else {
translation = seriesFillTypeTranslations.get(type);
}
this.seriesFillTypeTranslationMap.set(type, translation);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.fillSettingsFormGroup.disable({emitEvent: false});
} else {
this.fillSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: SeriesFillSettings): void {
this.modelValue = value;
this.fillSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateValidators() {
const type: SeriesFillType = this.fillSettingsFormGroup.get('type').value;
if (type === SeriesFillType.none) {
this.fillSettingsFormGroup.get('opacity').disable({emitEvent: false});
this.fillSettingsFormGroup.get('gradient').disable({emitEvent: false});
} else if (type === SeriesFillType.opacity) {
this.fillSettingsFormGroup.get('opacity').enable({emitEvent: false});
this.fillSettingsFormGroup.get('gradient').disable({emitEvent: false});
} else if (type === SeriesFillType.gradient) {
this.fillSettingsFormGroup.get('opacity').disable({emitEvent: false});
this.fillSettingsFormGroup.get('gradient').enable({emitEvent: false});
}
}
private updateModel() {
const value: SeriesFillSettings = this.fillSettingsFormGroup.getRawValue();
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}

62
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.html

@ -0,0 +1,62 @@
<!--
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]="timeSeriesChartKeySettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.legend-settings</div>
<mat-slide-toggle class="mat-slide" formControlName="showInLegend">
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.show-in-legend-hint' | translate}}">
{{ 'widgets.time-series-chart.series.show-in-legend' | translate }}
</div>
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="dataHiddenByDefault">
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.hidden-by-default-hint' | translate}}">
{{ 'widgets.time-series-chart.series.hidden-by-default' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-panel">
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.axis.y-axis</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="yAxisId">
<mat-option *ngFor="let yAxis of yAxisIds" [value]="yAxis">
{{ yAxis }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div *ngIf="chartType === TimeSeriesChartType.default; else chartTypeTitle" fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.series-type</div>
<tb-toggle-select formControlName="type">
<tb-toggle-option *ngFor="let type of timeSeriesChartSeriesTypes" [value]="type">{{ timeSeriesChartSeriesTypeTranslations.get(type) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-time-series-chart-line-settings [fxShow]="timeSeriesChartKeySettingsForm.get('type').value === TimeSeriesChartSeriesType.line"
[chartType]="chartType"
formControlName="lineSettings">
</tb-time-series-chart-line-settings>
<tb-time-series-chart-bar-settings [fxShow]="timeSeriesChartKeySettingsForm.get('type').value === TimeSeriesChartSeriesType.bar"
formControlName="barSettings">
</tb-time-series-chart-bar-settings>
</div>
</ng-container>
<ng-template #chartTypeTitle>
<div class="tb-form-panel-title">{{ timeSeriesChartTypeTranslations.get(chartType) | translate }}</div>
</ng-template>

116
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-key-settings.component.ts

@ -0,0 +1,116 @@
///
/// 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import {
timeSeriesChartKeyDefaultSettings,
TimeSeriesChartKeySettings,
TimeSeriesChartSeriesType,
timeSeriesChartSeriesTypes,
timeSeriesChartSeriesTypeTranslations, TimeSeriesChartType, timeSeriesChartTypeTranslations, TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { TimeSeriesChartWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-widget.models';
@Component({
selector: 'tb-time-series-chart-key-settings',
templateUrl: './time-series-chart-key-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class TimeSeriesChartKeySettingsComponent extends WidgetSettingsComponent {
TimeSeriesChartType = TimeSeriesChartType;
timeSeriesChartTypeTranslations = timeSeriesChartTypeTranslations;
TimeSeriesChartSeriesType = TimeSeriesChartSeriesType;
timeSeriesChartSeriesTypes = timeSeriesChartSeriesTypes;
timeSeriesChartSeriesTypeTranslations = timeSeriesChartSeriesTypeTranslations;
timeSeriesChartKeySettingsForm: UntypedFormGroup;
chartType = TimeSeriesChartType.default;
yAxisIds: TimeSeriesChartYAxisId[];
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.timeSeriesChartKeySettingsForm;
}
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
if (isDefinedAndNotNull(params.chartType)) {
this.chartType = params.chartType;
}
const widgetSettings = (widgetConfig.config?.settings || {}) as TimeSeriesChartWidgetSettings;
this.yAxisIds = widgetSettings.yAxes ? Object.keys(widgetSettings.yAxes) : ['default'];
}
protected defaultSettings(): WidgetSettings {
return mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings);
}
protected onSettingsSet(settings: WidgetSettings) {
const seriesSettings = settings as TimeSeriesChartKeySettings;
let yAxisId = seriesSettings.yAxisId;
if (!this.yAxisIds.includes(yAxisId)) {
yAxisId = 'default';
}
this.timeSeriesChartKeySettingsForm = this.fb.group({
yAxisId: [yAxisId, []],
showInLegend: [seriesSettings.showInLegend, []],
dataHiddenByDefault: [seriesSettings.dataHiddenByDefault, []],
type: [seriesSettings.type, []],
lineSettings: [seriesSettings.lineSettings, []],
barSettings: [seriesSettings.barSettings, []]
});
}
protected validatorTriggers(): string[] {
return ['showInLegend', 'type'];
}
protected updateValidators(_emitEvent: boolean) {
const showInLegend: boolean = this.timeSeriesChartKeySettingsForm.get('showInLegend').value;
const type: TimeSeriesChartSeriesType = this.timeSeriesChartKeySettingsForm.get('type').value;
if (showInLegend) {
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').enable();
} else {
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').patchValue(false, {emitEvent: false});
this.timeSeriesChartKeySettingsForm.get('dataHiddenByDefault').disable();
}
if (type === TimeSeriesChartSeriesType.line) {
this.timeSeriesChartKeySettingsForm.get('lineSettings').enable();
this.timeSeriesChartKeySettingsForm.get('barSettings').disable();
} else if (type === TimeSeriesChartSeriesType.bar) {
this.timeSeriesChartKeySettingsForm.get('lineSettings').disable();
this.timeSeriesChartKeySettingsForm.get('barSettings').enable();
}
}
}

116
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.html

@ -0,0 +1,116 @@
<!--
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]="lineSettingsFormGroup">
<div *ngIf="chartType !== TimeSeriesChartType.point" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.line.line</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="showLine">
{{ 'widgets.time-series-chart.series.line.show-line' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="step">
{{ 'widgets.time-series-chart.series.line.step-line' | translate }}
</mat-slide-toggle>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="stepType">
<mat-option *ngFor="let stepType of lineSeriesStepTypes" [value]="stepType">
{{ lineSeriesStepTypeTranslations.get(stepType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="smooth">
{{ 'widgets.time-series-chart.series.line.smooth-line' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.line-type</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="lineType">
<mat-option *ngFor="let lineType of timeSeriesLineTypes" [value]="lineType">
{{ timeSeriesLineTypeTranslations.get(lineType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.line-width</div>
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" formControlName="lineWidth" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="chartType !== TimeSeriesChartType.point; else pointSettings" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.series.point.points</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="showPoints">
{{ 'widgets.time-series-chart.series.point.show-points' | translate }}
</mat-slide-toggle>
</div>
<ng-container *ngTemplateOutlet="pointSettings"></ng-container>
</div>
<tb-time-series-chart-fill-settings
formControlName="fillAreaSettings">
</tb-time-series-chart-fill-settings>
</ng-container>
<ng-template #pointSettings [formGroup]="lineSettingsFormGroup">
<div class="tb-form-row space-between column-lt-md">
<mat-slide-toggle class="mat-slide" formControlName="showPointLabel">
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.series.point.point-label-hint' | translate}}">
{{ 'widgets.time-series-chart.series.point.point-label' | translate }}
</div>
</mat-slide-toggle>
<div fxLayout="row" fxFlex.lt-md fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" fxFlex.lt-md appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="pointLabelPosition">
<mat-option *ngFor="let position of seriesLabelPositions" [value]="position">
{{ seriesLabelPositionTranslations.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-font-settings formControlName="pointLabelFont"
clearButton
disabledLineHeight
forceSizeUnit="px"
[previewText]="pointLabelPreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="pointLabelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.series.point.point-shape</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="pointShape">
<mat-option *ngFor="let shape of timeSeriesChartShapes" [value]="shape">
{{ timeSeriesChartShapeTranslations.get(shape) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.series.point.point-size</div>
<mat-form-field class="medium-width number" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" formControlName="pointSize" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}"/>
</mat-form-field>
</div>
</ng-template>

192
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts

@ -0,0 +1,192 @@
///
/// 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, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
LineSeriesSettings,
lineSeriesStepTypes,
lineSeriesStepTypeTranslations,
seriesLabelPositions,
seriesLabelPositionTranslations,
timeSeriesChartShapes,
timeSeriesChartShapeTranslations, TimeSeriesChartType,
timeSeriesLineTypes,
timeSeriesLineTypeTranslations
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
@Component({
selector: 'tb-time-series-chart-line-settings',
templateUrl: './time-series-chart-line-settings.component.html',
styleUrls: ['./../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartLineSettingsComponent),
multi: true
}
]
})
export class TimeSeriesChartLineSettingsComponent implements OnInit, ControlValueAccessor {
TimeSeriesChartType = TimeSeriesChartType;
lineSeriesStepTypes = lineSeriesStepTypes;
lineSeriesStepTypeTranslations = lineSeriesStepTypeTranslations;
timeSeriesLineTypes = timeSeriesLineTypes;
timeSeriesLineTypeTranslations = timeSeriesLineTypeTranslations;
seriesLabelPositions = seriesLabelPositions;
seriesLabelPositionTranslations = seriesLabelPositionTranslations;
timeSeriesChartShapes = timeSeriesChartShapes;
timeSeriesChartShapeTranslations = timeSeriesChartShapeTranslations;
pointLabelPreviewFn = this._pointLabelPreviewFn.bind(this);
@Input()
disabled: boolean;
@Input()
chartType: TimeSeriesChartType;
private modelValue: LineSeriesSettings;
private propagateChange = null;
public lineSettingsFormGroup: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private dataKeyConfigComponent: DataKeyConfigComponent,
private fb: UntypedFormBuilder) {
}
ngOnInit(): void {
this.lineSettingsFormGroup = this.fb.group({
showLine: [null, []],
step: [null, []],
stepType: [null, []],
smooth: [null, []],
lineType: [null, []],
lineWidth: [null, [Validators.min(0)]],
showPoints: [null, []],
showPointLabel: [null, []],
pointLabelPosition: [null, []],
pointLabelFont: [null, []],
pointLabelColor: [null, []],
pointShape: [null, []],
pointSize: [null, [Validators.min(0)]],
fillAreaSettings: [null, []]
});
this.lineSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
merge(this.lineSettingsFormGroup.get('showLine').valueChanges,
this.lineSettingsFormGroup.get('step').valueChanges,
this.lineSettingsFormGroup.get('showPointLabel').valueChanges)
.subscribe(() => {
this.updateValidators();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.lineSettingsFormGroup.disable({emitEvent: false});
} else {
this.lineSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: LineSeriesSettings): void {
this.modelValue = value;
this.lineSettingsFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateValidators() {
const showLine: boolean = this.lineSettingsFormGroup.get('showLine').value;
const step: boolean = this.lineSettingsFormGroup.get('step').value;
const showPointLabel: boolean = this.lineSettingsFormGroup.get('showPointLabel').value;
if (showLine) {
this.lineSettingsFormGroup.get('step').enable({emitEvent: false});
if (step) {
this.lineSettingsFormGroup.get('stepType').enable({emitEvent: false});
this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false});
} else {
this.lineSettingsFormGroup.get('stepType').disable({emitEvent: false});
this.lineSettingsFormGroup.get('smooth').enable({emitEvent: false});
}
this.lineSettingsFormGroup.get('lineType').enable({emitEvent: false});
this.lineSettingsFormGroup.get('lineWidth').enable({emitEvent: false});
} else {
this.lineSettingsFormGroup.get('step').disable({emitEvent: false});
this.lineSettingsFormGroup.get('stepType').disable({emitEvent: false});
this.lineSettingsFormGroup.get('smooth').disable({emitEvent: false});
this.lineSettingsFormGroup.get('lineType').disable({emitEvent: false});
this.lineSettingsFormGroup.get('lineWidth').disable({emitEvent: false});
}
if (showPointLabel) {
this.lineSettingsFormGroup.get('pointLabelPosition').enable({emitEvent: false});
this.lineSettingsFormGroup.get('pointLabelFont').enable({emitEvent: false});
this.lineSettingsFormGroup.get('pointLabelColor').enable({emitEvent: false});
} else {
this.lineSettingsFormGroup.get('pointLabelPosition').disable({emitEvent: false});
this.lineSettingsFormGroup.get('pointLabelFont').disable({emitEvent: false});
this.lineSettingsFormGroup.get('pointLabelColor').disable({emitEvent: false});
}
}
private updateModel() {
this.modelValue = this.lineSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
private _pointLabelPreviewFn(): string {
const dataKey = this.dataKeyConfigComponent.modelValue;
const widgetConfig = this.dataKeyConfigComponent.widgetConfig;
const units = dataKey.units && dataKey.units.length ? dataKey.units : widgetConfig.config.units;
const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals :
(isDefinedAndNotNull(widgetConfig.config.decimals) ? widgetConfig.config.decimals : 2);
return formatValue(22, decimals, units, false);
}
}

179
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html

@ -0,0 +1,179 @@
<!--
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]="timeSeriesChartWidgetSettingsForm">
<tb-time-series-chart-y-axes-panel
formControlName="yAxes"
(axisRemoved)="yAxisRemoved($event)"
advanced>
</tb-time-series-chart-y-axes-panel>
<tb-time-series-chart-thresholds-panel
formControlName="thresholds"
[aliasController]="aliasController"
[dataKeyCallbacks]="dataKeyCallbacks"
[datasource]="datasource"
[widgetConfig]="widgetConfig?.config"
[yAxisIds]="yAxisIds">
</tb-time-series-chart-thresholds-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.chart-style</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="dataZoom">
{{ 'widgets.time-series-chart.data-zoom' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="stack">
<div tb-hint-tooltip-icon="{{'widgets.time-series-chart.stack-mode-hint' | translate}}">
{{ 'widgets.time-series-chart.stack-mode' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.time-series-chart.axis.x-axis</div>
<tb-time-series-chart-axis-settings
formControlName="xAxis"
advanced
axisType="xAxis">
</tb-time-series-chart-axis-settings>
</div>
<tb-time-series-no-aggregation-bar-width-settings
*ngIf="chartType === TimeSeriesChartType.default || chartType === TimeSeriesChartType.bar"
formControlName="noAggregationBarWidthSettings">
</tb-time-series-no-aggregation-bar-width-settings>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetSettingsForm.get('showLegend').value"
[disabled]="!timeSeriesChartWidgetSettingsForm.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'legend.label' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendLabelFont"
previewText="Temperature">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendLabelColor">
</tb-color-input>
</div>
</div>
<tb-legend-config hideDirection
formControlName="legendConfig">
</tb-legend-config>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeSeriesChartWidgetSettingsForm.get('showTooltip').value"
[disabled]="!timeSeriesChartWidgetSettingsForm.get('showTooltip').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.tooltip' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.trigger' | translate }}</div>
<mat-chip-listbox class="center-stretch" formControlName="tooltipTrigger">
<mat-chip-option [selectable]="timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.point"
[value]="EChartsTooltipTrigger.point">{{ 'tooltip.trigger-point' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').value !== EChartsTooltipTrigger.axis"
[value]="EChartsTooltipTrigger.axis">{{ 'tooltip.trigger-axis' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="tooltipValueFont"
[previewText]="tooltipValuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="tooltipShowDate">
{{ 'tooltip.date' | translate }}
</mat-slide-toggle>
<div fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex excludeLastUpdateAgo includeAuto
formControlName="tooltipDateFormat"></tb-date-format-select>
<tb-font-settings formControlName="tooltipDateFont"
[previewText]="tooltipDatePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipDateColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipBackgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-blur' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
<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">
<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>
</div>
</ng-container>

222
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts

@ -0,0 +1,222 @@
///
/// 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, Injector } from '@angular/core';
import {
DataKey,
Datasource,
legendPositions,
legendPositionTranslationMap,
WidgetSettings,
WidgetSettingsComponent
} from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models';
import { EChartsTooltipTrigger } from '../../chart/echarts-widget.models';
import {
timeSeriesChartWidgetDefaultSettings,
TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import {
TimeSeriesChartKeySettings,
TimeSeriesChartType, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
@Component({
selector: 'tb-time-series-chart-widget-settings',
templateUrl: './time-series-chart-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.widgetConfig.config.datasources;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get yAxisIds(): TimeSeriesChartYAxisId[] {
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetSettingsForm.get('yAxes').value;
return Object.keys(yAxes);
}
TimeSeriesChartType = TimeSeriesChartType;
EChartsTooltipTrigger = EChartsTooltipTrigger;
legendPositions = legendPositions;
legendPositionTranslationMap = legendPositionTranslationMap;
timeSeriesChartWidgetSettingsForm: UntypedFormGroup;
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this);
chartType: TimeSeriesChartType = TimeSeriesChartType.default;
constructor(protected store: Store<AppState>,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store);
}
public yAxisRemoved(yAxisId: TimeSeriesChartYAxisId): void {
if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources.length > 1) {
for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) {
const datasource = this.widgetConfig.config.datasources[i];
this.removeYaxisId(datasource.dataKeys, yAxisId);
}
}
}
protected settingsForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetSettingsForm;
}
protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
if (isDefinedAndNotNull(params.chartType)) {
this.chartType = params.chartType;
}
}
protected defaultSettings(): WidgetSettings {
return mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartWidgetDefaultSettings);
}
protected onSettingsSet(settings: WidgetSettings) {
this.timeSeriesChartWidgetSettingsForm = this.fb.group({
yAxes: [settings.yAxes, []],
thresholds: [settings.thresholds, []],
dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []],
xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
showLegend: [settings.showLegend, []],
legendLabelFont: [settings.legendLabelFont, []],
legendLabelColor: [settings.legendLabelColor, []],
legendConfig: [settings.legendConfig, []],
showTooltip: [settings.showTooltip, []],
tooltipTrigger: [settings.tooltipTrigger, []],
tooltipValueFont: [settings.tooltipValueFont, []],
tooltipValueColor: [settings.tooltipValueColor, []],
tooltipShowDate: [settings.tooltipShowDate, []],
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
background: [settings.background, []],
padding: [settings.padding, []]
});
}
protected validatorTriggers(): string[] {
return ['showLegend', 'showTooltip', 'tooltipShowDate'];
}
protected updateValidators(emitEvent: boolean) {
const showLegend: boolean = this.timeSeriesChartWidgetSettingsForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetSettingsForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').value;
if (showLegend) {
this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').enable();
this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').enable();
this.timeSeriesChartWidgetSettingsForm.get('legendConfig').enable();
} else {
this.timeSeriesChartWidgetSettingsForm.get('legendLabelFont').disable();
this.timeSeriesChartWidgetSettingsForm.get('legendLabelColor').disable();
this.timeSeriesChartWidgetSettingsForm.get('legendConfig').disable();
}
if (showTooltip) {
this.timeSeriesChartWidgetSettingsForm.get('tooltipTrigger').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false});
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable();
if (tooltipShowDate) {
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').enable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').enable();
} else {
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').disable();
}
} else {
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFont').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipValueColor').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').disable({emitEvent: false});
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFont').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateColor').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipDateInterval').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').disable();
this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable();
}
}
private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean {
let changed = false;
if (series) {
series.forEach(key => {
const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings);
if (keySettings.yAxisId === yAxisId) {
keySettings.yAxisId = 'default';
changed = true;
}
});
}
return changed;
}
private _tooltipValuePreviewFn(): string {
return formatValue(22, 0, '°C', false);
}
private _tooltipDatePreviewFn(): string {
const dateFormat: DateFormatSettings = this.timeSeriesChartWidgetSettingsForm.get('tooltipDateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

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

@ -28,6 +28,7 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an
import {
WidgetAction,
WidgetActionType,
widgetActionTypes,
widgetActionTypeTranslationMap,
widgetType
} from '@shared/models/widget.models';
@ -88,7 +89,7 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
@Input()
callbacks: WidgetActionCallbacks;
widgetActionTypes = Object.keys(WidgetActionType);
widgetActionTypes = widgetActionTypes;
widgetActionTypeTranslations = widgetActionTypeTranslationMap;
widgetActionType = WidgetActionType;

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

Loading…
Cancel
Save