Browse Source

Merge remote-tracking branch 'upstream/master' into queueStatsEntity

pull/10145/head
rusikv 3 years ago
parent
commit
6057c17acc
  1. 1153
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 1057
      application/src/main/data/json/demo/dashboards/thermostats.json
  3. 6
      application/src/main/data/json/system/widget_bundles/charts.json
  4. 32
      application/src/main/data/json/system/widget_types/bar_chart.json
  5. 32
      application/src/main/data/json/system/widget_types/line_chart.json
  6. 2
      application/src/main/data/json/system/widget_types/persistent_rpc_table.json
  7. 32
      application/src/main/data/json/system/widget_types/point_chart.json
  8. 2
      application/src/main/data/json/system/widget_types/rpc_debug_terminal.json
  9. 2
      application/src/main/data/json/system/widget_types/rpc_remote_shell.json
  10. 36
      application/src/main/data/json/system/widget_types/time_series_chart.json
  11. 5
      application/src/main/data/json/system/widget_types/timeseries_bar_chart.json
  12. 5
      application/src/main/data/json/system/widget_types/timeseries_line_chart.json
  13. 12
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  14. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  15. 43
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  16. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java
  17. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java
  19. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java
  20. 9
      application/src/main/java/org/thingsboard/server/service/mail/RefreshTokenExpCheckService.java
  21. 12
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  22. 42
      application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java
  23. 1
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  24. 1
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  25. 3
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  26. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  27. 7
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  28. 7
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  29. 5
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  30. 6
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  31. 13
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  32. 54
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  33. 4
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  34. 1
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  35. 10
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  36. 2
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  37. 10
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java
  38. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  39. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java
  40. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  41. 7
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
  42. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/NotificationInfo.java
  43. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  44. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java
  45. 4
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java
  46. 0
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientTest.java
  47. 151
      common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java
  48. 101
      common/transport/lwm2m/src/test/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDesTest.java
  49. 26
      common/transport/lwm2m/src/test/resources/15.xml
  50. 26
      common/transport/lwm2m/src/test/resources/17.xml
  51. 25
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  52. 26
      dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
  53. 43
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  54. 30
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  55. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
  56. 2
      ui-ngx/package.json
  57. 209
      ui-ngx/patches/echarts+5.5.0.patch
  58. 17
      ui-ngx/src/app/core/api/data-aggregator.ts
  59. 8
      ui-ngx/src/app/core/api/widget-subscription.ts
  60. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  61. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  62. 8
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  63. 4
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  64. 8
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  65. 41
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  66. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  67. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts
  68. 7
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html
  69. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts
  70. 7
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html
  71. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts
  72. 258
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  73. 367
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts
  74. 170
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  75. 69
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  76. 284
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  77. 7
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  78. 118
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  79. 21
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  80. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  81. 27
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  82. 5
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts
  83. 15
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  84. 3
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  85. 6
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  86. 9
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  87. 12
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  88. 12
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  89. 1
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss
  90. 98
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  91. 6
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html
  92. 11
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss
  93. 104
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts
  94. 12
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts
  95. 1
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
  96. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
  97. 212
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  98. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  99. 1
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  100. 166
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

