Browse Source

Merge remote-tracking branch 'upstream/master' into related-agg-cf/dx-improvements

# Conflicts:
#	dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
pull/14827/head
Dmytro Skarzhynets 6 months ago
parent
commit
5274e062a6
No known key found for this signature in database GPG Key ID: 2B51652F224037DF
  1. 3
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 5
      application/src/main/data/json/system/widget_types/gateway_configuration.json
  3. 5
      application/src/main/data/json/system/widget_types/gateway_configuration__single_device_.json
  4. 4
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  5. 2
      application/src/main/data/json/system/widget_types/gateway_custom_statistics.json
  6. 2
      application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json
  7. 4
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  8. 4
      application/src/main/data/json/system/widget_types/gateway_logs.json
  9. 29
      application/src/main/data/json/system/widget_types/gateway_markdown_html_card.json
  10. 3
      application/src/main/data/json/system/widget_types/gateway_status.json
  11. 2
      application/src/main/data/json/system/widget_types/service_rpc.json
  12. 1984
      application/src/main/data/resources/dashboards/gateways_dashboard.json
  13. 34
      application/src/main/data/resources/js_extensions/gateway-management-extension.js
  14. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java
  15. 38
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  16. 23
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  17. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java
  18. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  19. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  20. 8
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  21. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  22. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  23. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java
  24. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java
  25. 2
      application/src/main/resources/thingsboard.yml
  26. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  27. 30
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldEventType.java
  28. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java
  29. 22
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasUseLatestTsConfig.java
  30. 9
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java
  31. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java
  32. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java
  33. 11
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
  34. 18
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  35. 78
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java
  36. 11
      ui-ngx/patches/@angular+compiler+18.2.13.patch
  37. 2
      ui-ngx/src/app/core/services/calculated-field-form.service.ts
  38. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  39. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  40. 15
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  41. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  42. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html
  43. 48
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts
  44. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html
  45. 49
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts
  46. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html
  47. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts
  48. 2
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html
  49. 2
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts
  50. 2
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  51. 34
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  52. 2
      ui-ngx/src/app/shared/components/country-autocomplete.component.ts
  53. 69
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  54. 2
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  55. 2
      ui-ngx/src/app/shared/models/constants.ts
  56. 712
      ui-ngx/src/assets/locale/locale.constant-da_DK.json
  57. 731
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  58. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  59. 715
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  60. 9547
      ui-ngx/src/assets/locale/locale.constant-hi_IN.json
  61. 721
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  62. 11699
      ui-ngx/src/assets/locale/locale.constant-ja_JP.json
  63. 719
      ui-ngx/src/assets/locale/locale.constant-nl_NL.json
  64. 886
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json

3
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -17,6 +17,7 @@
"gateway_widgets.gateway_custom_statistics",
"gateway_widgets.gateway_general_chart_statistics",
"gateway_widgets.service_rpc",
"gateway_widgets.gateway_status"
"gateway_widgets.gateway_status",
"gateway_widgets.markdown_card"
]
}

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

@ -17,9 +17,10 @@
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\">\n</tb-gateway-form>\n",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-gateway-config-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"actions\":{}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetTitle\":\"Gateway Configuration\",\"archiveFileName\":\"configurationGateway\"},\"title\":\"Gateway Configuration\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
},
"externalId": null,
"tags": [
@ -41,4 +42,4 @@
"ble",
"bluetooth"
]
}
}

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

@ -17,9 +17,10 @@
"templateHtml": "<tb-gateway-form\n [ctx]=\"ctx\"\n [isStateForm]=\"true\">\n</tb-gateway-form>",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
"controllerScript": "self.onInit = function() {\n}\n\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\t\t\t\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\n",
"settingsSchema": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-gateway-config-single-device-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gatewayTitle\":\"Gateway configuration (Single device)\"},\"title\":\"Gateway configuration (Single device)\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gatewayTitle\":\"Gateway configuration (Single device)\"},\"title\":\"Gateway configuration (Single device)\"}"
},
"externalId": null,
"tags": [
@ -41,4 +42,4 @@
"ble",
"bluetooth"
]
}
}

4
application/src/main/data/json/system/widget_types/gateway_connectors.json

@ -19,7 +19,7 @@
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayConnectors?.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
},
"tags": [
"router",
@ -40,4 +40,4 @@
"ble",
"bluetooth"
]
}
}

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

@ -44,4 +44,4 @@
"ble",
"bluetooth"
]
}
}

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

@ -44,4 +44,4 @@
"ble",
"bluetooth"
]
}
}

4
application/src/main/data/json/system/widget_types/gateway_general_configuration.json

@ -18,7 +18,7 @@
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n self.ctx.$scope.defaultTab = self.ctx.stateController?.getStateParams()?.defaultTab;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway configuration\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway configuration\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
},
"tags": [
"router",
@ -39,4 +39,4 @@
"ble",
"bluetooth"
]
}
}

4
application/src/main/data/json/system/widget_types/gateway_logs.json

@ -20,7 +20,7 @@
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-gateway-logs-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":25000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway logs\",\"showTitleIcon\":false,\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false,\"useDashboardTimewindow\":false,\"displayTimewindow\":true}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":86400000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":300000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway logs\",\"showTitleIcon\":false,\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false,\"useDashboardTimewindow\":false,\"displayTimewindow\":true}"
},
"tags": [
"router",
@ -41,4 +41,4 @@
"ble",
"bluetooth"
]
}
}

