Browse Source

merge with develop/3.5.2

pull/8825/head
YevhenBondarenko 3 years ago
parent
commit
015429d750
  1. 4
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 12
      application/src/main/data/json/system/widget_bundles/charts.json
  3. 63
      application/src/main/data/upgrade/3.5.1/schema_update.sql
  4. 12
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  5. 253
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  6. 52
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  7. 2
      application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java
  8. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  9. 237
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  10. 18
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java
  11. 35
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java
  12. 19
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  14. 16
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  15. 3
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  16. 3
      application/src/main/resources/thingsboard.yml
  17. 9
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  18. 4
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  19. 1
      application/src/test/resources/application-test.properties
  20. 40
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java
  21. 34
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java
  22. 26
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java
  23. 35
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java
  24. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/edge/EdgeEventService.java
  25. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  26. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  27. 1
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java
  28. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
  29. 15
      common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
  30. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java
  31. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java
  32. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  33. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java
  34. 29
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  35. 21
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java
  36. 43
      dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java
  37. 2
      dao/src/main/resources/sql/schema-entities.sql
  38. 26
      dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java
  39. 3
      dao/src/test/resources/application-test.properties
  40. 5
      dao/src/test/resources/sql/system-test-psql.sql
  41. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  42. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  43. 17
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  44. 16
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  45. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  46. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss
  47. 15
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  48. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  49. 1
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss
  50. 14
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  51. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  52. 14
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts
  53. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html
  54. 7
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  55. 95
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html
  56. 173
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts
  57. 113
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html
  58. 167
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.ts
  59. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  60. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  61. 37
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  63. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  64. 33
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  65. 6
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html
  66. 20
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  67. 107
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss
  68. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  69. 17
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html
  70. 17
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  71. 11
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html
  72. 11
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts
  73. 2
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html
  74. 2
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  75. 1
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  76. 9
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  77. 19
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html
  78. 13
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  79. 13
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html
  80. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html
  81. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html
  82. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts
  83. 87
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html
  84. 104
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html
  85. 109
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  86. 297
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
  87. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts
  88. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html
  89. 72
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html
  90. 66
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss
  91. 15
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts
  92. 489
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  93. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts
  94. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts
  95. 56
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html
  96. 57
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts
  97. 50
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html
  98. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts
  99. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts
  100. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts

4
application/src/main/data/json/system/widget_bundles/cards.json

@ -64,7 +64,9 @@
"settingsDirective": "tb-timeseries-table-widget-settings", "settingsDirective": "tb-timeseries-table-widget-settings",
"dataKeySettingsDirective": "tb-timeseries-table-key-settings", "dataKeySettingsDirective": "tb-timeseries-table-key-settings",
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings", "latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true}" "hasBasicMode": true,
"basicModeDirective": "tb-timeseries-table-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":null}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"displayTimewindow\":true,\"configMode\":\"basic\"}"
} }
}, },
{ {

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

@ -161,7 +161,9 @@
"settingsDirective": "tb-flot-line-widget-settings", "settingsDirective": "tb-flot-line-widget-settings",
"dataKeySettingsDirective": "tb-flot-line-key-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" "hasBasicMode": true,
"basicModeDirective": "tb-flot-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"fontSize\":10,\"fontColor\":\"#545454\",\"showTooltip\":true,\"tooltipIndividual\":false,\"tooltipCumulative\":false,\"hideZeros\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"grid\":{\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1,\"color\":\"#545454\",\"backgroundColor\":null,\"tickColor\":\"#DDDDDD\"},\"xaxis\":{\"title\":null,\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"min\":0,\"max\":1.2,\"title\":null,\"showLabels\":true,\"color\":\"#545454\",\"tickSize\":null,\"tickDecimals\":0,\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"shadowSize\":4,\"smoothLines\":false,\"comparisonEnabled\":false,\"timeForComparison\":\"previousInterval\",\"comparisonCustomIntervalValue\":7200000,\"xaxisSecond\":{\"axisPosition\":\"top\",\"title\":null,\"showLabels\":true},\"showLegend\":true,\"legendConfig\":{\"direction\":\"column\",\"position\":\"right\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false,\"showLatest\":false},\"customLegendEnabled\":false,\"dataKeysListForLabels\":[]},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"configMode\":\"basic\",\"showTitleIcon\":false,\"titleIcon\":\"waterfall_chart\",\"iconColor\":\"#1F6BDD\"}"
} }
}, },
{ {
@ -183,7 +185,9 @@
"settingsDirective": "tb-flot-line-widget-settings", "settingsDirective": "tb-flot-line-widget-settings",
"dataKeySettingsDirective": "tb-flot-line-key-settings", "dataKeySettingsDirective": "tb-flot-line-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"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 < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"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 < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"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\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"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 Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" "hasBasicMode": true,
"basicModeDirective": "tb-flot-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"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 < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"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 < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"stack\":false,\"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\":\"\"},\"shadowSize\":4,\"smoothLines\":false,\"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 Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\"}"
} }
}, },
{ {
@ -204,7 +208,9 @@
"settingsDirective": "tb-flot-bar-widget-settings", "settingsDirective": "tb-flot-bar-widget-settings",
"dataKeySettingsDirective": "tb-flot-bar-key-settings", "dataKeySettingsDirective": "tb-flot-bar-key-settings",
"latestDataKeySettingsDirective": "tb-flot-latest-key-settings", "latestDataKeySettingsDirective": "tb-flot-latest-key-settings",
"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\":{}}" "hasBasicMode": true,
"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\"}"
} }
} }
] ]

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

