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",
"dataKeySettingsDirective": "tb-timeseries-table-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",
"dataKeySettingsDirective": "tb-flot-line-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",
"dataKeySettingsDirective": "tb-flot-line-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",
"dataKeySettingsDirective": "tb-flot-bar-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
-- 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
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
protected boolean doProcess(TbActorMsg msg) {
if (!ruleChainsInitialized) {
initTenantActors();
ruleChainsInitialized = true;
if (msg.getMsgType() != MsgType.APP_INIT_MSG && msg.getMsgType() != MsgType.PARTITION_CHANGE_MSG) {
log.warn("Rule Chains initialized by unexpected message: {}", msg);
if (MsgType.APP_INIT_MSG.equals(msg.getMsgType())) {
initTenantActors();
ruleChainsInitialized = true;
} else {
if (!msg.getMsgType().isIgnoreOnStart()) {
log.warn("Attempt to initialize Rule Chains by unexpected message: {}", msg);
}
return true;
}
}
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 DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION =
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\": \"LwRpk00000000\",\n" +
" \"type\": \"lwm2mProfileRpk\"\n" +
" },\n" +
" \"name\":\"Name_DeviceWithCredantial_AccessToken\",\n" +
" \"label\":\"Label_DeviceWithCredantial_AccessToken\",\n" +
" \"deviceProfileId\":{\n" +
" \"id\":\"9d9588c0-06c9-11ee-b618-19be30fdeb60\",\n" +
" \"entityType\":\"DEVICE_PROFILE\"\n" +
" }\n" +
" },\n" +
" \"credentials\": {\n" +
" \"id\": \"null\",\n" +
" \"createdTime\": 0,\n" +
" \"deviceId\": \"null\",\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" +
" \"credentialsType\": \"ACCESS_TOKEN\",\n" +
" \"credentialsId\": \"6hmxew8pmmzng4e3une2\"\n" +
" }\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_UPDATE_CREDENTIALS_ACCESS_TOKEN_PARAM_DESCRIPTION =
"{\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 +
@ -254,7 +434,7 @@ public class ControllerConstants {
" * '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";
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" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
@ -269,7 +449,7 @@ public class ControllerConstants {
" }\n" +
"}" +
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" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
@ -321,9 +501,9 @@ public class ControllerConstants {
" }\n" +
"}" +
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" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
@ -339,7 +519,8 @@ public class ControllerConstants {
" }\n" +
"}" +
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" +
" \"spec\":{\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_TEXT_SEARCH_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_RECEIVE_STEP_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 ",
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. " +
"You may find the example of LwM2M device and RPK credentials below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN +
"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 device with different type of credentials below: \n\n" +
"- 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. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@ -277,10 +296,27 @@ public class DeviceController extends BaseController {
return tbDeviceService.getDeviceCredentialsByDeviceId(device, getCurrentUser());
}
@ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " +
"Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value. " +
"Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device. " +
"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)
@ApiOperation(value = "Update device credentials (updateDeviceCredentials)",
notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. \" +\n" +
"Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value.\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.\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')")
@RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
@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));
checkEdgeId(edgeId, Operation.READ);
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);
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override
public void onSuccess(Void result) {
public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) {
sessionNewEvents.put(edgeId, true);
}
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.extern.slf4j.Slf4j;
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.EdgeUtils;
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.page.PageData;
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.AttributesRequestMsg;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Slf4j
@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 String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
@ -103,6 +105,12 @@ public final class EdgeGrpcSession implements Closeable {
private boolean connected;
private boolean syncCompleted;
private Long newStartTs;
private Long previousStartTs;
private Long newStartSeqId;
private Long previousStartSeqId;
private Long seqIdEnd;
private EdgeVersion edgeVersion;
private int maxInboundMessageSize;
@ -204,10 +212,10 @@ public final class EdgeGrpcSession implements Closeable {
EdgeEventFetcher next = cursor.getNext();
log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}",
edge.getTenantId(), edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName());
ListenableFuture<UUID> uuidListenableFuture = startProcessingEdgeEvents(next);
Futures.addCallback(uuidListenableFuture, new FutureCallback<>() {
ListenableFuture<Pair<Long, Long>> future = startProcessingEdgeEvents(next);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable UUID result) {
public void onSuccess(@Nullable Pair<Long, Long> result) {
doSync(cursor);
}
@ -307,36 +315,51 @@ public final class EdgeGrpcSession implements Closeable {
sendDownlinkMsg(edgeConfigMsg);
}
ListenableFuture<Void> processEdgeEvents() throws Exception {
SettableFuture<Void> result = SettableFuture.create();
ListenableFuture<Boolean> processEdgeEvents() throws Exception {
SettableFuture<Boolean> result = SettableFuture.create();
log.trace("[{}] starting processing edge events", this.sessionId);
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(
queueStartTs,
this.previousStartTs,
this.previousStartSeqId,
this.seqIdEnd,
false,
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService());
ListenableFuture<UUID> ifOffsetFuture = startProcessingEdgeEvents(fetcher);
Futures.addCallback(ifOffsetFuture, new FutureCallback<>() {
Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable UUID ifOffset) {
if (ifOffset != null) {
Long newStartTs = Uuids.unixTimestamp(ifOffset);
ListenableFuture<List<String>> updateFuture = updateQueueStartTs(newStartTs);
public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
if (newStartTsAndSeqId != null) {
ListenableFuture<List<String>> updateFuture = updateQueueStartTsAndSeqId(newStartTsAndSeqId);
Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<String> list) {
log.debug("[{}] queue offset was updated [{}][{}]", sessionId, ifOffset, newStartTs);
result.set(null);
log.debug("[{}] queue offset was updated [{}]", sessionId, newStartTsAndSeqId);
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
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);
}
}, ctx.getGrpcCallbackExecutorService());
} 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);
}
}
@ -354,14 +377,14 @@ public final class EdgeGrpcSession implements Closeable {
return result;
}
private ListenableFuture<UUID> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
SettableFuture<UUID> result = SettableFuture.create();
private ListenableFuture<Pair<Long, Long>> startProcessingEdgeEvents(EdgeEventFetcher fetcher) {
SettableFuture<Pair<Long, Long>> result = SettableFuture.create();
PageLink pageLink = fetcher.getPageLink(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount());
processEdgeEvents(fetcher, pageLink, 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 {
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) {
@ -377,8 +400,15 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && pageData.hasNext()) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId();
result.set(ifOffset);
EdgeEvent latestEdgeEvent = pageData.getData().get(pageData.getData().size() - 1);
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) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
DownlinkMsg downlinkMsg = null;
try {
switch (edgeEvent.getAction()) {
case UPDATED:
case ADDED:
case DELETED:
case ASSIGNED_TO_EDGE:
case UNASSIGNED_FROM_EDGE:
case ALARM_ACK:
case ALARM_CLEAR:
case CREDENTIALS_UPDATED:
case RELATION_ADD_OR_UPDATE:
case RELATION_DELETED:
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
case CREDENTIALS_REQUEST:
case RPC_CALL:
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
break;
case ATTRIBUTES_UPDATED:
case POST_ATTRIBUTES:
case ATTRIBUTES_DELETED:
case TIMESERIES_UPDATED:
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
break;
default:
log.warn("[{}][{}] Unsupported action type [{}]", edge.getTenantId(), this.sessionId, edgeEvent.getAction());
private List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
List<DownlinkMsg> result = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", edge.getTenantId(), this.sessionId, edgeEvent);
DownlinkMsg downlinkMsg = null;
try {
switch (edgeEvent.getAction()) {
case UPDATED:
case ADDED:
case DELETED:
case ASSIGNED_TO_EDGE:
case UNASSIGNED_FROM_EDGE:
case ALARM_ACK:
case ALARM_CLEAR:
case CREDENTIALS_UPDATED:
case RELATION_ADD_OR_UPDATE:
case RELATION_DELETED:
case CREDENTIALS_REQUEST:
case RPC_CALL:
case ASSIGNED_TO_CUSTOMER:
case UNASSIGNED_FROM_CUSTOMER:
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
log.trace("[{}][{}] entity message processed [{}]", edgeEvent.getTenantId(), this.sessionId, downlinkMsg);
break;
case ATTRIBUTES_UPDATED:
case POST_ATTRIBUTES:
case ATTRIBUTES_DELETED:
case TIMESERIES_UPDATED:
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edgeEvent);
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) {
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) {
return edgeEvents
.stream()
.map(this::convertToDownlinkMsg)
.filter(Objects::nonNull)
.collect(Collectors.toList());
private boolean isNewEdgeEventsAvailable() {
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(), this.newStartSeqId, null, pageLink);
return !edgeEvents.getData().isEmpty();
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to execute isNewEdgeEventsAvailable", edge.getTenantId(), edge.getId(), sessionId, e);
}
return false;
}
private ListenableFuture<Long> getQueueStartTs() {
ListenableFuture<Optional<AttributeKvEntry>> future =
ctx.getAttributesService().find(edge.getTenantId(), edge.getId(), DataConstants.SERVER_SCOPE, QUEUE_START_TS_ATTR_KEY);
return Futures.transform(future, attributeKvEntryOpt -> {
if (attributeKvEntryOpt != null && attributeKvEntryOpt.isPresent()) {
AttributeKvEntry attributeKvEntry = attributeKvEntryOpt.get();
return attributeKvEntry.getLongValue().isPresent() ? attributeKvEntry.getLongValue().get() : 0L;
} else {
return 0L;
private long findStartSeqIdFromOldestEventIfAny() {
long startSeqId = 0L;
try {
TimePageLink pageLink = new TimePageLink(1, 0, null, new SortOrder("createdTime"), null, null);
PageData<EdgeEvent> edgeEvents = ctx.getEdgeEventService().findEdgeEvents(edge.getTenantId(), edge.getId(), null, null, pageLink);
if (!edgeEvents.getData().isEmpty()) {
startSeqId = edgeEvents.getData().get(0).getSeqId() - 1;
}
}, 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) {
log.trace("[{}] updating QueueStartTs [{}][{}]", this.sessionId, edge.getId(), newStartTs);
List<AttributeKvEntry> attributes = Collections.singletonList(
new BaseAttributeKvEntry(
new LongDataEntry(QUEUE_START_TS_ATTR_KEY, newStartTs), System.currentTimeMillis()));
private ListenableFuture<List<String>> updateQueueStartTsAndSeqId(Pair<Long, Long> pair) {
this.newStartTs = pair.getFirst();
this.newStartSeqId = pair.getSecond();
log.trace("[{}] updateQueueStartTsAndSeqId [{}][{}][{}]", this.sessionId, edge.getId(), this.newStartTs, this.newStartSeqId);
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);
}
@ -693,8 +767,11 @@ public final class EdgeGrpcSession implements Closeable {
}
private void interruptPreviousSendDownlinkMsgsTask() {
log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(true);
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()
|| 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() {

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.stereotype.Component;
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.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityViewId;
@ -47,13 +50,22 @@ public class AlarmMsgConstructor {
String entityName = null;
switch (alarm.getOriginator().getEntityType()) {
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;
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;
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;
}
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;
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.EdgeEvent;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.edge.EdgeEventService;
@AllArgsConstructor
@Slf4j
public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
private final Long queueStartTs;
private Long seqIdStart;
@Getter
private Long seqIdEnd;
@Getter
private boolean seqIdNewCycleStarted;
private Long maxReadRecordsCount;
private final EdgeEventService edgeEventService;
@Override
@ -37,13 +45,32 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher {
pageSize,
0,
null,
new SortOrder("createdTime", SortOrder.Direction.ASC),
null,
queueStartTs,
null);
System.currentTimeMillis());
}
@Override
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 {
log.info("Skipping audit logs migration");
}
boolean skipEdgeEventsMigration = getEnv("TB_SKIP_EDGE_EVENTS_MIGRATION", false);
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");
}
migrateEdgeEvents("Starting edge events migration. ");
break;
case "3.5.1":
log.info("Updating data from version 3.5.1 to 3.5.2 ...");
migrateEdgeEvents("Starting edge events migration - adding seq_id column. ");
break;
default:
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
public void upgradeRuleNodes() {
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())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId())
.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) {
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) {

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

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

@ -497,6 +497,9 @@ cache:
entityCount:
timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_COUNT_TTL:1440}"
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:
timeToLiveInMinutes: "${CACHE_SPECS_ALARM_TYPES_TTL:60}"
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 = {
"edges.enabled=true",
"queue.rule-engine.stats.enabled=false",
})
abstract public class AbstractEdgeTest extends AbstractControllerTest {
@ -181,14 +182,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
@After
public void afterTest() throws Exception {
try {
edgeImitator.disconnect();
} catch (Exception ignored){}
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getUuidId())
.andExpect(status().isOk());
try {
edgeImitator.disconnect();
} catch (Exception ignored) {}
}
private void installation() {

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

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

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 DASHBOARD_TITLES_CACHE = "dashboardTitles";
public static final String ENTITY_COUNT_CACHE = "entityCount";
public static final String RESOURCE_INFO_CACHE = "resourceInfo";
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) {
if (cleared) {
return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
} 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)
public class EdgeEvent extends BaseData<EdgeEventId> {
private long seqId;
private TenantId tenantId;
private EdgeId edgeId;
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 AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private boolean acknowledged;
private boolean cleared;
private CustomerId alarmCustomerId;
@Override

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.msg;
import lombok.Getter;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@ -28,7 +29,7 @@ public enum MsgType {
*
* See {@link PartitionChangeMsg}
*/
PARTITION_CHANGE_MSG,
PARTITION_CHANGE_MSG(true),
APP_INIT_MSG,
@ -108,7 +109,7 @@ public enum MsgType {
* Message that is sent from the Device Actor to Rule Engine. Requires acknowledgement
*/
SESSION_TIMEOUT_MSG,
SESSION_TIMEOUT_MSG(true),
STATS_PERSIST_TICK_MSG,
@ -130,4 +131,14 @@ public enum MsgType {
EDGE_SYNC_REQUEST_TO_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
public PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) {
return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, pageLink, withTsUpdate);
public PageData<EdgeEvent> findEdgeEvents(TenantId tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) {
return edgeEventDao.findEdgeEvents(tenantId.getId(), edgeId, seqIdStart, seqIdEnd, pageLink);
}
@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 edgeId the edgeId
* @param seqIdStart the seq id start
* @param seqIdEnd the seq id end
* @param pageLink the pageLink
* @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.

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_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_TYPE_PROPERTY = "edge_event_type";
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_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_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_TYPE_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
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)
private UUID tenantId;
@ -120,6 +124,7 @@ public class EdgeEventEntity extends BaseSqlEntity<EdgeEvent> implements BaseEnt
edgeEvent.setAction(edgeEventAction);
edgeEvent.setBody(entityBody);
edgeEvent.setUid(edgeEventUid);
edgeEvent.setSeqId(seqId);
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 org.hibernate.exception.ConstraintViolationException;
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.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
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.page.PageData;
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.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
@ -45,7 +50,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
@Service("TbResourceDaoService")
@Slf4j
@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 ";
private final TbResourceDao resourceDao;
@ -55,10 +60,12 @@ public class BaseResourceService implements ResourceService {
@Override
public TbResource saveResource(TbResource resource) {
resourceValidator.validate(resource, TbResourceInfo::getTenantId);
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) {
publishEvictEvent(new ResourceInfoEvictEvent(resource.getTenantId(), resource.getId()));
ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("resource_unq_key")) {
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) {
log.trace("Executing findResourceInfoById [{}] [{}]", tenantId, 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
@ -169,13 +178,11 @@ public class BaseResourceService implements ResourceService {
}
};
protected Optional<ConstraintViolationException> extractConstraintViolationException(Exception t) {
if (t instanceof ConstraintViolationException) {
return Optional.of((ConstraintViolationException) t);
} else if (t.getCause() instanceof ConstraintViolationException) {
return Optional.of((ConstraintViolationException) (t.getCause()));
} else {
return Optional.empty();
@TransactionalEventListener(classes = ResourceInfoEvictEvent.class)
@Override
public void handleEvictEvent(ResourceInfoEvictEvent event) {
if (event.getResourceId() != null) {
cache.evict(new ResourceInfoCacheKey(event.getTenantId(), event.getResourceId()));
}
}
}

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 " +
"e.tenantId = :tenantId " +
"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 (:seqIdStart IS NULL OR e.seqId > :seqIdStart) " +
"AND (:seqIdEnd IS NULL OR e.seqId < :seqIdEnd) " +
"AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))"
)
Page<EdgeEventEntity> findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId,
@ -39,20 +41,7 @@ public interface EdgeEventRepository extends JpaRepository<EdgeEventEntity, UUID
@Param("textSearch") String textSearch,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("seqIdStart") Long seqIdStart,
@Param("seqIdEnd") Long seqIdEnd,
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.EdgeId;
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.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
@ -43,7 +44,9 @@ import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -118,7 +121,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
}
};
queue = new TbSqlBlockingQueueWrapper<>(params, hashcodeFunction, 1, statsFactory);
queue.init(logExecutor, v -> edgeEventInsertRepository.save(v),
queue.init(logExecutor, edgeEventInsertRepository::save,
Comparator.comparing(EdgeEventEntity::getTs)
);
}
@ -171,29 +174,23 @@ public class JpaBaseEdgeEventDao extends JpaAbstractDao<EdgeEventEntity, EdgeEve
@Override
public PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, TimePageLink pageLink, boolean withTsUpdate) {
if (withTsUpdate) {
return DaoUtil.toPageData(
edgeEventRepository
.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)));
public PageData<EdgeEvent> findEdgeEvents(UUID tenantId, EdgeId edgeId, Long seqIdStart, Long seqIdEnd, TimePageLink pageLink) {
List<SortOrder> sortOrders = new ArrayList<>();
if (pageLink.getSortOrder() != null) {
sortOrders.add(pageLink.getSortOrder());
}
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

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 (
seq_id INT GENERATED ALWAYS AS IDENTITY,
id uuid NOT NULL,
created_time bigint NOT NULL,
edge_id uuid,
@ -731,6 +732,7 @@ CREATE TABLE IF NOT EXISTS edge_event (
tenant_id uuid,
ts bigint NOT NULL
) PARTITION BY RANGE(created_time);
ALTER TABLE IF EXISTS edge_event ALTER COLUMN seq_id SET CYCLE;
CREATE TABLE IF NOT EXISTS rpc (
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);
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());
EdgeEvent saved = edgeEvents.getData().get(0);
@ -113,7 +113,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Futures.allAsList(futures).get();
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.assertEquals(2, edgeEvents.getData().size());
@ -122,7 +122,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Assert.assertTrue(edgeEvents.hasNext());
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.assertEquals(1, edgeEvents.getData().size());
@ -132,26 +132,6 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
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 {
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED);
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.maxSize=10000
cache.specs.resourceInfo.timeToLiveInMinutes=1440
cache.specs.resourceInfo.maxSize=10000
cache.specs.alarmTypes.timeToLiveInMinutes=60
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
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;
scheduleAlarmHarvesting(ctx, null);
ctx.addDeviceProfileListeners(this::onProfileUpdate, this::onDeviceUpdate);
initAlarmRuleState(false);
}
private void initAlarmRuleState(boolean printNewlyAddedDeviceStates) {
if (config.isFetchAlarmRulesStateOnStart()) {
log.info("[{}] Fetching alarm rule state", ctx.getSelfId());
int fetchCount = 0;
@ -86,7 +90,7 @@ public class TbDeviceProfileNode implements TbNode {
for (RuleNodeState rns : states.getData()) {
fetchCount++;
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);
ctx.tellSuccess(msg);
} else {
DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null);
DeviceState deviceState = getOrCreateDeviceState(ctx, deviceId, null, false);
if (deviceState != null) {
deviceState.process(ctx, msg);
} else {
@ -148,6 +152,7 @@ public class TbDeviceProfileNode implements TbNode {
public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
// Cleanup the cache for all entities that are no longer assigned to current server partitions
deviceStates.entrySet().removeIf(entry -> !ctx.isLocalEntity(entry.getKey()));
initAlarmRuleState(true);
}
@Override
@ -156,13 +161,16 @@ public class TbDeviceProfileNode implements TbNode {
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);
if (deviceState == null) {
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId);
if (deviceProfile != null) {
deviceState = new DeviceState(ctx, config, deviceId, new ProfileState(deviceProfile), rns);
deviceStates.put(deviceId, deviceState);
if (printNewlyAddedDeviceStates) {
log.info("[{}][{}] Device [{}] was added during PartitionChangeMsg", ctx.getTenantId(), ctx.getSelfId(), deviceId);
}
}
}
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 StringItemsListComponent from '@shared/components/string-items-list.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 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/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@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>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="hasBasicMode" (valueChange)="widgetConfigMode = $event" ignoreMdLgSize="true"
appearance="fill-invert" [options]="[
{
name: translate.instant('widget.basic-mode'),
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<button mat-icon-button
@ -57,6 +49,7 @@
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<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"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="tbEditWidget.hasBasicMode" (valueChange)="tbEditWidget.widgetConfigMode = $event" ignoreMdLgSize="true"
appearance="fill-invert" [options]="[
{
name: translate.instant('widget.basic-mode'),
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="tbEditWidget.widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<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(
(widgetTypeInfo) => {
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
config.title = 'New ' + widgetTypeInfo.widgetName;
if (!config.title) {
config.title = 'New ' + widgetTypeInfo.widgetName;
}
let newWidget: Widget = {
isSystemType: widget.isSystemType,
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;
}
}

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

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

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

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

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

@ -16,6 +16,7 @@
:host {
display: block;
.clear-alarm-rule {
max-width: 100%;
border: 2px groove rgba(0, 0, 0, .45);
border-radius: 4px;
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';
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 {
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({
declarations: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
imports: [
CommonModule,
SharedModule,
WidgetSettingsModule,
WidgetConfigComponentsModule
],
exports: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
@ -57,5 +67,7 @@ export class BasicWidgetConfigModule {
export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = {
'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-title" translate>widget-config.appearance</div>
<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 }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
@ -63,7 +63,7 @@
</div>
</div>
<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-option value="search">{{ 'action.search' | 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';
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-entities-table-basic-config',
@ -73,11 +75,7 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
protected onConfigSet(configData: WidgetConfigComponentData) {
this.entitiesTableWidgetConfigForm = this.fb.group({
timewindowConfig: [{
useDashboardTimewindow: configData.config.useDashboardTimewindow,
displayTimewindow: configData.config.useDashboardTimewindow,
timewindow: configData.config.timewindow
}, []],
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
columns: [this.getColumns(configData.config.datasources), []],
showTitle: [configData.config.showTitle, []],
@ -155,13 +153,13 @@ export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponen
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (config.settings?.enableSearch) {
if (isUndefined(config.settings?.enableSearch) || config.settings?.enableSearch) {
buttons.push('search');
}
if (config.settings?.enableSelectColumnDisplay) {
if (isUndefined(config.settings?.enableSelectColumnDisplay) || config.settings?.enableSelectColumnDisplay) {
buttons.push('columnsToDisplay');
}
if (config.enableFullscreen) {
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
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-title" translate>widget-config.appearance</div>
<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">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
@ -57,7 +57,7 @@
</div>
<div class="tb-widget-config-row space-between">
<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 }}">
</mat-form-field>
</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';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
@Component({
selector: 'tb-simple-card-basic-config',
@ -63,11 +64,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
protected onConfigSet(configData: WidgetConfigComponentData) {
this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [{
useDashboardTimewindow: configData.config.useDashboardTimewindow,
displayTimewindow: configData.config.useDashboardTimewindow,
timewindow: configData.config.timewindow
}, []],
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
label: [this.getDataKeyLabel(configData.config.datasources), []],
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">
<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-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
@ -147,7 +153,7 @@
</tb-widget-units>
</div>
<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 }}">
</mat-form-field>
</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 {
width: 60px;
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;
}
get hasAdditionalLatestDataKeys(): boolean {
return this.dataKeysPanelComponent.hasAdditionalLatestDataKeys;
}
get widget(): Widget {
return this.widgetConfigComponent.widget;
}
@ -165,7 +169,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.aliasController;
}
get datakeySettingsSchema(): JsonSettingsSchema {
get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
@ -173,6 +177,14 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective;
}
get latestDataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema;
}
get latestDataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective;
}
get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
}
@ -193,6 +205,10 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.dataKeysPanelComponent.dragEnabled;
}
get isLatestDataKeys(): boolean {
return this.hasAdditionalLatestDataKeys && this.keyRowFormGroup.get('latest').value === true;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
@ -212,6 +228,12 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
units: [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.updateModel()
);
@ -286,6 +308,11 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
decimals: value?.decimals
}, {emitEvent: false}
);
if (this.hasAdditionalLatestDataKeys) {
this.keyRowFormGroup.patchValue({
latest: (value as any)?.latest
}, {emitEvent: false});
}
this.cd.markForCheck();
}
@ -323,8 +350,8 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
data: {
dataKey: deepClone(this.modelValue),
dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general,
dataKeySettingsSchema: this.datakeySettingsSchema,
dataKeySettingsDirective: this.dataKeySettingsDirective,
dataKeySettingsSchema: this.isLatestDataKeys ? this.latestDataKeySettingsSchema : this.dataKeySettingsSchema,
dataKeySettingsDirective: this.isLatestDataKeys ? this.latestDataKeySettingsDirective : this.dataKeySettingsDirective,
dashboard: this.dashboard,
aliasController: this.aliasController,
widget: this.widget,
@ -399,7 +426,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
} else if (this.datasourceType === DatasourceType.entity && this.entityAliasId ||
this.datasourceType === DatasourceType.device && this.deviceId) {
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.entityField);
if (this.widgetType === widgetType.alarm) {
@ -428,7 +455,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
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) {
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-data-keys-table">
<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.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div>
@ -52,6 +53,7 @@
</div>
</div>
</div>
<tb-error *ngIf="errorText" noMargin [error]="errorText" style="padding-left: 12px;"></tb-error>
</div>
<div>
<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;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
&.tb-source-header {
width: 140px;
}
&.tb-color-header, &.tb-units-header, &.tb-decimals-header {
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()
noKeysText: string;
@Input()
requiredKeysText: string;
@Input()
datasourceType: DatasourceType;
@ -97,12 +100,18 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
@coerceBoolean()
hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideSourceSelection = false;
dataKeyType: DataKeyType;
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
keysListFormGroup: UntypedFormGroup;
errorText = '';
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
@ -111,6 +120,11 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get hasAdditionalLatestDataKeys(): boolean {
return !this.hideSourceSelection && this.widgetConfigComponent.widgetType === widgetType.timeseries &&
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
}
get datakeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
@ -119,6 +133,14 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
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) => {};
constructor(private fb: UntypedFormBuilder,
@ -196,7 +218,13 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
}
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: {
valid: false,
},
@ -226,6 +254,9 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema);
dataKey.label = '';
dataKey.decimals = 0;
if (this.hasAdditionalLatestDataKeys) {
(dataKey as any).latest = false;
}
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray;
const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]);
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">
<h2>{{ 'datakey.configuration' | translate }}</h2>
<span fxFlex></span>
<tb-toggle-header *ngIf="hasAdvanced" (valueChange)="dataKeyConfigMode = $event" ignoreMdLgSize="true"
<tb-toggle-select *ngIf="hasAdvanced"
appearance="fill-invert" [options]="dataKeyConfigHeaderOptions"
[value]="dataKeyConfigMode" name="dataKeyConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
[(ngModel)]="dataKeyConfigMode" [ngModelOptions]="{ standalone: true }">
</tb-toggle-select>
<button mat-icon-button
(click)="cancel()"
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.
-->
<div class="tb-datakey-config" *ngIf="dataKeyConfigMode === dataKeyConfigModes.general; else advanced"
<div *ngIf="dataKeyConfigMode === dataKeyConfigModes.general; else advanced"
[formGroup]="dataKeyFormGroup">
<div class="mat-padding" fxLayout="column" style="gap: 16px;">
<div class="tb-widget-config-panel">
@ -54,7 +54,7 @@
</div>
<div class="tb-widget-config-row space-between" *ngIf="!hideDataKeyDecimals">
<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 }}">
</mat-form-field>
</div>
@ -182,14 +182,12 @@
</div>
</div>
<ng-template #advanced>
<div class="tb-datakey-config" [formGroup]="dataKeySettingsFormGroup">
<div class="mat-padding" fxLayout="column">
<tb-widget-settings
[dashboard]="dashboard"
[aliasController]="aliasController"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
</div>
<div class="mat-padding" fxLayout="column" [formGroup]="dataKeySettingsFormGroup">
<tb-widget-settings
[dashboard]="dashboard"
[aliasController]="aliasController"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
</div>
</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({
selector: 'tb-data-key-config',
templateUrl: './data-key-config.component.html',
styleUrls: ['./data-key-config.component.scss'],
styleUrls: [],
providers: [
{
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">
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</div>
<tb-toggle-header *ngIf="basicMode" (valueChange)="datasourcesModeChange($event)" ignoreMdLgSize="true"
[options]="[
{
name: translate.instant('device.device'),
value: datasourceType.device
},
{
name: translate.instant('entity.entity-alias'),
value: datasourceType.entity
}
]" [value]="datasourcesMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<tb-toggle-select *ngIf="basicMode" [ngModel]="datasourcesMode" (ngModelChange)="datasourcesModeChange($event)"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option [value]="datasourceType.device">{{ 'device.device' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="datasourceType.entity">{{ 'entity.entity-alias' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div *ngIf="maxDatasources > 1"
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 {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources;
return this.forceSingleDatasource ? 1 : this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources;
}
public get singleDatasource(): boolean {
@ -108,6 +108,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean()
hideDataKeys = false;
@Input()
@coerceBoolean()
forceSingleDatasource = false;
@Input()
configMode: WidgetConfigMode;
@ -175,13 +179,20 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
this.datasourcesMode = this.detectDatasourcesMode(datasources);
let changed = false;
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) {
datasource.type = this.datasourcesMode;
changed = true;
}
this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false});
});
}
}
if (this.singleDatasource && !this.datasourcesFormArray.length) {
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 fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div>
<tb-toggle-header (valueChange)="timewindowConfig.get('useDashboardTimewindow').patchValue($event)" ignoreMdLgSize="true"
[options]="[
{ name: translate.instant('widget-config.use-dashboard-timewindow'), value: true},
{ name: translate.instant('widget-config.use-widget-timewindow'), value: false}
]"
[value]="timewindowConfig.get('useDashboardTimewindow').value" name="useDashboardTimewindow" useSelectOnMdLg="false">
</tb-toggle-header>
<tb-toggle-select formControlName="useDashboardTimewindow">
<tb-toggle-option [value]="true">{{ 'widget-config.use-dashboard-timewindow' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="false">{{ 'widget-config.use-widget-timewindow' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-widget-config-row">
<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 { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
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 { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefined } from '@core/utils';
export interface TimewindowConfigData {
useDashboardTimewindow: boolean;
@ -28,6 +29,14 @@ export interface TimewindowConfigData {
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({
selector: 'tb-timewindow-config-panel',
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.
-->
<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 }}">
</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) {
const tvPair = cellData.data[cellData.data.length -
1];
const value = tvPair[1];
const value = parseFloat(tvPair[1]);
if (value !== this.gauge.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 {
stack: boolean;
enableSelection: boolean;
shadowSize: number;
fontColor: string;
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 flotClickHandler = this.onFlotClick.bind(this);
private enableSelection: boolean;
private selectionMode: 'x' | null;
private readonly showTooltip: boolean;
private readonly animatedPie: boolean;
private pieDataAnimationDuration: number;
@ -131,6 +134,8 @@ export class TbFlot {
this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings;
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.tooltip = this.showTooltip ? $('#flot-series-tooltip') : null;
if (this.tooltip?.length === 0) {
@ -167,7 +172,7 @@ export class TbFlot {
};
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.xaxis = {
mode: 'time',
@ -1251,7 +1256,7 @@ export class TbFlot {
this.$element.css('pointer-events', '');
this.$element.addClass('mouse-events');
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('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>
<div [innerHTML]="'widgets.getting-started.tenant-admin.step2.content-before' | translate | safe: 'html'"></div>
<div class="tb-bordered-content">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false"
[options]="[
{
name: 'Ubuntu',
value: 'ubuntu'
},
{
name: 'MacOS',
value: 'macos'
},
{
name: 'Windows',
value: 'windows'
}
]">
<tb-toggle-header #publishCommandSteps value="ubuntu" name="publishCommandSteps" useSelectOnMdLg="false">
<tb-toggle-option value="ubuntu">Ubuntu</tb-toggle-option>
<tb-toggle-option value="macos">MacOS</tb-toggle-option>
<tb-toggle-option value="windows">Windows</tb-toggle-option>
</tb-toggle-header>
<ng-container [ngSwitch]="publishCommandSteps.value">
<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">
<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">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)" [options]="[
{
name: ctx.translate.instant('widgets.recent-dashboards.last'),
value: 'last'
},
{
name: ctx.translate.instant('widgets.recent-dashboards.starred'),
value: 'starred'
}
]">
<tb-toggle-header #dashboardsToggle [value]="toggleValue" name="usageToggle" (valueChange)="toggleValueChange($event)">
<tb-toggle-option value="last">{{ 'widgets.recent-dashboards.last' | translate }}</tb-toggle-option>
<tb-toggle-option value="starred">{{ 'widgets.recent-dashboards.starred' | translate }}</tb-toggle-option>
</tb-toggle-header>
<a *ngIf="authUser.authority === authority.TENANT_ADMIN" fxHide.md
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">
<div class="tb-card-header">
<a class="tb-title-link" routerLink="/usage">{{ 'widgets.usage-info.title' | translate }}</a>
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle" [options]="[
{
name: ctx.translate.instant('widgets.usage-info.entities'),
value: 'entities'
},
{
name: ctx.translate.instant('widgets.usage-info.api-calls'),
value: 'apiCalls'
}
]">
<tb-toggle-header #usageToggle [value]="toggleValue" name="usageToggle">
<tb-toggle-option value="entities">{{ 'widgets.usage-info.entities' | translate }}</tb-toggle-option>
<tb-toggle-option value="apiCalls">{{ 'widgets.usage-info.api-calls' | translate }}</tb-toggle-option>
</tb-toggle-header>
</div>
<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-title" translate>widgets.table.column-settings</div>
<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">
<input matInput formControlName="customTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<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 }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<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-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
@ -48,7 +48,7 @@
</div>
<div class="tb-widget-config-row space-between">
<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-option [value]="'enabled'">
{{ '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-title" translate>widgets.table.table-header</div>
<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">
<input matInput formControlName="entitiesTitle" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
@ -31,10 +31,10 @@
</div>
<div class="tb-widget-config-panel stroked">
<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 }}
</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 }}
</mat-slide-toggle>
</div>
@ -42,7 +42,7 @@
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.columns</div>
<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 }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
@ -50,7 +50,7 @@
</mat-form-field>
</div>
<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 }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
@ -85,12 +85,12 @@
</div>
<div class="tb-widget-config-panel">
<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 }}
</mat-slide-toggle>
<div class="tb-widget-config-row space-between">
<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 }}">
</mat-form-field>
</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({
selector: 'tb-label-widget-label',
templateUrl: './label-widget-label.component.html',
styleUrls: ['./label-widget-label.component.scss', './../widget-settings.scss'],
styleUrls: ['./label-widget-label.component.scss'],
providers: [
{
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.
-->
<section class="tb-widget-settings" [formGroup]="timeseriesTableKeySettingsForm" fxLayout="column">
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.cell-style</legend>
<ng-container [formGroup]="timeseriesTableKeySettingsForm">
<div class="tb-widget-config-panel">
<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-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.table.use-cell-style-function' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
@ -40,18 +71,17 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.cell-content</legend>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableKeySettingsForm.get('useCellContentFunction').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.table.use-cell-content-function' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
@ -65,30 +95,5 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<mat-form-field fxFlex class="mat-block">
<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>
</div>
</ng-container>

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.
-->
<section class="tb-widget-settings" [formGroup]="timeseriesTableLatestKeySettingsForm" fxLayout="column">
<mat-slide-toggle class="mat-slide" formControlName="show">
{{ 'widgets.table.show-latest-data-column' | translate }}
</mat-slide-toggle>
<mat-form-field [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" fxFlex class="mat-block">
<mat-label translate>widgets.table.latest-data-column-order</mat-label>
<input matInput type="number" step="1" formControlName="order">
</mat-form-field>
<fieldset [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.cell-style</legend>
<ng-container [formGroup]="timeseriesTableLatestKeySettingsForm">
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.column-settings</div>
<mat-slide-toggle class="mat-slide" formControlName="show">
{{ 'widgets.table.show-latest-data-column' | translate }}
</mat-slide-toggle>
<div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-row space-between">
<div translate>widgets.table.latest-data-column-order</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<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-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="useCellStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.table.use-cell-style-function' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
@ -47,18 +80,17 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.cell-content</legend>
</div>
<div [fxShow]="timeseriesTableLatestKeySettingsForm.get('show').value" class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableLatestKeySettingsForm.get('useCellContentFunction').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="useCellContentFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.table.use-cell-content-function' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
@ -72,30 +104,6 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<mat-form-field fxFlex class="mat-block">
<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>
</div>
</ng-container>

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.
-->
<section class="tb-widget-settings" [formGroup]="timeseriesTableWidgetSettingsForm" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.table.common-table-settings</legend>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableSearch">
{{ 'widgets.table.enable-search' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }}
</mat-checkbox>
</section>
<section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableStickyHeader">
{{ 'widgets.table.enable-sticky-header' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }}
</mat-checkbox>
</section>
</section>
<mat-form-field fxFlex class="mat-block">
<ng-container [formGroup]="timeseriesTableWidgetSettingsForm">
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.table-header</div>
<mat-slide-toggle class="mat-slide" formControlName="enableStickyHeader">
{{ 'widgets.table.enable-sticky-header' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="enableSearch">
{{ 'widgets.table.enable-search' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.columns</div>
<mat-slide-toggle class="mat-slide" formControlName="showTimestamp">
{{ 'widgets.table.display-timestamp' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showMilliseconds">
{{ 'widgets.table.display-milliseconds' | translate }}
</mat-slide-toggle>
<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-select formControlName="reserveSpaceForHiddenAction">
<mat-option [value]="'true'">
@ -47,41 +50,39 @@
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" fxLayoutGap="8px">
<mat-slide-toggle formControlName="showTimestamp">
{{ 'widgets.table.display-timestamp' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="showMilliseconds">
{{ 'widgets.table.display-milliseconds' | translate }}
</mat-slide-toggle>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap="8px" fxLayoutAlign.gt-xs="start center">
<mat-slide-toggle fxFlex formControlName="displayPagination">
{{ 'widgets.table.display-pagination' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-page-size</mat-label>
<input matInput type="number" min="1" step="1" formControlName="defaultPageSize">
</mat-form-field>
</section>
<mat-slide-toggle formControlName="useEntityLabel">
{{ 'widgets.table.use-entity-label-tab-name' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="hideEmptyLines">
{{ 'widgets.table.hide-empty-lines' | translate }}
</mat-slide-toggle>
</section>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.table.row-style</legend>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.pagination</div>
<mat-slide-toggle class="mat-slide" formControlName="displayPagination">
{{ 'widgets.table.display-pagination' | translate }}
</mat-slide-toggle>
<div class="tb-widget-config-row space-between">
<div translate>widgets.table.default-page-size</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="defaultPageSize" type="number" min="1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.table.table-tabs</div>
<mat-slide-toggle class="mat-slide" formControlName="useEntityLabel">
{{ 'widgets.table.use-entity-label-tab-name' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<div class="tb-widget-config-panel-title" style="padding-bottom: 16px;" translate>widgets.table.rows</div>
<mat-slide-toggle class="mat-slide" style="padding-left: 16px;" formControlName="hideEmptyLines">
{{ 'widgets.table.hide-empty-lines' | translate }}
</mat-slide-toggle>
<mat-expansion-panel class="tb-settings" [expanded]="timeseriesTableWidgetSettingsForm.get('useRowStyleFunction').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useRowStyleFunction" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="useRowStyleFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.table.use-row-style-function' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
@ -95,5 +96,5 @@
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
</section>
</div>
</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.
-->
<section class="tb-widget-settings" [formGroup]="flotKeySettingsFormGroup" fxLayout="column">
<fieldset class="fields-group" fxLayout="column">
<legend class="group-title" translate>widgets.chart.common-settings</legend>
<mat-checkbox formControlName="hideDataByDefault">
<ng-container [formGroup]="flotKeySettingsFormGroup">
<div class="tb-widget-config-panel">
<mat-slide-toggle class="mat-slide" formControlName="hideDataByDefault">
{{ 'widgets.chart.data-is-hidden-by-default' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="disableDataHiding">
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="disableDataHiding">
{{ 'widgets.chart.disable-data-hiding' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="removeFromLegend">
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="removeFromLegend">
{{ 'widgets.chart.remove-from-legend' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="excludeFromStacking">
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="excludeFromStacking">
{{ 'widgets.chart.exclude-from-stacking' | translate }}
</mat-checkbox>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.line-settings</legend>
</mat-slide-toggle>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('showLines').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showLines" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="showLines" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.show-line' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.line-width</mat-label>
<input matInput type="number" min="0" formControlName="lineWidth">
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.line-width' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="lineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<mat-checkbox formControlName="fillLines">
{{ 'widgets.chart.fill-line' | translate }}
</mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.fill-line-opacity</mat-label>
<input matInput type="number" min="0" max="1" step="0.1" formControlName="fillLinesOpacity">
</div>
<mat-slide-toggle class="mat-slide" formControlName="fillLines">
{{ 'widgets.chart.fill-line' | translate }}
</mat-slide-toggle>
<div [fxShow]="flotKeySettingsFormGroup.get('fillLines').value" class="tb-widget-config-row space-between">
<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>
</section>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.points-settings</legend>
</div>
<div class="tb-widget-config-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('showPoints').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showPoints" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="showPoints" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.show-points' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section fxLayout="column">
<section fxFlex fxLayout="row" fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.points-line-width</mat-label>
<input matInput type="number" min="0" formControlName="showPointsLineWidth">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.points-radius</mat-label>
<input matInput type="number" min="0" formControlName="showPointsRadius">
</mat-form-field>
</section>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.point-shape</mat-label>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.points-line-width' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="showPointsLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.points-radius' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="showPointsRadius" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</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-option [value]="'circle'">
{{ 'widgets.chart.point-shape-circle' | translate }}
@ -111,19 +115,19 @@
</mat-option>
</mat-select>
</mat-form-field>
<tb-js-func [fxShow]="flotKeySettingsFormGroup.get('showPointShape').value === 'custom'"
formControlName="pointShapeFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['ctx', 'x', 'y', 'radius', 'shadow']"
functionTitle="{{ 'widgets.chart.point-shape-draw-function' | translate }}"
helpId="widget/lib/flot/point_shape_format_fn">
</tb-js-func>
</section>
</div>
<tb-js-func [fxShow]="flotKeySettingsFormGroup.get('showPointShape').value === 'custom'"
formControlName="pointShapeFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['ctx', 'x', 'y', 'radius', 'shadow']"
functionTitle="{{ 'widgets.chart.point-shape-draw-function' | translate }}"
helpId="widget/lib/flot/point_shape_format_fn">
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.chart.tooltip-settings</div>
<tb-js-func
formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables"
@ -131,49 +135,57 @@
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.yaxis-settings</legend>
<mat-slide-toggle formControlName="showSeparateAxis" class="slide-block">
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.chart.vertical-axis</div>
<mat-slide-toggle class="mat-slide" formControlName="showSeparateAxis">
{{ 'widgets.chart.show-separate-axis' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.axis-title</mat-label>
<input matInput formControlName="axisTitle">
</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="axisMin">
<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="axisTitle" placeholder="{{ 'widget-config.set' | translate }}">
</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="axisMax">
</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="axisMin" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</section>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.axis-position</mat-label>
<mat-select formControlName="axisPosition">
<mat-option [value]="'left'">
{{ 'widgets.chart.axis-position-left' | translate }}
</mat-option>
<mat-option [value]="'right'">
{{ 'widgets.chart.axis-position-right' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.yaxis-tick-labels-settings</legend>
<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="axisTickSize">
</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="axisMax" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.axis-position' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="axisPosition">
<mat-option [value]="'left'">
{{ 'widgets.chart.axis-position-left' | translate }}
</mat-option>
<mat-option [value]="'right'">
{{ 'widgets.chart.axis-position-right' | translate }}
</mat-option>
</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 fxFlex class="mat-block">
<mat-label translate>widgets.chart.number-of-decimals</mat-label>
<input matInput type="number" min="0" formControlName="axisTickDecimals">
</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="axisTickSize" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</section>
</div>
<tb-js-func
formControlName="axisTicksFormatter"
[globalVariables]="functionScopeVariables"
@ -181,63 +193,58 @@
functionTitle="{{ 'widgets.chart.ticks-formatter-function' | translate }}"
helpId="widget/lib/flot/ticks_formatter_fn">
</tb-js-func>
</fieldset>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.thresholds</legend>
<div fxLayout="column">
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="thresholdDrop($event)">
<div cdkDrag class="tb-draggable" *ngFor="let thresholdControl of thresholdsFormArray().controls; trackBy: trackByThreshold;
let $index = index; last as isLast;"
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-flot-threshold [formControl]="thresholdControl"
[expanded]="thresholdControl.new"
[aliasController]="aliasController"
(removeThreshold)="removeThreshold($index)">
</tb-flot-threshold>
</div>
</div>
<div *ngIf="!thresholdsFormArray().controls.length">
<span translate fxLayoutAlign="center center"
class="tb-prompt">widgets.chart.no-thresholds</span>
</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 class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" fxLayout="row" fxLayoutAlign="space-between center">
<div translate>widgets.chart.thresholds</div>
<button mat-stroked-button
color="primary"
type="button" (click)="addThreshold()">
<span translate>widgets.chart.add-threshold</span>
</button>
</div>
<div *ngIf="thresholdsFormArray().length" class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="thresholdDrop($event)" fxLayout="column" fxLayoutGap="12px">
<div cdkDrag class="tb-draggable" *ngFor="let thresholdControl of thresholdsFormArray().controls; trackBy: trackByThreshold;
let $index = index;">
<tb-flot-threshold [formControl]="thresholdControl"
[expanded]="thresholdControl.new"
[aliasController]="aliasController"
(removeThreshold)="removeThreshold($index)">
</tb-flot-threshold>
</div>
</div>
</fieldset>
<fieldset class="fields-group fields-group-slider" formGroupName="comparisonSettings" >
<legend class="group-title" translate>widgets.chart.comparison-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value">
</div>
<div class="tb-widget-config-panel tb-slide-toggle" formGroupName="comparisonSettings">
<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"
[disabled]="!flotKeySettingsFormGroup.get('comparisonSettings.showValuesForComparison').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showValuesForComparison" (click)="$event.stopPropagation()"
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showValuesForComparison" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.show-values-for-comparison' | 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>
<section fxFlex fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.comparison-values-label</mat-label>
<input matInput formControlName="comparisonValuesLabel">
<div class="tb-widget-config-row">
<div class="fixed-title-width" translate>widgets.chart.comparison-values-label</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="comparisonValuesLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input fxFlex
formControlName="color"
icon="format_color_fill"
label="{{ 'widgets.chart.color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widgets.chart.comparison-line-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>
</fieldset>
</section>
</div>
</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});
} else {
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.
-->
<section class="tb-widget-settings" [formGroup]="flotLatestKeySettingsForm" fxLayout="column">
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.threshold-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotLatestKeySettingsForm.get('useAsThreshold').value">
<ng-container [formGroup]="flotLatestKeySettingsForm">
<div class="tb-widget-config-panel tb-slide-toggle">
<div class="tb-widget-config-panel-title" translate>widgets.chart.threshold-settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotLatestKeySettingsForm.get('useAsThreshold').value"
[disabled]="!flotLatestKeySettingsForm.get('useAsThreshold').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useAsThreshold" (click)="$event.stopPropagation()"
<mat-slide-toggle class="mat-slide" formControlName="useAsThreshold" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.use-as-threshold' | 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>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.threshold-line-width</mat-label>
<input type="number" min="0" matInput formControlName="thresholdLineWidth">
<div class="tb-widget-config-row space-between">
<div translate>widgets.chart.threshold-line-width</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="thresholdLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
<tb-color-input fxFlex
formControlName="thresholdColor"
icon="format_color_fill"
label="{{ 'widgets.chart.threshold-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widgets.chart.threshold-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="thresholdColor">
</tb-color-input>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
</section>
</div>
</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.
-->
<mat-expansion-panel class="flot-threshold" fxFlex [formGroup]="thresholdFormGroup" [(expanded)]="expanded">
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title>
<div fxLayout="row" fxFlex fxLayoutAlign="start center">
<div>{{ thresholdText() }}</div>
<div class="tb-color-preview" style="margin-left: 6px;">
<div class="tb-color-result" [ngStyle]="{background: thresholdFormGroup.get('color').value}"></div>
</div>
</div>
</mat-panel-title>
<span fxFlex></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeThreshold.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout="column" fxLayoutGap="0.5em">
<mat-divider></mat-divider>
<section class="tb-widget-settings" fxLayout="column">
<tb-value-source [aliasController]="aliasController" formControlName="valueSource"></tb-value-source>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.line-width</mat-label>
<input type="number" min="0" matInput formControlName="lineWidth">
</mat-form-field>
<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>
<div class="tb-flot-threshold">
<mat-expansion-panel class="tb-settings tb-flot-threshold-settings" fxFlex [formGroup]="thresholdFormGroup" [(expanded)]="expanded">
<mat-expansion-panel-header class="fill-width">
<div class="tb-threshold-header">
<div class="tb-threshold-text">{{ thresholdText() }}</div>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
<mat-divider vertical></mat-divider>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-value-source [aliasController]="aliasController" formControlName="valueSource"></tb-value-source>
<div class="tb-widget-config-row space-between">
<div translate>widgets.chart.line-width</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="lineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
<button *ngIf="!disabled" mat-icon-button
type="button"
(click)="removeThreshold.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>

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
* limitations under the License.
*/
:host {
display: block;
.mat-expansion-panel {
.tb-flot-threshold {
display: flex;
flex-direction: row;
align-items: start;
gap: 4px;
.mat-expansion-panel.tb-settings.tb-flot-threshold-settings {
box-shadow: none;
&.flot-threshold {
border: 1px groove rgba(0, 0, 0, .25);
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
&.mat-expanded {
height: 48px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.12);
.mat-expansion-panel-header {
height: 56px;
border-radius: 0;
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;
}
}
}
}
:host ::ng-deep {
.mat-expansion-panel {
&.flot-threshold {
.mat-expansion-panel-body {
padding: 0 8px 8px;
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 16px !important;
}
}
&.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 { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Component, EventEmitter, forwardRef, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -28,14 +34,15 @@ import { TbFlotKeyThreshold } from '@home/components/widget/lib/flot-widget.mode
@Component({
selector: 'tb-flot-threshold',
templateUrl: './flot-threshold.component.html',
styleUrls: ['./flot-threshold.component.scss', './../widget-settings.scss'],
styleUrls: ['./flot-threshold.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FlotThresholdComponent),
multi: true
}
]
],
encapsulation: ViewEncapsulation.None
})
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.
-->
<section class="tb-widget-settings" [formGroup]="flotSettingsFormGroup" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.common-settings</legend>
<mat-checkbox formControlName="stack">
<ng-container [formGroup]="flotSettingsFormGroup">
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.chart.common-settings</div>
<mat-slide-toggle class="mat-slide" formControlName="stack">
{{ 'widgets.chart.enable-stacking-mode' | translate }}
</mat-checkbox>
<section *ngIf="chartType === 'graph'" fxLayout="column" fxLayout.gt-xs="row"
fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.line-shadow-size</mat-label>
<input matInput type="number" min="0" formControlName="shadowSize">
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="enableSelection">
{{ 'widgets.chart.enable-selection-mode' | translate }}
</mat-slide-toggle>
<mat-slide-toggle *ngIf="chartType === 'graph'" class="mat-slide" formControlName="smoothLines">
{{ '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-checkbox fxFlex formControlName="smoothLines">
{{ 'widgets.chart.display-smooth-lines' | translate }}
</mat-checkbox>
</section>
<section *ngIf="chartType === 'bar'" fxLayout="column" fxLayout.gt-xs="row"
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">
</div>
<div *ngIf="chartType === 'bar'" class="tb-widget-config-row space-between">
<div translate>widgets.chart.default-bar-width</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="defaultBarWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.bar-alignment</mat-label>
</div>
<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-option [value]="'left'">
{{ 'widgets.chart.bar-alignment-left' | translate }}
@ -51,211 +54,255 @@
</mat-option>
</mat-select>
</mat-form-field>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.default-font-size</mat-label>
<input matInput type="number" min="0" formControlName="fontSize">
</div>
<div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-row space-between">
<div translate>widgets.chart.thresholds-line-width</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="thresholdsLineWidth" type="number" min="0" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input fxFlex
formControlName="fontColor"
icon="format_color_fill"
label="{{ 'widgets.chart.default-font-color' | translate }}" openOnInput colorClearButton>
</tb-color-input>
</section>
<mat-form-field *ngIf="chartType === 'graph' || chartType === 'bar'" fxFlex class="mat-block">
<mat-label translate>widgets.chart.thresholds-line-width</mat-label>
<input matInput type="number" min="0" formControlName="thresholdsLineWidth">
</mat-form-field>
</fieldset>
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.legend</legend>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widgets.chart.default-font' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="fontSize" 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="fontColor">
</tb-color-input>
</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-header fxLayout="row" style="min-height: 48px;">
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;">
{{ 'widget-config.display-legend' | translate }}
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.legend' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-legend-config formControlName="legendConfig"></tb-legend-config>
<tb-legend-config formControlName="legendConfig">
</tb-legend-config>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend>
</div>
<div class="tb-widget-config-panel">
<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-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showTooltip" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.show-tooltip' | translate }}
{{ 'widgets.chart.tooltip' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section fxLayout="column">
<mat-slide-toggle formControlName="tooltipIndividual" class="slide-block">
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipIndividual">
{{ 'widgets.chart.hover-individual-points' | translate }}
</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 }}
</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 }}
</mat-slide-toggle>
<tb-js-func
formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'latestData']"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func>
</section>
</div>
<tb-js-func
formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'latestData']"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group" formGroupName="grid">
<legend class="group-title" translate>widgets.chart.grid-settings</legend>
<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>
</div>
<div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-panel tb-slide-toggle">
<div class="tb-widget-config-panel-title" translate>widgets.chart.comparison-settings</div>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('comparisonEnabled').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="comparisonEnabled" (click)="$event.stopPropagation()"
<mat-expansion-panel-header fxLayout="row wrap" class="fill-width">
<mat-panel-title fxFlex="60">
<mat-slide-toggle class="mat-slide" formControlName="comparisonEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.enable-comparison' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.time-for-comparison</mat-label>
<div class="tb-widget-config-row space-between">
<div>{{ 'widgets.chart.time-for-comparison' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
@ -277,18 +324,29 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="flotSettingsFormGroup.get('timeForComparison').value === 'customInterval'" fxFlex class="mat-block">
<mat-label translate>widgets.chart.custom-interval-value</mat-label>
<input matInput type="number" min="0" formControlName="comparisonCustomIntervalValue">
</div>
<div [fxShow]="flotSettingsFormGroup.get('timeForComparison').value === 'customInterval'" class="tb-widget-config-row space-between">
<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>
<fieldset class="fields-group" formGroupName="xaxisSecond">
<legend class="group-title" translate>widgets.chart.comparison-x-axis-settings</legend>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.axis-title</mat-label>
<input matInput formControlName="title">
</div>
<div class="tb-widget-config-panel stroked" formGroupName="xaxisSecond">
<div class="tb-widget-config-panel-title" translate>widgets.chart.comparison-x-axis-settings</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>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.axis-position</mat-label>
</div>
<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-option [value]="'top'">
{{ 'widgets.chart.axis-position-top' | translate }}
@ -298,31 +356,28 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-slide-toggle formControlName="showLabels" class="slide-block">
{{ 'widgets.chart.show-tick-labels' | translate }}
</mat-slide-toggle>
</fieldset>
</section>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.custom-legend-settings</legend>
</div>
<div *ngIf="chartType === 'graph' || chartType === 'bar'" class="tb-widget-config-panel tb-slide-toggle">
<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-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-slide-toggle formControlName="customLegendEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;">
<mat-slide-toggle class="mat-slide" formControlName="customLegendEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.enable-custom-legend' | translate }}
</mat-slide-toggle>
</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
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.chart.label-keys-list</legend>
<div class="tb-widget-config-panel stroked">
<div class="tb-widget-config-panel-title" translate>widgets.chart.label-keys-list</div>
<div fxLayout="column">
<div class="tb-control-list tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="labelDataKeyDrop($event)">
@ -348,8 +403,8 @@
</button>
</div>
</div>
</fieldset>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
</section>
</div>
</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> => {
const settings: Partial<TbFlotSettings> = {
stack: false,
enableSelection: true,
fontColor: '#545454',
fontSize: 10,
showTooltip: true,
@ -148,6 +149,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
// Common settings
stack: [false, []],
enableSelection: [true, []],
fontSize: [10, [Validators.min(0)]],
fontColor: ['#545454', []],
@ -282,6 +284,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.disable({emitEvent: false});
} else {
this.flotSettingsFormGroup.enable({emitEvent: false});
this.updateValidators(false);
}
}
@ -377,9 +380,11 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
} else {
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent});
}
this.flotSettingsFormGroup.get('xaxisSecond').enable({emitEvent: false});
} else {
this.flotSettingsFormGroup.get('timeForComparison').disable({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent});
this.flotSettingsFormGroup.get('xaxisSecond').disable({emitEvent: false});
}
if (customLegendEnabled) {
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('timeForComparison').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('xaxisSecond').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({
selector: 'tb-label-data-key',
templateUrl: './label-data-key.component.html',
styleUrls: ['./label-data-key.component.scss', './../widget-settings.scss'],
styleUrls: ['./label-data-key.component.scss'],
providers: [
{
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.
-->
<form [formGroup]="legendConfigForm">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px">
<mat-form-field fxFlex>
<mat-label translate>legend.direction</mat-label>
<mat-select matInput formControlName="direction">
<ng-container [formGroup]="legendConfigForm">
<div class="tb-widget-config-row space-between">
<div>{{ 'legend.direction' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
<mat-option *ngFor="let direction of legendDirections" [value]="direction">
{{ legendDirectionTranslations.get(legendDirection[direction]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>legend.position</mat-label>
<mat-select matInput formControlName="position">
</div>
<div class="tb-widget-config-row space-between">
<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"
[disabled]="legendConfigForm.get('direction').value === legendDirection.row &&
(pos === legendPosition.left || pos === legendPosition.right)">
@ -37,28 +38,17 @@
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-checkbox formControlName="showMin" fxFlex>
{{ 'legend.show-min' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMax" fxFlex>
{{ 'legend.show-max' | translate }}
</mat-checkbox>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<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 class="tb-widget-config-row space-between">
<div translate>legend.show-values</div>
<mat-chip-listbox multiple formControlName="showValues">
<mat-chip-option value="min">{{ 'legend.min-option' | translate }}</mat-chip-option>
<mat-chip-option value="max">{{ 'legend.max-option' | translate }}</mat-chip-option>
<mat-chip-option value="average">{{ 'legend.average-option' | translate }}</mat-chip-option>
<mat-chip-option value="total">{{ 'legend.total-option' | translate }}</mat-chip-option>
<mat-chip-option value="latest">{{ 'legend.latest-option' | translate }}</mat-chip-option>
</mat-chip-listbox>
</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 { 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 {
LegendConfig,
@ -30,7 +30,7 @@ import { Subscription } from 'rxjs';
@Component({
selector: 'tb-legend-config',
templateUrl: './legend-config.component.html',
styleUrls: [],
styleUrls: ['./../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -62,12 +62,8 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
this.legendConfigForm = this.fb.group({
direction: [null, []],
position: [null, []],
sortDataKeys: [null, []],
showMin: [null, []],
showMax: [null, []],
showAvg: [null, []],
showTotal: [null, []],
showLatest: [null, []]
showValues: [[], []],
sortDataKeys: [null, []]
});
this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges
.subscribe((direction: LegendDirection) => {
@ -121,18 +117,49 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
this.legendConfigForm.patchValue({
direction: legendConfig.direction,
position: legendConfig.position,
sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false,
showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : 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
showValues: this.getShowValues(legendConfig),
sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false
}, {emitEvent: false});
}
this.onDirectionChanged(legendConfig.direction);
}
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.
-->
<section [formGroup]="valueSourceFormGroup" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.value-source.value-source</mat-label>
<mat-select formControlName="valueSource">
<mat-option [value]="'predefinedValue'">
<ng-container [formGroup]="valueSourceFormGroup">
<div style="display: flex;">
<tb-toggle-select formControlName="valueSource" style="min-width: 300px;">
<tb-toggle-option [value]="'predefinedValue'">
{{ 'widgets.value-source.predefined-value' | translate }}
</mat-option>
<mat-option [value]="'entityAttribute'">
</tb-toggle-option>
<tb-toggle-option [value]="'entityAttribute'">
{{ 'widgets.value-source.entity-attribute' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="valueSourceFormGroup.get('valueSource').value === 'predefinedValue'" fxFlex class="mat-block">
<mat-label translate>widgets.value-source.value</mat-label>
<input matInput type="number" formControlName="value">
</mat-form-field>
<section [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'"
fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label>{{ 'widgets.value-source.source-entity-alias' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}"
</tb-toggle-option>
</tb-toggle-select>
</div>
<div [fxShow]="valueSourceFormGroup.get('valueSource').value === 'predefinedValue'" class="tb-widget-config-row space-between">
<div translate>widgets.value-source.value</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="value" type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'" class="tb-widget-config-row space-between">
<div translate>widgets.value-source.source-entity-alias</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<input matInput type="text" placeholder="{{ 'widget-config.set' | translate }}"
#entityAliasInput
formControlName="entityAlias"
[matAutocomplete]="entityAliasAutocomplete">
@ -53,9 +53,11 @@
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label>{{ 'widgets.value-source.source-entity-attribute' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}"
</div>
<div [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'" class="tb-widget-config-row space-between">
<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
formControlName="attribute"
[matAutocomplete]="keyAutocomplete">
@ -73,5 +75,5 @@
</mat-option>
</mat-autocomplete>
</mat-form-field>
</section>
</section>
</div>
</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.
///
import { Component, ElementRef, forwardRef, HostBinding, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -39,7 +39,7 @@ export interface ValueSourceProperty {
@Component({
selector: 'tb-value-source',
templateUrl: './value-source.component.html',
styleUrls: [],
styleUrls: ['./../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -50,8 +50,6 @@ export interface ValueSourceProperty {
})
export class ValueSourceComponent extends PageComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.display') display = 'block';
@ViewChild('entityAliasInput') entityAliasInput: 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>> {
return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter(
mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
);
}),
)),
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({
selector: 'tb-fixed-color-level',
templateUrl: './fixed-color-level.component.html',
styleUrls: ['./fixed-color-level.component.scss', './../widget-settings.scss'],
styleUrls: ['./fixed-color-level.component.scss'],
providers: [
{
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({
selector: 'tb-gauge-highlight',
templateUrl: './gauge-highlight.component.html',
styleUrls: ['./gauge-highlight.component.scss', './../widget-settings.scss'],
styleUrls: ['./gauge-highlight.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

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

Loading…
Cancel
Save