29
application/src/main/data/json/system/widget_types/gateway_markdown_html_card.json

@ -0,0 +1,29 @@
{
"fqn": "gateway_widgets.markdown_card",
"name": "Gateway Markdown/HTML Card",
"deprecated": false,
"image": "tb-image;/api/images/system/markdown_html_card_system_widget_image.png",
"description": "Renders markdown/HTML using the configurable pattern or function with applied attributes or time series values.",
"descriptor": {
"type": "latest",
"sizeX": 5,
"sizeY": 3.5,
"resources": [
{
"url": "tb-resource;/api/resource/js_module/system/gateway-management-extension.js",
"isModule": true
}
],
"templateHtml": "<tb-markdown-widget \n [ctx]=\"ctx\">\n</tb-markdown-widget>",
"templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n",
"settingsDirective": "tb-markdown-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
},
"resources": null,
"scada": false,
"tags": [
"web",
"markup"
]
}

3
application/src/main/data/json/system/widget_types/gateway_status.json

@ -18,7 +18,8 @@
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayStatus?.onDataUpdated();\n};",
"settingsSchema": "",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}"
"dataKeySettingsSchema": "",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"HTML Card\",\"dropShadow\":true}"
},
"tags": [
"router",

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

@ -35,4 +35,4 @@
"inter-process communication",
"server request"
]
}
}

1984
application/src/main/data/resources/dashboards/gateways_dashboard.json

File diff suppressed because it is too large

34
application/src/main/data/resources/js_extensions/gateway-management-extension.js

File diff suppressed because one or more lines are too long

2
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldArgumentResetMsg.java

@ -16,6 +16,7 @@
package org.thingsboard.server.actors.calculatedField;
import lombok.Data;
import org.thingsboard.server.common.data.cf.CalculatedFieldEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@ -27,6 +28,7 @@ public class CalculatedFieldArgumentResetMsg implements ToCalculatedFieldSystemM
private final TenantId tenantId;
private final CalculatedFieldCtx ctx;
private final CalculatedFieldEventType eventType;
private final TbCallback callback;
@Override