@ -15,7 +15,7 @@
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-persistent-table-widget-settings",
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}"
"defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent RPC table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"}}"
},
"externalId": null,
"tags": [

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

File diff suppressed because one or more lines are too long

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

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>",
"templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n",
"controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetDeviceName && subscription.targetDeviceName.length) {\n deviceName = subscription.targetDeviceName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}",
"controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-rpc-terminal-widget-settings",

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

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

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

File diff suppressed because one or more lines are too long

12
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -32,8 +32,6 @@ import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
@ -52,13 +50,12 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -74,8 +71,8 @@ import org.thingsboard.server.common.data.sync.vc.VcUtils;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.auth.oauth2.CookieUtils;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
@ -93,7 +90,6 @@ import java.io.IOException;
import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@ -113,9 +109,7 @@ public class AdminController extends BaseController {
private final SmsService smsService;
private final AdminSettingsService adminSettingsService;
private final SystemSecurityService systemSecurityService;
@Lazy
private final JwtSettingsService jwtSettingsService;
@Lazy
private final JwtTokenFactory tokenFactory;
private final EntitiesVersionControlService versionControlService;
private final TbAutoCommitSettingsService autoCommitSettingsService;

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -19,6 +19,7 @@ import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.asset.AssetProfileService;
@ -150,6 +151,9 @@ public class EdgeContextComponent {
@Autowired
private ResourceService resourceService;
@Autowired
private RateLimitService rateLimitService;
@Autowired
private NotificationRuleProcessor notificationRuleProcessor;

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

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -109,6 +110,7 @@ public final class EdgeGrpcSession implements Closeable {
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private static final String RATE_LIMIT_REACHED = "Rate limit reached";
private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
@ -264,36 +266,51 @@ public final class EdgeGrpcSession implements Closeable {
}
private void onUplinkMsg(UplinkMsg uplinkMsg) {
if (isRateLimitViolated(uplinkMsg)) {
return;
}
ListenableFuture<List<Void>> future = processUplinkMsg(uplinkMsg);
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Void> result) {
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(true).build();
sendDownlinkMsg(ResponseMsg.newBuilder()
.setUplinkResponseMsg(uplinkResponseMsg)
.build());
sendResponseMessage(uplinkMsg.getUplinkMsgId(), true, null);
}
@Override
public void onFailure(Throwable t) {
String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t);
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(false).setErrorMsg(errorMsg).build();
sendDownlinkMsg(ResponseMsg.newBuilder()
.setUplinkResponseMsg(uplinkResponseMsg)
.build());
sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg);
}
}, ctx.getGrpcCallbackExecutorService());
}
private boolean isRateLimitViolated(UplinkMsg uplinkMsg) {
if (!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES, tenantId) ||
!ctx.getRateLimitService().checkRateLimit(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, tenantId, edge.getId())) {
String errorMsg = String.format("Failed to process uplink message. %s", RATE_LIMIT_REACHED);
sendResponseMessage(uplinkMsg.getUplinkMsgId(), false, errorMsg);
return true;
}
return false;
}
private void sendResponseMessage(int uplinkMsgId, boolean success, String errorMsg) {
UplinkResponseMsg.Builder responseBuilder = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsgId)
.setSuccess(success);
if (errorMsg != null) {
responseBuilder.setErrorMsg(errorMsg);
}
sendDownlinkMsg(ResponseMsg.newBuilder()
.setUplinkResponseMsg(responseBuilder.build())
.build());
}
private void onDownlinkResponse(DownlinkResponseMsg msg) {
try {
if (msg.getSuccess()) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
log.debug("[{}][{}] Msg has been processed successfully!Msg Id: [{}], Msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg);
log.debug("[{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg);
} else {
log.error("[{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", this.tenantId, edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg());
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/asset/AssetMsgConstructorV1.java

@ -63,6 +63,7 @@ public class AssetMsgConstructorV1 extends BaseAssetMsgConstructor {
@Override
public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) {
assetProfile = JacksonUtil.clone(assetProfile);
imageService.inlineImageForEdge(assetProfile);
AssetProfileUpdateMsg.Builder builder = AssetProfileUpdateMsg.newBuilder()
.setMsgType(msgType)
@ -89,4 +90,5 @@ public class AssetMsgConstructorV1 extends BaseAssetMsgConstructor {
}
return builder.build();
}
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/dashboard/DashboardMsgConstructorV1.java

@ -33,6 +33,7 @@ public class DashboardMsgConstructorV1 extends BaseDashboardMsgConstructor {
@Override
public DashboardUpdateMsg constructDashboardUpdatedMsg(UpdateMsgType msgType, Dashboard dashboard) {
dashboard = JacksonUtil.clone(dashboard);
imageService.inlineImagesForEdge(dashboard);
DashboardUpdateMsg.Builder builder = DashboardUpdateMsg.newBuilder()
.setMsgType(msgType)
@ -52,4 +53,5 @@ public class DashboardMsgConstructorV1 extends BaseDashboardMsgConstructor {
}
return builder.build();
}
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/device/DeviceMsgConstructorV1.java

@ -95,6 +95,7 @@ public class DeviceMsgConstructorV1 extends BaseDeviceMsgConstructor {
@Override
public DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) {
deviceProfile = JacksonUtil.clone(deviceProfile);
imageService.inlineImageForEdge(deviceProfile);
DeviceProfileUpdateMsg.Builder builder = DeviceProfileUpdateMsg.newBuilder()
.setMsgType(msgType)
@ -140,4 +141,5 @@ public class DeviceMsgConstructorV1 extends BaseDeviceMsgConstructor {
}
return builder.build();
}
}

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/widget/WidgetMsgConstructorV1.java

@ -43,6 +43,7 @@ public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor {
@Override
public WidgetsBundleUpdateMsg constructWidgetsBundleUpdateMsg(UpdateMsgType msgType, WidgetsBundle widgetsBundle, List<String> widgets) {
widgetsBundle = JacksonUtil.clone(widgetsBundle);
imageService.inlineImageForEdge(widgetsBundle);
WidgetsBundleUpdateMsg.Builder builder = WidgetsBundleUpdateMsg.newBuilder()
.setMsgType(msgType)
@ -68,6 +69,7 @@ public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor {
@Override
public WidgetTypeUpdateMsg constructWidgetTypeUpdateMsg(UpdateMsgType msgType, WidgetTypeDetails widgetTypeDetails, EdgeVersion edgeVersion) {
widgetTypeDetails = JacksonUtil.clone(widgetTypeDetails);
imageService.inlineImagesForEdge(widgetTypeDetails);
WidgetTypeUpdateMsg.Builder builder = WidgetTypeUpdateMsg.newBuilder()
.setMsgType(msgType)
@ -109,4 +111,5 @@ public class WidgetMsgConstructorV1 extends BaseWidgetMsgConstructor {
}
return builder.build();
}
}

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

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

12
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.EntityId;
@ -62,7 +63,6 @@ import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.TopicService;
@ -204,6 +204,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
private void processNotificationRequestAsync(NotificationProcessingContext ctx, List<NotificationTarget> targets, FutureCallback<NotificationRequestStats> callback) {
notificationExecutor.submit(() -> {
long startTs = System.currentTimeMillis();
NotificationRequestId requestId = ctx.getRequest().getId();
for (NotificationTarget target : targets) {
try {
@ -219,9 +220,16 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return;
}
}
log.debug("[{}] Notification request processing is finished", requestId);
NotificationRequestStats stats = ctx.getStats();
long time = System.currentTimeMillis() - startTs;
int sent = stats.getTotalSent().get();
int errors = stats.getTotalErrors().get();
if (errors > 0) {
log.info("[{}][{}] Notification request processing finished in {} ms (sent: {}, errors: {})", ctx.getTenantId(), requestId, time, sent, errors);
} else {
log.info("[{}][{}] Notification request processing finished in {} ms (sent: {})", ctx.getTenantId(), requestId, time, sent);
}
updateRequestStats(ctx, requestId, stats);
if (callback != null) {
callback.onSuccess(stats);

42
application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Strings;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
import lombok.RequiredArgsConstructor;
@ -61,14 +63,7 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
String subject = processedTemplate.getSubject();
String body = processedTemplate.getBody();
Map<String, String> data = Optional.ofNullable(processedTemplate.getAdditionalConfig())
.map(JacksonUtil::toFlatMap).orElseGet(HashMap::new);
Optional.ofNullable(ctx.getRequest().getInfo())
.map(NotificationInfo::getStateEntityId)
.ifPresent(stateEntityId -> {
data.put("stateEntityId", stateEntityId.getId().toString());
data.put("stateEntityType", stateEntityId.getEntityType().name());
});
Map<String, String> data = getNotificationData(processedTemplate, ctx);
for (String token : mobileSessions.keySet()) {
try {
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data);
@ -87,6 +82,37 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
}
}
private Map<String, String> getNotificationData(MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
Map<String, String> data = Optional.ofNullable(processedTemplate.getAdditionalConfig())
.filter(JsonNode::isObject).map(JacksonUtil::toFlatMap).orElseGet(HashMap::new);
NotificationInfo info = ctx.getRequest().getInfo();
if (info == null) {
return data;
}
Optional.ofNullable(info.getStateEntityId()).ifPresent(stateEntityId -> {
data.put("stateEntityId", stateEntityId.getId().toString());
data.put("stateEntityType", stateEntityId.getEntityType().name());
if (!"true".equals(data.get("onClick.enabled")) && info.getDashboardId() != null) {
data.put("onClick.enabled", "true");
data.put("onClick.linkType", "DASHBOARD");
data.put("onClick.setEntityIdInState", "true");
data.put("onClick.dashboardId", info.getDashboardId().toString());
}
});
data.put("notificationType", ctx.getNotificationType().name());
switch (ctx.getNotificationType()) {
case ALARM:
case ALARM_ASSIGNMENT:
case ALARM_COMMENT:
info.getTemplateData().forEach((key, value) -> {
data.put("info." + key, value);
});
break;
}
data.replaceAll((key, value) -> Strings.nullToEmpty(value));
return data;
}
@Override
public void check(TenantId tenantId) throws Exception {
NotificationSettings systemSettings = notificationSettingsService.findNotificationSettings(TenantId.SYS_TENANT_ID);

1
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -65,6 +65,7 @@ public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerP
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId())
.dashboardId(alarmInfo.getDashboardId())
.build();
}

1
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -76,6 +76,7 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc
.alarmSeverity(alarm.getSeverity())
.alarmStatus(alarm.getStatus())
.alarmCustomerId(alarm.getCustomerId())
.dashboardId(alarm.getDashboardId())
.build();
}

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

@ -22,11 +22,11 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@ -111,6 +111,7 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
.acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId())
.dashboardId(alarmInfo.getDashboardId())
.build();
}

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -171,7 +171,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
GitVersionControlQueueService vcQueueService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
Optional<JwtSettingsService> jwtSettingsService,
JwtSettingsService jwtSettingsService,
NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService) {

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

@ -54,6 +54,7 @@ import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineConsumerContext;
import org.thingsboard.server.service.queue.ruleengine.TbRuleEngineQueueConsumerManager;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
@ -85,9 +86,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbAssetProfileCache assetProfileCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, ApplicationEventPublisher eventPublisher) {
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService,
eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
eventPublisher, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

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

@ -49,7 +49,6 @@ import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsServ
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -75,14 +74,14 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final ApplicationEventPublisher eventPublisher;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final Optional<JwtSettingsService> jwtSettingsService;
protected final JwtSettingsService jwtSettingsService;
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, ApplicationEventPublisher eventPublisher,
TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, JwtSettingsService jwtSettingsService) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
@ -181,7 +180,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
jwtSettingsService.reloadJwtSettings();
return;
} else {
tenantProfileCache.evict(tenantId);

5
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java

@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
@ -42,9 +41,7 @@ import java.util.Optional;
@Slf4j
public class DefaultJwtSettingsService implements JwtSettingsService {
@Lazy
private final AdminSettingsService adminSettingsService;
@Lazy
private final Optional<TbClusterService> tbClusterService;
private final Optional<NotificationCenter> notificationCenter;
private final JwtSettingsValidator jwtSettingsValidator;
@ -107,11 +104,13 @@ public class DefaultJwtSettingsService implements JwtSettingsService {
@Override
public JwtSettings reloadJwtSettings() {
log.trace("Executing reloadJwtSettings");
return getJwtSettings(true);
}
@Override
public JwtSettings getJwtSettings() {
log.trace("Executing getJwtSettings");
return getJwtSettings(false);
}

6
application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java

@ -71,6 +71,8 @@ public class RateLimitServiceTest {
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit);
profileConfiguration.setEdgeEventRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit);
profileConfiguration.setEdgeUplinkMessagesRateLimits(rateLimit);
profileConfiguration.setEdgeUplinkMessagesRateLimitsPerEdge(rateLimit);
updateTenantProfileConfiguration(profileConfiguration);
for (LimitedApi limitedApi : List.of(
@ -80,7 +82,9 @@ public class RateLimitServiceTest {
LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES,
LimitedApi.EDGE_EVENTS,
LimitedApi.EDGE_EVENTS_PER_EDGE
LimitedApi.EDGE_EVENTS_PER_EDGE,
LimitedApi.EDGE_UPLINK_MESSAGES,
LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE
)) {
testRateLimits(limitedApi, max, tenantId);
}

13
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -60,6 +60,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -99,6 +100,8 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected NotificationSettingsService notificationSettingsService;
@Autowired
protected SqlPartitioningRepository partitioningRepository;
@Autowired
protected DefaultNotifications defaultNotifications;
public static final String DEFAULT_NOTIFICATION_SUBJECT = "Just a test";
public static final NotificationType DEFAULT_NOTIFICATION_TYPE = NotificationType.GENERAL;
@ -249,10 +252,14 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
}
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB);
return createNotificationRule(triggerConfig, subject, text, List.of(targets), NotificationDeliveryMethod.WEB);
}
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, List<NotificationTargetId> targets, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, deliveryMethods);
NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " " + Arrays.toString(targets));
rule.setName(triggerConfig.getTriggerType() + " " + targets);
rule.setEnabled(true);
rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType());
@ -260,7 +267,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(List.of(targets)));
recipientsConfig.setTargets(DaoUtil.toUUIDs(targets));
rule.setRecipientsConfig(recipientsConfig);
return saveNotificationRule(rule);