@ -53,6 +53,69 @@ $$;
-- NOTIFICATION CONFIGS VERSION CONTROL END -- NOTIFICATION CONFIGS VERSION CONTROL END
-- EDGE EVENTS MIGRATION START
DO
$$
DECLARE table_partition RECORD;
BEGIN
-- in case of running the upgrade script a second time:
IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_edge_event')) THEN
ALTER TABLE edge_event RENAME TO old_edge_event;
CREATE INDEX IF NOT EXISTS idx_old_edge_event_created_time_tmp ON old_edge_event(created_time);
ALTER INDEX IF EXISTS idx_edge_event_tenant_id_and_created_time RENAME TO idx_old_edge_event_tenant_id_and_created_time;
FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
FROM pg_tables WHERE tablename LIKE 'edge_event_%'
LOOP
EXECUTE format('ALTER TABLE %s RENAME TO old_edge_event_%s', table_partition.name, table_partition.partition_ts);
END LOOP;
ELSE
RAISE NOTICE 'Table old_edge_event already exists, leaving as is';
END IF;
END;
$$;
CREATE TABLE IF NOT EXISTS edge_event (
seq_id INT GENERATED ALWAYS AS IDENTITY,
id uuid NOT NULL,
created_time bigint NOT NULL,
edge_id uuid,
edge_event_type varchar(255),
edge_event_uid varchar(255),
entity_id uuid,
edge_event_action varchar(255),
body varchar(10000000),
tenant_id uuid,
ts bigint NOT NULL
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_edge_event_tenant_id_and_created_time ON edge_event(tenant_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_edge_event_id ON edge_event(id);
ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
CREATE OR REPLACE PROCEDURE migrate_edge_event(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
LANGUAGE plpgsql AS
$$
DECLARE
p RECORD;
partition_end_ts BIGINT;
BEGIN
FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms
LOOP
partition_end_ts = p.partition_ts + partition_size_ms;
RAISE NOTICE '[edge_event] Partition to create : [%-%]', p.partition_ts, partition_end_ts;
EXECUTE format('CREATE TABLE IF NOT EXISTS edge_event_%s PARTITION OF edge_event ' ||
'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts);
END LOOP;
INSERT INTO edge_event (id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts)
SELECT id, created_time, edge_id, edge_event_type, edge_event_uid, entity_id, edge_event_action, body, tenant_id, ts
FROM old_edge_event
WHERE created_time >= start_time_ms AND created_time < end_time_ms;
END;
$$;
-- EDGE EVENTS MIGRATION END
ALTER TABLE resource ALTER TABLE resource
ADD COLUMN IF NOT EXISTS etag varchar; ADD COLUMN IF NOT EXISTS etag varchar;

12
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -73,10 +73,14 @@ public class AppActor extends ContextAwareActor {
@Override @Override
protected boolean doProcess(TbActorMsg msg) { protected boolean doProcess(TbActorMsg msg) {
if (!ruleChainsInitialized) { if (!ruleChainsInitialized) {
initTenantActors(); if (MsgType.APP_INIT_MSG.equals(msg.getMsgType())) {
ruleChainsInitialized = true; initTenantActors();
if (msg.getMsgType() != MsgType.APP_INIT_MSG && msg.getMsgType() != MsgType.PARTITION_CHANGE_MSG) { ruleChainsInitialized = true;
log.warn("Rule Chains initialized by unexpected message: {}", msg); } else {
if (!msg.getMsgType().isIgnoreOnStart()) {
log.warn("Attempt to initialize Rule Chains by unexpected message: {}", msg);
}
return true;
} }
} }
switch (msg.getMsgType()) { switch (msg.getMsgType()) {

253
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -207,42 +207,222 @@ public class ControllerConstants {
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. "; protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION = protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
"{\n" + "{\n" +
" \"device\": {\n" + " \"device\": {\n" +
" \"name\": \"LwRpk00000000\",\n" + " \"name\":\"Name_DeviceWithCredantial_AccessToken\",\n" +
" \"type\": \"lwm2mProfileRpk\"\n" + " \"label\":\"Label_DeviceWithCredantial_AccessToken\",\n" +
" },\n" + " \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" + " \"credentials\": {\n" +
" \"id\": \"null\",\n" + " \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"createdTime\": 0,\n" + " \"credentialsId\": \"6hmxew8pmmzng4e3une2\"\n" +
" \"deviceId\": \"null\",\n" + " }\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\": {\n" +
" \"client\": {\n" +
" \"endpoint\": \"LwRpk00000000\",\n" +
" \"securityConfigClientMode\": \"RPK\",\n" +
" \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" +
" },\n" +
" \"bootstrap\": {\n" +
" \"bootstrapServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" },\n" +
" \"lwm2mServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}"; "}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN = protected static final String DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; "{\n" +
" \"id\": {\n" +
" \"id\":\"c886a090-168d-11ee-87c9-6f157dbc816a\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"c5fb3ac0-168d-11ee-87c9-6f157dbc816a\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une4\"\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_AccessToken_Default\",\n" +
" \"label\":\"Label_DeviceWithCredantial_AccessToken_Default\",\n" +
" \"type\": \"default\"\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une3\"\n" +
" }\n" +
"}";
protected static final String certificateValue = "\"-----BEGIN CERTIFICATE----- " +
"MIICMTCCAdegAwIBAgIUI9dBuwN6pTtK6uZ03rkiCwV4wEYwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTYxN1oXDTI0MDMyODE0NTYxN1owbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlQ2VydGlmaWNhdGVAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9Zo791qKQiGNBm11r4ZGxh+w+ossZL3xc46ufq5QckQHP7zkD2XDAcmP5GvdkM1sBFN9AWaCkQfNnWmfERsOOKNTMFEwHQYDVR0OBBYEFFFc5uyCyglQoZiKhzXzMcQ3BKORMB8GA1UdIwQYMBaAFFFc5uyCyglQoZiKhzXzMcQ3BKORMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANbA9CuhoOifZMMmqkpuld+65CR+ItKdXeRAhLMZuccuAiB0FSQB34zMutXrZj1g8Gl5OkE7YryFHbei1z0SveHR8g== " +
"-----END CERTIFICATE-----\"";
protected static final String certificateId = "\"84f5911765abba1f96bf4165604e9e90338fc6214081a8e623b6ff9669aedb27\"";
protected static final String certificateValueUpdate = "\"-----BEGIN CERTIFICATE----- " +
"MIICMTCCAdegAwIBAgIUUEKxS9hTz4l+oLUMF0LV6TC/gCIwCgYIKoZIzj0EAwIwbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MB4XDTIzMDMyOTE0NTczNloXDTI0MDMyODE0NTczNlowbjELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRowGAYDVQQKDBFUaGluZ3NCb2FyZCwgSW5jLjEwMC4GA1UEAwwnZGV2aWNlUHJvZmlsZUNlcnRAWDUwOVByb3Zpc2lvblN0cmF0ZWd5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECMlWO72krDoUL9FQjUmSCetkhaEGJUfQkdSfkLSNa0GyAEIMbfmzI4zITeapunu4rGet3EMyLydQzuQanBicp6NTMFEwHQYDVR0OBBYEFHpZ78tPnztNii4Da/yCw6mhEIL3MB8GA1UdIwQYMBaAFHpZ78tPnztNii4Da/yCw6mhEIL3MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgJ7qyMFqNcwSYkH6o+UlQXzLWfwZbNjVk+aR7foAZNGsCIQDsd7v3WQIGHiArfZeDs1DLEDuV/2h6L+ZNoGNhEKL+1A== " +
"-----END CERTIFICATE-----\"";
protected static final String certificateIdUpdate = "\"6b8adb49015500e51a527acd332b51684ab9b49b4ade03a9582a44c455e2e9b6\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_X509_Certificate\",\n" +
" \"label\":\"Label_DeviceWithCredantial_X509_Certificate\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"X509_CERTIFICATE\",\n" +
" \"credentialsId\": " + certificateId + ",\n" +
" \"credentialsValue\": " + certificateValue + "\n" +
" }\n" +
"}";
protected static final String DEVICE_UPDATE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION =
"{\n" +
" \"id\": {\n" +
" \"id\":\"309bd9c0-14f4-11ee-9fc9-d9b7463abb63\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"3092b200-14f4-11ee-9fc9-d9b7463abb63\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"X509_CERTIFICATE\",\n" +
" \"credentialsId\": " + certificateIdUpdate + ",\n" +
" \"credentialsValue\": " + certificateValueUpdate + "\n" +
"}";
protected static final String MQTT_BASIC_VALUE = "\"{\\\"clientId\\\":\\\"5euh5nzm34bjjh1efmlt\\\",\\\"userName\\\":\\\"onasd1lgwasmjl7v2v7h\\\",\\\"password\\\":\\\"b9xtm4ny8kt9zewaga5o\\\"}\"";
protected static final String MQTT_BASIC_VALUE_UPDATE = "\"{\\\"clientId\\\":\\\"juy03yv4owqxcmqhqtvk\\\",\\\"userName\\\":\\\"ov19fxca0cyjn7lm7w7u\\\",\\\"password\\\":\\\"twy94he114dfi9usyk1o\\\"}\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_DeviceWithCredantial_MQTT_Basic\",\n" +
" \"label\":\"Label_DeviceWithCredantial_MQTT_Basic\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"MQTT_BASIC\",\n" +
" \"credentialsValue\": " + MQTT_BASIC_VALUE + "\n" +
" }\n" +
"}";
protected static final String DEVICE_UPDATE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION =
"{\n" +
" \"id\": {\n" +
" \"id\":\"d877ffb0-14f5-11ee-9fc9-d9b7463abb63\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"d875dcd0-14f5-11ee-9fc9-d9b7463abb63\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"MQTT_BASIC\",\n" +
" \"credentialsValue\": " + MQTT_BASIC_VALUE_UPDATE + "\n" +
"}";
protected static final String CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION =
" \"{" +
"\\\"client\\\":{ " +
"\\\"endpoint\\\":\\\"LwRpk00000000\\\", " +
"\\\"securityConfigClientMode\\\":\\\"RPK\\\", " +
"\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\"" +
" }, " +
"\\\"bootstrap\\\":{ " +
"\\\"bootstrapServer\\\":{ " +
"\\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}, " +
"\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}" +
"} " +
"}\"";
protected static final String CREDENTIALS_VALUE_UPDATE_LVM2M_RPK_DESCRIPTION =
" \"{" +
"\\\"client\\\":{ " +
"\\\"endpoint\\\":\\\"LwRpk00000000\\\", " +
"\\\"securityConfigClientMode\\\":\\\"RPK\\\", " +
"\\\"key\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdvBZZ2vQRK9wgDhctj6B1c7bxR3Z0wYg1+YdoYFnVUKWb+rIfTTyYK9tmQJx5Vlb5fxdLnVv1RJOPiwsLIQbAA==\\\"" +
" }, " +
"\\\"bootstrap\\\":{ " +
"\\\"bootstrapServer\\\":{ " +
"\\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}, " +
"\\\"lwm2mServer\\\":{ \\\"securityMode\\\":\\\"RPK\\\", " +
"\\\"clientPublicKeyOrId\\\":\\\"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\\\", " +
"\\\"clientSecretKey\\\":\\\"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\\\"" +
"}" +
"} " +
"}\"";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\":\"Name_LwRpk00000000\",\n" +
" \"label\":\"Label_LwRpk00000000\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"a660bd50-10ef-11ee-8737-b5634e73c779\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\":\n" + CREDENTIALS_VALUE_LVM2M_RPK_DESCRIPTION + "\n" +
" }\n" +
"}";
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION =
"{\n" +
" \"id\": {\n" +
" \"id\":\"e238d4d0-1689-11ee-98c6-1713c1be5a8e\"\n" +
" },\n" +
" \"deviceId\": {\n" +
" \"id\":\"e232e160-1689-11ee-98c6-1713c1be5a8e\",\n" +
" \"entityType\":\"DEVICE\"\n" +
" },\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\":\n" + CREDENTIALS_VALUE_UPDATE_LVM2M_RPK_DESCRIPTION + "\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_DEFAULT_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_X509_CERTIFICATE_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_MQTT_BASIC_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE + protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
@ -254,7 +434,7 @@ public class ControllerConstants {
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" + " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n"; " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"schedule\":{\n" + " \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" + " \"type\":\"SPECIFIC_TIME\",\n" +
@ -269,7 +449,7 @@ public class ControllerConstants {
" }\n" + " }\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END; MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"schedule\":{\n" + " \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" + " \"type\":\"CUSTOM\",\n" +
@ -321,9 +501,9 @@ public class ControllerConstants {
" }\n" + " }\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END; MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"spec\":{\n" + " \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" + " \"type\":\"REPEATING\",\n" +
@ -339,7 +519,8 @@ public class ControllerConstants {
" }\n" + " }\n" +
"}" + "}" +
MARKDOWN_CODE_BLOCK_END; MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" + "{\n" +
" \"spec\":{\n" + " \"spec\":{\n" +
" \"type\":\"DURATION\",\n" + " \"type\":\"DURATION\",\n" +

52
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -93,7 +93,15 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
@ -182,9 +190,20 @@ public class DeviceController extends BaseController {
@ApiOperation(value = "Create Device (saveDevice) with credentials ", @ApiOperation(value = "Create Device (saveDevice) with credentials ",
notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK +
"Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " + "Requires to provide the Device Credentials object as well as an existing device profile ID or use \"default\".\n" +
"You may find the example of LwM2M device and RPK credentials below: \n\n" + "You may find the example of device with different type of credentials below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN + "- Credentials type: <b>\"Access token\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device profile default</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_ACCESS_TOKEN_DEFAULT_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@ -277,10 +296,27 @@ public class DeviceController extends BaseController {
return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser()); return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser());
} }
@ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " + @ApiOperation(value = "Update device credentials (updateDeviceCredentials)",
"Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value. " + notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. \" +\n" +
"Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device. " + "Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value.\n" +
"The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'." + TENANT_AUTHORITY_PARAGRAPH) "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device.\n" +
"The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'.\n" +
"You may find the example of device with different type of credentials below: \n\n" +
"- Credentials type: <b>\"Access token\"</b> with <b>device ID</b> and with <b>device ID</b> below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_ACCESS_TOKEN_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"X509\"</b> with <b>device profile ID</b> below: \n\n" +
"Note: <b>credentialsId</b> - format <b>Sha3Hash</b>, <b>certificateValue</b> - format <b>PEM</b> (with \"--BEGIN CERTIFICATE----\" and -\"----END CERTIFICATE-\").\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_X509_CERTIFICATE_DESCRIPTION_MARKDOWN + "\n\n" +
"- Credentials type: <b>\"MQTT_BASIC\"</b> with <b>device profile ID</b> below: \n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_MQTT_BASIC_DESCRIPTION_MARKDOWN + "\n\n" +
"- You may find the example of <b>LwM2M</b> device and <b>RPK</b> credentials below: \n\n" +
"Note: LwM2M device - only existing device profile ID (Transport configuration -> Transport type: \"LWM2M\".\n\n" +
DEVICE_UPDATE_CREDENTIALS_PARAM_LVM2M_RPK_DESCRIPTION_MARKDOWN + "\n\n" +
"Update to real value:\n" +
" - 'id' (this is id of Device Credentials -> \"Get Device Credentials (getDeviceCredentialsByDeviceId)\",\n" +
" - 'deviceId.id' (this is id of Device).\n" +
"Remove 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device entity." +
TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/device/credentials", method = RequestMethod.POST) @RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
@ResponseBody @ResponseBody

2
application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java

@ -85,6 +85,6 @@ public class EdgeEventController extends BaseController {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
checkEdgeId(edgeId, Operation.READ); checkEdgeId(edgeId, Operation.READ);
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false)); return checkNotNull(edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink));
} }
} }

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

@ -341,7 +341,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
sessionNewEvents.put(edgeId, false); sessionNewEvents.put(edgeId, false);
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override @Override
public void onSuccess(Void result) { public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) {
sessionNewEvents.put(edgeId, true);
}
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);
} }

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

@ -24,6 +24,7 @@ import io.grpc.stub.StreamObserver;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.data.util.Pair;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -35,6 +36,8 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
@ -68,17 +71,15 @@ import org.thingsboard.server.service.edge.rpc.fetch.GeneralEdgeEventFetcher;
import java.io.Closeable; import java.io.Closeable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Data @Data
@ -89,6 +90,7 @@ public final class EdgeGrpcSession implements Closeable {
private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private final UUID sessionId; private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener; private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
@ -103,6 +105,12 @@ public final class EdgeGrpcSession implements Closeable {
private boolean connected; private boolean connected;
private boolean syncCompleted; private boolean syncCompleted;
private Long newStartTs;
private Long previousStartTs;
private Long newStartSeqId;
private Long previousStartSeqId;
private Long seqIdEnd;
private EdgeVersion edgeVersion; private EdgeVersion edgeVersion;
private int maxInboundMessageSize; private int maxInboundMessageSize;
@ -204,10 +212,10 @@ public final class EdgeGrpcSession implements Closeable {
EdgeEventFetcher next = cursor.getNext(); EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
ListenableFuture<UUID> uuidListenableFuture = startProcessingEdgeEvents(next); ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next);
Futures.addCallback(uuidListenableFuture, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable UUID result) { public void onSuccess(@Nullable Pair<Long, Long> result) {
doSync(cursor); doSync(cursor);
} }
@ -307,36 +315,51 @@ public final class EdgeGrpcSession implements Closeable {
sendDownlinkMsg(edgeConfigMsg); sendDownlinkMsg(edgeConfigMsg);
} }
ListenableFuture<Void> processEdgeEvents() throws Exception { ListenableFuture<Boolean> processEdgeEvents() throws Exception {
SettableFuture<Void> result = SettableFuture.create(); SettableFuture<Boolean> result = SettableFuture.create();
log.trace("[{}] starting processing edge events", this.sessionId); log.trace("[{}] starting processing edge events", this.sessionId);
if (isConnected() && isSyncCompleted()) { if (isConnected() && isSyncCompleted()) {
Long queueStartTs = getQueueStartTs().get(); Pair<Long, Long> startTsAndSeqId = getQueueStartTsAndSeqId().get();
this.previousStartTs = startTsAndSeqId.getFirst();
this.previousStartSeqId = startTsAndSeqId.getSecond();
GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher( GeneralEdgeEventFetcher fetcher = new GeneralEdgeEventFetcher(
queueStartTs, this.previousStartTs,
this.previousStartSeqId,
this.seqIdEnd,
false,
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService()); ctx.getEdgeEventService());
ListenableFuture<UUID> ifOffsetFuture = startProcessingEdgeEvents(fetcher); Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
Futures.addCallback(ifOffsetFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable UUID ifOffset) { public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
if (ifOffset != null) { if (newStartTsAndSeqId != null) {
Long newStartTs = Uuids.unixTimestamp(ifOffset); ListenableFuture<List<String>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
ListenableFuture<List<String>> updateFuture = updateQueueStartTs(newStartTs);
Futures.addCallback(updateFuture, new FutureCallback<>() { Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable List<String> list) { public void onSuccess(@Nullable List<String> list) {
log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs); log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId);
result.set(null); if (fetcher.isSeqIdNewCycleStarted()) {
seqIdEnd = fetcher.getSeqIdEnd();
boolean newEventsAvailable = isNewEdgeEventsAvailable();
result.set(newEventsAvailable);
} else {
seqIdEnd = null;
boolean newEventsAvailable = isSeqIdStartedNewCycle();
if (!newEventsAvailable) {
newEventsAvailable = isNewEdgeEventsAvailable();
}
result.set(newEventsAvailable);
}
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}] Failed to update queue offset [{}]", sessionId, ifOffset, t); log.error("[{}] Failed to update queue offset [{}]", sessionId, newStartTsAndSeqId, t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}] ifOffset is null. Skipping iteration without db update", sessionId); log.trace("[{}] newStartTsAndSeqId is null. Skipping iteration without db update", sessionId);
result.set(null); result.set(null);
} }
} }
@ -354,14 +377,14 @@ public final class EdgeGrpcSession implements Closeable {
return result; return result;
} }
private ListenableFuture<UUID> startProcessingEdgeEvents(EdgeEventFetcher fetcher) { private ListenableFuture<Pair<Long, Long>> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
SettableFuture<UUID> result = SettableFuture.create(); SettableFuture<Pair<Long, Long>> result = SettableFuture.create();
PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()); PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount());
processEdgeEvents(fetcher, pageLink, result); processEdgeEvents(fetcher, pageLink, result);
return result; return result;
} }
private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<UUID> result) { private void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) {
try { try {
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
@ -377,8 +400,15 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && pageData.hasNext()) { if (isConnected() && pageData.hasNext()) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result); processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else { } else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1);
result.set(ifOffset); UUID idOffset = latestEdgeEvent.getUuidId();
if (idOffset != null) {
Long newStartTs = Uuids.unixTimestamp(idOffset);
long newStartSeqId = latestEdgeEvent.getSeqId();
result.set(Pair.of(newStartTs, newStartSeqId));
} else {
result.set(null);
}
} }
} }
} }
@ -461,69 +491,113 @@ public final class EdgeGrpcSession implements Closeable {
} }
} }
private DownlinkMsg convertToDownlinkMsg(EdgeEvent edgeEvent) { private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent); List<DownlinkMsg> result = new ArrayList<>();
DownlinkMsg downlinkMsg = null; for (EdgeEvent edgeEvent : edgeEvents) {
try { log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
switch (edgeEvent.getAction()) { DownlinkMsg downlinkMsg = null;
case UPDATED: try {
case ADDED: switch (edgeEvent.getAction()) {
case DELETED: case UPDATED:
case ASSIGNED_TO_EDGE: case ADDED:
case UNASSIGNED_FROM_EDGE: case DELETED:
case ALARM_ACK: case ASSIGNED_TO_EDGE:
case ALARM_CLEAR: case UNASSIGNED_FROM_EDGE:
case CREDENTIALS_UPDATED: case ALARM_ACK:
case RELATION_ADD_OR_UPDATE: case ALARM_CLEAR:
case RELATION_DELETED: case CREDENTIALS_UPDATED:
case ASSIGNED_TO_CUSTOMER: case RELATION_ADD_OR_UPDATE:
case UNASSIGNED_FROM_CUSTOMER: case RELATION_DELETED:
case CREDENTIALS_REQUEST: case CREDENTIALS_REQUEST:
case RPC_CALL: case RPC_CALL:
downlinkMsg = convertEntityEventToDownlink(edgeEvent); case ASSIGNED_TO_CUSTOMER:
log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg); case UNASSIGNED_FROM_CUSTOMER:
break; downlinkMsg = convertEntityEventToDownlink(edgeEvent);
case ATTRIBUTES_UPDATED: log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
case POST_ATTRIBUTES: break;
case ATTRIBUTES_DELETED: case ATTRIBUTES_UPDATED:
case TIMESERIES_UPDATED: case POST_ATTRIBUTES:
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent); case ATTRIBUTES_DELETED:
break; case TIMESERIES_UPDATED:
default: downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction()); break;
default:
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
}
} catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e);
}
if (downlinkMsg != null) {
result.add(downlinkMsg);
}
}
return result;
}
private ListenableFuture<Pair<Long, Long>> getQueueStartTsAndSeqId() {
ListenableFuture<List<AttributeKvEntry>> future =
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(QUEUE_START_TS_ATTR_KEY, QUEUE_START_SEQ_ID_ATTR_KEY));
return Futures.transform(future, attributeKvEntries -> {
long startTs = 0L;
long startSeqId = 0L;
for (AttributeKvEntry attributeKvEntry : attributeKvEntries) {
if (QUEUE_START_TS_ATTR_KEY.equals(attributeKvEntry.getKey())) {
startTs = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
}
if (QUEUE_START_SEQ_ID_ATTR_KEY.equals(attributeKvEntry.getKey())) {
startSeqId = attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
}
}
if (startSeqId == 0L) {
startSeqId = findStartSeqIdFromOldestEventIfAny();
} }
return Pair.of(startTs, startSeqId);
}, ctx.getGrpcCallbackExecutorService());
}
private boolean isSeqIdStartedNewCycle() {
try {
TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), 0L, this.previousStartSeqId == 0 ? null : this.previousStartSeqId - 1, pageLink);
return !edgeEvents.getData().isEmpty();
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Exception during converting edge event to downlink msg", edge.getTenantId(), this.sessionId, e); log.error("[{}][{}][{}] Failed to execute isSeqIdStartedNewCycle", edge.getTenantId(), edge.getId(), sessionId, e);
} }
return downlinkMsg; return false;
} }
private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) { private boolean isNewEdgeEventsAvailable() {
return edgeEvents try {
.stream() TimePageLink pageLink = new TimePageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount(), 0, null, null, this.newStartTs, System.currentTimeMillis());
.map(this::convertToDownlinkMsg) PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), this.newStartSeqId, null, pageLink);
.filter(Objects::nonNull) return !edgeEvents.getData().isEmpty();
.collect(Collectors.toList()); } catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e);
}
return false;
} }
private ListenableFuture<Long> getQueueStartTs() { private long findStartSeqIdFromOldestEventIfAny() {
ListenableFuture<Optional<AttributeKvEntry>> future = long startSeqId = 0L;
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY); try {
return Futures.transform(future, attributeKvEntryOpt -> { TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime"), null, null);
if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) { PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink);
AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get(); if (!edgeEvents.getData().isEmpty()) {
return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L; startSeqId = edgeEvents.getData().get(0).getSeqId() - 1;
} else {
return 0L;
} }
}, ctx.getGrpcCallbackExecutorService()); } catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute findStartSeqIdFromOldestEventIfAny", edge.getTenantId(), edge.getId(), sessionId, e);
}
return startSeqId;
} }
private ListenableFuture<List<String>> updateQueueStartTs(Long newStartTs) { private ListenableFuture<List<String>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) {
log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs); this.newStartTs = pair.getFirst();
List<AttributeKvEntry> attributes = Collections.singletonList( this.newStartSeqId = pair.getSecond();
new BaseAttributeKvEntry( log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);
new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis())); List<AttributeKvEntry> attributes = Arrays.asList(
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_TS_ATTR_KEY, this.newStartTs), System.currentTimeMillis()),
new BaseAttributeKvEntry(new LongDataEntry(QUEUE_START_SEQ_ID_ATTR_KEY, this.newStartSeqId), System.currentTimeMillis()));
return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes); return ctx.getAttributesService().save(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, attributes);
} }
@ -693,8 +767,11 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void interruptPreviousSendDownlinkMsgsTask() { private void interruptPreviousSendDownlinkMsgsTask() {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()
stopCurrentSendDownlinkMsgsTask(true); || sessionState.getScheduledSendDownlinkTask() != null && !sessionState.getScheduledSendDownlinkTask().isCancelled()) {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
}
} }
private void interruptGeneralProcessingOnSync() { private void interruptGeneralProcessingOnSync() {

18
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AlarmMsgConstructor.java

@ -18,7 +18,10 @@ package org.thingsboard.server.service.edge.rpc.constructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
@ -47,13 +50,22 @@ public class AlarmMsgConstructor {
String entityName = null; String entityName = null;
switch (alarm.getOriginator().getEntityType()) { switch (alarm.getOriginator().getEntityType()) {
case DEVICE: case DEVICE:
entityName = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId())).getName(); Device deviceById = deviceService.findDeviceById(tenantId, new DeviceId(alarm.getOriginator().getId()));
if (deviceById != null) {
entityName = deviceById.getName();
}
break; break;
case ASSET: case ASSET:
entityName = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId())).getName(); Asset assetById = assetService.findAssetById(tenantId, new AssetId(alarm.getOriginator().getId()));
if (assetById != null) {
entityName = assetById.getName();
}
break; break;
case ENTITY_VIEW: case ENTITY_VIEW:
entityName = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId())).getName(); EntityView entityViewById = entityViewService.findEntityViewById(tenantId, new EntityViewId(alarm.getOriginator().getId()));
if (entityViewById != null) {
entityName = entityViewById.getName();
}
break; break;
} }
AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder() AlarmUpdateMsg.Builder builder = AlarmUpdateMsg.newBuilder()

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

@ -16,19 +16,27 @@
package org.thingsboard.server.service.edge.rpc.fetch; package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class GeneralEdgeEventFetcher implements EdgeEventFetcher { public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
private final Long queueStartTs; private final Long queueStartTs;
private Long seqIdStart;
@Getter
private Long seqIdEnd;
@Getter
private boolean seqIdNewCycleStarted;
private Long maxReadRecordsCount;
private final EdgeEventService edgeEventService; private final EdgeEventService edgeEventService;
@Override @Override
@ -37,13 +45,32 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
pageSize, pageSize,
0, 0,
null, null,
new SortOrder("createdTime", SortOrder.Direction.ASC), null,
queueStartTs, queueStartTs,
null); System.currentTimeMillis());
} }
@Override @Override
public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) { public PageData<EdgeEvent> fetchEdgeEvents(TenantId tenantId, Edge edge, PageLink pageLink) {
return edgeEventService.findEdgeEvents(tenantId, edge.getId(), (TimePageLink) pageLink, true); try {
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().isEmpty()) {
this.seqIdEnd = Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount);
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, seqIdEnd, (TimePageLink) pageLink);
if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) {
log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId());
this.seqIdNewCycleStarted = true;
this.seqIdStart = 0L;
} else {
edgeEvents = new PageData<>();
log.warn("[{}] unexpected edge notification message received. " +
"no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId());
}
}
return edgeEvents;
} catch (Exception e) {
log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId());
}
return new PageData<>();
} }
} }

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

@ -202,22 +202,27 @@ public class DefaultDataUpdateService implements DataUpdateService {
} else { } else {
log.info("Skipping audit logs migration"); log.info("Skipping audit logs migration");
} }
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false); migrateEdgeEvents("Starting edge events migration. ");
if (!skipEdgeEventsMigration) {
log.info("Starting edge events migration. Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
break; break;
case "3.5.1": case "3.5.1":
log.info("Updating data from version 3.5.1 to 3.5.2 ..."); log.info("Updating data from version 3.5.1 to 3.5.2 ...");
migrateEdgeEvents("Starting edge events migration - adding seq_id column. ");
break; break;
default: default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
} }
} }
private void migrateEdgeEvents(String logPrefix) {
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
if (!skipEdgeEventsMigration) {
log.info(logPrefix + "Can be skipped with TB_SKIP_EDGE_EVENTS_MIGRATION env variable set to true");
edgeEventDao.migrateEdgeEvents();
} else {
log.info("Skipping edge events migration");
}
}
@Override @Override
public void upgradeRuleNodes() { public void upgradeRuleNodes() {
try { try {

2
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -108,6 +108,8 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
.alarmOriginatorName(alarmInfo.getOriginatorName()) .alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmSeverity(alarmInfo.getSeverity()) .alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus()) .alarmStatus(alarmInfo.getStatus())
.acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId()) .alarmCustomerId(alarmInfo.getCustomerId())
.build(); .build();
} }

16
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -259,7 +259,21 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
} }
void launchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { void launchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix)); if (isReady) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
} else {
scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
}
}
private void scheduleLaunchConsumer(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {
repartitionExecutor.schedule(() -> {
if (isReady) {
consumersExecutor.execute(() -> consumerLoop(consumer, configuration, stats, threadSuffix));
} else {
scheduleLaunchConsumer(consumer, configuration, stats, threadSuffix);
}
}, 10, TimeUnit.SECONDS);
} }
void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) { void consumerLoop(TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> consumer, org.thingsboard.server.common.data.queue.Queue configuration, TbRuleEngineConsumerStats stats, String threadSuffix) {

3
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -68,7 +68,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected volatile ExecutorService consumersExecutor; protected volatile ExecutorService consumersExecutor;
protected volatile ExecutorService notificationsConsumerExecutor; protected volatile ExecutorService notificationsConsumerExecutor;
protected volatile boolean stopped = false; protected volatile boolean stopped = false;
protected volatile boolean isReady = false;
protected final ActorSystemContext actorContext; protected final ActorSystemContext actorContext;
protected final DataDecodingEncodingService encodingService; protected final DataDecodingEncodingService encodingService;
protected final TbTenantProfileCache tenantProfileCache; protected final TbTenantProfileCache tenantProfileCache;
@ -108,6 +108,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
public void onApplicationEvent(ApplicationReadyEvent event) { public void onApplicationEvent(ApplicationReadyEvent event) {
log.info("Subscribing to notifications: {}", nfConsumer.getTopic()); log.info("Subscribing to notifications: {}", nfConsumer.getTopic());
this.nfConsumer.subscribe(); this.nfConsumer.subscribe();
this.isReady = true;
launchNotificationsConsumer(); launchNotificationsConsumer();
launchMainConsumers(); launchMainConsumers();
} }

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

@ -497,6 +497,9 @@ cache:
entityCount: entityCount:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}" timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}"
maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}" maxSize: "${CACHE_SPECS_ENTITY_COUNT_MAX_SIZE:100000}"
resourceInfo:
timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_INFO_TTL:1440}"
maxSize: "${CACHE_SPECS_RESOURCE_INFO_MAX_SIZE:100000}"
alarmTypes: alarmTypes:
timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}" timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}"
maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}" maxSize: "${CACHE_SPECS_ALARM_TYPES_MAX_SIZE:10000}"

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