38
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.cf.CalculatedFieldEventType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
@ -75,7 +76,6 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT;
import static org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry.getValueForTsRecord;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
@ -169,7 +169,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
if (msg.getStateAction() != StateAction.REFRESH_CTX) {
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, msg.getEventType().name(), msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
@ -196,7 +196,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs);
fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true));
processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, null);
processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, msg.getEventType().name());
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
@ -256,7 +256,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
}
if (state.isSizeOk()) {
processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, TbMsgType.RELATION_ADD_OR_UPDATE.name(), msg.getCallback());
} else {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
}
@ -283,7 +283,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, TbMsgType.RELATION_DELETED.name(), msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
@ -293,6 +293,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
PropagationArgumentEntry entry = new PropagationArgumentEntry();
entry.setRemoved(msg.getRelatedEntityId());
propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), null, TbMsgType.RELATION_DELETED.name(), null, null);
}
}
msg.getCallback().onSuccess();
}
@ -323,13 +326,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
callback.onSuccess();
} else {
if (proto.getTsDataCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toMsgType(proto));
} else if (proto.getAttrDataCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toMsgType(proto));
} else if (proto.getRemovedTsKeysCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, msg.getEntityId(), proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithFetchedValue(ctx, msg.getEntityId(), proto.getRemovedTsKeysList()), toTbMsgId(proto), toMsgType(proto));
} else if (proto.getRemovedAttrKeysCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toMsgType(proto));
} else {
callback.onSuccess();
}
@ -379,7 +382,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
if (state.isSizeOk()) {
log.debug("[{}][{}] Reevaluating CF state", entityId, cfId);
processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, REEVALUATION_MSG, msg.getCallback());
processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, CalculatedFieldEventType.REEVALUATION_MSG.name(), msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
@ -399,23 +402,23 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toMsgType(proto));
}
private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toMsgType(proto));
}
private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toMsgType(proto));
}
private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toMsgType(proto));
}
private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, TbCallback callback,
Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException {
Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, String msgType) throws CalculatedFieldException {
if (newArgValues.isEmpty()) {
log.debug("[{}] No new argument values to process for CF.", ctx.getCfId());
callback.onSuccess();
@ -453,7 +456,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (!updatedArgs.isEmpty() || justRestored) {
cfIdList = new ArrayList<>(cfIdList);
cfIdList.add(ctx.getCfId());
String msgType = tbMsgType == null ? null : tbMsgType.name();
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, msgType, callback);
} else {
callback.onSuccess();
@ -777,9 +779,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return null;
}
private TbMsgType toTbMsgType(CalculatedFieldTelemetryMsgProto proto) {
private String toMsgType(CalculatedFieldTelemetryMsgProto proto) {
if (!proto.getTbMsgType().isEmpty()) {
return TbMsgType.valueOf(proto.getTbMsgType());
return proto.getTbMsgType();
}
return null;
}

23
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -17,6 +17,7 @@ package org.thingsboard.server.actors.calculatedField;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.function.TriConsumer;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldEventType;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel;
@ -43,6 +45,7 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -278,7 +281,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (!cfsToReinit.isEmpty()) {
MultipleTbCallback cfsReinitCallback = new MultipleTbCallback(cfsToReinit.size(), callback);
cfsToReinit.forEach(ctx -> applyToTargetCfEntityActors(ctx, cfsReinitCallback, (id, cb) -> initCfForEntity(id, ctx, StateAction.REINIT, cb)));
cfsToReinit.forEach(ctx -> applyToTargetCfEntityActors(ctx, cfsReinitCallback, (id, cb) -> initCfForEntity(id, ctx, StateAction.REINIT, CalculatedFieldEventType.TENANT_PROFILE_UPDATED, cb)));
} else {
callback.onSuccess();
}
@ -309,8 +312,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var fieldsCount = entityIdFields.size() + profileIdFields.size();
if (fieldsCount > 0) {
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback));
profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback));
entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, multiCallback));
profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, multiCallback));
} else {
callback.onSuccess();
}
@ -329,7 +332,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
var entityId = msg.getEntityId();
oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback));
newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback));
newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, multiCallback));
} else {
callback.onSuccess();
}
@ -384,7 +387,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
EntityId mainId,
MultipleTbCallback parentCallback,
TriConsumer<EntityId, CalculatedFieldCtx, TbCallback> relationAction) {
if (isMyPartition(mainId, parentCallback)) {
if (!isMyPartition(mainId, parentCallback)) {
return;
}
List<CalculatedFieldCtx> cfsByEntityIdAndProfile = getCalculatedFieldsByEntityIdAndProfile(mainId);
@ -429,7 +432,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, cb));
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, CalculatedFieldEventType.INITIALIZED, cb));
}
}
}
@ -503,7 +506,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
oldCfCtx.close();
callback.onFailure(t);
}
}, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb));
}, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, CalculatedFieldEventType.UPDATED, cb));
}
}
}
@ -643,7 +646,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfs.forEach(cf -> {
if (isMyPartition(entityId, callback)) {
if (cf.hasCurrentOwnerSourceArguments()) {
CalculatedFieldArgumentResetMsg argResetMsg = new CalculatedFieldArgumentResetMsg(tenantId, cf, callback);
CalculatedFieldArgumentResetMsg argResetMsg = new CalculatedFieldArgumentResetMsg(tenantId, cf, CalculatedFieldEventType.OWNER_CHANGED, callback);
log.debug("Pushing CF argument reset msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(argResetMsg);
} else {
@ -750,9 +753,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback));
}
private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, StateAction stateAction, TbCallback callback) {
private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, StateAction stateAction, CalculatedFieldEventType eventType, TbCallback callback) {
log.debug("Pushing entity init CF msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, stateAction, callback));
getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, stateAction, eventType, callback));
}
private boolean isMyPartition(EntityId entityId, TbCallback callback) {

2
application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java

@ -16,6 +16,7 @@
package org.thingsboard.server.actors.calculatedField;
import lombok.Data;
import org.thingsboard.server.common.data.cf.CalculatedFieldEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@ -28,6 +29,7 @@ public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg
private final TenantId tenantId;
private final CalculatedFieldCtx ctx;
private final StateAction stateAction;
private final CalculatedFieldEventType eventType;
private final TbCallback callback;
@Override

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -21,6 +21,7 @@ import lombok.Getter;
import lombok.Setter;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
@ -142,8 +143,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
protected void validateNewEntry(String key, ArgumentEntry newEntry) {
}
protected ObjectNode toSimpleResult(boolean useLatestTs, ObjectNode valuesNode) {
if (!useLatestTs) {
protected ObjectNode toResultNode(ObjectNode valuesNode) {
if (ctx.getOutput().getType() == OutputType.ATTRIBUTES || !ctx.isUseLatestTs()) {
return valuesNode;
}
long latestTs = getLatestTimestamp();

6
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
@ -187,7 +188,6 @@ public class CalculatedFieldCtx implements Closeable {
.collect(Collectors.toList());
if (argBasedConfig instanceof ExpressionBasedCalculatedFieldConfiguration expressionBasedConfig) {
this.expression = expressionBasedConfig.getExpression();
this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) argBasedConfig).isUseLatestTs();
}
if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration geofencingConfig) {
geofencingConfig.getZoneGroups().forEach((zoneGroupName, config) -> {
@ -209,8 +209,8 @@ public class CalculatedFieldCtx implements Closeable {
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : DISABLED_INTERVAL_VALUE;
}
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfig) {
this.useLatestTs = aggConfig.isUseLatestTs();
if (calculatedField.getConfiguration() instanceof HasUseLatestTsConfig hasUseLatestTsConfig) {
this.useLatestTs = hasUseLatestTsConfig.isUseLatestTs();
}
this.systemContext = systemContext;
this.tbelInvokeService = systemContext.getTbelInvokeService();

8
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -22,13 +22,13 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.EqualsAndHashCode;
import net.objecthunter.exp4j.Expression;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.NumberUtils;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.common.util.NumberUtils;
import java.util.Map;
@ -53,7 +53,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
Output output = ctx.getOutput();
Object result = NumberUtils.roundResult(expressionResult, output.getDecimalsByDefault());
JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result);
JsonNode outputResult = createResultJson(output.getName(), result);
return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder()
.outputStrategy(output.getStrategy())
@ -63,7 +63,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
.build());
}
private JsonNode createResultJson(boolean useLatestTs, String outputName, Object result) {
private JsonNode createResultJson(String outputName, Object result) {
ObjectNode valuesNode = JacksonUtil.newObjectNode();
if (result instanceof Double doubleValue) {
valuesNode.put(outputName, doubleValue);
@ -72,7 +72,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
} else {
valuesNode.set(outputName, JacksonUtil.valueToTree(result));
}
return toSimpleResult(useLatestTs, valuesNode);
return toResultNode(valuesNode);
}
@Override

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java

@ -201,7 +201,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
.outputStrategy(output.getStrategy())
.type(output.getType())
.scope(output.getScope())
.result(toSimpleResult(ctx.isUseLatestTs(), aggResult))
.result(toResultNode(aggResult))
.build());
} else {
return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY);

7
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.cf.ctx.state.geofencing;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -136,7 +135,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState imp
.outputStrategy(ctx.getOutput().getStrategy())
.type(outputType)
.scope(ctx.getOutput().getScope())
.result(toResultNode(outputType, valuesNode))
.result(toResultNode(valuesNode))
.build();
if (relationFutures.isEmpty()) {
return Futures.immediateFuture(result);
@ -186,10 +185,6 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState imp
}
}
private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) {
return toSimpleResult(outputType == OutputType.TIME_SERIES, valuesNode);
}
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
boolean prevInside = zoneResults.stream()