54
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -30,8 +30,12 @@ import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
@ -49,6 +53,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
@ -771,6 +776,55 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
verifyNoMoreInteractions(firebaseService);
}
@Test
public void testMobileAppNotifications_ruleBased() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginTenantAdmin();
mobileToken = "tenantFcmToken";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(),
DefaultNotifications.alarmComment.getSubject(), DefaultNotifications.alarmComment.getText(),
List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP);
Device device = createDevice("test", "test");
UUID alarmDashboardId = UUID.randomUUID();
Alarm alarm = Alarm.builder()
.type("test")
.tenantId(tenantId)
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.details(JacksonUtil.newObjectNode()
.put("dashboardId", alarmDashboardId.toString()))
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
AlarmComment comment = new AlarmComment();
comment.setComment(JacksonUtil.newObjectNode()
.put("text", "text"));
doPost("/api/alarm/" + alarm.getId() + "/comment", comment, AlarmComment.class);
ArgumentCaptor<Map<String, String>> msgCaptor = ArgumentCaptor.forClass(Map.class);
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken"), eq("Comment on 'test' alarm"),
eq(TENANT_ADMIN_EMAIL + " added comment: text"),
msgCaptor.capture());
});
Map<String, String> firebaseMessageData = msgCaptor.getValue();
assertThat(firebaseMessageData.keySet()).doesNotContainNull().doesNotContain("");
assertThat(firebaseMessageData.values()).doesNotContainNull();
assertThat(firebaseMessageData.get("info.userEmail")).isEqualTo(TENANT_ADMIN_EMAIL);
assertThat(firebaseMessageData.get("info.alarmType")).isEqualTo("test");
assertThat(firebaseMessageData.get("onClick.enabled")).isEqualTo("true");
assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD");
assertThat(firebaseMessageData.get("onClick.dashboardId")).isEqualTo(alarmDashboardId.toString());
}
@Test
public void testMobileSettings_tenantLevel() throws Exception {
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();

4
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -26,6 +26,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -93,7 +94,6 @@ import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.queue.notification.DefaultNotificationDeduplicationService;
import org.thingsboard.server.service.notification.rule.cache.DefaultNotificationRulesCache;
import org.thingsboard.server.service.state.DeviceStateService;
@ -140,8 +140,6 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationRuleProcessor notificationRuleProcessor;
@Autowired
private DefaultNotifications defaultNotifications;
@Autowired
private DefaultNotificationRulesCache notificationRulesCache;
@Autowired
private DeviceStateService deviceStateService;

1
application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java

@ -37,7 +37,6 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import java.util.Calendar;
import java.util.Date;

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.alarm;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
@ -30,6 +31,7 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -37,6 +39,8 @@ import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Created by ashvayka on 11.05.17.
@ -160,4 +164,10 @@ public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, Ha
}
}
@JsonIgnore
public DashboardId getDashboardId() {
return Optional.ofNullable(getDetails()).map(details -> details.get("dashboardId"))
.filter(JsonNode::isTextual).map(id -> new DashboardId(UUID.fromString(id.asText()))).orElse(null);
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -33,6 +33,8 @@ public enum LimitedApi {
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true),
EDGE_UPLINK_MESSAGES_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimitsPerEdge, "Edge uplink messages per edge", false),
PASSWORD_RESET(false, true),
TWO_FA_VERIFICATION_CODE_SEND(false, true),
TWO_FA_VERIFICATION_CODE_CHECK(false, true),