@ -100,6 +100,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@TestPropertySource(properties = { @TestPropertySource(properties = {
"edges.enabled=true", "edges.enabled=true",
"queue.rule-engine.stats.enabled=false",
}) })
abstract public class AbstractEdgeTest extends AbstractControllerTest { abstract public class AbstractEdgeTest extends AbstractControllerTest {
@ -181,14 +182,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@After @After
public void afterTest() throws Exception { public void afterTest() throws Exception {
try {
edgeImitator.disconnect();
} catch (Exception ignored){}
loginSysAdmin(); loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getUuidId()) doDelete("/api/tenant/" + savedTenant.getUuidId())
.andExpect(status().isOk()); .andExpect(status().isOk());
try {
edgeImitator.disconnect();
} catch (Exception ignored) {}
} }
private void installation() { private void installation() {

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

@ -94,8 +94,6 @@ public class EdgeImitator {
@Getter @Getter
private UplinkResponseMsg latestResponseMsg; private UplinkResponseMsg latestResponseMsg;
private boolean connected = false;
public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException { public EdgeImitator(String host, int port, String routingKey, String routingSecret) throws NoSuchFieldException, IllegalAccessException {
edgeRpcClient = new EdgeGrpcClient(); edgeRpcClient = new EdgeGrpcClient();
messagesLatch = new CountDownLatch(0); messagesLatch = new CountDownLatch(0);
@ -120,7 +118,6 @@ public class EdgeImitator {
} }
public void connect() { public void connect() {
connected = true;
edgeRpcClient.connect(routingKey, routingSecret, edgeRpcClient.connect(routingKey, routingSecret,
this::onUplinkResponse, this::onUplinkResponse,
this::onEdgeUpdate, this::onEdgeUpdate,
@ -131,7 +128,6 @@ public class EdgeImitator {
} }
public void disconnect() throws InterruptedException { public void disconnect() throws InterruptedException {
connected = false;
edgeRpcClient.disconnect(false); edgeRpcClient.disconnect(false);
} }

1
application/src/test/resources/application-test.properties

@ -14,6 +14,7 @@ edges.enabled=false
edges.storage.no_read_records_sleep=500 edges.storage.no_read_records_sleep=500
edges.storage.sleep_between_batches=500 edges.storage.sleep_between_batches=500
actors.rpc.sequential=true actors.rpc.sequential=true
queue.rule-engine.stats.enabled=true
# Transports disabled to speed up the context init. Particular transport will be enabled with @TestPropertySource in respective tests # Transports disabled to speed up the context init. Particular transport will be enabled with @TestPropertySource in respective tests
transport.http.enabled=false transport.http.enabled=false

40
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.resourceInfo;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serializable;
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class ResourceInfoCacheKey implements Serializable {
private final TenantId tenantId;
private final TbResourceId tbResourceId;
@Override
public String toString() {
return tenantId + "_" + tbResourceId;
}
}

34
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCaffeineCache.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.resourceInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.TbResourceInfo;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("ResourceInfoCache")
public class ResourceInfoCaffeineCache extends CaffeineTbTransactionalCache<ResourceInfoCacheKey, TbResourceInfo> {
public ResourceInfoCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.RESOURCE_INFO_CACHE);
}
}

26
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoEvictEvent.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.resourceInfo;
import lombok.Data;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class ResourceInfoEvictEvent {
private final TenantId tenantId;
private final TbResourceId resourceId;
}

35
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoRedisCache.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cache.resourceInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbFSTRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.TbResourceInfo;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("ResourceInfoCache")
public class ResourceInfoRedisCache extends RedisTbTransactionalCache<ResourceInfoCacheKey, TbResourceInfo> {
public ResourceInfoRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.RESOURCE_INFO_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

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

@ -26,7 +26,7 @@ public interface EdgeEventService {
ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent); ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent);
PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink);
/** /**
* Executes stored procedure to cleanup old edge events. * Executes stored procedure to cleanup old edge events.

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

@ -44,5 +44,6 @@ public class CacheConstants {
public static final String USER_SETTINGS_CACHE = "userSettings"; public static final String USER_SETTINGS_CACHE = "userSettings";
public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles"; public static final String DASHBOARD_TITLES_CACHE = "dashboardTitles";
public static final String ENTITY_COUNT_CACHE = "entityCount"; public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo";
public static final String ALARM_TYPES_CACHE = "alarmTypes"; public static final String ALARM_TYPES_CACHE = "alarmTypes";
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -153,7 +153,6 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
} }
public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) { public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) {
if (cleared) { if (cleared) {
return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
} else { } else {

1
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEvent.java

@ -31,6 +31,7 @@ import java.util.UUID;
@ToString(callSuper = true) @ToString(callSuper = true)
public class EdgeEvent extends BaseData<EdgeEventId> { public class EdgeEvent extends BaseData<EdgeEventId> {
private long seqId;
private TenantId tenantId; private TenantId tenantId;
private EdgeId edgeId; private EdgeId edgeId;
private EdgeEventActionType action; private EdgeEventActionType action;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java

@ -42,6 +42,8 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
private String alarmOriginatorName; private String alarmOriginatorName;
private AlarmSeverity alarmSeverity; private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus; private AlarmStatus alarmStatus;
private boolean acknowledged;
private boolean cleared;
private CustomerId alarmCustomerId; private CustomerId alarmCustomerId;
@Override @Override

15
common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.msg; package org.thingsboard.server.common.msg;
import lombok.Getter;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@ -28,7 +29,7 @@ public enum MsgType {
* *
* See {@link PartitionChangeMsg} * See {@link PartitionChangeMsg}
*/ */
PARTITION_CHANGE_MSG, PARTITION_CHANGE_MSG(true),
APP_INIT_MSG, APP_INIT_MSG,
@ -108,7 +109,7 @@ public enum MsgType {
* Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement * Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement
*/ */
SESSION_TIMEOUT_MSG, SESSION_TIMEOUT_MSG(true),
STATS_PERSIST_TICK_MSG, STATS_PERSIST_TICK_MSG,
@ -130,4 +131,14 @@ public enum MsgType {
EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG, EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG,
EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG; EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG;
@Getter
private final boolean ignoreOnStart;
MsgType() {
this.ignoreOnStart = false;
}
MsgType(boolean ignoreOnStart) {
this.ignoreOnStart = ignoreOnStart;
}
} }

4
dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java

@ -42,8 +42,8 @@ public class BaseEdgeEventService implements EdgeEventService {
} }
@Override @Override
public PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { public PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) {
return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, pageLink, withTsUpdate); return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, seqIdStart, seqIdEnd, pageLink);
} }
@Override @Override

4
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeEventDao.java

@ -43,10 +43,12 @@ public interface EdgeEventDao extends Dao<EdgeEvent> {
* *
* @param tenantId the tenantId * @param tenantId the tenantId
* @param edgeId the edgeId * @param edgeId the edgeId
* @param seqIdStart the seq id start
* @param seqIdEnd the seq id end
* @param pageLink the pageLink * @param pageLink the pageLink
* @return the event list * @return the event list
*/ */
PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate); PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink);
/** /**
* Executes stored procedure to cleanup old edge events. * Executes stored procedure to cleanup old edge events.

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -535,6 +535,7 @@ public class ModelConstants {
*/ */
public static final String EDGE_EVENT_TABLE_NAME = "edge_event"; public static final String EDGE_EVENT_TABLE_NAME = "edge_event";
public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String EDGE_EVENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
public static final String EDGE_EVENT_SEQUENTIAL_ID_PROPERTY = "seq_id";
public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id"; public static final String EDGE_EVENT_EDGE_ID_PROPERTY = "edge_id";
public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type"; public static final String EDGE_EVENT_TYPE_PROPERTY = "edge_event_type";
public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action"; public static final String EDGE_EVENT_ACTION_PROPERTY = "edge_event_action";

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java

@ -43,6 +43,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PR
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_SEQUENTIAL_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY;
@ -57,6 +58,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@NoArgsConstructor @NoArgsConstructor
public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEntity<EdgeEvent> { public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEntity<EdgeEvent> {
@Column(name = EDGE_EVENT_SEQUENTIAL_ID_PROPERTY)
protected long seqId;
@Column(name = EDGE_EVENT_TENANT_ID_PROPERTY) @Column(name = EDGE_EVENT_TENANT_ID_PROPERTY)
private UUID tenantId; private UUID tenantId;
@ -120,6 +124,7 @@ public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEnt
edgeEvent.setAction(edgeEventAction); edgeEvent.setAction(edgeEventAction);
edgeEvent.setBody(entityBody); edgeEvent.setBody(entityBody);
edgeEvent.setUid(edgeEventUid); edgeEvent.setUid(edgeEventUid);
edgeEvent.setSeqId(seqId);
return edgeEvent; return edgeEvent;
} }

29
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -20,7 +20,11 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.cache.device.DeviceCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
@ -31,6 +35,7 @@ import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
@ -45,7 +50,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TbResourceDaoService") @Service("TbResourceDaoService")
@Slf4j @Slf4j
@AllArgsConstructor @AllArgsConstructor
public class BaseResourceService implements ResourceService { public class BaseResourceService extends AbstractCachedEntityService<ResourceInfoCacheKey, TbResourceInfo, ResourceInfoEvictEvent> implements ResourceService {
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId "; public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
private final TbResourceDao resourceDao; private final TbResourceDao resourceDao;
@ -55,10 +60,12 @@ public class BaseResourceService implements ResourceService {
@Override @Override
public TbResource saveResource(TbResource resource) { public TbResource saveResource(TbResource resource) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId); resourceValidator.validate(resource, TbResourceInfo::getTenantId);
try { try {
return resourceDao.save(resource.getTenantId(), resource); TbResource saved = resourceDao.save(resource.getTenantId(), resource);
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
return saved;
} catch (Exception t) { } catch (Exception t) {
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) { if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) {
String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName"; String field = ResourceType.LWM2M_MODEL.equals(resource.getResourceType()) ? "resourceKey" : "fileName";
@ -86,7 +93,9 @@ public class BaseResourceService implements ResourceService {
public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) { public TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId); log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
return resourceInfoDao.findById(tenantId, resourceId.getId());
return cache.getAndPutInTransaction(new ResourceInfoCacheKey(tenantId, resourceId),
() -> resourceInfoDao.findById(tenantId, resourceId.getId()), true);
} }
@Override @Override
@ -169,13 +178,11 @@ public class BaseResourceService implements ResourceService {
} }
}; };
protected Optional<ConstraintViolationException> extractConstraintViolationException(Exception t) { @TransactionalEventListener(classes = ResourceInfoEvictEvent.class)
if (t instanceof ConstraintViolationException) { @Override
return Optional.of((ConstraintViolationException) t); public void handleEvictEvent(ResourceInfoEvictEvent event) {
} else if (t.getCause() instanceof ConstraintViolationException) { if (event.getResourceId() != null) {
return Optional.of((ConstraintViolationException) (t.getCause())); cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId()));
} else {
return Optional.empty();
} }
} }
} }

21
dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java

@ -30,8 +30,10 @@ public interface EdgeEventRepository extends JpaRepository<EdgeEventEntity, UUID
@Query("SELECT e FROM EdgeEventEntity e WHERE " + @Query("SELECT e FROM EdgeEventEntity e WHERE " +
"e.tenantId = :tenantId " + "e.tenantId = :tenantId " +
"AND e.edgeId = :edgeId " + "AND e.edgeId = :edgeId " +
"AND (:startTime IS NULL OR e.createdTime > :startTime) " + "AND (:startTime IS NULL OR e.createdTime >= :startTime) " +
"AND (:endTime IS NULL OR e.createdTime <= :endTime) " + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " +
"AND (:seqIdStart IS NULL OR e.seqId > :seqIdStart) " +
"AND (:seqIdEnd IS NULL OR e.seqId < :seqIdEnd) " +
"AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" "AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))"
) )
Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId,
@ -39,20 +41,7 @@ public interface EdgeEventRepository extends JpaRepository<EdgeEventEntity, UUID
@Param("textSearch") String textSearch, @Param("textSearch") String textSearch,
@Param("startTime") Long startTime, @Param("startTime") Long startTime,
@Param("endTime") Long endTime, @Param("endTime") Long endTime,
@Param("seqIdStart") Long seqIdStart,
@Param("seqIdEnd") Long seqIdEnd,
Pageable pageable); Pageable pageable);
@Query("SELECT e FROM EdgeEventEntity e WHERE " +
"e.tenantId = :tenantId " +
"AND e.edgeId = :edgeId " +
"AND (:startTime IS NULL OR e.createdTime > :startTime) " +
"AND (:endTime IS NULL OR e.createdTime <= :endTime) " +
"AND e.edgeEventAction <> 'TIMESERIES_UPDATED' " +
"AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))"
)
Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(@Param("tenantId") UUID tenantId,
@Param("edgeId") UUID edgeId,
@Param("textSearch") String textSearch,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
Pageable pageable);
} }

43
dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeEventId; import org.thingsboard.server.common.data.id.EdgeEventId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
@ -43,7 +44,9 @@ import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -118,7 +121,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
} }
}; };
queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, 1, statsFactory); queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, 1, statsFactory);
queue.init(logExecutor, v -> edgeEventInsertRepository.save(v), queue.init(logExecutor, edgeEventInsertRepository::save,
Comparator.comparing(EdgeEventEntity::getTs) Comparator.comparing(EdgeEventEntity::getTs)
); );
} }
@ -171,29 +174,23 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
@Override @Override
public PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) { public PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) {
if (withTsUpdate) { List<SortOrder> sortOrders = new ArrayList<>();
return DaoUtil.toPageData( if (pageLink.getSortOrder() != null) {
edgeEventRepository sortOrders.add(pageLink.getSortOrder());
.findEdgeEventsByTenantIdAndEdgeId(
tenantId,
edgeId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getStartTime(),
pageLink.getEndTime(),
DaoUtil.toPageable(pageLink)));
} else {
return DaoUtil.toPageData(
edgeEventRepository
.findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(
tenantId,
edgeId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getStartTime(),
pageLink.getEndTime(),
DaoUtil.toPageable(pageLink)));
} }
sortOrders.add(new SortOrder("seqId"));
return DaoUtil.toPageData(
edgeEventRepository
.findEdgeEventsByTenantIdAndEdgeId(
tenantId,
edgeId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
pageLink.getStartTime(),
pageLink.getEndTime(),
seqIdStart,
seqIdEnd,
DaoUtil.toPageable(pageLink, sortOrders)));
} }
@Override @Override

2
dao/src/main/resources/sql/schema-entities.sql

@ -720,6 +720,7 @@ CREATE TABLE IF NOT EXISTS edge (
); );
CREATE TABLE IF NOT EXISTS edge_event ( CREATE TABLE IF NOT EXISTS edge_event (
seq_id INT GENERATED ALWAYS AS IDENTITY,
id uuid NOT NULL, id uuid NOT NULL,
created_time bigint NOT NULL, created_time bigint NOT NULL,
edge_id uuid, edge_id uuid,
@ -731,6 +732,7 @@ CREATE TABLE IF NOT EXISTS edge_event (
tenant_id uuid, tenant_id uuid,
ts bigint NOT NULL ts bigint NOT NULL
) PARTITION BY RANGE(created_time); ) PARTITION BY RANGE(created_time);
ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
CREATE TABLE IF NOT EXISTS rpc ( CREATE TABLE IF NOT EXISTS rpc (
id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY, id uuid NOT NULL CONSTRAINT rpc_pkey PRIMARY KEY,

26
dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java

@ -71,7 +71,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED); EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED);
edgeEventService.saveAsync(edgeEvent).get(); edgeEventService.saveAsync(edgeEvent).get();
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, new TimePageLink(1), false); PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, new TimePageLink(1));
Assert.assertFalse(edgeEvents.getData().isEmpty()); Assert.assertFalse(edgeEvents.getData().isEmpty());
EdgeEvent saved = edgeEvents.getData().get(0); EdgeEvent saved = edgeEvents.getData().get(0);
@ -113,7 +113,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Futures.allAsList(futures).get(); Futures.allAsList(futures).get();
TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime); TimePageLink pageLink = new TimePageLink(2, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), startTime, endTime);
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true); PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink);
Assert.assertNotNull(edgeEvents.getData()); Assert.assertNotNull(edgeEvents.getData());
Assert.assertEquals(2, edgeEvents.getData().size()); Assert.assertEquals(2, edgeEvents.getData().size());
@ -122,7 +122,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Assert.assertTrue(edgeEvents.hasNext()); Assert.assertTrue(edgeEvents.hasNext());
Assert.assertNotNull(pageLink.nextPageLink()); Assert.assertNotNull(pageLink.nextPageLink());
edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink.nextPageLink(), true); edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, pageLink.nextPageLink());
Assert.assertNotNull(edgeEvents.getData()); Assert.assertNotNull(edgeEvents.getData());
Assert.assertEquals(1, edgeEvents.getData().size()); Assert.assertEquals(1, edgeEvents.getData().size());
@ -132,26 +132,6 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
edgeEventService.cleanupEvents(1); edgeEventService.cleanupEvents(1);
} }
@Test
public void findEdgeEventsWithTsUpdateAndWithout() throws Exception {
EdgeId edgeId = new EdgeId(Uuids.timeBased());
DeviceId deviceId = new DeviceId(Uuids.timeBased());
TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime", SortOrder.Direction.ASC));
EdgeEvent edgeEventWithTsUpdate = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.TIMESERIES_UPDATED);
edgeEventService.saveAsync(edgeEventWithTsUpdate).get();
PageData<EdgeEvent> allEdgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, true);
PageData<EdgeEvent> edgeEventsWithoutTsUpdate = edgeEventService.findEdgeEvents(tenantId, edgeId, pageLink, false);
Assert.assertNotNull(allEdgeEvents.getData());
Assert.assertNotNull(edgeEventsWithoutTsUpdate.getData());
Assert.assertEquals(1, allEdgeEvents.getData().size());
Assert.assertEquals(allEdgeEvents.getData().get(0).getUuidId(), edgeEventWithTsUpdate.getUuidId());
Assert.assertTrue(edgeEventsWithoutTsUpdate.getData().isEmpty());
}
private ListenableFuture<Void> saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { private ListenableFuture<Void> saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception {
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED);
edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); edgeEvent.setId(new EdgeEventId(Uuids.startOf(time)));

3
dao/src/test/resources/application-test.properties

@ -74,6 +74,9 @@ cache.specs.dashboardTitles.maxSize=10000
cache.specs.entityCount.timeToLiveInMinutes=1440 cache.specs.entityCount.timeToLiveInMinutes=1440
cache.specs.entityCount.maxSize=10000 cache.specs.entityCount.maxSize=10000
cache.specs.resourceInfo.timeToLiveInMinutes=1440
cache.specs.resourceInfo.maxSize=10000
cache.specs.alarmTypes.timeToLiveInMinutes=60 cache.specs.alarmTypes.timeToLiveInMinutes=60
cache.specs.alarmTypes.maxSize=10000 cache.specs.alarmTypes.maxSize=10000