4
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java

@ -24,7 +24,6 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
@ -171,8 +170,7 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState
throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " +
"Only Latest telemetry or Attribute arguments supported for 'Arguments Only' propagation mode!");
});
ObjectNode result = toSimpleResult(output.getType() == OutputType.TIME_SERIES, valuesNode);
telemetryCfBuilder.result(result);
telemetryCfBuilder.result(toResultNode(valuesNode));
return telemetryCfBuilder.build();
}

10
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -47,6 +48,9 @@ import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.UUID;
import static org.thingsboard.server.common.data.cf.CalculatedFieldType.SCRIPT;
import static org.thingsboard.server.common.data.cf.CalculatedFieldType.SIMPLE;
@Slf4j
@Component
@TbCoreComponent
@ -91,7 +95,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId);
if (calculatedField != null) {
if (calculatedField != null && isValidCfToSend(calculatedField.getType(), edgeVersion)) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField);
return DownlinkMsg.newBuilder()
@ -111,6 +115,10 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i
return null;
}
private boolean isValidCfToSend(CalculatedFieldType type, EdgeVersion edgeVersion) {
return edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber() || (type == SIMPLE || type == SCRIPT);
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.CALCULATED_FIELD;

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

@ -1330,7 +1330,7 @@ transport:
# URL of gateways dashboard repository
repository_url: "${TB_GATEWAY_DASHBOARD_SYNC_REPOSITORY_URL:https://github.com/thingsboard/gateway-management-extensions-dist.git}"
# Branch of gateways dashboard repository to work with
branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.0.0}"
branch: "${TB_GATEWAY_DASHBOARD_SYNC_BRANCH:release/4.3.0}"
# Fetch frequency in hours for gateways dashboard repository
fetch_frequency: "${TB_GATEWAY_DASHBOARD_SYNC_FETCH_FREQUENCY:24}"

2
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -106,8 +106,6 @@ public class DataConstants {
public static final String RPC_FAILED = "RPC_FAILED";
public static final String RPC_DELETED = "RPC_DELETED";
public static final String REEVALUATION_MSG = "REEVALUATION_MSG";
public static final String DEFAULT_SECRET_KEY = "";
public static final String SECRET_KEY_FIELD_NAME = "secretKey";
public static final String DURATION_MS_FIELD_NAME = "durationMs";

30
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldEventType.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2026 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.common.data.cf;
public enum CalculatedFieldEventType {
INITIALIZED,
UPDATED,
TENANT_PROFILE_UPDATED,
OWNER_CHANGED,
RELATION_ADD_OR_UPDATE,
RELATION_DELETED,
REEVALUATION_MSG
}

3
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.cf.configuration;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
@ -24,6 +25,8 @@ public abstract class BaseCalculatedFieldConfiguration implements ExpressionBase
protected Map<String, Argument> arguments;
protected String expression;
@NotNull
protected Output output;
@Override

22
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/HasUseLatestTsConfig.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2026 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.common.data.cf.configuration;
public interface HasUseLatestTsConfig {
boolean isUseLatestTs();
}

9
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -27,7 +28,7 @@ import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements HasRelationPathLevel {
public class PropagationCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements HasRelationPathLevel, HasUseLatestTsConfig {
public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx";
@ -37,6 +38,12 @@ public class PropagationCalculatedFieldConfiguration extends BaseCalculatedField
private boolean applyExpressionToResolvedArguments;
@Override
@JsonIgnore
public boolean isUseLatestTs() {
return output.getType() == OutputType.TIME_SERIES;
}
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.PROPAGATION;

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java

@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@Data
@EqualsAndHashCode(callSuper = true)
public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration {
public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements ExpressionBasedCalculatedFieldConfiguration, HasUseLatestTsConfig {
private boolean useLatestTs;

3
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/RelatedEntitiesAggregationCalculatedFieldConfiguration.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel;
import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
@ -30,7 +31,7 @@ import org.thingsboard.server.common.data.relation.RelationPathLevel;
import java.util.Map;
@Data
public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasRelationPathLevel {
public class RelatedEntitiesAggregationCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasRelationPathLevel, HasUseLatestTsConfig {
@NotNull
private RelationPathLevel relation;

11
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java

@ -22,7 +22,9 @@ import lombok.Data;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.HasUseLatestTsConfig;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
@ -35,7 +37,7 @@ import java.util.Set;
import static java.util.stream.Collectors.toSet;
@Data
public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration {
public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration, HasUseLatestTsConfig {
@Valid
@NotNull
@ -48,8 +50,15 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
private boolean scheduledUpdateEnabled;
private Integer scheduledUpdateInterval;
@NotNull
private Output output;
@Override
@JsonIgnore
public boolean isUseLatestTs() {
return output.getType() == OutputType.TIME_SERIES;
}
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.GEOFENCING;

18
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -18,12 +18,14 @@ package org.thingsboard.server.dao.service;
import org.apache.commons.lang3.RandomUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.AttributesOutput;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
@ -110,6 +112,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration));
AttributesOutput out = new AttributesOutput();
out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out);
// Get tenant profile min.
int min = tbTenantProfileCache.get(tenantId)
.getDefaultProfileConfiguration()
@ -164,6 +170,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration));
AttributesOutput out = new AttributesOutput();
out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out);
// Create & save Calculated Field
CalculatedField cf = new CalculatedField();
cf.setTenantId(tenantId);
@ -198,6 +208,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration));
AttributesOutput out = new AttributesOutput();
out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out);
// Get tenant profile min.
int min = tbTenantProfileCache.get(tenantId)
.getDefaultProfileConfiguration()
@ -268,6 +282,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
cf.setConfigurationVersion(0);
cf.setConfiguration(cfg);
var out = new AttributesOutput();
out.setScope(AttributeScope.SERVER_SCOPE);
cfg.setOutput(out);
// WHEN
CalculatedField saved = calculatedFieldService.save(cf);