10
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationRequestStats.java

@ -31,14 +31,20 @@ import java.util.concurrent.atomic.AtomicInteger;
public class NotificationRequestStats {
private final Map<NotificationDeliveryMethod, AtomicInteger> sent;
@JsonIgnore
private final AtomicInteger totalSent;
private final Map<NotificationDeliveryMethod, Map<String, String>> errors;
@JsonIgnore
private final AtomicInteger totalErrors;
private String error;
@JsonIgnore
private final Map<NotificationDeliveryMethod, Set<Object>> processedRecipients;
public NotificationRequestStats() {
this.sent = new ConcurrentHashMap<>();
this.totalSent = new AtomicInteger();
this.errors = new ConcurrentHashMap<>();
this.totalErrors = new AtomicInteger();
this.processedRecipients = new ConcurrentHashMap<>();
}
@ -47,13 +53,16 @@ public class NotificationRequestStats {
@JsonProperty("errors") Map<NotificationDeliveryMethod, Map<String, String>> errors,
@JsonProperty("error") String error) {
this.sent = sent;
this.totalSent = null;
this.errors = errors;
this.totalErrors = null;
this.error = error;
this.processedRecipients = Collections.emptyMap();
}
public void reportSent(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient) {
sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet();
totalSent.incrementAndGet();
}
public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, NotificationRecipient recipient) {
@ -65,6 +74,7 @@ public class NotificationRequestStats {
errorMessage = error.getClass().getSimpleName();
}
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getTitle(), errorMessage);
totalErrors.incrementAndGet();
}
public void reportProcessed(NotificationDeliveryMethod deliveryMethod, Object recipientId) {

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -31,4 +31,5 @@ public enum NotificationType {
RATE_LIMITS,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE
}

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
@ -55,6 +56,7 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private CustomerId alarmCustomerId;
private DashboardId dashboardId;
@Override
public Map<String, String> getTemplateData() {
@ -94,4 +96,9 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
return alarmOriginator;
}
@Override
public DashboardId getDashboardId() {
return dashboardId;
}
}

7
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map;
@ -50,6 +51,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private CustomerId alarmCustomerId;
private DashboardId dashboardId;
@Override
public Map<String, String> getTemplateData() {
@ -80,4 +82,9 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
return alarmOriginator;
}
@Override
public DashboardId getDashboardId() {
return dashboardId;
}
}

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