5
dao/src/test/resources/sql/system-test-psql.sql

@ -1,2 +1,5 @@
--PostgreSQL specific truncate to fit constraints --PostgreSQL specific truncate to fit constraints
TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm; TRUNCATE TABLE device_credentials, device, device_profile, asset, asset_profile, ota_package, rule_node_state, rule_node, rule_chain, alarm_comment, alarm, entity_alarm;
-- Decrease seq_id column to make sure to cover cases of new sequential cycle during the tests
ALTER SEQUENCE edge_event_seq_id_seq MAXVALUE 256;

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -76,6 +76,10 @@ public class TbDeviceProfileNode implements TbNode {
this.ctx = ctx; this.ctx = ctx;
scheduleAlarmHarvesting(ctx, null); scheduleAlarmHarvesting(ctx, null);
ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate); ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate);
initAlarmRuleState(false);
}
private void initAlarmRuleState(boolean printNewlyAddedDeviceStates) {
if (config.isFetchAlarmRulesStateOnStart()) { if (config.isFetchAlarmRulesStateOnStart()) {
log.info("[{}] Fetching alarm rule state", ctx.getSelfId()); log.info("[{}] Fetching alarm rule state", ctx.getSelfId());
int fetchCount = 0; int fetchCount = 0;
@ -86,7 +90,7 @@ public class TbDeviceProfileNode implements TbNode {
for (RuleNodeState rns : states.getData()) { for (RuleNodeState rns : states.getData()) {
fetchCount++; fetchCount++;
if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) { if (rns.getEntityId().getEntityType().equals(EntityType.DEVICE) && ctx.isLocalEntity(rns.getEntityId())) {
getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns); getOrCreateDeviceState(ctx, new DeviceId(rns.getEntityId().getId()), rns, printNewlyAddedDeviceStates);
} }
} }
} }
@ -130,7 +134,7 @@ public class TbDeviceProfileNode implements TbNode {
removeDeviceState(deviceId); removeDeviceState(deviceId);
ctx.tellSuccess(msg); ctx.tellSuccess(msg);
} else { } else {
DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null); DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null, false);
if (deviceState != null) { if (deviceState != null) {
deviceState.process(ctx, msg); deviceState.process(ctx, msg);
} else { } else {
@ -148,6 +152,7 @@ public class TbDeviceProfileNode implements TbNode {
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
// Cleanup the cache for all entities that are no longer assigned to current server partitions // Cleanup the cache for all entities that are no longer assigned to current server partitions
deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey())); deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey()));
initAlarmRuleState(true);
} }
@Override @Override
@ -156,13 +161,16 @@ public class TbDeviceProfileNode implements TbNode {
deviceStates.clear(); deviceStates.clear();
} }
protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns) { protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) {
DeviceState deviceState = deviceStates.get(deviceId); DeviceState deviceState = deviceStates.get(deviceId);
if (deviceState == null) { if (deviceState == null) {
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId);
if (deviceProfile != null) { if (deviceProfile != null) {
deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns); deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns);
deviceStates.put(deviceId, deviceState); deviceStates.put(deviceId, deviceState);
if (printNewlyAddedDeviceStates) {
log.info("[{}][{}] Device [{}] was added during PartitionChangeMsg", ctx.getTenantId(), ctx.getSelfId(), deviceId);
}
} }
} }
return deviceState; return deviceState;

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

@ -179,6 +179,7 @@ import * as ProtobufContentComponent from '@shared/components/protobuf-content.c
import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component'; import * as SlackConversationAutocompleteComponent from '@shared/components/slack-conversation-autocomplete.component';
import * as StringItemsListComponent from '@shared/components/string-items-list.component'; import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component'; import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -478,6 +479,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent, '@shared/components/slack-conversation-autocomplete.component': SlackConversationAutocompleteComponent,
'@shared/components/string-items-list.component': StringItemsListComponent, '@shared/components/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent, '@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent, '@home/components/entity/entities-table.component': EntitiesTableComponent,

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

@ -20,18 +20,10 @@
<h2 translate>widget.add</h2> <h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span> <span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="hasBasicMode" (valueChange)="widgetConfigMode = $event" ignoreMdLgSize="true" <tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
appearance="fill-invert" [options]="[ <tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
{ <tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
name: translate.instant('widget.basic-mode'), </tb-toggle-select>
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div> <div [tb-help]="helpLinkIdForWidgetType()"></div>
</div> </div>
<button mat-icon-button <button mat-icon-button
@ -57,6 +49,7 @@
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill" <tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController" [aliasController]="aliasController"
[stateController]="stateController" [stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget" [widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"> [widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content"> <div class="tb-preview-panel-content">

16
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -360,18 +360,10 @@
[isReadOnly]="true" [isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()"> (closeDetails)="onEditWidgetClosed()">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px"> <div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="tbEditWidget.hasBasicMode" (valueChange)="tbEditWidget.widgetConfigMode = $event" ignoreMdLgSize="true" <tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" [(ngModel)]="tbEditWidget.widgetConfigMode">
appearance="fill-invert" [options]="[ <tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
{ <tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
name: translate.instant('widget.basic-mode'), </tb-toggle-select>
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="tbEditWidget.widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div> <div [tb-help]="helpLinkIdForWidgetType()"></div>
</div> </div>
<tb-edit-widget #tbEditWidget <tb-edit-widget #tbEditWidget

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

@ -1152,7 +1152,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe( this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => { (widgetTypeInfo) => {
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo); const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
config.title = 'New ' + widgetTypeInfo.widgetName; if (!config.title) {
config.title = 'New ' + widgetTypeInfo.widgetName;
}
let newWidget: Widget = { let newWidget: Widget = {
isSystemType: widget.isSystemType, isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias, bundleAlias: widget.bundleAlias,

3
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss

@ -163,7 +163,8 @@ tb-dashboard-toolbar {
} }
} }
tb-states-component { tb-states-component,
tb-entity-state-controller {
pointer-events: all; pointer-events: all;
} }
} }

15
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -54,11 +54,14 @@
</button> </button>
</div> </div>
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="widget-preview-section" <div *ngIf="previewMode" class="widget-preview-background">
[aliasController]="aliasController" <tb-widget-preview class="widget-preview-section"
[stateController]="stateController" [aliasController]="aliasController"
[widget]="widget" [stateController]="stateController"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"> [dashboardTimewindow]="dashboard.configuration.timewindow"
</tb-widget-preview> [widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
</tb-widget-preview>
</div>
</fieldset> </fieldset>
</form> </form>

10
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss

@ -14,9 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.widget-preview-section { .widget-preview-background {
position: absolute; position: absolute;
top: 72px; top: 72px;
left: 0;
right: 0;
bottom: 0;
background: #fff;
}
.widget-preview-section {
position: absolute;
top: 0;
left: 16px; left: 16px;
right: 16px; right: 16px;
bottom: 16px; bottom: 16px;

1
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss

@ -16,6 +16,7 @@
:host { :host {
display: block; display: block;
.clear-alarm-rule { .clear-alarm-rule {
max-width: 100%;
border: 2px groove rgba(0, 0, 0, .45); border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;

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

@ -30,24 +30,34 @@ import {
} from '@home/components/widget/config/basic/cards/entities-table-basic-config.component'; } from '@home/components/widget/config/basic/cards/entities-table-basic-config.component';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component'; import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyRowComponent } from '@home/components/widget/config/basic/common/data-key-row.component'; import { DataKeyRowComponent } from '@home/components/widget/config/basic/common/data-key-row.component';
import {
TimeseriesTableBasicConfigComponent
} from '@home/components/widget/config/basic/cards/timeseries-table-basic-config.component';
import { FlotBasicConfigComponent } from '@home/components/widget/config/basic/chart/flot-basic-config.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
WidgetActionsPanelComponent, WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent, SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent, EntitiesTableBasicConfigComponent,
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
DataKeyRowComponent, DataKeyRowComponent,
DataKeysPanelComponent DataKeysPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
WidgetSettingsModule,
WidgetConfigComponentsModule WidgetConfigComponentsModule
], ],
exports: [ exports: [
WidgetActionsPanelComponent, WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent, SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent, EntitiesTableBasicConfigComponent,
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
DataKeyRowComponent, DataKeyRowComponent,
DataKeysPanelComponent DataKeysPanelComponent
] ]
@ -57,5 +67,7 @@ export class BasicWidgetConfigModule {
export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = { export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = {
'tb-simple-card-basic-config': SimpleCardBasicConfigComponent, 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent,
'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent 'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent,
'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent,
'tb-flot-basic-config': FlotBasicConfigComponent
}; };

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

@ -40,7 +40,7 @@
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div> <div class="tb-widget-config-panel-title" translate>widget-config.appearance</div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="showTitle"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }} {{ 'widget-config.card-title' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
@ -63,7 +63,7 @@
</div> </div>
</div> </div>
<div class="tb-widget-config-row space-between"> <div class="tb-widget-config-row space-between">
<div translate>widgets.table.show-card-buttons</div> <div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons"> <mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="search">{{ 'action.search' | translate }}</mat-chip-option> <mat-chip-option value="search">{{ 'action.search' | translate }}</mat-chip-option>
<mat-chip-option value="columnsToDisplay">{{ 'widgets.table.columns-to-display' | translate }}</mat-chip-option> <mat-chip-option value="columnsToDisplay">{{ 'widgets.table.columns-to-display' | translate }}</mat-chip-option>

14
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts

@ -28,6 +28,8 @@ import {
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
@Component({ @Component({
selector: 'tb-entities-table-basic-config', selector: 'tb-entities-table-basic-config',
@ -73,11 +75,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
protected onConfigSet(configData: WidgetConfigComponentData) { protected onConfigSet(configData: WidgetConfigComponentData) {
this.entitiesTableWidgetConfigForm = this.fb.group({ this.entitiesTableWidgetConfigForm = this.fb.group({
timewindowConfig: [{ timewindowConfig: [getTimewindowConfig(configData.config), []],
useDashboardTimewindow: configData.config.useDashboardTimewindow,
displayTimewindow: configData.config.useDashboardTimewindow,
timewindow: configData.config.timewindow
}, []],
datasources: [configData.config.datasources, []], datasources: [configData.config.datasources, []],
columns: [this.getColumns(configData.config.datasources), []], columns: [this.getColumns(configData.config.datasources), []],
showTitle: [configData.config.showTitle, []], showTitle: [configData.config.showTitle, []],
@ -155,13 +153,13 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
private getCardButtons(config: WidgetConfig): string[] { private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = []; const buttons: string[] = [];
if (config.settings?.enableSearch) { if (isUndefined(config.settings?.enableSearch) || config.settings?.enableSearch) {
buttons.push('search'); buttons.push('search');
} }
if (config.settings?.enableSelectColumnDisplay) { if (isUndefined(config.settings?.enableSelectColumnDisplay) || config.settings?.enableSelectColumnDisplay) {
buttons.push('columnsToDisplay'); buttons.push('columnsToDisplay');
} }
if (config.enableFullscreen) { if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen'); buttons.push('fullscreen');
} }
return buttons; return buttons;

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

@ -31,7 +31,7 @@
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div> <div class="tb-widget-config-panel-title" translate>widget-config.appearance</div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<div translate>widgets.simple-card.label</div> <div class="fixed-title-width" translate>widgets.simple-card.label</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
@ -57,7 +57,7 @@
</div> </div>
<div class="tb-widget-config-row space-between"> <div class="tb-widget-config-row space-between">
<div translate>widget-config.decimals-short</div> <div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>

7
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts

@ -27,6 +27,7 @@ import {
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
@Component({ @Component({
selector: 'tb-simple-card-basic-config', selector: 'tb-simple-card-basic-config',
@ -63,11 +64,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
protected onConfigSet(configData: WidgetConfigComponentData) { protected onConfigSet(configData: WidgetConfigComponentData) {
this.simpleCardWidgetConfigForm = this.fb.group({ this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [{ timewindowConfig: [getTimewindowConfig(configData.config), []],
useDashboardTimewindow: configData.config.useDashboardTimewindow,
displayTimewindow: configData.config.useDashboardTimewindow,
timewindow: configData.config.timewindow
}, []],
datasources: [configData.config.datasources, []], datasources: [configData.config.datasources, []],
label: [this.getDataKeyLabel(configData.config.datasources), []], label: [this.getDataKeyLabel(configData.config.datasources), []],
labelPosition: [configData.config.settings?.labelPosition, []], labelPosition: [configData.config.settings?.labelPosition, []],

95
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html

@ -0,0 +1,95 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="timeseriesTableWidgetConfigForm">
<tb-timewindow-config-panel formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeys
forceSingleDatasource
formControlName="datasources">
</tb-datasources>
<tb-data-keys-panel
panelTitle="{{ 'widgets.table.columns' | translate }}"
addKeyTitle="{{ 'widgets.table.add-column' | translate }}"
keySettingsTitle="{{ 'widgets.table.column-settings' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}"
requiredKeysText="{{ 'widgets.table.timeseries-column-error' | translate }}"
hideDataKeyColor
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="columns">
</tb-data-keys-panel>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between same-padding">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-material-icon-select asBoxInput
[color]="timeseriesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="search">{{ 'action.search' | translate }}</mat-chip-option>
<mat-chip-option value="columnsToDisplay">{{ 'widgets.table.columns-to-display' | translate }}</mat-chip-option>
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

173
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.ts

@ -0,0 +1,173 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } 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, WidgetConfig } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { deepClone, isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-timeseries-table-basic-config',
templateUrl: './timeseries-table-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class TimeseriesTableBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.timeseriesTableWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
timeseriesTableWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.timeseriesTableWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData,
[{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
this.timeseriesTableWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
columns: [this.getColumns(configData.config.datasources), []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
cardButtons: [this.getCardButtons(configData.config), []],
color: [configData.config.color, []],
backgroundColor: [configData.config.backgroundColor, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.setColumns(config.columns, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.showTitleIcon = config.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.color = config.color;
this.widgetConfig.config.backgroundColor = config.backgroundColor;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showTitleIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.timeseriesTableWidgetConfigForm.get('showTitle').value;
const showTitleIcon: boolean = this.timeseriesTableWidgetConfigForm.get('showTitleIcon').value;
if (showTitle) {
this.timeseriesTableWidgetConfigForm.get('title').enable();
this.timeseriesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false});
if (showTitleIcon) {
this.timeseriesTableWidgetConfigForm.get('titleIcon').enable();
this.timeseriesTableWidgetConfigForm.get('iconColor').enable();
} else {
this.timeseriesTableWidgetConfigForm.get('titleIcon').disable();
this.timeseriesTableWidgetConfigForm.get('iconColor').disable();
}
} else {
this.timeseriesTableWidgetConfigForm.get('title').disable();
this.timeseriesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false});
this.timeseriesTableWidgetConfigForm.get('titleIcon').disable();
this.timeseriesTableWidgetConfigForm.get('iconColor').disable();
}
this.timeseriesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent});
this.timeseriesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false});
this.timeseriesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent});
this.timeseriesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent});
}
private getColumns(datasources?: Datasource[]): DataKey[] {
if (datasources && datasources.length) {
const dataKeys = deepClone(datasources[0].dataKeys) || [];
dataKeys.forEach(k => {
(k as any).latest = false;
});
const latestDataKeys = deepClone(datasources[0].latestDataKeys) || [];
latestDataKeys.forEach(k => {
(k as any).latest = true;
});
return dataKeys.concat(latestDataKeys);
}
return [];
}
private setColumns(columns: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
const dataKeys = deepClone(columns.filter(c => !(c as any).latest));
dataKeys.forEach(k => delete (k as any).latest);
const latestDataKeys = deepClone(columns.filter(c => (c as any).latest));
latestDataKeys.forEach(k => delete (k as any).latest);
datasources[0].dataKeys = dataKeys;
datasources[0].latestDataKeys = latestDataKeys;
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.settings?.enableSearch) || config.settings?.enableSearch) {
buttons.push('search');
}
if (isUndefined(config.settings?.enableSelectColumnDisplay) || config.settings?.enableSelectColumnDisplay) {
buttons.push('columnsToDisplay');
}
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.settings.enableSearch = buttons.includes('search');
config.settings.enableSelectColumnDisplay = buttons.includes('columnsToDisplay');
config.enableFullscreen = buttons.includes('fullscreen');
}
}

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

@ -0,0 +1,113 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="flotWidgetConfigForm">
<tb-timewindow-config-panel formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
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 }}"
hideSourceSelection
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between same-padding">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-material-icon-select asBoxInput
[color]="flotWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between">
<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-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.chart.chart-appearance</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="verticalLines">
{{ 'widgets.chart.vertical-grid-lines' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="horizontalLines">
{{ 'widgets.chart.horizontal-grid-lines' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="flotWidgetConfigForm.get('showLegend').value" [disabled]="!flotWidgetConfigForm.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>
<tb-legend-config formControlName="legendConfig">
</tb-legend-config>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

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

@ -0,0 +1,167 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } 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, WidgetConfig } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isUndefined } from '@core/utils';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-flot-basic-config',
templateUrl: './flot-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class FlotBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.flotWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
flotWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.flotWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData,
[{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
this.flotWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
series: [this.getSeries(configData.config.datasources), []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
cardButtons: [this.getCardButtons(configData.config), []],
color: [configData.config.color, []],
backgroundColor: [configData.config.backgroundColor, []],
verticalLines: [configData.config.settings?.grid?.verticalLines, []],
horizontalLines: [configData.config.settings?.grid?.horizontalLines, []],
showLegend: [configData.config.settings?.showLegend, []],
legendConfig: [configData.config.settings?.legendConfig, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.showTitleIcon = config.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.backgroundColor = config.backgroundColor;
this.widgetConfig.config.settings.grid = this.widgetConfig.config.settings.grid || {};
this.widgetConfig.config.settings.grid.verticalLines = config.verticalLines;
this.widgetConfig.config.settings.grid.horizontalLines = config.horizontalLines;
this.widgetConfig.config.settings.showLegend = config.showLegend;
this.widgetConfig.config.settings.legendConfig = config.legendConfig;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showTitleIcon', 'showLegend'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.flotWidgetConfigForm.get('showTitle').value;
const showTitleIcon: boolean = this.flotWidgetConfigForm.get('showTitleIcon').value;
const showLegend: boolean = this.flotWidgetConfigForm.get('showLegend').value;
if (showTitle) {
this.flotWidgetConfigForm.get('title').enable();
this.flotWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false});
if (showTitleIcon) {
this.flotWidgetConfigForm.get('titleIcon').enable();
this.flotWidgetConfigForm.get('iconColor').enable();
} else {
this.flotWidgetConfigForm.get('titleIcon').disable();
this.flotWidgetConfigForm.get('iconColor').disable();
}
} else {
this.flotWidgetConfigForm.get('title').disable();
this.flotWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false});
this.flotWidgetConfigForm.get('titleIcon').disable();
this.flotWidgetConfigForm.get('iconColor').disable();
}
if (showLegend) {
this.flotWidgetConfigForm.get('legendConfig').enable();
} else {
this.flotWidgetConfigForm.get('legendConfig').disable();
}
this.flotWidgetConfigForm.get('title').updateValueAndValidity({emitEvent});
this.flotWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false});
this.flotWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent});
this.flotWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent});
this.flotWidgetConfigForm.get('legendConfig').updateValueAndValidity({emitEvent});
}
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 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');
}
}

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

@ -16,6 +16,12 @@
--> -->
<div [formGroup]="keyRowFormGroup" class="tb-data-key-row"> <div [formGroup]="keyRowFormGroup" class="tb-data-key-row">
<mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>
<mat-option [value]="true">{{ 'datakey.latest' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="tb-inline-field tb-key-field" subscriptSizing="dynamic"> <mat-form-field fxFlex class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList> <mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type" <mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
@ -147,7 +153,7 @@
</tb-widget-units> </tb-widget-units>
</div> </div>
<div class="tb-decimals-field"> <div class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field center number" subscriptSizing="dynamic"> <mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>

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

@ -43,6 +43,10 @@
} }
} }
.tb-source-field {
width: 140px;
}
.tb-color-field, .tb-units-field, .tb-decimals-field { .tb-color-field, .tb-units-field, .tb-decimals-field {
width: 60px; width: 60px;
display: flex; display: flex;

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

@ -153,6 +153,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.widgetConfigCallbacks; return this.widgetConfigComponent.widgetConfigCallbacks;
} }
get hasAdditionalLatestDataKeys(): boolean {
return this.dataKeysPanelComponent.hasAdditionalLatestDataKeys;
}
get widget(): Widget { get widget(): Widget {
return this.widgetConfigComponent.widget; return this.widgetConfigComponent.widget;
} }
@ -165,7 +169,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.aliasController; return this.widgetConfigComponent.aliasController;
} }
get datakeySettingsSchema(): JsonSettingsSchema { get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
} }
@ -173,6 +177,14 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective; return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective;
} }
get latestDataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema;
}
get latestDataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective;
}
get isEntityDatasource(): boolean { get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType); return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
} }
@ -193,6 +205,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.dataKeysPanelComponent.dragEnabled; return this.dataKeysPanelComponent.dragEnabled;
} }
get isLatestDataKeys(): boolean {
return this.hasAdditionalLatestDataKeys && this.keyRowFormGroup.get('latest').value === true;
}
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
@ -212,6 +228,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
units: [null, []], units: [null, []],
decimals: [null, []], decimals: [null, []],
}); });
if (this.hasAdditionalLatestDataKeys) {
this.keyRowFormGroup.addControl('latest', this.fb.control(false));
this.keyRowFormGroup.valueChanges.subscribe(
() => this.clearKeySearchCache()
);
}
this.keyRowFormGroup.valueChanges.subscribe( this.keyRowFormGroup.valueChanges.subscribe(
() => this.updateModel() () => this.updateModel()
); );
@ -286,6 +308,11 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
decimals: value?.decimals decimals: value?.decimals
}, {emitEvent: false} }, {emitEvent: false}
); );
if (this.hasAdditionalLatestDataKeys) {
this.keyRowFormGroup.patchValue({
latest: (value as any)?.latest
}, {emitEvent: false});
}
this.cd.markForCheck(); this.cd.markForCheck();
} }
@ -323,8 +350,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
data: { data: {
dataKey: deepClone(this.modelValue), dataKey: deepClone(this.modelValue),
dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general, dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general,
dataKeySettingsSchema: this.datakeySettingsSchema, dataKeySettingsSchema: this.isLatestDataKeys ? this.latestDataKeySettingsSchema : this.dataKeySettingsSchema,
dataKeySettingsDirective: this.dataKeySettingsDirective, dataKeySettingsDirective: this.isLatestDataKeys ? this.latestDataKeySettingsDirective : this.dataKeySettingsDirective,
dashboard: this.dashboard, dashboard: this.dashboard,
aliasController: this.aliasController, aliasController: this.aliasController,
widget: this.widget, widget: this.widget,
@ -399,7 +426,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
} else if (this.datasourceType === DatasourceType.entity && this.entityAliasId || } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId ||
this.datasourceType === DatasourceType.device && this.deviceId) { this.datasourceType === DatasourceType.device && this.deviceId) {
const dataKeyTypes = [DataKeyType.timeseries]; const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { if (this.isLatestDataKeys || this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute); dataKeyTypes.push(DataKeyType.attribute);
dataKeyTypes.push(DataKeyType.entityField); dataKeyTypes.push(DataKeyType.entityField);
if (this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.alarm) {
@ -428,7 +455,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
} }
private addKeyFromChipValue(chip: DataKey) { private addKeyFromChipValue(chip: DataKey) {
this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema); this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsSchema);
if (!this.keyRowFormGroup.get('label').value) { if (!this.keyRowFormGroup.get('label').value) {
this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false}); this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false});
} }

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