78
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldEventType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
@ -51,11 +52,15 @@ import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
@ -791,6 +796,79 @@ public class CalculatedFieldTest extends AbstractContainerTest {
testRestClient.deleteAsset(asset2.getId());
}
@Test
public void testDebugEvents() {
// --- Arrange entities ---
String deviceToken = "12345678901";
Device device = testRestClient.postDevice(deviceToken, createDevice("Propagation Device", deviceProfileId));
Asset asset1 = testRestClient.postAsset(createAsset("Propagated Asset 1", null));
// Create relations FROM asset 1 TO device
EntityRelation rel1 = new EntityRelation(asset1.getId(), device.getId(), EntityRelation.CONTAINS_TYPE);
testRestClient.postEntityRelation(rel1);
// --- Build CF: PROPAGATION ---
CalculatedField saved = createPropagationCF(device.getId());
// Create relations FROM asset 2 TO device
Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null));
EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE);
testRestClient.postEntityRelation(rel2);
// Telemetry on device
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25.1}"));
// Delete relation between asset 1 and device
testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId());
// --- Assert propagated calculation (arguments-only mode) ---
await().alias("check debug events")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
List<String> eventTypes = testRestClient.getEvents(saved.getId(), EventType.DEBUG_CALCULATED_FIELD, tenantId, new TimePageLink(4, 0, null, SortOrder.BY_CREATED_TIME_DESC)).getData().stream()
.map(e -> e.getBody().get("msgType").asText())
.toList();
assertThat(eventTypes).as("Check sequence of debug events")
.containsSequence(
CalculatedFieldEventType.RELATION_DELETED.name(),
TbMsgType.POST_TELEMETRY_REQUEST.name(),
CalculatedFieldEventType.RELATION_ADD_OR_UPDATE.name(),
CalculatedFieldEventType.INITIALIZED.name()
);
});
testRestClient.deleteCalculatedFieldIfExists(saved.getId());
testRestClient.deleteDeviceIfExists(device.getId());
testRestClient.deleteAsset(asset1.getId());
testRestClient.deleteAsset(asset2.getId());
}
private CalculatedField createPropagationCF(EntityId entityId) {
CalculatedField cf = new CalculatedField();
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.PROPAGATION);
cf.setName("Propagation CF (args-only)");
cf.setConfigurationVersion(1);
PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration();
cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode
Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("deviceTemperature", arg));
cfg.setOutput(new TimeSeriesOutput());
cf.setConfiguration(cfg);
cf.setDebugSettings(DebugSettings.all());
return testRestClient.postCalculatedField(cf);
}
private CalculatedField createOccupancyCF(EntityId entityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setName("Occupancy");

11
ui-ngx/patches/@angular+compiler+18.2.13.patch

@ -1,5 +1,5 @@
diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs
index a00b189..260e7be 100755
index a00b189..58896de 100755
--- a/node_modules/@angular/compiler/fesm2022/compiler.mjs
+++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs
@@ -18631,6 +18631,7 @@ function SECURITY_SCHEMA() {
@ -86,3 +86,12 @@ index a00b189..260e7be 100755
]);
registerContext(SecurityContext.RESOURCE_URL, [
'applet|code',
@@ -18659,6 +18729,8 @@ function SECURITY_SCHEMA() {
'object|codebase',
'object|data',
'script|src',
+ 'script|href',
+ 'script|xlink:href',
]);
}
return _SECURITY_SCHEMA;

2
ui-ngx/src/app/core/services/calculated-field-form.service.ts