@ -22,6 +22,7 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map;
@ -45,6 +46,7 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
private boolean acknowledged;
private boolean cleared;
private CustomerId alarmCustomerId;
private DashboardId dashboardId;
@Override
public Map<String, String> getTemplateData() {
@ -70,4 +72,9 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
return alarmOriginator;
}
@Override
public DashboardId getDashboardId() {
return dashboardId;
}
}

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/NotificationInfo.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification.info;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map;
@ -33,4 +34,8 @@ public interface NotificationInfo {
return null;
}
default DashboardId getDashboardId() {
return null;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -83,6 +83,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String edgeEventRateLimits;
private String edgeEventRateLimitsPerEdge;
private String edgeUplinkMessagesRateLimits;
private String edgeUplinkMessagesRateLimitsPerEdge;
private int defaultStorageTtlDays;
private int alarmsTtlDays;

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

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

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

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

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

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

@ -0,0 +1,151 @@
/**
* Copyright © 2016-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.transport.lwm2m.server.store.util;
import org.eclipse.leshan.core.link.Link;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.request.Identity;
import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.server.registration.Registration;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.TransportResourceCache;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class LwM2MClientSerDesTest {
@Test
public void serializeDeserialize() throws Exception {
LwM2mClient client = new LwM2mClient("nodeId", "testEndpoint");
TransportDeviceInfo tdi = new TransportDeviceInfo();
tdi.setPowerMode(PowerMode.PSM);
tdi.setPsmActivityTimer(10000L);
tdi.setPagingTransmissionWindow(2000L);
tdi.setEdrxCycle(3000L);
tdi.setTenantId(TenantId.fromUUID(UUID.randomUUID()));
tdi.setCustomerId(new CustomerId(UUID.randomUUID()));
tdi.setDeviceId(new DeviceId(UUID.randomUUID()));
tdi.setDeviceProfileId(new DeviceProfileId(UUID.randomUUID()));
tdi.setDeviceName("testDevice");
tdi.setDeviceType("testType");
ValidateDeviceCredentialsResponse credentialsResponse = ValidateDeviceCredentialsResponse.builder()
.deviceInfo(tdi)
.build();
client.init(credentialsResponse, UUID.randomUUID());
Registration registration =
new Registration.Builder("test", "testEndpoint", Identity
.unsecure(new InetSocketAddress(1000)))
.supportedContentFormats()
.supportedObjects(Map.of(15, "1.0", 17, "1.0"))
.objectLinks(new Link[]{new Link("/")})
.build();
client.setRegistration(registration);
client.setState(LwM2MClientState.REGISTERED);
client.getSharedAttributes().put("key1", TransportProtos.TsKvProto.newBuilder().setTs(0).setKv(TransportProtos.KeyValueProto.newBuilder().setStringV("test").build()).build());
client.getSharedAttributes().put("key2", TransportProtos.TsKvProto.newBuilder().setTs(1).setKv(TransportProtos.KeyValueProto.newBuilder().setDoubleV(1.02).build()).build());
TransportResourceCache resourceCache = mock(TransportResourceCache.class);
LwM2mTransportContext context = mock(LwM2mTransportContext.class);
LwM2mClientContext clientContext = mock(LwM2mClientContext.class);
var provider = new LwM2mVersionedModelProvider(clientContext, new LwM2mTransportServerHelper(context), context);
TbResource resource15 = new TbResource();
resource15.setData(Files.readAllBytes(Path.of(this.getClass().getClassLoader().getResource("15.xml").toURI())));
TbResource resource17 = new TbResource();
resource17.setData(Files.readAllBytes(Path.of(this.getClass().getClassLoader().getResource("17.xml").toURI())));
when(resourceCache.get(any(), any(), eq("15_1.0"))).thenReturn(Optional.of(resource15));
when(resourceCache.get(any(), any(), eq("17_1.0"))).thenReturn(Optional.of(resource17));
when(context.getTransportResourceCache()).thenReturn(resourceCache);
when(clientContext.getClientByEndpoint(any())).thenReturn(client);
LwM2mResource singleResource = LwM2mSingleResource.newStringResource(15, "testValue");
LwM2mResource multipleResource = LwM2mMultipleResource.newStringResource(17, Map.of(0, "testValue", 1, "testValue"));
client.saveResourceValue("/15_1.0/0/0", singleResource, provider, WriteRequest.Mode.UPDATE);
client.saveResourceValue("/17_1.0/0/0", multipleResource, provider, WriteRequest.Mode.UPDATE);
byte[] bytes = LwM2MClientSerDes.serialize(client);
Assert.assertNotNull(bytes);
LwM2mClient desClient = LwM2MClientSerDes.deserialize(bytes);
assertEquals(client.getNodeId(), desClient.getNodeId());
assertEquals(client.getEndpoint(), desClient.getEndpoint());
assertEquals(client.getSharedAttributes(), desClient.getSharedAttributes());
assertEquals(client.getKeyTsLatestMap(), desClient.getKeyTsLatestMap());
assertEquals(client.getTenantId(), desClient.getTenantId());
assertEquals(client.getProfileId(), desClient.getProfileId());
assertEquals(client.getDeviceId(), desClient.getDeviceId());
assertEquals(client.getState(), desClient.getState());
assertEquals(client.getSession(), desClient.getSession());
assertEquals(client.getPowerMode(), desClient.getPowerMode());
assertEquals(client.getPsmActivityTimer(), desClient.getPsmActivityTimer());
assertEquals(client.getPagingTransmissionWindow(), desClient.getPagingTransmissionWindow());
assertEquals(client.getEdrxCycle(), desClient.getEdrxCycle());
assertEquals(client.getRegistration(), desClient.getRegistration());
assertEquals(client.isAsleep(), desClient.isAsleep());
assertEquals(client.getLastUplinkTime(), desClient.getLastUplinkTime());
assertEquals(client.getSleepTask(), desClient.getSleepTask());
assertEquals(client.getClientSupportContentFormats(), desClient.getClientSupportContentFormats());
assertEquals(client.getDefaultContentFormat(), desClient.getDefaultContentFormat());
assertEquals(client.getRetryAttempts().get(), desClient.getRetryAttempts().get());
assertEquals(client.getLastSentRpcId(), desClient.getLastSentRpcId());
Map<String, ResourceValue> expectedResources = client.getResources();
Map<String, ResourceValue> actualResources = desClient.getResources();
assertNotNull(actualResources);
assertEquals(expectedResources.size(), actualResources.size());
expectedResources.forEach((key, value) -> assertEquals(value.toString(), actualResources.get(key).toString()));
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2
ui-ngx/package.json

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

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

@ -0,0 +1,209 @@
diff --git a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
index d6c05f3..d09baed 100644
--- a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
+++ b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js
@@ -362,7 +362,10 @@ var DataZoomModel = /** @class */function (_super) {
return axisProxy.getDataValueWindow();
}
} else {
- return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow();
+ var axisProxy = this.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ return axisProxy.getDataValueWindow();
+ }
}
};
/**
@@ -381,10 +384,10 @@ var DataZoomModel = /** @class */function (_super) {
var axisInfo = this._targetAxisInfoMap.get(axisDim);
for (var j = 0; j < axisInfo.indexList.length; j++) {
var proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]);
- if (proxy.hostedBy(this)) {
+ if (proxy && proxy.hostedBy(this)) {
return proxy;
}
- if (!firstProxy) {
+ if (proxy && !firstProxy) {
firstProxy = proxy;
}
}
diff --git a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
index 06469b2..ccfbfa6 100644
--- a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
+++ b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js
@@ -96,11 +96,14 @@ var getRangeHandlers = {
range[0] = (range[0] - percentPoint) * scale + percentPoint;
range[1] = (range[1] - percentPoint) * scale + percentPoint;
// Restrict range.
- var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
- sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
- this.range = range;
- if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
- return range;
+ var proxy = this.dataZoomModel.findRepresentativeAxisProxy();
+ if (proxy) {
+ var minMaxSpan = proxy.getMinMaxSpan();
+ sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan);
+ this.range = range;
+ if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) {
+ return range;
+ }
}
},
pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e) {
diff --git a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
index 98912e0..2e809af 100644
--- a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
+++ b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js
@@ -64,7 +64,7 @@ var DEFAULT_MOVE_HANDLE_SIZE = 7;
var HORIZONTAL = 'horizontal';
var VERTICAL = 'vertical';
var LABEL_GAP = 5;
-var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter'];
+var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter', 'custom'];
var REALTIME_ANIMATION_CONFIG = {
easing: 'cubicOut',
duration: 100,
@@ -359,30 +359,33 @@ var SliderZoomView = /** @class */function (_super) {
var result;
var ecModel = this.ecModel;
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- var seriesModels = dataZoomModel.getAxisProxy(axisDim, axisIndex).getTargetSeriesModels();
- each(seriesModels, function (seriesModel) {
- if (result) {
- return;
- }
- if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
- return;
- }
- var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
- var otherDim = getOtherDim(axisDim);
- var otherAxisInverse;
- var coordSys = seriesModel.coordinateSystem;
- if (otherDim != null && coordSys.getOtherAxis) {
- otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
- }
- otherDim = seriesModel.getData().mapDimension(otherDim);
- result = {
- thisAxis: thisAxis,
- series: seriesModel,
- thisDim: axisDim,
- otherDim: otherDim,
- otherAxisInverse: otherAxisInverse
- };
- }, this);
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ var seriesModels = axisProxy.getTargetSeriesModels();
+ each(seriesModels, function (seriesModel) {
+ if (result) {
+ return;
+ }
+ if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) {
+ return;
+ }
+ var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis;
+ var otherDim = getOtherDim(axisDim);
+ var otherAxisInverse;
+ var coordSys = seriesModel.coordinateSystem;
+ if (otherDim != null && coordSys.getOtherAxis) {
+ otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse;
+ }
+ otherDim = seriesModel.getData().mapDimension(otherDim);
+ result = {
+ thisAxis: thisAxis,
+ series: seriesModel,
+ thisDim: axisDim,
+ otherDim: otherDim,
+ otherAxisInverse: otherAxisInverse
+ };
+ }, this);
+ }
}, this);
return result;
};
@@ -530,12 +533,17 @@ var SliderZoomView = /** @class */function (_super) {
var dataZoomModel = this.dataZoomModel;
var handleEnds = this._handleEnds;
var viewExtend = this._getViewExtent();
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();
- var percentExtent = [0, 100];
- sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
- var lastRange = this._range;
- var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
- return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
+ var proxy = dataZoomModel.findRepresentativeAxisProxy();
+ if (proxy) {
+ var minMaxSpan = proxy.getMinMaxSpan();
+ var percentExtent = [0, 100];
+ sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null);
+ var lastRange = this._range;
+ var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]);
+ return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1];
+ } else {
+ return false;
+ }
};
SliderZoomView.prototype._updateView = function (nonRealtime) {
var displaybles = this._displayables;
diff --git a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
index ce98fed..361b138 100644
--- a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
+++ b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js
@@ -90,7 +90,10 @@ var dataZoomProcessor = {
// init stage and not after action dispatch handler, because
// reset should be called after seriesData.restoreData.
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel);
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ axisProxy.reset(dataZoomModel);
+ }
});
// Caution: data zoom filtering is order sensitive when using
// percent range and no min/max/scale set on axis.
@@ -107,7 +110,10 @@ var dataZoomProcessor = {
// So we should filter x-axis after reset x-axis immediately,
// and then reset y-axis and filter y-axis.
dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) {
- dataZoomModel.getAxisProxy(axisDim, axisIndex).filterData(dataZoomModel, api);
+ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex);
+ if (axisProxy) {
+ axisProxy.filterData(dataZoomModel, api);
+ }
});
});
ecModel.eachComponent('dataZoom', function (dataZoomModel) {
diff --git a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
index cf8d6bc..9b30ec1 100644
--- a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
+++ b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js
@@ -109,9 +109,12 @@ var DataZoomFeature = /** @class */function (_super) {
var axisModel = axis.model;
var dataZoomModel = findDataZoom(dimName, axisModel, ecModel);
// Restrict range.
- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan();
- if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
- minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
+ var proxy = dataZoomModel.findRepresentativeAxisProxy(axisModel);
+ if (proxy) {
+ var minMaxSpan = proxy.getMinMaxSpan();
+ if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) {
+ minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan);
+ }
}
dataZoomModel && (snapshot[dataZoomModel.id] = {
dataZoomId: dataZoomModel.id,
diff --git a/node_modules/echarts/lib/component/tooltip/TooltipView.js b/node_modules/echarts/lib/component/tooltip/TooltipView.js
index b8a9b95..8e4cb2f 100644
--- a/node_modules/echarts/lib/component/tooltip/TooltipView.js
+++ b/node_modules/echarts/lib/component/tooltip/TooltipView.js
@@ -360,7 +360,7 @@ var TooltipView = /** @class */function (_super) {
each(itemCoordSys.dataByAxis, function (axisItem) {
var axisModel = ecModel.getComponent(axisItem.axisDim + 'Axis', axisItem.axisIndex);
var axisValue = axisItem.value;
- if (!axisModel || axisValue == null) {
+ if (!axisModel || !axisModel.axis || axisValue == null) {
return;
}
var axisValueLabel = axisPointerViewHelper.getValueLabel(axisValue, axisModel.axis, ecModel, axisItem.seriesDataIndices, axisItem.valueLabelOpt);

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

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

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

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

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

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

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

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

8
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -571,6 +571,14 @@
[type]="rateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT">
</tb-rate-limits>
</div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex="50" formControlName="edgeUplinkMessagesRateLimits"
[type]="rateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="edgeUplinkMessagesRateLimitsPerEdge"
[type]="rateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT">
</tb-rate-limits>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>

4
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -108,7 +108,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
wsUpdatesPerSessionRateLimit: [null, []],
cassandraQueryTenantRateLimitsConfiguration: [null, []],
edgeEventRateLimits: [null, []],
edgeEventRateLimitsPerEdge: [null, []]
edgeEventRateLimitsPerEdge: [null, []],
edgeUplinkMessagesRateLimits: [null, []],
edgeUplinkMessagesRateLimitsPerEdge: [null, []]
});
this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe(

8
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts

@ -37,7 +37,9 @@ export enum RateLimitsType {
TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT',
TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT',
EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT',
EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT'
EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT',
EDGE_UPLINK_MESSAGES_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_RATE_LIMIT',
EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT = 'EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT'
}
export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
@ -58,6 +60,8 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.tenant-notification-requests-per-rule-rate-limit'],
[RateLimitsType.EDGE_EVENTS_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-rate-limit'],
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-uplink-messages-per-edge-rate-limit'],
]
);
@ -79,6 +83,8 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-notification-requests-per-rule-rate-limit-title'],
[RateLimitsType.EDGE_EVENTS_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-rate-limit'],
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-per-edge-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-rate-limit'],
[RateLimitsType.EDGE_UPLINK_MESSAGES_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-uplink-messages-per-edge-rate-limit'],
]
);

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

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

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

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

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

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

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

@ -205,6 +205,13 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput

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

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

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

@ -179,6 +179,13 @@
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="tooltipDateInterval">
<div tb-hint-tooltip-icon="{{'tooltip.show-date-time-interval-hint' | translate}}">
{{ 'tooltip.show-date-time-interval' | translate }}
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput

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

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

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

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

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

@ -0,0 +1,367 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Injector } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
DataKey,
Datasource,
legendPositions,
legendPositionTranslationMap,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isDefinedAndNotNull, isUndefined, mergeDeep } from '@core/utils';
import {
cssSizeToStrSize,
DateFormatProcessor,
DateFormatSettings,
resolveCssSize
} from '@shared/models/widget-settings.models';
import {
timeSeriesChartWidgetDefaultSettings,
TimeSeriesChartWidgetSettings
} from '@home/components/widget/lib/chart/time-series-chart-widget.models';
import { EChartsTooltipTrigger } from '@home/components/widget/lib/chart/echarts-widget.models';
import {
TimeSeriesChartKeySettings, TimeSeriesChartThreshold,
TimeSeriesChartType, TimeSeriesChartYAxes,
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
@Component({
selector: 'tb-time-series-chart-basic-config',
templateUrl: './time-series-chart-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.timeSeriesChartWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get yAxisIds(): TimeSeriesChartYAxisId[] {
const yAxes: TimeSeriesChartYAxes = this.timeSeriesChartWidgetConfigForm.get('yAxes').value;
return Object.keys(yAxes);
}
TimeSeriesChartType = TimeSeriesChartType;
EChartsTooltipTrigger = EChartsTooltipTrigger;
legendPositions = legendPositions;
legendPositionTranslationMap = legendPositionTranslationMap;
timeSeriesChartWidgetConfigForm: UntypedFormGroup;
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this);
chartType: TimeSeriesChartType = TimeSeriesChartType.default;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
public yAxisRemoved(yAxisId: TimeSeriesChartYAxisId): void {
if (this.widgetConfig.config.datasources && this.widgetConfig.config.datasources.length > 1) {
for (let i = 1; i < this.widgetConfig.config.datasources.length; i++) {
const datasource = this.widgetConfig.config.datasources[i];
this.removeYaxisId(datasource.dataKeys, yAxisId);
}
}
}
protected configForm(): UntypedFormGroup {
return this.timeSeriesChartWidgetConfigForm;
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
if (isDefinedAndNotNull(params.chartType)) {
this.chartType = params.chartType;
}
super.setupConfig(widgetConfig);
}
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] {
return [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, units: '°C', decimals: 0 }];
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: TimeSeriesChartWidgetSettings = mergeDeep<TimeSeriesChartWidgetSettings>({} as TimeSeriesChartWidgetSettings,
timeSeriesChartWidgetDefaultSettings, configData.config.settings as TimeSeriesChartWidgetSettings);
const iconSize = resolveCssSize(configData.config.iconSize);
this.timeSeriesChartWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
yAxes: [settings.yAxes, []],
series: [this.getSeries(configData.config.datasources), []],
thresholds: [settings.thresholds, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showIcon: [configData.config.showTitleIcon, []],
iconSize: [iconSize[0], [Validators.min(0)]],
iconSizeUnit: [iconSize[1], []],
icon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
dataZoom: [settings.dataZoom, []],
stack: [settings.stack, []],
xAxis: [settings.xAxis, []],
noAggregationBarWidthSettings: [settings.noAggregationBarWidthSettings, []],
showLegend: [settings.showLegend, []],
legendLabelFont: [settings.legendLabelFont, []],
legendLabelColor: [settings.legendLabelColor, []],
legendConfig: [settings.legendConfig, []],
showTooltip: [settings.showTooltip, []],
tooltipTrigger: [settings.tooltipTrigger, []],
tooltipValueFont: [settings.tooltipValueFont, []],
tooltipValueColor: [settings.tooltipValueColor, []],
tooltipShowDate: [settings.tooltipShowDate, []],
tooltipDateFormat: [settings.tooltipDateFormat, []],
tooltipDateFont: [settings.tooltipDateFont, []],
tooltipDateColor: [settings.tooltipDateColor, []],
tooltipDateInterval: [settings.tooltipDateInterval, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
animation: [settings.animation, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
padding: [settings.padding, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources);
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showIcon;
this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit);
this.widgetConfig.config.titleIcon = config.icon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.thresholds = config.thresholds;
this.widgetConfig.config.settings.dataZoom = config.dataZoom;
this.widgetConfig.config.settings.stack = config.stack;
this.widgetConfig.config.settings.yAxes = config.yAxes;
this.widgetConfig.config.settings.xAxis = config.xAxis;
this.widgetConfig.config.settings.noAggregationBarWidthSettings = config.noAggregationBarWidthSettings;
this.widgetConfig.config.settings.showLegend = config.showLegend;
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont;
this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor;
this.widgetConfig.config.settings.legendConfig = config.legendConfig;
this.widgetConfig.config.settings.showTooltip = config.showTooltip;
this.widgetConfig.config.settings.tooltipTrigger = config.tooltipTrigger;
this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont;
this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor;
this.widgetConfig.config.settings.tooltipShowDate = config.tooltipShowDate;
this.widgetConfig.config.settings.tooltipDateFormat = config.tooltipDateFormat;
this.widgetConfig.config.settings.tooltipDateFont = config.tooltipDateFont;
this.widgetConfig.config.settings.tooltipDateColor = config.tooltipDateColor;
this.widgetConfig.config.settings.tooltipDateInterval = config.tooltipDateInterval;
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
this.widgetConfig.config.settings.animation = config.animation;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.settings.padding = config.padding;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showIcon', 'showLegend', 'showTooltip', 'tooltipShowDate'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.timeSeriesChartWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.timeSeriesChartWidgetConfigForm.get('showIcon').value;
const showLegend: boolean = this.timeSeriesChartWidgetConfigForm.get('showLegend').value;
const showTooltip: boolean = this.timeSeriesChartWidgetConfigForm.get('showTooltip').value;
const tooltipShowDate: boolean = this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').value;
if (showTitle) {
this.timeSeriesChartWidgetConfigForm.get('title').enable();
this.timeSeriesChartWidgetConfigForm.get('titleFont').enable();
this.timeSeriesChartWidgetConfigForm.get('titleColor').enable();
this.timeSeriesChartWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.timeSeriesChartWidgetConfigForm.get('iconSize').enable();
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').enable();
this.timeSeriesChartWidgetConfigForm.get('icon').enable();
this.timeSeriesChartWidgetConfigForm.get('iconColor').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('iconSize').disable();
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable();
this.timeSeriesChartWidgetConfigForm.get('icon').disable();
this.timeSeriesChartWidgetConfigForm.get('iconColor').disable();
}
} else {
this.timeSeriesChartWidgetConfigForm.get('title').disable();
this.timeSeriesChartWidgetConfigForm.get('titleFont').disable();
this.timeSeriesChartWidgetConfigForm.get('titleColor').disable();
this.timeSeriesChartWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.timeSeriesChartWidgetConfigForm.get('iconSize').disable();
this.timeSeriesChartWidgetConfigForm.get('iconSizeUnit').disable();
this.timeSeriesChartWidgetConfigForm.get('icon').disable();
this.timeSeriesChartWidgetConfigForm.get('iconColor').disable();
}
if (showLegend) {
this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').enable();
this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').enable();
this.timeSeriesChartWidgetConfigForm.get('legendConfig').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('legendLabelFont').disable();
this.timeSeriesChartWidgetConfigForm.get('legendLabelColor').disable();
this.timeSeriesChartWidgetConfigForm.get('legendConfig').disable();
}
if (showTooltip) {
this.timeSeriesChartWidgetConfigForm.get('tooltipTrigger').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false});
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').enable();
if (tooltipShowDate) {
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').enable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').enable();
} else {
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable();
}
} else {
this.timeSeriesChartWidgetConfigForm.get('tooltipValueFont').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').disable({emitEvent: false});
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateFont').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipDateInterval').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').disable();
this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').disable();
}
}
private getSeries(datasources?: Datasource[]): DataKey[] {
if (datasources && datasources.length) {
return datasources[0].dataKeys || [];
}
return [];
}
private setSeries(series: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
datasources[0].dataKeys = series;
}
}
private removeYaxisId(series: DataKey[], yAxisId: TimeSeriesChartYAxisId): boolean {
let changed = false;
if (series) {
series.forEach(key => {
const keySettings = ((key.settings || {}) as TimeSeriesChartKeySettings);
if (keySettings.yAxisId === yAxisId) {
keySettings.yAxisId = 'default';
changed = true;
}
});
}
return changed;
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _tooltipValuePreviewFn(): string {
return formatValue(22, 0, '°C', false);
}
private _tooltipDatePreviewFn(): string {
const dateFormat: DateFormatSettings = this.timeSeriesChartWidgetConfigForm.get('tooltipDateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -37,7 +37,6 @@ import {
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { formatValue } from '@core/utils';
import { DataKey } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
@ -54,18 +53,13 @@ import {
ECharts,
echartsModule,
EChartsOption,
EChartsSeriesItem,
echartsTooltipFormatter,
NamedDataSet,
toNamedData
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { IntervalMath } from '@shared/models/time/time.models';
interface BarChartDataItem {
id: string;
dataKey: DataKey;
data: NamedDataSet;
enabled: boolean;
}
type BarChartDataItem = EChartsSeriesItem;
interface BarChartLegendItem {
id: string;
@ -386,7 +380,7 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
appendTo: 'body',
axisPointer: {
type: 'shadow'
},

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

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

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

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

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

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

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

@ -318,7 +318,7 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
tooltip: {
trigger: 'axis',
confine: true,
appendToBody: true,
appendTo: 'body',
axisPointer: {
type: 'shadow'
},

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

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

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

@ -0,0 +1,166 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient';
import { Interval, IntervalMath } from '@shared/models/time/time.models';
import { LabelFormatterCallback, SeriesLabelOption } from 'echarts/types/src/util/types';
import {
TimeSeriesChartDataItem,
TimeSeriesChartNoAggregationBarWidthStrategy
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { CustomSeriesRenderItemParams } from 'echarts';
import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared';
import { isNumeric } from '@core/utils';
import * as echarts from 'echarts/core';
export interface BarVisualSettings {
color: string | LinearGradientObject;
borderColor: string;
borderWidth: number;
borderRadius: number;
}
export interface BarRenderSharedContext {
timeInterval: Interval;
noAggregationBarWidthStrategy: TimeSeriesChartNoAggregationBarWidthStrategy;
noAggregationWidthRelative: boolean;
noAggregationWidth: number;
}
export interface BarRenderContext {
shared: BarRenderSharedContext;
barsCount?: number;
barIndex?: number;
noAggregation?: boolean;
visualSettings?: BarVisualSettings;
labelOption?: SeriesLabelOption;
barStackIndex?: number;
currentStackItems?: TimeSeriesChartDataItem[];
}
export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI,
renderCtx: BarRenderContext): CustomSeriesRenderItemReturn => {
const time = api.value(0) as number;
let start = api.value(2) as number;
let end = api.value(3) as number;
let interval = end - start;
const ts = start ? start : time;
const separateBar = renderCtx.noAggregation &&
renderCtx.shared.noAggregationBarWidthStrategy === TimeSeriesChartNoAggregationBarWidthStrategy.separate;
if (renderCtx.noAggregation) {
if (renderCtx.shared.noAggregationWidthRelative) {
const scaleWidth = api.getWidth() / api.size([1,0])[0];
interval = scaleWidth * (renderCtx.shared.noAggregationWidth / 100);
} else {
interval = renderCtx.shared.noAggregationWidth;
}
start = time - interval / 2;
end = time + interval / 2;
}
if (!start || !end || !interval) {
interval = IntervalMath.numberValue(renderCtx.shared.timeInterval);
start = time - interval / 2;
}
const gap = 0.3;
const barInterval = separateBar ? interval : interval / (renderCtx.barsCount + gap * (renderCtx.barsCount + 3));
const intervalGap = barInterval * gap * 2;
const barGap = barInterval * gap;
const value = api.value(1);
const startTime = separateBar ? start : start + intervalGap + (barInterval + barGap) * renderCtx.barIndex;
const delta = barInterval;
let offset = 0;
if (renderCtx.currentStackItems?.length) {
for (let i = 0; i < renderCtx.barStackIndex; i++) {
const stackItem = renderCtx.currentStackItems[i];
const dataName = ts + '';
const data = stackItem.data.find(d => d.name === dataName);
if (data) {
const val = data.value[1];
if (isNumeric(val)) {
offset += Number(val);
}
}
}
}
let lowerLeft: number[];
if (offset !== 0 && isNumeric(value)) {
lowerLeft = api.coord([startTime, value >= 0 ? Number(value) + offset : offset]);
} else {
lowerLeft = api.coord([startTime, value >= 0 ? value : 0]);
}
const size = api.size([delta, value]);
const width = size[0];
const height = size[1];
const coordSys: {x: number; y: number; width: number; height: number} = params.coordSys as any;
const rectShape = echarts.graphic.clipRectByRect({
x: lowerLeft[0],
y: lowerLeft[1],
width,
height
}, {
x: coordSys.x,
y: coordSys.y,
width: coordSys.width,
height: coordSys.height
});
const zeroPos = api.coord([0, offset]);
const style: any = {
fill: renderCtx.visualSettings.color,
stroke: renderCtx.visualSettings.borderColor,
lineWidth: renderCtx.visualSettings.borderWidth
};
if (renderCtx.labelOption.show) {
let position = renderCtx.labelOption.position;
if (value < 0) {
if (position === 'top') {
position = 'bottom';
} else if (position === 'bottom') {
position = 'top';
}
}
style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)({value: [null, value]} as any);
style.textDistance = 5;
style.textPosition = position;
style.rich = renderCtx.labelOption.rich;
}
let borderRadius: number[];
if (value < 0) {
borderRadius = [0, 0, renderCtx.visualSettings.borderRadius, renderCtx.visualSettings.borderRadius];
} else {
borderRadius = [renderCtx.visualSettings.borderRadius, renderCtx.visualSettings.borderRadius, 0, 0];
}
return rectShape && {
type: 'rect',
id: time + '',
shape: {...rectShape, r: borderRadius},
style,
focus: 'series',
transition: 'all',
enterFrom: {
style: { opacity: 0 },
shape: { height: 0, y: zeroPos[1] }
}
};
};

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

Loading…
Cancel
Save