@ -19,6 +19,7 @@
<div class="tb-widget-config-panel-title">{{ panelTitle }}</div> <div class="tb-widget-config-panel-title">{{ panelTitle }}</div>
<div class="tb-data-keys-table"> <div class="tb-data-keys-table">
<div class="tb-data-keys-header"> <div class="tb-data-keys-header">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div> <div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div> <div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div> <div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div>
@ -52,6 +53,7 @@
</div> </div>
</div> </div>
</div> </div>
<tb-error *ngIf="errorText" noMargin [error]="errorText" style="padding-left: 12px;"></tb-error>
</div> </div>
<div> <div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()"> <button type="button" mat-stroked-button color="primary" (click)="addKey()">

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

@ -35,6 +35,9 @@
line-height: 20px; line-height: 20px;
letter-spacing: 0.2px; letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
&.tb-source-header {
width: 140px;
}
&.tb-color-header, &.tb-units-header, &.tb-decimals-header { &.tb-color-header, &.tb-units-header, &.tb-decimals-header {
width: 60px; width: 60px;
} }

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

@ -84,6 +84,9 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@Input() @Input()
noKeysText: string; noKeysText: string;
@Input()
requiredKeysText: string;
@Input() @Input()
datasourceType: DatasourceType; datasourceType: DatasourceType;
@ -97,12 +100,18 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean() @coerceBoolean()
hideDataKeyColor = false; hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideSourceSelection = false;
dataKeyType: DataKeyType; dataKeyType: DataKeyType;
alarmKeys: Array<DataKey>; alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>; functionTypeKeys: Array<DataKey>;
keysListFormGroup: UntypedFormGroup; keysListFormGroup: UntypedFormGroup;
errorText = '';
get widgetType(): widgetType { get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType; return this.widgetConfigComponent.widgetType;
} }
@ -111,6 +120,11 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
return this.widgetConfigComponent.widgetConfigCallbacks; return this.widgetConfigComponent.widgetConfigCallbacks;
} }
get hasAdditionalLatestDataKeys(): boolean {
return !this.hideSourceSelection && this.widgetConfigComponent.widgetType === widgetType.timeseries &&
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
}
get datakeySettingsSchema(): JsonSettingsSchema { get datakeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema; return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
} }
@ -119,6 +133,14 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
return this.keysFormArray().controls.length > 1; return this.keysFormArray().controls.length > 1;
} }
get noKeys(): boolean {
let keys: DataKey[] = this.keysListFormGroup.get('keys').value;
if (this.hasAdditionalLatestDataKeys) {
keys = keys.filter(k => !(k as any).latest);
}
return keys.length === 0;
}
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
@ -196,7 +218,13 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
} }
public validate(c: UntypedFormControl) { public validate(c: UntypedFormControl) {
return this.keysListFormGroup.valid ? null : { this.errorText = '';
let valid = this.keysListFormGroup.valid;
if (this.noKeys && this.requiredKeysText) {
valid = false;
this.errorText = this.requiredKeysText;
}
return valid ? null : {
dataKeyRows: { dataKeyRows: {
valid: false, valid: false,
}, },
@ -226,6 +254,9 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema); const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema);
dataKey.label = ''; dataKey.label = '';
dataKey.decimals = 0; dataKey.decimals = 0;
if (this.hasAdditionalLatestDataKeys) {
(dataKey as any).latest = false;
}
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray; const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray;
const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]); const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]);
keysArray.push(keyControl); keysArray.push(keyControl);

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