@ -71,7 +71,7 @@ export class CalculatedFieldFormService {
takeUntilDestroyed(destroyRef)
).subscribe(([prevType, nextType]) => {
const shouldCheck = isEditActive ? isEditActive() : true;
if (shouldCheck) {
if (shouldCheck && prevType !== nextType) {
if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) ||
![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) {
form.get('configuration').setValue({} as CalculatedFieldConfiguration, { emitEvent: false });

2
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html

@ -41,7 +41,7 @@
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="filters-text flex-1">{{ filterControl.value?.argument }}</div>
<div class="filters-text flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<div class="filters-text flex-1">{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[filterControl.value?.valueType]).name | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index, readonly)"

12
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts

@ -30,7 +30,8 @@ import { Observable } from 'rxjs';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType
EntityKeyValueType,
entityKeyValueTypesMap
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils';
@ -38,11 +39,7 @@ import {
AlarmRuleFilterDialogComponent,
AlarmRuleFilterDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
import {
AlarmRuleFilter,
areFilterAndPredicateArgumentsValid,
FilterPredicateTypeTranslationMap
} from "@shared/models/alarm-rule.models";
import { AlarmRuleFilter, areFilterAndPredicateArgumentsValid } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@ -83,7 +80,8 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
areFilterAndPredicateArgumentsValid = areFilterAndPredicateArgumentsValid;
complexOperationTranslationMap = complexOperationTranslationMap;
FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap
entityKeyValueTypes = entityKeyValueTypesMap;
entityKeyValueTypeEnum = EntityKeyValueType;
private propagateChange = (v: any) => { };

15
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts

@ -120,7 +120,12 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
takeUntilDestroyed()
).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => {
this.updatedModel(value);
})
});
this.relatedAggregationConfiguration.get('output').valueChanges.pipe(
takeUntilDestroyed(),
).subscribe(() => {
this.toggleScopeByOutputType();
});
}
validate(): ValidationErrors | null {
@ -158,4 +163,12 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
value.scheduledUpdateInterval = this.minAllowedScheduledUpdateIntervalInSecForCF;
this.propagateChange(value);
}
private toggleScopeByOutputType(): void {
if (this.relatedAggregationConfiguration.get('output').value.type === OutputType.Attribute) {
this.relatedAggregationConfiguration.get('useLatestTs').disable({emitEvent: false});
} else {
this.relatedAggregationConfiguration.get('useLatestTs').enable({emitEvent: false});
}
}
}

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -200,7 +200,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
}
private toggleScopeByOutputType(): void {
if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute) {
if (this.isScript || this.simpleConfiguration.get('output').value.type === OutputType.Attribute || this.disabled) {
this.simpleConfiguration.get('useLatestTs').disable({emitEvent: false});
} else {
this.simpleConfiguration.get('useLatestTs').enable({emitEvent: false});

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

@ -126,7 +126,6 @@
subscriptSizing="dynamic"
[predefinedValuesButton]="true"
[predefinedValues]="predefinedValues"
[disabled]="!segmentedButtonWidgetConfigForm.get('leftAppearance').get('showLabel').value"
placeholderText="{{ 'widget-config.set' | translate }}"
formControlName="label"
></tb-string-pattern-autocomplete>
@ -164,7 +163,6 @@
subscriptSizing="dynamic"
[predefinedValuesButton]="true"
[predefinedValues]="predefinedValues"
[disabled]="!segmentedButtonWidgetConfigForm.get('rightAppearance').get('showLabel').value"
placeholderText="{{ 'widget-config.set' | translate }}"
formControlName="label"
></tb-string-pattern-autocomplete>

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

@ -142,4 +142,52 @@ export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigCompon
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['appearance.leftAppearance.showLabel', 'appearance.leftAppearance.showIcon', 'appearance.rightAppearance.showLabel', 'appearance.rightAppearance.showIcon',];
}
protected updateValidators(emitEvent: boolean) {
const showLeftLabel: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.showLabel').value;
const showRightLabel: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.showLabel').value;
const showLeftIcon: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.showIcon').value;
const showRightIcon: boolean = this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.showIcon').value;
if (showLeftLabel) {
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.label').enable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.labelFont').enable({emitEvent});
} else {
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.label').disable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.labelFont').disable({emitEvent});
}
if (showRightLabel) {
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.label').enable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.labelFont').enable({emitEvent});
} else {
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.label').disable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.labelFont').disable({emitEvent});
}
if (showLeftIcon) {
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.icon').enable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSize').enable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSizeUnit').enable({emitEvent});
} else {
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.icon').disable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSize').disable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.leftAppearance.iconSizeUnit').disable({emitEvent});
}
if (showRightIcon) {
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.icon').enable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSize').enable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSizeUnit').enable({emitEvent});
} else {
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.icon').disable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSize').disable({emitEvent});
this.segmentedButtonWidgetConfigForm.get('appearance.rightAppearance.iconSizeUnit').disable({emitEvent});
}
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html

@ -117,7 +117,6 @@
brackets="curly"
subscriptSizing="dynamic"
predefinedValuesButton="true"
[disabled]="!segmentedButtonWidgetSettingsForm.get('leftAppearance').get('showLabel').value"
[predefinedValues]="predefinedValues"
placeholderText="{{ 'widget-config.set' | translate }}"
formControlName="label"
@ -155,7 +154,6 @@
brackets="curly"
subscriptSizing="dynamic"
predefinedValuesButton="true"
[disabled]="!segmentedButtonWidgetSettingsForm.get('rightAppearance').get('showLabel').value"
[predefinedValues]="predefinedValues"
placeholderText="{{ 'widget-config.set' | translate }}"
formControlName="label"

49
ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts

@ -38,6 +38,7 @@ import {
WidgetButtonToggleState,
widgetButtonToggleStatesTranslations
} from '@home/components/widget/lib/button/segmented-button-widget.models';
import { ValueCardLayout } from '@home/components/widget/lib/cards/value-card-widget.models';
@Component({
selector: 'tb-segmented-button-widget-settings',
@ -139,4 +140,52 @@ export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsCompon
})
});
}
protected validatorTriggers(): string[] {
return ['appearance.leftAppearance.showLabel', 'appearance.leftAppearance.showIcon', 'appearance.rightAppearance.showLabel', 'appearance.rightAppearance.showIcon',];
}
protected updateValidators(emitEvent: boolean) {
const showLeftLabel: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.showLabel').value;
const showRightLabel: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.showLabel').value;
const showLeftIcon: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.showIcon').value;
const showRightIcon: boolean = this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.showIcon').value;
if (showLeftLabel) {
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.label').enable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.labelFont').enable({emitEvent});
} else {
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.label').disable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.labelFont').disable({emitEvent});
}
if (showRightLabel) {
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.label').enable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.labelFont').enable({emitEvent});
} else {
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.label').disable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.labelFont').disable({emitEvent});
}
if (showLeftIcon) {
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.icon').enable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSize').enable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSizeUnit').enable({emitEvent});
} else {
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.icon').disable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSize').disable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.leftAppearance.iconSizeUnit').disable({emitEvent});
}
if (showRightIcon) {
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.icon').enable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSize').enable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSizeUnit').enable({emitEvent});
} else {
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.icon').disable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSize').disable({emitEvent});
this.segmentedButtonWidgetSettingsForm.get('appearance.rightAppearance.iconSizeUnit').disable({emitEvent});
}
}
}

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.html

@ -18,15 +18,10 @@
<section class="tb-widget-settings flex flex-col" [formGroup]="sendRpcWidgetSettingsForm">
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.rpc.common-settings</legend>
<tb-string-pattern-autocomplete class="mat-block flex-1"
brackets="curly"
predefinedValuesButton="true"
label="{{'widgets.rpc.common-settings' | translate }}"
[disabled]="!sendRpcWidgetSettingsForm.get('showLabel').value"
[predefinedValues]="predefinedValues"
placeholderText="{{ 'widgets.rpc.widget-title' | translate }}"
formControlName="title"
></tb-string-pattern-autocomplete>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.rpc.widget-title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field class="mat-block flex-1">
<mat-label translate>widgets.rpc.button-label</mat-label>
<input matInput formControlName="buttonText">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/send-rpc-widget-settings.component.ts

@ -15,7 +15,7 @@
///
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent, widgetTitleAutocompleteValues } from '@shared/models/widget.models';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -32,8 +32,6 @@ export class SendRpcWidgetSettingsComponent extends WidgetSettingsComponent {
contentTypes = ContentType;
predefinedValues = widgetTitleAutocompleteValues;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);

2
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -208,7 +208,7 @@
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-center justify-start" (mousedown)="toggleExtensionPanel($event, providersForm.length, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
<mat-slide-toggle class="mat-slide flex items-center justify-start" (mousedown)="toggleExtensionPanel($event, providersForm.length+1, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
formControlName="verificationCodeCheckRateLimitEnable">
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}
</mat-slide-toggle>

2
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts

@ -69,6 +69,8 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
this.build2faSettingsForm();
this.twoFaService.getTwoFaSettings().subscribe((setting) => {
this.setAuthConfigFormValue(setting);
this.twoFaFormGroup.markAsUntouched();
this.twoFaFormGroup.markAsPristine();
});
}

2
ui-ngx/src/app/modules/home/pages/security/security.component.html

@ -54,7 +54,7 @@
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" name="current-password" formControlName="currentPassword" autocomplete="current-password"/>
<tb-toggle-password [class.!hidden]="!changePassword.get('currentPassword').dirty && !changePassword.get('currentPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('currentPassword').hasError('passwordsNotMatch')">
<mat-error *ngIf="changePassword.get('currentPassword').hasError('differencePassword')">
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
</mat-error>
</mat-form-field>

34
ui-ngx/src/app/modules/home/pages/security/security.component.ts