@ -19,10 +19,10 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'datakey.configuration' | translate }}</h2> <h2>{{ 'datakey.configuration' | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<tb-toggle-header *ngIf="hasAdvanced" (valueChange)="dataKeyConfigMode = $event" ignoreMdLgSize="true" <tb-toggle-select *ngIf="hasAdvanced"
appearance="fill-invert" [options]="dataKeyConfigHeaderOptions" appearance="fill-invert" [options]="dataKeyConfigHeaderOptions"
[value]="dataKeyConfigMode" name="dataKeyConfigModeHeader" useSelectOnMdLg="false"> [(ngModel)]="dataKeyConfigMode" [ngModelOptions]="{ standalone: true }">
</tb-toggle-header> </tb-toggle-select>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-datakey-config" *ngIf="dataKeyConfigMode === dataKeyConfigModes.general; else advanced" <div *ngIf="dataKeyConfigMode === dataKeyConfigModes.general; else advanced"
[formGroup]="dataKeyFormGroup"> [formGroup]="dataKeyFormGroup">
<div class="mat-padding" fxLayout="column" style="gap: 16px;"> <div class="mat-padding" fxLayout="column" style="gap: 16px;">
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
@ -54,7 +54,7 @@
</div> </div>
<div class="tb-widget-config-row space-between" *ngIf="!hideDataKeyDecimals"> <div class="tb-widget-config-row space-between" *ngIf="!hideDataKeyDecimals">
<div translate>widget-config.decimals-short</div> <div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
@ -182,14 +182,12 @@
</div> </div>
</div> </div>
<ng-template #advanced> <ng-template #advanced>
<div class="tb-datakey-config" [formGroup]="dataKeySettingsFormGroup"> <div class="mat-padding" fxLayout="column" [formGroup]="dataKeySettingsFormGroup">
<div class="mat-padding" fxLayout="column"> <tb-widget-settings
<tb-widget-settings [dashboard]="dashboard"
[dashboard]="dashboard" [aliasController]="aliasController"
[aliasController]="aliasController" [widget]="widget"
[widget]="widget" formControlName="settings">
formControlName="settings"> </tb-widget-settings>
</tb-widget-settings>
</div>
</div> </div>
</ng-template> </ng-template>

107
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss

@ -1,107 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../scss/constants';
:host {
.tb-datakey-config {
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
legend + * {
display: block;
margin-top: 16px;
}
&.fields-group-slider {
padding: 0;
legend {
margin-left: 16px;
}
> .tb-settings {
margin-top: 0;
padding: 0 16px 8px;
}
}
}
.tb-hint.after-fields {
margin-top: -0.75em;
max-width: fit-content;
line-height: 15px;
}
}
}
:host ::ng-deep {
.tb-datakey-config {
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
padding: 0;
color: rgba(0, 0, 0, 0.87);
&:hover {
background: none;
}
.mat-expansion-indicator {
padding: 2px;
}
}
&.comparison {
.mat-expansion-panel-header {
height: fit-content;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
.mat-slide {
margin: 8px 0;
}
}
}

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

@ -57,7 +57,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-data-key-config', selector: 'tb-data-key-config',
templateUrl: './data-key-config.component.html', templateUrl: './data-key-config.component.html',
styleUrls: ['./data-key-config.component.scss'], styleUrls: [],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

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

@ -22,18 +22,11 @@
<div fxFlex fxLayout="row" fxLayoutAlign="center center" *ngIf="timeseriesKeyError"> <div fxFlex fxLayout="row" fxLayoutAlign="center center" *ngIf="timeseriesKeyError">
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error> <mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</div> </div>
<tb-toggle-header *ngIf="basicMode" (valueChange)="datasourcesModeChange($event)" ignoreMdLgSize="true" <tb-toggle-select *ngIf="basicMode" [ngModel]="datasourcesMode" (ngModelChange)="datasourcesModeChange($event)"
[options]="[ [ngModelOptions]="{ standalone: true }">
{ <tb-toggle-option [value]="datasourceType.device">{{ 'device.device' | translate }}</tb-toggle-option>
name: translate.instant('device.device'), <tb-toggle-option [value]="datasourceType.entity">{{ 'entity.entity-alias' | translate }}</tb-toggle-option>
value: datasourceType.device </tb-toggle-select>
},
{
name: translate.instant('entity.entity-alias'),
value: datasourceType.entity
}
]" [value]="datasourcesMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
</div> </div>
<div *ngIf="maxDatasources > 1" <div *ngIf="maxDatasources > 1"
class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div> class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div>

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

@ -69,7 +69,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
} }
public get maxDatasources(): number { public get maxDatasources(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources; return this.forceSingleDatasource ? 1 : this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources;
} }
public get singleDatasource(): boolean { public get singleDatasource(): boolean {
@ -108,6 +108,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean() @coerceBoolean()
hideDataKeys = false; hideDataKeys = false;
@Input()
@coerceBoolean()
forceSingleDatasource = false;
@Input() @Input()
configMode: WidgetConfigMode; configMode: WidgetConfigMode;
@ -175,13 +179,20 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
this.datasourcesMode = this.detectDatasourcesMode(datasources); this.datasourcesMode = this.detectDatasourcesMode(datasources);
let changed = false; let changed = false;
if (datasources) { if (datasources) {
datasources.forEach((datasource) => { let length;
if (this.maxDatasources === -1) {
length = datasources.length;
} else {
length = Math.min(this.maxDatasources, datasources.length);
}
for (let i = 0; i < length; i++) {
const datasource = datasources[i];
if (this.basicMode && datasource.type !== this.datasourcesMode) { if (this.basicMode && datasource.type !== this.datasourcesMode) {
datasource.type = this.datasourcesMode; datasource.type = this.datasourcesMode;
changed = true; changed = true;
} }
this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false}); this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false});
}); }
} }
if (this.singleDatasource && !this.datasourcesFormArray.length) { if (this.singleDatasource && !this.datasourcesFormArray.length) {
this.addDatasource(false); this.addDatasource(false);

11
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html

@ -18,13 +18,10 @@
<div [formGroup]="timewindowConfig" class="tb-widget-config-panel"> <div [formGroup]="timewindowConfig" class="tb-widget-config-panel">
<div fxLayout="row" fxLayoutAlign="space-between center"> <div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div> <div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div>
<tb-toggle-header (valueChange)="timewindowConfig.get('useDashboardTimewindow').patchValue($event)" ignoreMdLgSize="true" <tb-toggle-select formControlName="useDashboardTimewindow">
[options]="[ <tb-toggle-option [value]="true">{{ 'widget-config.use-dashboard-timewindow' | translate }}</tb-toggle-option>
{ name: translate.instant('widget-config.use-dashboard-timewindow'), value: true}, <tb-toggle-option [value]="false">{{ 'widget-config.use-widget-timewindow' | translate }}</tb-toggle-option>
{ name: translate.instant('widget-config.use-widget-timewindow'), value: false} </tb-toggle-select>
]"
[value]="timewindowConfig.get('useDashboardTimewindow').value" name="useDashboardTimewindow" useSelectOnMdLg="false">
</tb-toggle-header>
</div> </div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<tb-timewindow asButton="true" <tb-timewindow asButton="true"

11
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts

@ -17,10 +17,11 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { widgetType } from '@shared/models/widget.models'; import { WidgetConfig, widgetType } from '@shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefined } from '@core/utils';
export interface TimewindowConfigData { export interface TimewindowConfigData {
useDashboardTimewindow: boolean; useDashboardTimewindow: boolean;
@ -28,6 +29,14 @@ export interface TimewindowConfigData {
timewindow: Timewindow; timewindow: Timewindow;
} }
export const getTimewindowConfig = (config: WidgetConfig): TimewindowConfigData => ({
useDashboardTimewindow: isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true,
displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true,
timewindow: config.timewindow
});
@Component({ @Component({
selector: 'tb-timewindow-config-panel', selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html', templateUrl: './timewindow-config-panel.component.html',

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

@ -15,6 +15,6 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field appearance="outline" class="tb-inline-field center" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field" subscriptSizing="dynamic">
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>

2
ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts

@ -85,7 +85,7 @@ export abstract class TbBaseGauge<S, O extends GenericOptions> {
if (cellData.data.length > 0) { if (cellData.data.length > 0) {
const tvPair = cellData.data[cellData.data.length - const tvPair = cellData.data[cellData.data.length -
1]; 1];
const value = tvPair[1]; const value = parseFloat(tvPair[1]);
if (value !== this.gauge.value) { if (value !== this.gauge.value) {
this.gauge.value = value; this.gauge.value = value;
} }

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

@ -136,6 +136,7 @@ export interface TbFlotYAxisSettings {
export interface TbFlotBaseSettings { export interface TbFlotBaseSettings {
stack: boolean; stack: boolean;
enableSelection: boolean;
shadowSize: number; shadowSize: number;
fontColor: string; fontColor: string;
fontSize: number; fontSize: number;

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

@ -117,6 +117,9 @@ export class TbFlot {
private mouseleaveHandler = this.onFlotMouseLeave.bind(this); private mouseleaveHandler = this.onFlotMouseLeave.bind(this);
private flotClickHandler = this.onFlotClick.bind(this); private flotClickHandler = this.onFlotClick.bind(this);
private enableSelection: boolean;
private selectionMode: 'x' | null;
private readonly showTooltip: boolean; private readonly showTooltip: boolean;
private readonly animatedPie: boolean; private readonly animatedPie: boolean;
private pieDataAnimationDuration: number; private pieDataAnimationDuration: number;
@ -131,6 +134,8 @@ export class TbFlot {
this.chartType = this.chartType || 'line'; this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings; this.settings = ctx.settings as TbFlotSettings;
this.utils = this.ctx.$injector.get(UtilsService); this.utils = this.ctx.$injector.get(UtilsService);
this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
this.selectionMode = this.enableSelection ? 'x' : null;
this.showTooltip = isDefined(this.settings.showTooltip) ? this.settings.showTooltip : true; this.showTooltip = isDefined(this.settings.showTooltip) ? this.settings.showTooltip : true;
this.tooltip = this.showTooltip ? $('#flot-series-tooltip') : null; this.tooltip = this.showTooltip ? $('#flot-series-tooltip') : null;
if (this.tooltip?.length === 0) { if (this.tooltip?.length === 0) {
@ -167,7 +172,7 @@ export class TbFlot {
}; };
if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') { if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
this.options.selection = { mode : 'x' }; this.options.selection = { mode: this.selectionMode, touch: true };
this.options.xaxes = []; this.options.xaxes = [];
this.xaxis = { this.xaxis = {
mode: 'time', mode: 'time',
@ -1251,7 +1256,7 @@ export class TbFlot {
this.$element.css('pointer-events', ''); this.$element.css('pointer-events', '');
this.$element.addClass('mouse-events'); this.$element.addClass('mouse-events');
if (this.chartType !== 'pie') { if (this.chartType !== 'pie') {
this.options.selection = {mode: 'x'}; this.options.selection = {mode: this.selectionMode, touch: true};
this.$element.bind('plotselected', this.flotSelectHandler); this.$element.bind('plotselected', this.flotSelectHandler);
this.$element.bind('dblclick', this.dblclickHandler); this.$element.bind('dblclick', this.dblclickHandler);
} }

19
ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html

@ -111,21 +111,10 @@
</ng-template> </ng-template>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step2.content-before' | translate | safe: 'html'"></div> <div [innerHTML]="'widgets.getting-started.tenant-admin.step2.content-before' | translate | safe: 'html'"></div>
<div class="tb-bordered-content"> <div class="tb-bordered-content">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false" <tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false">
[options]="[ <tb-toggle-option value="ubuntu">Ubuntu</tb-toggle-option>
{ <tb-toggle-option value="macos">MacOS</tb-toggle-option>
name: 'Ubuntu', <tb-toggle-option value="windows">Windows</tb-toggle-option>
value: 'ubuntu'
},
{
name: 'MacOS',
value: 'macos'
},
{
name: 'Windows',
value: 'windows'
}
]">
</tb-toggle-header> </tb-toggle-header>
<ng-container [ngSwitch]="publishCommandSteps.value"> <ng-container [ngSwitch]="publishCommandSteps.value">
<ng-template [ngSwitchCase]="'ubuntu'"> <ng-template [ngSwitchCase]="'ubuntu'">

13
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html

@ -20,16 +20,9 @@
<div class="tb-card-header"> <div class="tb-card-header">
<a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a> <a class="tb-title-link" routerLink="/dashboards">{{ 'widgets.recent-dashboards.title' | translate }}</a>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.lt-md="space-between center" fxLayoutGap="8px">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[ <tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)">
{ <tb-toggle-option value="last">{{ 'widgets.recent-dashboards.last' | translate }}</tb-toggle-option>
name: ctx.translate.instant('widgets.recent-dashboards.last'), <tb-toggle-option value="starred">{{ 'widgets.recent-dashboards.starred' | translate }}</tb-toggle-option>
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
</tb-toggle-header> </tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md <a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a> mat-flat-button color="primary" routerLink="/dashboards" [queryParams]="{action: 'add'}">{{ 'dashboard.add' | translate }}</a>

13
ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.html

@ -19,16 +19,9 @@
<ng-container *ngIf="hasUsageInfoAccess; else noDataAvailable"> <ng-container *ngIf="hasUsageInfoAccess; else noDataAvailable">
<div class="tb-card-header"> <div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a> <a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[ <tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle">
{ <tb-toggle-option value="entities">{{ 'widgets.usage-info.entities' | translate }}</tb-toggle-option>
name: ctx.translate.instant('widgets.usage-info.entities'), <tb-toggle-option value="apiCalls">{{ 'widgets.usage-info.api-calls' | translate }}</tb-toggle-option>
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
</tb-toggle-header> </tb-toggle-header>
</div> </div>
<ng-container [ngSwitch]="usageToggle.value"> <ng-container [ngSwitch]="usageToggle.value">

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html

@ -19,20 +19,20 @@
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div> <div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<div>{{ 'widgets.table.custom-title' | translate }}</div> <div class="fixed-title-width">{{ 'widgets.table.custom-title' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="customTitle" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="customTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-widget-config-row space-between"> <div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.column-width' | translate }}</div> <div>{{ 'widgets.table.column-width' | translate }}</div>
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="columnWidth" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="columnWidth" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-widget-config-row space-between"> <div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.default-column-visibility' | translate }}</div> <div>{{ 'widgets.table.default-column-visibility' | translate }}</div>
<mat-form-field style="width: 220px;" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="defaultColumnVisibility"> <mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'"> <mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }} {{ 'widgets.table.column-visibility-visible' | translate }}
@ -48,7 +48,7 @@
</div> </div>
<div class="tb-widget-config-row space-between"> <div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.column-selection-to-display' | translate }}</div> <div>{{ 'widgets.table.column-selection-to-display' | translate }}</div>
<mat-form-field style="width: 220px;" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="columnSelectionToDisplay"> <mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'"> <mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }} {{ 'widgets.table.column-selection-to-display-enabled' | translate }}

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html

@ -19,7 +19,7 @@
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.table-header</div> <div class="tb-widget-config-panel-title" translate>widgets.table.table-header</div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<div>{{ 'widgets.table.entities-table-title' | translate }}</div> <div class="fixed-title-width">{{ 'widgets.table.entities-table-title' | translate }}</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="entitiesTitle" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="entitiesTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
@ -31,10 +31,10 @@
</div> </div>
<div class="tb-widget-config-panel stroked"> <div class="tb-widget-config-panel stroked">
<div class="tb-widget-config-panel-title" translate>widgets.table.header-buttons</div> <div class="tb-widget-config-panel-title" translate>widgets.table.header-buttons</div>
<mat-slide-toggle class="mat-slide no-margin" formControlName="enableSearch"> <mat-slide-toggle class="mat-slide" formControlName="enableSearch">
{{ 'widgets.table.enable-search' | translate }} {{ 'widgets.table.enable-search' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle class="mat-slide no-margin" formControlName="enableSelectColumnDisplay"> <mat-slide-toggle class="mat-slide" formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }} {{ 'widgets.table.enable-select-column-display' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
@ -42,7 +42,7 @@
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.columns</div> <div class="tb-widget-config-panel-title" translate>widgets.table.columns</div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="displayEntityName"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="displayEntityName">
{{ 'widgets.table.display-entity-name' | translate }} {{ 'widgets.table.display-entity-name' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
@ -50,7 +50,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-widget-config-row"> <div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="displayEntityName"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="displayEntityName">
{{ 'widgets.table.display-entity-label' | translate }} {{ 'widgets.table.display-entity-label' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
@ -85,12 +85,12 @@
</div> </div>
<div class="tb-widget-config-panel"> <div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.pagination</div> <div class="tb-widget-config-panel-title" translate>widgets.table.pagination</div>
<mat-slide-toggle class="mat-slide no-margin" formControlName="displayPagination"> <mat-slide-toggle class="mat-slide" formControlName="displayPagination">
{{ 'widgets.table.display-pagination' | translate }} {{ 'widgets.table.display-pagination' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div class="tb-widget-config-row space-between"> <div class="tb-widget-config-row space-between">
<div translate>widgets.table.default-page-size</div> <div translate>widgets.table.default-page-size</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="defaultPageSize" type="number" min="1" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="defaultPageSize" type="number" min="1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/label-widget-label.component.ts

@ -33,7 +33,7 @@ export interface LabelWidgetLabel {
@Component({ @Component({
selector: 'tb-label-widget-label', selector: 'tb-label-widget-label',
templateUrl: './label-widget-label.component.html', templateUrl: './label-widget-label.component.html',
styleUrls: ['./label-widget-label.component.scss', './../widget-settings.scss'], styleUrls: ['./label-widget-label.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

87
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html

@ -15,18 +15,49 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="timeseriesTableKeySettingsForm" fxLayout="column"> <ng-container [formGroup]="timeseriesTableKeySettingsForm">
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.table.cell-style</legend> <div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.default-column-visibility' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.column-selection-to-display' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableKeySettingsForm.get('useCellStyleFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableKeySettingsForm.get('useCellStyleFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useCellStyleFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-cell-style-function' | translate }} {{ 'widgets.table.use-cell-style-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -40,18 +71,17 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.table.cell-content</legend>
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableKeySettingsForm.get('useCellContentFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableKeySettingsForm.get('useCellContentFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useCellContentFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-cell-content-function' | translate }} {{ 'widgets.table.use-cell-content-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -65,30 +95,5 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<mat-form-field fxFlex class="mat-block"> </ng-container>
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

104
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html

@ -15,25 +15,58 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="timeseriesTableLatestKeySettingsForm" fxLayout="column"> <ng-container [formGroup]="timeseriesTableLatestKeySettingsForm">
<mat-slide-toggle class="mat-slide" formControlName="show"> <div class="tb-widget-config-panel">
{{ 'widgets.table.show-latest-data-column' | translate }} <div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div>
</mat-slide-toggle> <mat-slide-toggle class="mat-slide" formControlName="show">
<mat-form-field [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" fxFlex class="mat-block"> {{ 'widgets.table.show-latest-data-column' | translate }}
<mat-label translate>widgets.table.latest-data-column-order</mat-label> </mat-slide-toggle>
<input matInput type="number" step="1" formControlName="order"> <div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-row space-between">
</mat-form-field> <div translate>widgets.table.latest-data-column-order</div>
<fieldset [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="fields-group fields-group-slider"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<legend class="group-title" translate>widgets.table.cell-style</legend> <input matInput formControlName="order" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.default-column-visibility' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-row space-between">
<div>{{ 'widgets.table.column-selection-to-display' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableLatestKeySettingsForm.get('useCellStyleFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableLatestKeySettingsForm.get('useCellStyleFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useCellStyleFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-cell-style-function' | translate }} {{ 'widgets.table.use-cell-style-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -47,18 +80,17 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="fields-group fields-group-slider"> <div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.table.cell-content</legend>
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableLatestKeySettingsForm.get('useCellContentFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableLatestKeySettingsForm.get('useCellContentFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useCellContentFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-cell-content-function' | translate }} {{ 'widgets.table.use-cell-content-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -72,30 +104,6 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<mat-form-field fxFlex class="mat-block"> </ng-container>
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>

109
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -15,28 +15,31 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="timeseriesTableWidgetSettingsForm" fxLayout="column"> <ng-container [formGroup]="timeseriesTableWidgetSettingsForm">
<fieldset class="fields-group"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.table.common-table-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.table.table-header</div>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> <mat-slide-toggle class="mat-slide" formControlName="enableStickyHeader">
<section fxLayout="column" fxFlex> {{ 'widgets.table.enable-sticky-header' | translate }}
<mat-checkbox formControlName="enableSearch"> </mat-slide-toggle>
{{ 'widgets.table.enable-search' | translate }} <mat-slide-toggle class="mat-slide" formControlName="enableSearch">
</mat-checkbox> {{ 'widgets.table.enable-search' | translate }}
<mat-checkbox formControlName="enableSelectColumnDisplay"> </mat-slide-toggle>
{{ 'widgets.table.enable-select-column-display' | translate }} <mat-slide-toggle class="mat-slide" formControlName="enableSelectColumnDisplay">
</mat-checkbox> {{ 'widgets.table.enable-select-column-display' | translate }}
</section> </mat-slide-toggle>
<section fxLayout="column" fxFlex> </div>
<mat-checkbox formControlName="enableStickyHeader"> <div class="tb-widget-config-panel">
{{ 'widgets.table.enable-sticky-header' | translate }} <div class="tb-widget-config-panel-title" translate>widgets.table.columns</div>
</mat-checkbox> <mat-slide-toggle class="mat-slide" formControlName="showTimestamp">
<mat-checkbox formControlName="enableStickyAction"> {{ 'widgets.table.display-timestamp' | translate }}
{{ 'widgets.table.enable-sticky-action' | translate }} </mat-slide-toggle>
</mat-checkbox> <mat-slide-toggle class="mat-slide" formControlName="showMilliseconds">
</section> {{ 'widgets.table.display-milliseconds' | translate }}
</section> </mat-slide-toggle>
<mat-form-field fxFlex class="mat-block"> <mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex subscriptSizing="dynamic">
<mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label> <mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label>
<mat-select formControlName="reserveSpaceForHiddenAction"> <mat-select formControlName="reserveSpaceForHiddenAction">
<mat-option [value]="'true'"> <mat-option [value]="'true'">
@ -47,41 +50,39 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<section fxLayout="column" fxLayoutGap="8px"> </div>
<mat-slide-toggle formControlName="showTimestamp"> <div class="tb-widget-config-panel">
{{ 'widgets.table.display-timestamp' | translate }} <div class="tb-widget-config-panel-title" translate>widgets.table.pagination</div>
</mat-slide-toggle> <mat-slide-toggle class="mat-slide" formControlName="displayPagination">
<mat-slide-toggle formControlName="showMilliseconds"> {{ 'widgets.table.display-pagination' | translate }}
{{ 'widgets.table.display-milliseconds' | translate }} </mat-slide-toggle>
</mat-slide-toggle> <div class="tb-widget-config-row space-between">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center"> <div translate>widgets.table.default-page-size</div>
<mat-slide-toggle fxFlex formControlName="displayPagination"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
{{ 'widgets.table.display-pagination' | translate }} <input matInput formControlName="defaultPageSize" type="number" min="1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-slide-toggle> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label translate>widgets.table.default-page-size</mat-label> </div>
<input matInput type="number" min="1" step="1" formControlName="defaultPageSize"> <div class="tb-widget-config-panel">
</mat-form-field> <div class="tb-widget-config-panel-title" translate>widgets.table.table-tabs</div>
</section> <mat-slide-toggle class="mat-slide" formControlName="useEntityLabel">
<mat-slide-toggle formControlName="useEntityLabel"> {{ 'widgets.table.use-entity-label-tab-name' | translate }}
{{ 'widgets.table.use-entity-label-tab-name' | translate }} </mat-slide-toggle>
</mat-slide-toggle> </div>
<mat-slide-toggle formControlName="hideEmptyLines"> <div class="tb-widget-config-panel tb-slide-toggle">
{{ 'widgets.table.hide-empty-lines' | translate }} <div class="tb-widget-config-panel-title" style="padding-bottom: 16px;" translate>widgets.table.rows</div>
</mat-slide-toggle> <mat-slide-toggle class="mat-slide" style="padding-left: 16px;" formControlName="hideEmptyLines">
</section> {{ 'widgets.table.hide-empty-lines' | translate }}
</fieldset> </mat-slide-toggle>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.row-style</legend>
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableWidgetSettingsForm.get('useRowStyleFunction').value"> <mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableWidgetSettingsForm.get('useRowStyleFunction').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="useRowStyleFunction" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useRowStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.table.use-row-style-function' | translate }} {{ 'widgets.table.use-row-style-function' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
@ -95,5 +96,5 @@
</tb-js-func> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
</section> </ng-container>

297
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html

@ -15,81 +15,85 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="flotKeySettingsFormGroup" fxLayout="column"> <ng-container [formGroup]="flotKeySettingsFormGroup">
<fieldset class="fields-group" fxLayout="column"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.chart.common-settings</legend> <mat-slide-toggle class="mat-slide" formControlName="hideDataByDefault">
<mat-checkbox formControlName="hideDataByDefault">
{{ 'widgets.chart.data-is-hidden-by-default' | translate }} {{ 'widgets.chart.data-is-hidden-by-default' | translate }}
</mat-checkbox> </mat-slide-toggle>
<mat-checkbox formControlName="disableDataHiding"> <mat-slide-toggle class="mat-slide" formControlName="disableDataHiding">
{{ 'widgets.chart.disable-data-hiding' | translate }} {{ 'widgets.chart.disable-data-hiding' | translate }}
</mat-checkbox> </mat-slide-toggle>
<mat-checkbox formControlName="removeFromLegend"> <mat-slide-toggle class="mat-slide" formControlName="removeFromLegend">
{{ 'widgets.chart.remove-from-legend' | translate }} {{ 'widgets.chart.remove-from-legend' | translate }}
</mat-checkbox> </mat-slide-toggle>
<mat-checkbox formControlName="excludeFromStacking"> <mat-slide-toggle class="mat-slide" formControlName="excludeFromStacking">
{{ 'widgets.chart.exclude-from-stacking' | translate }} {{ 'widgets.chart.exclude-from-stacking' | translate }}
</mat-checkbox> </mat-slide-toggle>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.chart.line-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('showLines').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('showLines').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showLines" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="showLines" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.chart.show-line' | translate }} {{ 'widgets.chart.show-line' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<section fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center"> <div class="tb-widget-config-row space-between">
<mat-form-field fxFlex class="mat-block"> <div>{{ 'widgets.chart.line-width' | translate }}</div>
<mat-label translate>widgets.chart.line-width</mat-label> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="lineWidth"> <input matInput formControlName="lineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="fillLines"> </div>
{{ 'widgets.chart.fill-line' | translate }} <mat-slide-toggle class="mat-slide" formControlName="fillLines">
</mat-checkbox> {{ 'widgets.chart.fill-line' | translate }}
<mat-form-field fxFlex class="mat-block"> </mat-slide-toggle>
<mat-label translate>widgets.chart.fill-line-opacity</mat-label> <div [fxShow]="flotKeySettingsFormGroup.get('fillLines').value" class="tb-widget-config-row space-between">
<input matInput type="number" min="0" max="1" step="0.1" formControlName="fillLinesOpacity"> <div>{{ 'widgets.chart.fill-line-opacity' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="fillLinesOpacity" type="number" min="0" max="1" step="0.1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</section> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.chart.points-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('showPoints').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('showPoints').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showPoints" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="showPoints" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.chart.show-points' | translate }} {{ 'widgets.chart.show-points' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<section fxLayout="column"> <div class="tb-widget-config-row space-between">
<section fxFlex fxLayout="row" fxLayoutGap="8px"> <div>{{ 'widgets.chart.points-line-width' | translate }}</div>
<mat-form-field fxFlex class="mat-block"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<mat-label translate>widgets.chart.points-line-width</mat-label> <input matInput formControlName="showPointsLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<input matInput type="number" min="0" formControlName="showPointsLineWidth"> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label translate>widgets.chart.points-radius</mat-label> <div class="tb-widget-config-row space-between">
<input matInput type="number" min="0" formControlName="showPointsRadius"> <div>{{ 'widgets.chart.points-radius' | translate }}</div>
</mat-form-field> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
</section> <input matInput formControlName="showPointsRadius" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<mat-form-field fxFlex class="mat-block"> <span matSuffix>px</span>
<mat-label translate>widgets.chart.point-shape</mat-label> </mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.point-shape' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="showPointShape"> <mat-select formControlName="showPointShape">
<mat-option [value]="'circle'"> <mat-option [value]="'circle'">
{{ 'widgets.chart.point-shape-circle' | translate }} {{ 'widgets.chart.point-shape-circle' | translate }}
@ -111,19 +115,19 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-js-func [fxShow]="flotKeySettingsFormGroup.get('showPointShape').value === 'custom'" </div>
formControlName="pointShapeFormatter" <tb-js-func [fxShow]="flotKeySettingsFormGroup.get('showPointShape').value === 'custom'"
[globalVariables]="functionScopeVariables" formControlName="pointShapeFormatter"
[functionArgs]="['ctx', 'x', 'y', 'radius', 'shadow']" [globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.chart.point-shape-draw-function' | translate }}" [functionArgs]="['ctx', 'x', 'y', 'radius', 'shadow']"
helpId="widget/lib/flot/point_shape_format_fn"> functionTitle="{{ 'widgets.chart.point-shape-draw-function' | translate }}"
</tb-js-func> helpId="widget/lib/flot/point_shape_format_fn">
</section> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset class="fields-group"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.tooltip-settings</div>
<tb-js-func <tb-js-func
formControlName="tooltipValueFormatter" formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
@ -131,49 +135,57 @@
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn"> helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func> </tb-js-func>
</fieldset> </div>
<fieldset class="fields-group"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.chart.yaxis-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.vertical-axis</div>
<mat-slide-toggle formControlName="showSeparateAxis" class="slide-block"> <mat-slide-toggle class="mat-slide" formControlName="showSeparateAxis">
{{ 'widgets.chart.show-separate-axis' | translate }} {{ 'widgets.chart.show-separate-axis' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-row">
<mat-label translate>widgets.chart.axis-title</mat-label> <div class="fixed-title-width" translate>widgets.chart.axis-title</div>
<input matInput formControlName="axisTitle"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
</mat-form-field> <input matInput formControlName="axisTitle" placeholder="{{ 'widget-config.set' | translate }}">
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.min-scale-value</mat-label>
<input matInput type="number" formControlName="axisMin">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label translate>widgets.chart.max-scale-value</mat-label> <div class="tb-widget-config-row space-between">
<input matInput type="number" formControlName="axisMax"> <div translate>widgets.chart.min-scale-value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="axisMin" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</section> </div>
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-row space-between">
<mat-label translate>widgets.chart.axis-position</mat-label> <div translate>widgets.chart.max-scale-value</div>
<mat-select formControlName="axisPosition"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<mat-option [value]="'left'"> <input matInput formControlName="axisMax" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
{{ 'widgets.chart.axis-position-left' | translate }} </mat-form-field>
</mat-option> </div>
<mat-option [value]="'right'"> <div class="tb-widget-config-row space-between">
{{ 'widgets.chart.axis-position-right' | translate }} <div>{{ 'widgets.chart.axis-position' | translate }}</div>
</mat-option> <mat-form-field appearance="outline" subscriptSizing="dynamic">
</mat-select> <mat-select formControlName="axisPosition">
</mat-form-field> <mat-option [value]="'left'">
<fieldset class="fields-group"> {{ 'widgets.chart.axis-position-left' | translate }}
<legend class="group-title" translate>widgets.chart.yaxis-tick-labels-settings</legend> </mat-option>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> <mat-option [value]="'right'">
<mat-form-field fxFlex class="mat-block"> {{ 'widgets.chart.axis-position-right' | translate }}
<mat-label translate>widgets.chart.tick-step-size</mat-label> </mat-option>
<input matInput type="number" formControlName="axisTickSize"> </mat-select>
</mat-form-field>
</div>
<div class="tb-widget-config-panel stroked">
<div class="tb-widget-config-panel-title" translate>widgets.chart.ticks</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="axisTickDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label translate>widgets.chart.number-of-decimals</mat-label> <div class="tb-widget-config-row space-between">
<input matInput type="number" min="0" formControlName="axisTickDecimals"> <div translate>widgets.chart.tick-step-size</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="axisTickSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</section> </div>
<tb-js-func <tb-js-func
formControlName="axisTicksFormatter" formControlName="axisTicksFormatter"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
@ -181,63 +193,58 @@
functionTitle="{{ 'widgets.chart.ticks-formatter-function' | translate }}" functionTitle="{{ 'widgets.chart.ticks-formatter-function' | translate }}"
helpId="widget/lib/flot/ticks_formatter_fn"> helpId="widget/lib/flot/ticks_formatter_fn">
</tb-js-func> </tb-js-func>
</fieldset> </div>
</fieldset> </div>
<fieldset class="fields-group"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.chart.thresholds</legend> <div class="tb-widget-config-panel-title" fxLayout="row" fxLayoutAlign="space-between center">
<div fxLayout="column"> <div translate>widgets.chart.thresholds</div>
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical" <button mat-stroked-button
(cdkDropListDropped)="thresholdDrop($event)"> color="primary"
<div cdkDrag class="tb-draggable" *ngFor="let thresholdControl of thresholdsFormArray().controls; trackBy: trackByThreshold; type="button" (click)="addThreshold()">
let $index = index; last as isLast;" <span translate>widgets.chart.add-threshold</span>
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> </button>
<tb-flot-threshold [formControl]="thresholdControl" </div>
[expanded]="thresholdControl.new" <div *ngIf="thresholdsFormArray().length" class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
[aliasController]="aliasController" (cdkDropListDropped)="thresholdDrop($event)" fxLayout="column" fxLayoutGap="12px">
(removeThreshold)="removeThreshold($index)"> <div cdkDrag class="tb-draggable" *ngFor="let thresholdControl of thresholdsFormArray().controls; trackBy: trackByThreshold;
</tb-flot-threshold> let $index = index;">
</div> <tb-flot-threshold [formControl]="thresholdControl"
</div> [expanded]="thresholdControl.new"
<div *ngIf="!thresholdsFormArray().controls.length"> [aliasController]="aliasController"
<span translate fxLayoutAlign="center center" (removeThreshold)="removeThreshold($index)">
class="tb-prompt">widgets.chart.no-thresholds</span> </tb-flot-threshold>
</div>
<div style="padding-top: 16px;">
<button mat-raised-button color="primary"
type="button"
(click)="addThreshold()">
<span translate>widgets.chart.add-threshold</span>
</button>
</div> </div>
</div> </div>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider" formGroupName="comparisonSettings" > <div class="tb-widget-config-panel tb-slide-toggle" formGroupName="comparisonSettings">
<legend class="group-title" translate>widgets.chart.comparison-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.comparison-settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value"
[disabled]="!flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title fxFlex="60"> <mat-panel-title>
<mat-slide-toggle formControlName="showValuesForComparison" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="showValuesForComparison" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.chart.show-values-for-comparison' | translate }} {{ 'widgets.chart.show-values-for-comparison' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<section fxFlex fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> <div class="tb-widget-config-row">
<mat-form-field fxFlex class="mat-block"> <div class="fixed-title-width" translate>widgets.chart.comparison-values-label</div>
<mat-label translate>widgets.chart.comparison-values-label</mat-label> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="comparisonValuesLabel"> <input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-color-input fxFlex </div>
formControlName="color" <div class="tb-widget-config-row space-between same-padding">
icon="format_color_fill" <div>{{ 'widgets.chart.comparison-line-color' | translate }}</div>
label="{{ 'widgets.chart.color' | translate }}" openOnInput colorClearButton> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
</tb-color-input> <mat-divider vertical></mat-divider>
</section> <tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
</section> </ng-container>

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.ts

@ -225,6 +225,7 @@ export class FlotKeySettingsComponent extends PageComponent implements OnInit, C
this.flotKeySettingsFormGroup.disable({emitEvent: false}); this.flotKeySettingsFormGroup.disable({emitEvent: false});
} else { } else {
this.flotKeySettingsFormGroup.enable({emitEvent: false}); this.flotKeySettingsFormGroup.enable({emitEvent: false});
this.updateValidators(false);
} }
} }

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-latest-key-settings.component.html

@ -15,34 +15,37 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="flotLatestKeySettingsForm" fxLayout="column"> <ng-container [formGroup]="flotLatestKeySettingsForm">
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.chart.threshold-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.threshold-settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotLatestKeySettingsForm.get('useAsThreshold').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotLatestKeySettingsForm.get('useAsThreshold').value"
[disabled]="!flotLatestKeySettingsForm.get('useAsThreshold').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<mat-slide-toggle formControlName="useAsThreshold" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="useAsThreshold" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.chart.use-as-threshold' | translate }} {{ 'widgets.chart.use-as-threshold' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> <div class="tb-widget-config-row space-between">
<mat-form-field fxFlex class="mat-block"> <div translate>widgets.chart.threshold-line-width</div>
<mat-label translate>widgets.chart.threshold-line-width</mat-label> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input type="number" min="0" matInput formControlName="thresholdLineWidth"> <input matInput formControlName="thresholdLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field> </mat-form-field>
<tb-color-input fxFlex </div>
formControlName="thresholdColor" <div class="tb-widget-config-row space-between same-padding">
icon="format_color_fill" <div>{{ 'widgets.chart.threshold-color' | translate }}</div>
label="{{ 'widgets.chart.threshold-color' | translate }}" openOnInput colorClearButton> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
</tb-color-input> <mat-divider vertical></mat-divider>
</section> <tb-color-input asBoxInput
formControlName="thresholdColor">
</tb-color-input>
</div>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
</section> </ng-container>

72
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html

@ -15,44 +15,34 @@
limitations under the License. limitations under the License.
--> -->
<mat-expansion-panel class="flot-threshold" fxFlex [formGroup]="thresholdFormGroup" [(expanded)]="expanded"> <div class="tb-flot-threshold">
<mat-expansion-panel-header> <mat-expansion-panel class="tb-settings tb-flot-threshold-settings" fxFlex [formGroup]="thresholdFormGroup" [(expanded)]="expanded">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <mat-expansion-panel-header class="fill-width">
<mat-panel-title> <div class="tb-threshold-header">
<div fxLayout="row" fxFlex fxLayoutAlign="start center"> <div class="tb-threshold-text">{{ thresholdText() }}</div>
<div>{{ thresholdText() }}</div> <mat-divider vertical></mat-divider>
<div class="tb-color-preview" style="margin-left: 6px;"> <tb-color-input asBoxInput
<div class="tb-color-result" [ngStyle]="{background: thresholdFormGroup.get('color').value}"></div> formControlName="color">
</div> </tb-color-input>
</div> <mat-divider vertical></mat-divider>
</mat-panel-title> </div>
<span fxFlex></span> </mat-expansion-panel-header>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" <ng-template matExpansionPanelContent>
type="button" <tb-value-source [aliasController]="aliasController" formControlName="valueSource"></tb-value-source>
(click)="removeThreshold.emit()" <div class="tb-widget-config-row space-between">
matTooltip="{{ 'action.remove' | translate }}" <div translate>widgets.chart.line-width</div>
matTooltipPosition="above"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<mat-icon>delete</mat-icon> <input matInput formControlName="lineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</button> <span matSuffix>px</span>
</div> </mat-form-field>
</mat-expansion-panel-header> </div>
<ng-template matExpansionPanelContent> </ng-template>
<div fxLayout="column" fxLayoutGap="0.5em"> </mat-expansion-panel>
<mat-divider></mat-divider> <button *ngIf="!disabled" mat-icon-button
<section class="tb-widget-settings" fxLayout="column"> type="button"
<tb-value-source [aliasController]="aliasController" formControlName="valueSource"></tb-value-source> (click)="removeThreshold.emit()"
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> matTooltip="{{ 'action.remove' | translate }}"
<mat-form-field fxFlex class="mat-block"> matTooltipPosition="above">
<mat-label translate>widgets.chart.line-width</mat-label> <mat-icon>delete</mat-icon>
<input type="number" min="0" matInput formControlName="lineWidth"> </button>
</mat-form-field> </div>
<tb-color-input fxFlex
formControlName="color"
icon="format_color_fill"
label="{{ 'widgets.chart.color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
</section>
</div>
</ng-template>
</mat-expansion-panel>

66
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss

@ -13,28 +13,60 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host {
display: block; .tb-flot-threshold {
.mat-expansion-panel { display: flex;
flex-direction: row;
align-items: start;
gap: 4px;
.mat-expansion-panel.tb-settings.tb-flot-threshold-settings {
box-shadow: none; box-shadow: none;
&.flot-threshold { border-radius: 6px;
border: 1px groove rgba(0, 0, 0, .25); border: 1px solid rgba(0, 0, 0, 0.12);
.mat-expansion-panel-header { .mat-expansion-panel-header {
padding: 0 24px 0 8px; height: 56px;
&.mat-expanded { border-radius: 0;
height: 48px; display: flex;
flex-direction: row;
align-items: stretch;
.tb-threshold-header {
flex: 1;
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
padding-left: 16px;
.mat-divider-vertical {
height: 100%;
} }
} }
.tb-threshold-text {
flex: 1;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.15px;
}
.mat-expansion-indicator {
margin-right: 22px;
margin-left: 22px;
margin-top: 12px;
}
} }
} > .mat-expansion-panel-content {
} > .mat-expansion-panel-body {
padding: 16px !important;
:host ::ng-deep {
.mat-expansion-panel {
&.flot-threshold {
.mat-expansion-panel-body {
padding: 0 8px 8px;
} }
} }
&.mat-expanded {
.mat-expansion-panel-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
}
}
> .mdc-icon-button {
margin-top: 4px;
color: rgba(0, 0, 0, 0.54);
} }
} }

15
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.ts

@ -15,8 +15,14 @@
/// ///
import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component'; import { ValueSourceProperty } from '@home/components/widget/lib/settings/common/value-source.component';
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, forwardRef, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -28,14 +34,15 @@ import { TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.mode
@Component({ @Component({
selector: 'tb-flot-threshold', selector: 'tb-flot-threshold',
templateUrl: './flot-threshold.component.html', templateUrl: './flot-threshold.component.html',
styleUrls: ['./flot-threshold.component.scss', './../widget-settings.scss'], styleUrls: ['./flot-threshold.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FlotThresholdComponent), useExisting: forwardRef(() => FlotThresholdComponent),
multi: true multi: true
} }
] ],
encapsulation: ViewEncapsulation.None
}) })
export class FlotThresholdComponent extends PageComponent implements OnInit, ControlValueAccessor { export class FlotThresholdComponent extends PageComponent implements OnInit, ControlValueAccessor {

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

@ -15,30 +15,33 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="flotSettingsFormGroup" fxLayout="column"> <ng-container [formGroup]="flotSettingsFormGroup">
<fieldset class="fields-group"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.chart.common-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.common-settings</div>
<mat-checkbox formControlName="stack"> <mat-slide-toggle class="mat-slide" formControlName="stack">
{{ 'widgets.chart.enable-stacking-mode' | translate }} {{ 'widgets.chart.enable-stacking-mode' | translate }}
</mat-checkbox> </mat-slide-toggle>
<section *ngIf="chartType === 'graph'" fxLayout="column" fxLayout.gt-xs="row" <mat-slide-toggle class="mat-slide" formControlName="enableSelection">
fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center"> {{ 'widgets.chart.enable-selection-mode' | translate }}
<mat-form-field fxFlex class="mat-block"> </mat-slide-toggle>
<mat-label translate>widgets.chart.line-shadow-size</mat-label> <mat-slide-toggle *ngIf="chartType === 'graph'" class="mat-slide" formControlName="smoothLines">
<input matInput type="number" min="0" formControlName="shadowSize"> {{ 'widgets.chart.display-smooth-lines' | translate }}
</mat-slide-toggle>
<div *ngIf="chartType === 'graph'" class="tb-widget-config-row space-between">
<div translate>widgets.chart.line-shadow-size</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="shadowSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<mat-checkbox fxFlex formControlName="smoothLines"> </div>
{{ 'widgets.chart.display-smooth-lines' | translate }} <div *ngIf="chartType === 'bar'" class="tb-widget-config-row space-between">
</mat-checkbox> <div translate>widgets.chart.default-bar-width</div>
</section> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<section *ngIf="chartType === 'bar'" fxLayout="column" fxLayout.gt-xs="row" <input matInput formControlName="defaultBarWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.default-bar-width</mat-label>
<input matInput type="number" min="0" formControlName="defaultBarWidth">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label translate>widgets.chart.bar-alignment</mat-label> <div *ngIf="chartType === 'bar'" class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.bar-alignment' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="barAlignment"> <mat-select formControlName="barAlignment">
<mat-option [value]="'left'"> <mat-option [value]="'left'">
{{ 'widgets.chart.bar-alignment-left' | translate }} {{ 'widgets.chart.bar-alignment-left' | translate }}
@ -51,211 +54,255 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</section> </div>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px"> <div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-row space-between">
<mat-form-field fxFlex class="mat-block"> <div translate>widgets.chart.thresholds-line-width</div>
<mat-label translate>widgets.chart.default-font-size</mat-label> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="fontSize"> <input matInput formControlName="thresholdsLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-color-input fxFlex </div>
formControlName="fontColor" <div class="tb-widget-config-row space-between same-padding">
icon="format_color_fill" <div>{{ 'widgets.chart.default-font' | translate }}</div>
label="{{ 'widgets.chart.default-font-color' | translate }}" openOnInput colorClearButton> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
</tb-color-input> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
</section> <input matInput formControlName="fontSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<mat-form-field *ngIf="chartType === 'graph' || chartType === 'bar'" fxFlex class="mat-block"> <span matSuffix>px</span>
<mat-label translate>widgets.chart.thresholds-line-width</mat-label> </mat-form-field>
<input matInput type="number" min="0" formControlName="thresholdsLineWidth"> <mat-divider vertical></mat-divider>
</mat-form-field> <tb-color-input asBoxInput
</fieldset> formControlName="fontColor">
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider"> </tb-color-input>
<legend class="group-title" translate>widget-config.legend</legend> </div>
</div>
</div>
<div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-panel tb-slide-toggle">
<div class="tb-widget-config-panel-title" translate>widget-config.legend</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showLegend').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row" style="min-height: 48px;"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60"> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;"> fxLayoutAlign="center">
{{ 'widget-config.display-legend' | translate }} {{ 'widget-config.legend' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxFlex="40" style="white-space: nowrap;" fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-legend-config formControlName="legendConfig"></tb-legend-config> <tb-legend-config formControlName="legendConfig">
</tb-legend-config>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset class="fields-group fields-group-slider"> <div class="tb-widget-config-panel">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.axis</div>
<div class="tb-widget-config-panel stroked" formGroupName="yaxis">
<div class="tb-widget-config-panel-title" translate>widgets.chart.vertical-axis</div>
<div class="tb-widget-config-row">
<div class="fixed-title-width" translate>widgets.chart.axis-title</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widgets.chart.min-scale-value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="min" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widgets.chart.max-scale-value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="max" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-panel stroked tb-slide-toggle">
<div class="tb-widget-config-panel-title" translate>widgets.chart.ticks</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('yaxis.showLabels').value">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="showLabels" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.ticks' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widgets.chart.tick-step-size</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tickSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<tb-js-func
formControlName="ticksFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['value']"
functionTitle="{{ 'widgets.chart.ticks-formatter-function' | translate }}"
helpId="widget/lib/flot/ticks_formatter_fn">
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-widget-config-panel stroked" formGroupName="xaxis">
<div class="tb-widget-config-panel-title" translate>widgets.chart.horizontal-axis</div>
<div class="tb-widget-config-row">
<div class="fixed-title-width" translate>widgets.chart.axis-title</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-panel stroked tb-slide-toggle">
<div class="tb-widget-config-panel-title" translate>widgets.chart.ticks</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('xaxis.showLabels').value">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="showLabels" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.ticks' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
</div>
<div class="tb-widget-config-panel" formGroupName="grid">
<div class="tb-widget-config-panel-title" translate>widgets.chart.chart-background</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="verticalLines">
{{ 'widgets.chart.vertical-grid-lines' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="horizontalLines">
{{ 'widgets.chart.horizontal-grid-lines' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widgets.chart.grid-lines-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="tickColor">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widgets.chart.border' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="outlineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widgets.chart.background-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<div class="tb-widget-config-panel-title" translate>widgets.chart.tooltip</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showTooltip').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showTooltip').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showTooltip" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.chart.show-tooltip' | translate }} {{ 'widgets.chart.tooltip' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<section fxLayout="column"> <div class="tb-widget-config-row">
<mat-slide-toggle formControlName="tooltipIndividual" class="slide-block"> <mat-slide-toggle class="mat-slide" formControlName="tooltipIndividual">
{{ 'widgets.chart.hover-individual-points' | translate }} {{ 'widgets.chart.hover-individual-points' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle formControlName="tooltipCumulative" class="slide-block"> </div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipCumulative">
{{ 'widgets.chart.show-cumulative-values' | translate }} {{ 'widgets.chart.show-cumulative-values' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle formControlName="hideZeros" class="slide-block"> </div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="hideZeros">
{{ 'widgets.chart.hide-zero-false-values' | translate }} {{ 'widgets.chart.hide-zero-false-values' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<tb-js-func </div>
formControlName="tooltipValueFormatter" <tb-js-func
[globalVariables]="functionScopeVariables" formControlName="tooltipValueFormatter"
[functionArgs]="['value', 'latestData']" [globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" [functionArgs]="['value', 'latestData']"
helpId="widget/lib/flot/tooltip_value_format_fn"> functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
</tb-js-func> helpId="widget/lib/flot/tooltip_value_format_fn">
</section> </tb-js-func>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset class="fields-group" formGroupName="grid"> <div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.chart.grid-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.comparison-settings</div>
<mat-checkbox formControlName="verticalLines">
{{ 'widgets.chart.show-vertical-lines' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="horizontalLines">
{{ 'widgets.chart.show-horizontal-lines' | translate }}
</mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.grid-outline-border-width</mat-label>
<input matInput type="number" min="0" formControlName="outlineWidth">
</mat-form-field>
<tb-color-input fxFlex
formControlName="color"
icon="format_color_fill"
label="{{ 'widgets.chart.primary-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
<tb-color-input fxFlex
formControlName="backgroundColor"
icon="format_color_fill"
label="{{ 'widgets.chart.background-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
<tb-color-input fxFlex
formControlName="tickColor"
icon="format_color_fill"
label="{{ 'widgets.chart.ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</fieldset>
<fieldset class="fields-group" formGroupName="xaxis">
<legend class="group-title" translate>widgets.chart.xaxis-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.axis-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.xaxis-tick-labels-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('xaxis.showLabels').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showLabels" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.show-tick-labels' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-color-input fxFlex
formControlName="color"
icon="format_color_fill"
label="{{ 'widgets.chart.ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</ng-template>
</mat-expansion-panel>
</fieldset>
</fieldset>
<fieldset class="fields-group" formGroupName="yaxis">
<legend class="group-title" translate>widgets.chart.yaxis-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.axis-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.min-scale-value</mat-label>
<input matInput type="number" formControlName="min">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.max-scale-value</mat-label>
<input matInput type="number" formControlName="max">
</mat-form-field>
</section>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.yaxis-tick-labels-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('yaxis.showLabels').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showLabels" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.show-tick-labels' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-color-input fxFlex
formControlName="color"
icon="format_color_fill"
label="{{ 'widgets.chart.ticks-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.tick-step-size</mat-label>
<input matInput type="number" formControlName="tickSize">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.number-of-decimals</mat-label>
<input matInput type="number" min="0" formControlName="tickDecimals">
</mat-form-field>
</section>
<tb-js-func
formControlName="ticksFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['value']"
functionTitle="{{ 'widgets.chart.ticks-formatter-function' | translate }}"
helpId="widget/lib/flot/ticks_formatter_fn">
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
</fieldset>
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.comparison-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('comparisonEnabled').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('comparisonEnabled').value">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="comparisonEnabled" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="comparisonEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center"> fxLayoutAlign="center">
{{ 'widgets.chart.enable-comparison' | translate }} {{ 'widgets.chart.enable-comparison' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<section fxLayout="column"> <div class="tb-widget-config-row space-between">
<mat-form-field fxFlex class="mat-block"> <div>{{ 'widgets.chart.time-for-comparison' | translate }}</div>
<mat-label translate>widgets.chart.time-for-comparison</mat-label> <mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeForComparison"> <mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'"> <mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
@ -277,18 +324,29 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field [fxShow]="flotSettingsFormGroup.get('timeForComparison').value === 'customInterval'" fxFlex class="mat-block"> </div>
<mat-label translate>widgets.chart.custom-interval-value</mat-label> <div [fxShow]="flotSettingsFormGroup.get('timeForComparison').value === 'customInterval'" class="tb-widget-config-row space-between">
<input matInput type="number" min="0" formControlName="comparisonCustomIntervalValue"> <div translate>widgets.chart.custom-interval-value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="comparisonCustomIntervalValue" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<fieldset class="fields-group" formGroupName="xaxisSecond"> </div>
<legend class="group-title" translate>widgets.chart.comparison-x-axis-settings</legend> <div class="tb-widget-config-panel stroked" formGroupName="xaxisSecond">
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-panel-title" translate>widgets.chart.comparison-x-axis-settings</div>
<mat-label translate>widgets.chart.axis-title</mat-label> <div class="tb-widget-config-row">
<input matInput formControlName="title"> <div class="fixed-title-width" translate>widgets.chart.axis-title</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label translate>widgets.chart.axis-position</mat-label> <div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="showLabels">
{{ 'widgets.chart.show-tick-labels' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.axis-position' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="axisPosition"> <mat-select formControlName="axisPosition">
<mat-option [value]="'top'"> <mat-option [value]="'top'">
{{ 'widgets.chart.axis-position-top' | translate }} {{ 'widgets.chart.axis-position-top' | translate }}
@ -298,31 +356,28 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-slide-toggle formControlName="showLabels" class="slide-block"> </div>
{{ 'widgets.chart.show-tick-labels' | translate }} </div>
</mat-slide-toggle>
</fieldset>
</section>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider"> <div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-panel tb-slide-toggle">
<legend class="group-title" translate>widgets.chart.custom-legend-settings</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.custom-legend-settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('customLegendEnabled').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('customLegendEnabled').value">
<mat-expansion-panel-header fxLayout="row" style="height: max-content; min-height: 48px;"> <mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60"> <mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="customLegendEnabled" (click)="$event.stopPropagation()" <mat-slide-toggle class="mat-slide" formControlName="customLegendEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;"> fxLayoutAlign="center">
{{ 'widgets.chart.enable-custom-legend' | translate }} {{ 'widgets.chart.enable-custom-legend' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxFlex="40" style="white-space: nowrap;" fxLayoutAlign="end center" fxHide.xs translate> <mat-panel-description fxFlex="40" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings widget-config.advanced-settings
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<fieldset class="fields-group"> <div class="tb-widget-config-panel stroked">
<legend class="group-title" translate>widgets.chart.label-keys-list</legend> <div class="tb-widget-config-panel-title" translate>widgets.chart.label-keys-list</div>
<div fxLayout="column"> <div fxLayout="column">
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical" <div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="labelDataKeyDrop($event)"> (cdkDropListDropped)="labelDataKeyDrop($event)">
@ -348,8 +403,8 @@
</button> </button>
</div> </div>
</div> </div>
</fieldset> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </div>
</section> </ng-container>

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

@ -45,6 +45,7 @@ import { defaultLegendConfig, widgetType } from '@shared/models/widget.models';
export const flotDefaultSettings = (chartType: ChartType): Partial<TbFlotSettings> => { export const flotDefaultSettings = (chartType: ChartType): Partial<TbFlotSettings> => {
const settings: Partial<TbFlotSettings> = { const settings: Partial<TbFlotSettings> = {
stack: false, stack: false,
enableSelection: true,
fontColor: '#545454', fontColor: '#545454',
fontSize: 10, fontSize: 10,
showTooltip: true, showTooltip: true,
@ -148,6 +149,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
// Common settings // Common settings
stack: [false, []], stack: [false, []],
enableSelection: [true, []],
fontSize: [10, [Validators.min(0)]], fontSize: [10, [Validators.min(0)]],
fontColor: ['#545454', []], fontColor: ['#545454', []],
@ -282,6 +284,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.disable({emitEvent: false}); this.flotSettingsFormGroup.disable({emitEvent: false});
} else { } else {
this.flotSettingsFormGroup.enable({emitEvent: false}); this.flotSettingsFormGroup.enable({emitEvent: false});
this.updateValidators(false);
} }
} }
@ -377,9 +380,11 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
} else { } else {
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent});
} }
this.flotSettingsFormGroup.get('xaxisSecond').enable({emitEvent: false});
} else { } else {
this.flotSettingsFormGroup.get('timeForComparison').disable({emitEvent: false}); this.flotSettingsFormGroup.get('timeForComparison').disable({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent});
this.flotSettingsFormGroup.get('xaxisSecond').disable({emitEvent: false});
} }
if (customLegendEnabled) { if (customLegendEnabled) {
this.flotSettingsFormGroup.get('dataKeysListForLabels').enable({emitEvent}); this.flotSettingsFormGroup.get('dataKeysListForLabels').enable({emitEvent});
@ -390,6 +395,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('xaxisSecond').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false});
} }
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/label-data-key.component.ts

@ -47,7 +47,7 @@ export function labelDataKeyValidator(control: AbstractControl): ValidationError
@Component({ @Component({
selector: 'tb-label-data-key', selector: 'tb-label-data-key',
templateUrl: './label-data-key.component.html', templateUrl: './label-data-key.component.html',
styleUrls: ['./label-data-key.component.scss', './../widget-settings.scss'], styleUrls: ['./label-data-key.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

56
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html

@ -15,20 +15,21 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="legendConfigForm"> <ng-container [formGroup]="legendConfigForm">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" <div class="tb-widget-config-row space-between">
fxLayoutGap="8px"> <div>{{ 'legend.direction' | translate }}</div>
<mat-form-field fxFlex> <mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>legend.direction</mat-label> <mat-select formControlName="direction">
<mat-select matInput formControlName="direction">
<mat-option *ngFor="let direction of legendDirections" [value]="direction"> <mat-option *ngFor="let direction of legendDirections" [value]="direction">
{{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex> </div>
<mat-label translate>legend.position</mat-label> <div class="tb-widget-config-row space-between">
<mat-select matInput formControlName="position"> <div>{{ 'legend.position' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="position">
<mat-option *ngFor="let pos of legendPositions" [value]="pos" <mat-option *ngFor="let pos of legendPositions" [value]="pos"
[disabled]="legendConfigForm.get('direction').value === legendDirection.row && [disabled]="legendConfigForm.get('direction').value === legendDirection.row &&
(pos === legendPosition.left || pos === legendPosition.right)"> (pos === legendPosition.left || pos === legendPosition.right)">
@ -37,28 +38,17 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div class="tb-widget-config-row space-between">
<mat-checkbox formControlName="showMin" fxFlex> <div translate>legend.show-values</div>
{{ 'legend.show-min' | translate }} <mat-chip-listbox multiple formControlName="showValues">
</mat-checkbox> <mat-chip-option value="min">{{ 'legend.min-option' | translate }}</mat-chip-option>
<mat-checkbox formControlName="showMax" fxFlex> <mat-chip-option value="max">{{ 'legend.max-option' | translate }}</mat-chip-option>
{{ 'legend.show-max' | translate }} <mat-chip-option value="average">{{ 'legend.average-option' | translate }}</mat-chip-option>
</mat-checkbox> <mat-chip-option value="total">{{ 'legend.total-option' | translate }}</mat-chip-option>
</div> <mat-chip-option value="latest">{{ 'legend.latest-option' | translate }}</mat-chip-option>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> </mat-chip-listbox>
<mat-checkbox formControlName="showAvg" fxFlex>
{{ 'legend.show-avg' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showTotal" fxFlex>
{{ 'legend.show-total' | translate }}
</mat-checkbox>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-checkbox formControlName="showLatest" fxFlex>
{{ 'legend.show-latest' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="sortDataKeys" fxFlex>
{{ 'legend.sort-legend' | translate }}
</mat-checkbox>
</div> </div>
</form> <mat-slide-toggle class="mat-slide" formControlName="sortDataKeys">
{{ 'legend.sort-legend' | translate }}
</mat-slide-toggle>
</ng-container>

57
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts

@ -15,7 +15,7 @@
/// ///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { isDefined } from '@core/utils'; import { isDefined } from '@core/utils';
import { import {
LegendConfig, LegendConfig,
@ -30,7 +30,7 @@ import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'tb-legend-config', selector: 'tb-legend-config',
templateUrl: './legend-config.component.html', templateUrl: './legend-config.component.html',
styleUrls: [], styleUrls: ['./../widget-settings.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -62,12 +62,8 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
this.legendConfigForm = this.fb.group({ this.legendConfigForm = this.fb.group({
direction: [null, []], direction: [null, []],
position: [null, []], position: [null, []],
sortDataKeys: [null, []], showValues: [[], []],
showMin: [null, []], sortDataKeys: [null, []]
showMax: [null, []],
showAvg: [null, []],
showTotal: [null, []],
showLatest: [null, []]
}); });
this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges
.subscribe((direction: LegendDirection) => { .subscribe((direction: LegendDirection) => {
@ -121,18 +117,49 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
this.legendConfigForm.patchValue({ this.legendConfigForm.patchValue({
direction: legendConfig.direction, direction: legendConfig.direction,
position: legendConfig.position, position: legendConfig.position,
sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false, showValues: this.getShowValues(legendConfig),
showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false, sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false
showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false,
showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false,
showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false,
showLatest: isDefined(legendConfig.showLatest) ? legendConfig.showLatest : false
}, {emitEvent: false}); }, {emitEvent: false});
} }
this.onDirectionChanged(legendConfig.direction); this.onDirectionChanged(legendConfig.direction);
} }
private legendConfigUpdated() { private legendConfigUpdated() {
this.propagateChange(this.legendConfigForm.value); const configValue = this.legendConfigForm.value;
const legendConfig: Partial<LegendConfig> = {
direction: configValue.direction,
position: configValue.position,
sortDataKeys: configValue.sortDataKeys
};
this.setShowValues(configValue.showValues, legendConfig);
this.propagateChange(legendConfig);
}
private getShowValues(legendConfig: LegendConfig): string[] {
const showValues: string[] = [];
if (isDefined(legendConfig.showMin) && legendConfig.showMin) {
showValues.push('min');
}
if (isDefined(legendConfig.showMax) && legendConfig.showMax) {
showValues.push('max');
}
if (isDefined(legendConfig.showAvg) && legendConfig.showAvg) {
showValues.push('average');
}
if (isDefined(legendConfig.showTotal) && legendConfig.showTotal) {
showValues.push('total');
}
if (isDefined(legendConfig.showLatest) && legendConfig.showLatest) {
showValues.push('latest');
}
return showValues;
}
private setShowValues(showValues: string[], legendConfig: Partial<LegendConfig>) {
legendConfig.showMin = showValues.includes('min');
legendConfig.showMax = showValues.includes('max');
legendConfig.showAvg = showValues.includes('average');
legendConfig.showTotal = showValues.includes('total');
legendConfig.showLatest = showValues.includes('latest');
} }
} }

50
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html

@ -15,27 +15,27 @@
limitations under the License. limitations under the License.
--> -->
<section [formGroup]="valueSourceFormGroup" fxLayout="column"> <ng-container [formGroup]="valueSourceFormGroup">
<mat-form-field fxFlex class="mat-block"> <div style="display: flex;">
<mat-label translate>widgets.value-source.value-source</mat-label> <tb-toggle-select formControlName="valueSource" style="min-width: 300px;">
<mat-select formControlName="valueSource"> <tb-toggle-option [value]="'predefinedValue'">
<mat-option [value]="'predefinedValue'">
{{ 'widgets.value-source.predefined-value' | translate }} {{ 'widgets.value-source.predefined-value' | translate }}
</mat-option> </tb-toggle-option>
<mat-option [value]="'entityAttribute'"> <tb-toggle-option [value]="'entityAttribute'">
{{ 'widgets.value-source.entity-attribute' | translate }} {{ 'widgets.value-source.entity-attribute' | translate }}
</mat-option> </tb-toggle-option>
</mat-select> </tb-toggle-select>
</mat-form-field> </div>
<mat-form-field [fxShow]="valueSourceFormGroup.get('valueSource').value === 'predefinedValue'" fxFlex class="mat-block"> <div [fxShow]="valueSourceFormGroup.get('valueSource').value === 'predefinedValue'" class="tb-widget-config-row space-between">
<mat-label translate>widgets.value-source.value</mat-label> <div translate>widgets.value-source.value</div>
<input matInput type="number" formControlName="value"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
</mat-form-field> <input matInput formControlName="value" type="number" placeholder="{{ 'widget-config.set' | translate }}">
<section [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'" </mat-form-field>
fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign="start center"> </div>
<mat-form-field fxFlex class="mat-block"> <div [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'" class="tb-widget-config-row space-between">
<mat-label>{{ 'widgets.value-source.source-entity-alias' | translate }}</mat-label> <div translate>widgets.value-source.source-entity-alias</div>
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}" <mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<input matInput type="text" placeholder="{{ 'widget-config.set' | translate }}"
#entityAliasInput #entityAliasInput
formControlName="entityAlias" formControlName="entityAlias"
[matAutocomplete]="entityAliasAutocomplete"> [matAutocomplete]="entityAliasAutocomplete">
@ -53,9 +53,11 @@
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> </div>
<mat-label>{{ 'widgets.value-source.source-entity-attribute' | translate }}</mat-label> <div [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'" class="tb-widget-config-row space-between">
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}" <div translate>widgets.value-source.source-entity-attribute</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<input matInput type="text" placeholder="{{ 'widget-config.set' | translate }}"
#keyInput #keyInput
formControlName="attribute" formControlName="attribute"
[matAutocomplete]="keyAutocomplete"> [matAutocomplete]="keyAutocomplete">
@ -73,5 +75,5 @@
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</section> </div>
</section> </ng-container>

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts

@ -14,8 +14,8 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, ElementRef, forwardRef, HostBinding, Input, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -39,7 +39,7 @@ export interface ValueSourceProperty {
@Component({ @Component({
selector: 'tb-value-source', selector: 'tb-value-source',
templateUrl: './value-source.component.html', templateUrl: './value-source.component.html',
styleUrls: [], styleUrls: ['./../widget-settings.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -50,8 +50,6 @@ export interface ValueSourceProperty {
}) })
export class ValueSourceComponent extends PageComponent implements OnInit, ControlValueAccessor { export class ValueSourceComponent extends PageComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.display') display = 'block';
@ViewChild('entityAliasInput') entityAliasInput: ElementRef; @ViewChild('entityAliasInput') entityAliasInput: ElementRef;
@ViewChild('keyInput') keyInput: ElementRef; @ViewChild('keyInput') keyInput: ElementRef;
@ -212,15 +210,13 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr
private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> { private fetchEntityKeys(entityAliasId: string, dataKeyTypes: Array<DataKeyType>): Observable<Array<DataKey>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe( return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(
return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter, aliasInfo.entityFilter,
dataKeyTypes, [], dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe( ).pipe(
catchError(() => of([])) catchError(() => of([]))
); )),
}),
catchError(() => of([] as Array<DataKey>)) catchError(() => of([] as Array<DataKey>))
); );
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/fixed-color-level.component.ts

@ -50,7 +50,7 @@ export function fixedColorLevelValidator(control: AbstractControl): ValidationEr
@Component({ @Component({
selector: 'tb-fixed-color-level', selector: 'tb-fixed-color-level',
templateUrl: './fixed-color-level.component.html', templateUrl: './fixed-color-level.component.html',
styleUrls: ['./fixed-color-level.component.scss', './../widget-settings.scss'], styleUrls: ['./fixed-color-level.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/gauge-highlight.component.ts

@ -31,7 +31,7 @@ export interface GaugeHighlight {
@Component({ @Component({
selector: 'tb-gauge-highlight', selector: 'tb-gauge-highlight',
templateUrl: './gauge-highlight.component.html', templateUrl: './gauge-highlight.component.html',
styleUrls: ['./gauge-highlight.component.scss', './../widget-settings.scss'], styleUrls: ['./gauge-highlight.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

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

Loading…
Cancel
Save