@ -333,22 +333,24 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
onChangePassword(form: FormGroupDirective): void {
if (this.changePassword.valid) {
this.authService.changePassword(this.changePassword.get('currentPassword').value,
this.changePassword.get('newPassword').value, {ignoreErrors: true}).subscribe(() => {
this.discardChanges(form);
},
(error) => {
if (error.status === 400 && error.error.message === 'Current password doesn\'t match!') {
this.changePassword.get('currentPassword').setErrors({differencePassword: true});
} else if (error.status === 400 && error.error.message.startsWith('Password must')) {
this.loadPasswordPolicy();
} else if (error.status === 400 && error.error.message.startsWith('Password was already used')) {
this.changePassword.get('newPassword').setErrors({alreadyUsed: error.error.message});
} else {
this.store.dispatch(new ActionNotificationShow({
message: error.error.message,
type: 'error',
target: 'changePassword'
}));
this.changePassword.get('newPassword').value, {ignoreErrors: true}).subscribe({
next: () => {
this.discardChanges(form);
},
error: (error) => {
if (error.status === 400 && error.error.message === 'Current password doesn\'t match!') {
this.changePassword.get('currentPassword').setErrors({ differencePassword: true });
} else if (error.status === 400 && error.error.message.startsWith('Password must')) {
this.loadPasswordPolicy();
} else if (error.status === 400 && error.error.message.startsWith('Password was already used')) {
this.changePassword.get('newPassword').setErrors({ alreadyUsed: error.error.message });
} else {
this.store.dispatch(new ActionNotificationShow({
message: error.error.message,
type: 'error',
target: 'changePassword'
}));
}
}
});
} else {

2
ui-ngx/src/app/shared/components/country-autocomplete.component.ts

@ -130,7 +130,7 @@ export class CountryAutocompleteComponent implements OnInit, OnChanges, ControlV
if (changes.required) {
const requiredChanges = changes.required;
if (requiredChanges.currentValue !== requiredChanges.previousValue) {
if (requiredChanges.currentValue) {
if (this.required) {
this.countryFormGroup.get('country').addValidators(Validators.required);
} else {
this.countryFormGroup.get('country').removeValidators(Validators.required);

69
ui-ngx/src/app/shared/components/time-unit-input.component.ts

@ -134,39 +134,13 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
if (isDefinedAndNotNull(this.maxTime)) {
this.updatedAllowTimeUnitInterval(this.maxTime);
}
if (this.required || this.maxTime || isDefinedAndNotNull(this.minTime) || this.stepMultipleOf) {
const timeControl = this.timeInputForm.get('time');
const validators = [Validators.pattern(/^\d*$/)];
if (this.required) {
validators.push(Validators.required);
}
if (this.maxTime) {
validators.push((control: AbstractControl) =>
Validators.max(Math.floor(this.maxTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control)
);
}
if (isDefinedAndNotNull(this.minTime)) {
this.minValueValidator = this.minValue;
validators.push((control: AbstractControl) =>
Validators.min(this.minValueValidator)(control)
);
}
if (isDefinedAndNotNull(this.stepMultipleOf) && this.stepMultipleOf > 0) {
validators.push(this.createStepMultipleOfValidator());
}
timeControl.setValidators(validators);
timeControl.updateValueAndValidity({ emitEvent: false });
}
this.refreshTimeValidators();
this.timeInputForm.get('timeUnit').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
if (isDefinedAndNotNull(this.minTime)) {
this.minValueValidator = this.minValue;
}
this.timeInputForm.get('time').updateValueAndValidity({onlySelf: true});
this.refreshTimeValidators();
this.timeInputForm.get('time').markAsTouched({onlySelf: true});
});
@ -201,11 +175,10 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
if (isDefinedAndNotNull(this.maxTime)) {
this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
this.updatedAllowTimeUnitInterval(this.maxTime);
this.timeInputForm.get('time').updateValueAndValidity({emitEvent: false});
}
}
if (propName === 'minTime') {
this.minValueValidator = isDefinedAndNotNull(this.minTime) ? this.minValue : 0;
if (['minTime', 'maxTime', 'required', 'stepMultipleOf'].includes(propName)) {
this.refreshTimeValidators();
}
}
}
@ -312,4 +285,38 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
}
}
private refreshTimeValidators() {
const timeControl = this.timeInputForm.get('time');
if (!timeControl) return;
const currentUnit = this.timeInputForm.get('timeUnit')?.value as TimeUnit;
if (!currentUnit) return;
const secondsInUnit = this.timeIntervalsInSec.get(currentUnit) ?? 1;
const validators: ValidatorFn[] = [Validators.pattern(/^\d*$/)];
if (this.required) {
validators.push(Validators.required);
}
if (isDefinedAndNotNull(this.minTime)) {
this.minValueValidator = Math.ceil(this.minTime / secondsInUnit);
validators.push(Validators.min(this.minValueValidator));
} else {
this.minValueValidator = 0;
validators.push(Validators.min(this.minValueValidator));
}
if (isDefinedAndNotNull(this.maxTime)) {
const maxInCurrentUnit = Math.floor(this.maxTime / secondsInUnit);
validators.push(Validators.max(maxInCurrentUnit));
}
if (isDefinedAndNotNull(this.stepMultipleOf) && this.stepMultipleOf > 0) {
validators.push(this.createStepMultipleOfValidator());
}
timeControl.setValidators(validators);
timeControl.updateValueAndValidity({ onlySelf: true, emitEvent: false });
}
}

2
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -91,7 +91,7 @@ export enum CalculatedFieldType {
ALARM = 'ALARM',
}
interface CalculatedFieldTypeTranslate {
export interface CalculatedFieldTypeTranslate {
name: string;
hint?: string;
}

2
ui-ngx/src/app/shared/models/constants.ts

@ -217,7 +217,7 @@ export const HelpLinks = {
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`,
aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`,
apiKeys: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/api-keys`,
apiKeys: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/api-keys/`,
timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`,
trendzSettings: `${helpBaseUrl}/docs/trendz/`,
alarmRules: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/alarm-rules/`,

712
ui-ngx/src/assets/locale/locale.constant-da_DK.json

File diff suppressed because it is too large

731
ui-ngx/src/assets/locale/locale.constant-de_DE.json

File diff suppressed because it is too large

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

@ -10216,6 +10216,7 @@
"es_ES": "español (España)",
"fa_IR": "فارسی (ایران)",
"fr_FR": "français (France)",
"hi_IN": "हिन्दी (भारत)",
"it_IT": "italiano (Italia)",
"ja_JP": "日本語 (日本)",
"ka_GE": "ქართული (საქართველო)",

715
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

File diff suppressed because it is too large

9547
ui-ngx/src/assets/locale/locale.constant-hi_IN.json

File diff suppressed because it is too large

721
ui-ngx/src/assets/locale/locale.constant-it_IT.json

File diff suppressed because it is too large

11699
ui-ngx/src/assets/locale/locale.constant-ja_JP.json

File diff suppressed because it is too large

719
ui-ngx/src/assets/locale/locale.constant-nl_NL.json

File diff suppressed because it is too large

886
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

File diff suppressed because it is too large
Loading…
Cancel
Save