Browse Source

merge with master

pull/11607/head
YevhenBondarenko 2 years ago
parent
commit
705dd90886
  1. 158
      application/src/main/data/json/system/scada_symbols/leak-sensor.svg
  2. 683
      application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg
  3. 683
      application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg
  4. 16
      application/src/main/data/json/system/scada_symbols/stand-filter.svg
  5. 276
      application/src/main/data/json/system/scada_symbols/waterstop.svg
  6. 4
      application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
  7. 2
      application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java
  8. 63
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  9. 1
      application/src/main/resources/thingsboard.yml
  10. 4
      application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java
  11. 4
      application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java
  12. 108
      application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java
  13. 148
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttClientSideRpcIntegrationTest.java
  14. 32
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java
  15. 2
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java
  16. 128
      common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java
  17. 37
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  18. 94
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  19. 4
      common/transport/coap/src/main/proto/efento/proto_config.proto
  20. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java
  21. 50
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  22. 25
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/GatewaySessionLimits.java
  23. 29
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/SessionLimits.java
  24. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  25. 5
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
  26. 7
      dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java
  27. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  28. 2
      dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java
  30. 4
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java
  31. 12
      dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java
  32. 12
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java
  33. 2
      msa/js-executor/config/default.yml
  34. 26
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java
  35. 2
      pom.xml
  36. 9
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  37. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java
  38. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java
  39. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  40. 46
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  41. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  42. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  43. 65
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java
  44. 36
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java
  45. 5
      ui-ngx/angular.json
  46. 9
      ui-ngx/src/app/core/api/alias-controller.ts
  47. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  48. 2
      ui-ngx/src/app/core/auth/auth.service.ts
  49. 20
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  50. 11
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  51. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  52. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  53. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
  54. 85
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  55. 426
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts
  56. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts
  57. 23
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  58. 36
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  59. 45
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  60. 1
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  61. 6
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  62. 176
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  63. 6
      ui-ngx/src/app/modules/home/models/services.map.ts
  64. 4
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  65. 5
      ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts
  66. 3
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html
  67. 16
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts
  68. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html
  69. 3
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  70. 7
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts
  71. 25
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html
  72. 10
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss
  73. 75
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts
  74. 254
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  75. 6
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html
  76. 8
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts
  77. 35
      ui-ngx/src/app/shared/components/js-func.component.ts
  78. 38
      ui-ngx/src/app/shared/components/svg-xml.component.html
  79. 85
      ui-ngx/src/app/shared/components/svg-xml.component.scss
  80. 211
      ui-ngx/src/app/shared/components/svg-xml.component.ts
  81. 31
      ui-ngx/src/app/shared/components/toggle-header.component.html
  82. 38
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  83. 12
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  84. 4
      ui-ngx/src/app/shared/components/toggle-select.component.html
  85. 22
      ui-ngx/src/app/shared/components/toggle-select.component.scss
  86. 14
      ui-ngx/src/app/shared/components/toggle-select.component.ts
  87. 17
      ui-ngx/src/app/shared/models/ace/ace.models.ts
  88. 3
      ui-ngx/src/app/shared/models/dashboard.models.ts
  89. 2
      ui-ngx/src/app/shared/models/widget.models.ts
  90. 3
      ui-ngx/src/app/shared/shared.module.ts
  91. 19
      ui-ngx/src/assets/locale/locale.constant-en_US.json

158
application/src/main/data/json/system/scada_symbols/leak-sensor.svg

@ -0,0 +1,158 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" fill="none" version="1.1" viewBox="0 0 100 100">
<tb:metadata xmlns=""><![CDATA[{
"title": "Leak sensor",
"description": "Leak sensor",
"searchTags": [
"leak"
],
"widgetSizeX": 1,
"widgetSizeY": 1,
"tags": [
{
"tag": "background",
"stateRenderFunction": "var fill = ctx.properties.backgroundColor;\nelement.attr({fill: fill});",
"actions": null
},
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "ctx.api.callAction(event, 'click');"
}
}
},
{
"tag": "icon",
"stateRenderFunction": "var leak = ctx.values.leak;\nif (leak) {\n element.attr({stroke: ctx.properties.leakColor});\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n} else {\n element.attr({stroke: ctx.properties.defaultColor});\n ctx.api.resetAnimation(element);\n}\n",
"actions": null
}
],
"behavior": [
{
"id": "leak",
"name": "{i18n:scada.symbol.leak}",
"hint": "{i18n:scada.symbol.water-leak-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": {
"action": "GET_TIME_SERIES",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": null,
"key": "state"
},
"getTimeSeries": {
"key": "leak"
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "click",
"name": "{i18n:scada.symbol.on-click}",
"hint": "{i18n:scada.symbol.on-click-hint}",
"group": null,
"type": "widgetAction",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": {
"type": "doNothing",
"targetDashboardStateId": null,
"openRightLayout": false,
"setEntityId": false,
"stateEntityParamName": null
}
}
],
"properties": [
{
"id": "backgroundColor",
"name": "{i18n:scada.symbol.background-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "defaultColor",
"name": "{i18n:scada.symbol.default-color}",
"type": "color",
"default": "#1C943E",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "leakColor",
"name": "{i18n:scada.symbol.leak-color}",
"type": "color",
"default": "#D12730",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata><rect x=".5" y=".5" width="99" height="99" rx="23.5" fill="#fff" tb:tag="background"/><rect x=".5" y=".5" width="99" height="99" rx="23.5" fill="url(#paint0_linear_2089_217648)"/><rect x=".5" y=".5" width="99" height="99" rx="23.5" fill="url(#paint1_linear_2089_217648)"/><rect x=".5" y=".5" width="99" height="99" rx="23.5" stroke="#fff"/><path d="m56.668 52.309c0 2.04-0.7902 3.9966-2.1967 5.4391-0.7985 0.819-1.7586 1.4347-2.8035 1.8135m10-6.7401c0-10.962-11.667-19.487-11.667-19.487s-11.667 8.5256-11.667 19.487c0 3.2301 1.2292 6.3283 3.4171 8.6125 2.1879 2.284 5.1556 3.5659 8.2497 3.5659 3.0942 0 6.0615-1.2826 8.2493-3.5666 2.188-2.2842 3.4172-5.3817 3.4172-8.6118z" stroke="#1C943E" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" tb:tag="icon"/><path d="m33.631 0s-33.631 0-33.631 16.75v82.09c0 0.6628 0.89543 1.1601 2 1.1601h96c1.1045 0 2-0.4973 2-1.1601v-82.09c0-16.75-33.035-16.75-33.035-16.75h-16.965zm33.535 20.3c-0.64434 0-1.1666 0.3134-1.1666 0.7v75.1c0 0.3866 0.52234 0.7 1.1666 0.7h7.3336c0.64434 0 1.1666-0.3134 1.1666-0.7v-75.1c0-0.3866-0.52233-0.7-1.1666-0.7z" fill="#000" fill-opacity="0" style="stroke-width:.059761" tb:tag="clickArea"/><defs>
<linearGradient id="paint0_linear_2089_217648" x1="100" x2="1.5002" y1="50.119" y2="49.881" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset="0"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".05"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".15"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".51114"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".85"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".95"/>
<stop stop-color="#020202" stop-opacity=".2" offset=".99341"/>
</linearGradient>
<linearGradient id="paint1_linear_2089_217648" x1="50" x2="50" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset=".01"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".05"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".15343"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".5"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".85"/>
<stop stop-opacity=".12" offset=".95"/>
<stop stop-opacity=".2" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

683
application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 55 KiB

683
application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 55 KiB

16
application/src/main/data/json/system/scada_symbols/stand-filter.svg

@ -7,7 +7,7 @@
],
"widgetSizeX": 3,
"widgetSizeY": 5,
"stateRenderFunction": "",
"stateRenderFunction": "var running = ctx.values.running;\nif (running) {\n ctx.api.enable(svg.children());\n} else {\n ctx.api.disable(svg.children());\n}",
"tags": [
{
"tag": "background",
@ -25,7 +25,7 @@
},
{
"tag": "filterMode",
"stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var i = 0;\n if (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n }\n if (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n }\n if (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n }\n if (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n }\n if (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n }\n if (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n }\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n if (isFinite(filtrationMode)) {\n setFilterModeColorsByMap(filtrationMode, running);\n }\n}\n\nif (running) {\n ctx.api.enable(element.children());\n} else {\n ctx.api.disable(element.children());\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}",
"stateRenderFunction": "var defaultBorderColor = ctx.properties.defaultBorderColor;\nvar activeBorderColor = ctx.properties.activeBorderColor;\nvar defaultLabelColor = ctx.properties.defaultLabelColor;\nvar activeLabelColor = ctx.properties.activeLabelColor;\nvar boxBackground = ctx.properties.modeBoxBackground;\n\nvar running = ctx.values.running;\nvar filtrationMode = ctx.values.filtrationMode;\n\nvar filtrationMap = {};\nvar bottomShift = 104;\nvar rightShift = 226;\nvar middleShift = 121;\n\nvar filterModeSet = element.remember('filterModeSet');\n\nvar i = 0;\nif (ctx.properties.filtrationMode) {\n i++;\n filtrationMap[i] = 'filter';\n}\nif (ctx.properties.wasteMode) {\n i++;\n filtrationMap[i] = 'waste';\n}\nif (ctx.properties.backwashMode) {\n i++;\n filtrationMap[i] = 'backwash';\n}\nif (ctx.properties.recirculateMode) {\n i++;\n filtrationMap[i] = 'recirculate';\n}\nif (ctx.properties.rinseMode) {\n i++;\n filtrationMap[i] = 'rinse';\n}\nif (ctx.properties.closedMode) {\n i++;\n filtrationMap[i] = 'closed';\n}\n\nif (!filterModeSet) {\n element.remember('filterModeSet', true);\n var clone = element.children()[0];\n setFilterModeColors(clone);\n element.clear();\n \n var filterMode = Object.values(filtrationMap);\n var lastToMiddle = filterMode.length % 2;\n \n filterMode.forEach((mode, index, arr) => {\n var template = clone.clone();\n var x = (index % 2) * rightShift;\n var y = Math.floor((index % filterMode.length) / 2) * bottomShift;\n if (index === filterMode.length-1 && lastToMiddle) {\n x = middleShift;\n }\n template.attr({'class': mode}).css('cursor', 'pointer').translate(x, y);\n ctx.api.text(template.findOne('text'), capitalizeFirstLetter(mode));\n template.click((event) => click(event, getFilterModeKey(mode)));\n element.add(template);\n })\n}\n\nif (isFinite(filtrationMode)) {\n if (element.findOne('.active')) {\n setFilterModeColors(element.findOne('.active'));\n }\n setFilterModeColorsByMap(filtrationMode, running);\n}\n\nfunction click(event, index) {\n var filtrationMode = ctx.values.filtrationMode;\n if (ctx.values.running && isFinite(filtrationMode)) {\n ctx.api.disable(element.children());\n var newValue = +index;\n if (newValue === filtrationMode) {\n newValue = 0;\n } else {\n setFilterModeColorsByMap(filtrationMode);\n }\n ctx.api.setValue('filtrationMode', newValue);\n ctx.api.callAction(event, 'filtrationModeUpdateState', newValue, {\n next: () => {\n setFilterModeColorsByMap(newValue ? newValue: filtrationMode, newValue);\n ctx.api.enable(element.children());\n },\n error: () => {\n setFilterModeColorsByMap(newValue);\n ctx.api.setValue('filtrationMode', filtrationMode);\n ctx.api.enable(element.children());\n }\n });\n }\n}\n\nfunction getFilterModeKey(value) {\n return Object.keys(filtrationMap).find(key => filtrationMap[key] === value);\n}\n\nfunction setFilterModeColorsByMap(mode, active = false) {\n var filterBox = element.findOne('g.'+filtrationMap[mode])\n if (filterBox) {\n return setFilterModeColors(filterBox, active);\n }\n}\n\nfunction setFilterModeColors(filterBox, active = false) {\n if (filterBox) {\n if (active) {\n filterBox.addClass('active');\n } else {\n filterBox.removeClass('active');\n }\n var borderColor = active ? activeBorderColor : defaultBorderColor;\n var labelColor = active ? activeLabelColor : defaultLabelColor;\n filterBox.children()[0].fill(boxBackground);\n filterBox.children()[1].stroke(borderColor);\n filterBox.children()[2].fill(borderColor);\n ctx.api.font(filterBox.findOne('text'), null, labelColor);\n }\n}\n\nfunction capitalizeFirstLetter(string) {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}",
"actions": null
}
],
@ -159,7 +159,7 @@
"id": "filtrationMode",
"name": "{i18n:scada.symbol.filter-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -175,7 +175,7 @@
"id": "wasteMode",
"name": "{i18n:scada.symbol.waste-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -191,7 +191,7 @@
"id": "backwashMode",
"name": "{i18n:scada.symbol.backwash-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -207,7 +207,7 @@
"id": "recirculateMode",
"name": "{i18n:scada.symbol.recirculate-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -223,7 +223,7 @@
"id": "rinseMode",
"name": "{i18n:scada.symbol.rinse-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,
@ -239,7 +239,7 @@
"id": "closedMode",
"name": "{i18n:scada.symbol.closed-mode}",
"type": "switch",
"default": false,
"default": true,
"required": null,
"subLabel": null,
"divider": null,

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

276
application/src/main/data/json/system/scada_symbols/waterstop.svg

@ -0,0 +1,276 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200">
<tb:metadata xmlns=""><![CDATA[{
"title": "Water stop",
"description": "Remotely controlled water shutoff valve with configurable connectors and various states.",
"searchTags": [
"water stop"
],
"widgetSizeX": 1,
"widgetSizeY": 1,
"stateRenderFunction": "",
"tags": [
{
"tag": "clickArea",
"stateRenderFunction": null,
"actions": {
"click": {
"actionFunction": "var opened = ctx.values.opened;\nvar action = opened ? 'close' : 'open';\n\nctx.api.callAction(event, action, undefined, {\n next: () => {\n ctx.api.setValue('opened', !opened);\n }\n});"
}
}
},
{
"tag": "pipe-color",
"stateRenderFunction": "var color = ctx.properties.pipeColor;\nelement.attr({fill: color});",
"actions": null
},
{
"tag": "walve",
"stateRenderFunction": "var opened = ctx.values.opened;\nif (opened) {\n element.attr({fill: ctx.properties.openedColor});\n ctx.api.resetAnimation(element);\n} else {\n element.attr({fill: ctx.properties.closedColor});\n if (ctx.values.closeAnimation) {\n ctx.api.animate(element, 500).attr({opacity: 0.15}).loop(0, true);\n } else {\n ctx.api.resetAnimation(element);\n }\n}\n",
"actions": null
}
],
"behavior": [
{
"id": "opened",
"name": "{i18n:scada.symbol.opened}",
"hint": "{i18n:scada.symbol.opened-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": "{i18n:scada.symbol.opened}",
"defaultGetValueSettings": {
"action": "GET_ATTRIBUTE",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"scope": "SHARED_SCOPE",
"key": "open"
},
"getTimeSeries": {
"key": "state"
},
"dataToValue": {
"type": "NONE",
"dataToValueFunction": "/* Should return boolean value */\nreturn data;",
"compareToValue": true
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
},
{
"id": "open",
"name": "{i18n:scada.symbol.open}",
"hint": "{i18n:scada.symbol.open-hint}",
"group": null,
"type": "action",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": {
"action": "SET_ATTRIBUTE",
"executeRpc": {
"method": "setState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"setAttribute": {
"scope": "SHARED_SCOPE",
"key": "open"
},
"putTimeSeries": {
"key": "state"
},
"valueToData": {
"type": "CONSTANT",
"constantValue": false,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},
"defaultWidgetActionSettings": null
},
{
"id": "close",
"name": "{i18n:scada.symbol.close}",
"hint": "{i18n:scada.symbol.close-hint}",
"group": null,
"type": "action",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": null,
"defaultGetValueSettings": null,
"defaultSetValueSettings": {
"action": "SET_ATTRIBUTE",
"executeRpc": {
"method": "setState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"setAttribute": {
"scope": "SHARED_SCOPE",
"key": "open"
},
"putTimeSeries": {
"key": "state"
},
"valueToData": {
"type": "CONSTANT",
"constantValue": true,
"valueToDataFunction": "/* Convert input boolean value to RPC parameters or attribute/time-series value */\nreturn value;"
}
},
"defaultWidgetActionSettings": null
},
{
"id": "closeAnimation",
"name": "{i18n:scada.symbol.close-state-animation}",
"hint": "{i18n:scada.symbol.close-state-animation-hint}",
"group": null,
"type": "value",
"valueType": "BOOLEAN",
"trueLabel": null,
"falseLabel": null,
"stateLabel": "{i18n:scada.symbol.animation}",
"defaultGetValueSettings": {
"action": "DO_NOTHING",
"defaultValue": false,
"executeRpc": {
"method": "getState",
"requestTimeout": 5000,
"requestPersistent": false,
"persistentPollingInterval": 1000
},
"getAttribute": {
"key": "state",
"scope": null
},
"getTimeSeries": {
"key": "state"
},
"dataToValue": {
"type": "NONE",
"compareToValue": true,
"dataToValueFunction": "/* Should return boolean value */\nreturn data;"
}
},
"defaultSetValueSettings": null,
"defaultWidgetActionSettings": null
}
],
"properties": [
{
"id": "openedColor",
"name": "{i18n:scada.symbol.opened-color}",
"type": "color",
"default": "#1C943E",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "closedColor",
"name": "{i18n:scada.symbol.closed-color}",
"type": "color",
"default": "#D12730",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
},
{
"id": "pipeColor",
"name": "{i18n:scada.symbol.pipe-color}",
"type": "color",
"default": "#FFFFFF",
"required": null,
"subLabel": null,
"divider": null,
"fieldSuffix": null,
"disableOnProperty": null,
"rowClass": "",
"fieldClass": "",
"min": null,
"max": null,
"step": null
}
]
}]]></tb:metadata><path d="M14 64H186V136H14V64Z" fill="#fff" tb:tag="pipe-color"/><path d="M14 64H186V136H14V64Z" fill="url(#paint0_linear_2089_217683)"/><path d="m15.5 65.5h169v69h-169v-69z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><rect x="187.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><rect x="1.5" y="51.5" width="11" height="97" rx="5.5" fill="#D9D9D9" stroke="#727171" stroke-width="3"/><path d="m70.5 46c0 1.933 1.567 3.5 3.5 3.5l52 1e-4c1.933 0 3.5-1.567 3.5-3.5v-37c0-4.6944-3.806-8.5-8.5-8.5h-42c-4.6944 0-8.5 3.8056-8.5 8.5v37z" fill="#1C943E" tb:tag="walve"/><path d="m70.5 46c0 1.933 1.567 3.5 3.5 3.5l52 1e-4c1.933 0 3.5-1.567 3.5-3.5v-37c0-4.6944-3.806-8.5-8.5-8.5h-42c-4.6944 0-8.5 3.8056-8.5 8.5v37z" fill="url(#paint1_linear_2089_217683)"/><path d="m70.5 46c0 1.933 1.567 3.5 3.5 3.5l52 1e-4c1.933 0 3.5-1.567 3.5-3.5v-37c0-4.6944-3.806-8.5-8.5-8.5h-42c-4.6944 0-8.5 3.8056-8.5 8.5v37z" stroke="#fff"/><path d="m110 136v-72h-21v72h21z" fill="#1C943E" tb:tag="walve"/><path d="m110 136v-72h-21v72h21z" fill="url(#paint2_linear_2089_217683)"/><path d="m110 136v-72h-21v72h21z" fill="url(#paint3_linear_2089_217683)"/><path d="m109.5 135.5v-71h-20v71h20z" stroke="#000" stroke-opacity=".12"/><path d="m126 64c2.209 0 4-1.7908 4-3.9999v-6c0-2.2092-1.791-4.0001-4-4.0001h-52c-2.2091 0-4 1.7909-4 4v6c0 2.2091 1.7909 4 4 4h52z" fill="#647484"/><path d="m126 64c2.209 0 4-1.7908 4-3.9999v-6c0-2.2092-1.791-4.0001-4-4.0001h-52c-2.2091 0-4 1.7909-4 4v6c0 2.2091 1.7909 4 4 4h52z" fill="url(#paint4_linear_2089_217683)"/><path d="m126 62.5c1.381 0 2.5-1.1193 2.5-2.5v-6c0-1.3807-1.119-2.5-2.5-2.5h-52c-1.3807 0-2.5 1.1193-2.5 2.5v6c0 1.3807 1.1193 2.5 2.5 2.5h52z" stroke="#000" stroke-opacity=".12" stroke-width="3"/><path d="m67.262 0s-67.262 0-67.262 33.5v164.18c0 1.3256 1.7909 2.3202 4 2.3202h192c2.209 0 4-0.9946 4-2.3202v-164.18c0-33.5-66.07-33.5-66.07-33.5h-33.93zm67.07 40.6c-1.2887 0-2.3332 0.6268-2.3332 1.4v150.2c0 0.7732 1.0447 1.4 2.3332 1.4h14.667c1.2887 0 2.3332-0.6268 2.3332-1.4v-150.2c0-0.7732-1.0447-1.4-2.3332-1.4z" fill="#000" fill-opacity="0" tb:tag="clickArea"/><defs>
<linearGradient id="paint0_linear_2089_217683" x1="58.72" x2="58.53" y1="64" y2="136" gradientUnits="userSpaceOnUse">
<stop stop-color="#727171" offset="0"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".26388"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".41759"/>
<stop stop-color="#fff" stop-opacity="0" offset=".49829"/>
<stop stop-color="#727171" stop-opacity=".1" offset=".58094"/>
<stop stop-color="#727171" stop-opacity=".35" offset=".71855"/>
<stop stop-color="#727171" offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2089_217683" x1="134.3" x2="65.705" y1="8.0405" y2="9.0306" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".14093"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".39937"/>
<stop stop-color="#fff" stop-opacity=".3" offset=".49829"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".59721"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".85946"/>
<stop stop-color="#020202" stop-opacity=".35" offset="1"/>
</linearGradient>
<linearGradient id="paint2_linear_2089_217683" x1="99.475" x2="100.06" y1="64" y2="134.92" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".35" offset="0"/>
<stop stop-color="#020202" stop-opacity=".15" offset=".15823"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".28193"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".50291"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".72128"/>
<stop stop-color="#020202" stop-opacity=".15" offset=".8423"/>
<stop stop-color="#020202" stop-opacity=".35" offset=".99341"/>
</linearGradient>
<linearGradient id="paint3_linear_2089_217683" x1="110" x2="89" y1="100" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset=".01"/>
<stop stop-color="#020202" stop-opacity=".12" offset=".05"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".15343"/>
<stop stop-color="#fff" stop-opacity=".2" offset=".5"/>
<stop stop-color="#fff" stop-opacity=".12" offset=".85"/>
<stop stop-opacity=".12" offset=".95"/>
<stop stop-opacity=".2" offset="1"/>
</linearGradient>
<linearGradient id="paint4_linear_2089_217683" x1="72.287" x2="127.11" y1="55.78" y2="57.142" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202" stop-opacity=".2" offset="0"/>
<stop stop-color="#020202" stop-opacity=".18" offset=".090959"/>
<stop stop-color="#020202" stop-opacity=".1" offset=".1"/>
<stop stop-color="#020202" stop-opacity=".08" offset=".20513"/>
<stop stop-color="#020202" stop-opacity=".05" offset=".21555"/>
<stop stop-color="#020202" stop-opacity=".02" offset=".36962"/>
<stop stop-color="#fff" stop-opacity=".01" offset=".37768"/>
<stop stop-color="#fff" stop-opacity=".01" offset=".62413"/>
<stop stop-color="#020202" stop-opacity=".02" offset=".6313"/>
<stop stop-color="#020202" stop-opacity=".05" offset=".77601"/>
<stop stop-color="#020202" stop-opacity=".08" offset=".7898"/>
<stop stop-color="#020202" stop-opacity=".1" offset=".9"/>
<stop stop-color="#020202" stop-opacity=".18" offset=".90846"/>
<stop stop-color="#020202" stop-opacity=".2" offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

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

@ -34,6 +34,9 @@
"left_flow_meter",
"horizontal_inline_flow_meter",
"vertical_inline_flow_meter",
"left_analog_water_level_meter",
"right_analog_water_level_meter",
"leak_sensor",
"centrifugal_pump",
"small_right_motor_pump",
"small_left_motor_pump",
@ -50,6 +53,7 @@
"vertical_wheel_valve",
"horizontal_ball_valve",
"vertical_ball_valve",
"water_stop",
"vertical_tank",
"stand_vertical_tank",
"cylindrical_tank",

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

@ -75,7 +75,7 @@ public class OAuth2Controller extends BaseController {
@ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " +
"to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " +
"the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)")
@PostMapping(value = "/noauth/oauth2/client")
@PostMapping(value = "/noauth/oauth2Clients")
public List<OAuth2ClientLoginInfo> getOAuth2Clients(HttpServletRequest request,
@Parameter(description = "Mobile application package name, to find OAuth2 clients " +
"where there is configured mobile application with such package name")

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

@ -157,7 +157,8 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
private final DbTypeInfoComponent dbTypeInfoComponent;
private final TbApiUsageReportClient apiUsageReportClient;
private final NotificationRuleProcessor notificationRuleProcessor;
@Autowired @Lazy
@Autowired
@Lazy
private TelemetrySubscriptionService tsSubService;
@Value("${state.defaultInactivityTimeoutInSec}")
@ -362,14 +363,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
if (proto.getAdded()) {
Futures.addCallback(fetchDeviceState(device), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable DeviceStateData state) {
public void onSuccess(DeviceStateData state) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, device.getId());
if (addDeviceUsingState(tpi, state)) {
save(deviceId, ACTIVITY_STATE, false);
Set<DeviceId> deviceIds = partitionedEntities.get(tpi);
boolean isMyPartition = deviceIds != null;
if (isMyPartition) {
deviceIds.add(state.getDeviceId());
initializeActivityState(deviceId, state);
callback.onSuccess();
} else {
log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}"
, tenantId, deviceId, tpi.getFullTopicName());
log.debug("[{}][{}] Device belongs to external partition. Probably rebalancing is in progress. Topic: {}", tenantId, deviceId, tpi.getFullTopicName());
callback.onFailure(new RuntimeException("Device belongs to external partition " + tpi.getFullTopicName() + "!"));
}
}
@ -400,6 +403,21 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
}
private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
cleanupEntity(deviceId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
Set<DeviceId> deviceIdSet = partitionedEntities.get(tpi);
if (deviceIdSet != null) {
deviceIdSet.remove(deviceId);
}
}
private void initializeActivityState(DeviceId deviceId, DeviceStateData fetchedState) {
DeviceStateData cachedState = deviceStates.putIfAbsent(fetchedState.getDeviceId(), fetchedState);
boolean activityState = Objects.requireNonNullElse(cachedState, fetchedState).getState().isActive();
save(deviceId, ACTIVITY_STATE, activityState);
}
@Override
protected Map<TopicPartitionInfo, List<ListenableFuture<?>>> onAddedPartitions(Set<TopicPartitionInfo> addedPartitions) {
var result = new HashMap<TopicPartitionInfo, List<ListenableFuture<?>>>();
@ -436,10 +454,16 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
if (devicePackFutureHolder.future == null || !devicePackFutureHolder.future.isCancelled()) {
for (var state : states) {
if (!addDeviceUsingState(entry.getKey(), state)) {
return;
TopicPartitionInfo tpi = entry.getKey();
Set<DeviceId> deviceIds = partitionedEntities.get(tpi);
boolean isMyPartition = deviceIds != null;
if (isMyPartition) {
deviceIds.add(state.getDeviceId());
deviceStates.putIfAbsent(state.getDeviceId(), state);
checkAndUpdateState(state.getDeviceId(), state);
} else {
log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName());
}
checkAndUpdateState(state.getDeviceId(), state);
}
log.info("[{}] Initialized {} out of {} device states", entry.getKey().getPartition().orElse(0), counter.addAndGet(states.size()), entry.getValue().size());
}
@ -475,18 +499,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
}
}
private boolean addDeviceUsingState(TopicPartitionInfo tpi, DeviceStateData state) {
Set<DeviceId> deviceIds = partitionedEntities.get(tpi);
if (deviceIds != null) {
deviceIds.add(state.getDeviceId());
deviceStates.putIfAbsent(state.getDeviceId(), state);
return true;
} else {
log.debug("[{}] Device belongs to external partition {}", state.getDeviceId(), tpi.getFullTopicName());
return false;
}
}
void checkStates() {
try {
final long ts = getCurrentTimeMillis();
@ -619,15 +631,6 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
return cleanup;
}
private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
cleanupEntity(deviceId);
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId);
Set<DeviceId> deviceIdSet = partitionedEntities.get(tpi);
if (deviceIdSet != null) {
deviceIdSet.remove(deviceId);
}
}
@Override
protected void cleanupEntityOnPartitionRemoval(DeviceId deviceId) {
cleanupEntity(deviceId);

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

@ -1660,6 +1660,7 @@ queue:
connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}"
# RabbitMQ has a timeout for connection handshake. When clients run in heavily constrained environments, it may be necessary to increase the timeout
handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}"
max_poll_messages: "${TB_QUEUE_RABBIT_MQ_MAX_POLL_MESSAGES:1}"
queue-properties:
# RabbitMQ properties for Rule Engine queues
rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"

4
application/src/test/java/org/thingsboard/server/controller/DomainControllerTest.java

@ -107,8 +107,8 @@ public class DomainControllerTest extends AbstractControllerTest {
doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId()));
DomainInfo retrievedDomainInfo = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId());
assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client),
new OAuth2ClientInfo(savedOAuth2Client2))));
assertThat(retrievedDomainInfo).isEqualTo(new DomainInfo(savedDomain, List.of(new OAuth2ClientInfo(savedOAuth2Client2),
new OAuth2ClientInfo(savedOAuth2Client))));
doPut("/api/domain/" + savedDomain.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
DomainInfo retrievedDomainInfo2 = doGet("/api/domain/info/{id}", DomainInfo.class, savedDomain.getId().getId());

4
application/src/test/java/org/thingsboard/server/controller/MobileAppControllerTest.java

@ -109,8 +109,8 @@ public class MobileAppControllerTest extends AbstractControllerTest {
doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client.getId().getId(), savedOAuth2Client2.getId().getId()));
MobileAppInfo retrievedMobileAppInfo = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client),
new OAuth2ClientInfo(savedOAuth2Client2))));
assertThat(retrievedMobileAppInfo).isEqualTo(new MobileAppInfo(savedMobileApp, List.of(new OAuth2ClientInfo(savedOAuth2Client2),
new OAuth2ClientInfo(savedOAuth2Client))));
doPut("/api/mobileApp/" + savedMobileApp.getId() + "/oauth2Clients", List.of(savedOAuth2Client2.getId().getId()));
MobileAppInfo retrievedMobileAppInfo2 = doGet("/api/mobileApp/info/{id}", MobileAppInfo.class, savedMobileApp.getId().getId());

108
application/src/test/java/org/thingsboard/server/service/state/DefaultDeviceStateServiceTest.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.state;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -28,8 +29,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo;
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.notification.rule.trigger.DeviceActivityTrigger;
@ -41,11 +44,13 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.sql.query.EntityQueryRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@ -66,6 +71,7 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
@ -1070,4 +1076,106 @@ public class DefaultDeviceStateServiceTest {
then(service).should().fetchDeviceStateDataUsingSeparateRequests(deviceId);
}
@Test
public void givenDeviceAdded_whenOnQueueMsg_thenShouldCacheAndSaveActivityToFalse() throws InterruptedException {
// GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
// WHEN
service.onQueueMsg(proto, TbCallback.EMPTY);
// THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(false);
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(false), any());
});
}
@Test
public void givenDeviceActivityEventHappenedAfterAdded_whenOnDeviceActivity_thenShouldCacheAndSaveActivityToTrue() throws InterruptedException {
// GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder()
.active(false)
.inactivityTimeout(service.getDefaultInactivityTimeoutInSec())
.build();
DeviceStateData stateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.deviceCreationTime(currentTime - 10000)
.state(deviceState)
.metaData(TbMsgMetaData.EMPTY)
.build();
service.deviceStates.put(deviceId, stateData);
// WHEN
service.onDeviceActivity(tenantId, deviceId, currentTime);
// THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true);
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(LAST_ACTIVITY_TIME), eq(currentTime), any());
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any());
});
}
@Test
public void givenDeviceActivityEventHappenedBeforeAdded_whenOnQueueMsg_thenShouldSaveActivityStateUsingValueFromCache() throws InterruptedException {
// GIVEN
final long defaultTimeout = 1000;
initStateService(defaultTimeout);
given(deviceService.findDeviceById(any(TenantId.class), any(DeviceId.class))).willReturn(new Device(deviceId));
given(attributesService.find(any(TenantId.class), any(EntityId.class), any(AttributeScope.class), anyList())).willReturn(Futures.immediateFuture(Collections.emptyList()));
long currentTime = System.currentTimeMillis();
DeviceState deviceState = DeviceState.builder()
.active(true)
.lastConnectTime(currentTime - 8000)
.lastActivityTime(currentTime - 4000)
.lastDisconnectTime(0)
.lastInactivityAlarmTime(0)
.inactivityTimeout(3000)
.build();
DeviceStateData stateData = DeviceStateData.builder()
.tenantId(tenantId)
.deviceId(deviceId)
.deviceCreationTime(currentTime - 10000)
.state(deviceState)
.build();
service.deviceStates.put(deviceId, stateData);
// WHEN
TransportProtos.DeviceStateServiceMsgProto proto = TransportProtos.DeviceStateServiceMsgProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.setAdded(true)
.setUpdated(false)
.setDeleted(false)
.build();
service.onQueueMsg(proto, TbCallback.EMPTY);
// THEN
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(service.deviceStates.get(deviceId).getState().isActive()).isEqualTo(true);
then(telemetrySubscriptionService).should().saveAttrAndNotify(eq(TenantId.SYS_TENANT_ID), eq(deviceId), eq(AttributeScope.SERVER_SCOPE), eq(ACTIVITY_STATE), eq(true), any());
});
}
}

148
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/MqttClientSideRpcIntegrationTest.java

@ -0,0 +1,148 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.mqttv3.rpc;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties;
import org.thingsboard.server.transport.mqtt.limits.GatewaySessionLimits;
import org.thingsboard.server.transport.mqtt.limits.SessionLimits;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient;
import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestSubscribeOnTopicCallback;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_REQUESTS_TOPIC;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_RPC_RESPONSE_TOPIC;
@DaoSqlTest
public class MqttClientSideRpcIntegrationTest extends AbstractMqttIntegrationTest {
@Value("${transport.mqtt.netty.max_payload_size}")
private Integer maxPayloadSize;
@Value("${transport.mqtt.msg_queue_size_per_device_limit}")
private int maxInflightMessages;
@Test
public void getSessionLimitsRpcForDeviceTest() throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
DefaultTenantProfileConfiguration profileConfiguration = tenantProfile.getDefaultProfileConfiguration();
profileConfiguration.setTransportDeviceMsgRateLimit("20:600");
profileConfiguration.setTransportDeviceTelemetryMsgRateLimit("10:600");
profileConfiguration.setTransportDeviceTelemetryDataPointsRateLimit("40:600");
doPost("/api/tenantProfile", tenantProfile);
var expectedLimits = new SessionLimits();
var deviceLimits = new SessionLimits.SessionRateLimits(profileConfiguration.getTransportDeviceMsgRateLimit(),
profileConfiguration.getTransportDeviceTelemetryMsgRateLimit(),
profileConfiguration.getTransportDeviceTelemetryDataPointsRateLimit());
expectedLimits.setRateLimits(deviceLimits);
expectedLimits.setMaxPayloadSize(maxPayloadSize);
expectedLimits.setMaxInflightMessages(maxInflightMessages);
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.deviceName("Test Get Service Configuration")
.transportPayloadType(TransportPayloadType.JSON)
.build();
processBeforeTest(configProperties);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken);
MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(DEVICE_RPC_RESPONSE_TOPIC + "1");
client.setCallback(callback);
client.subscribeAndWait(DEVICE_RPC_RESPONSE_SUB_TOPIC, MqttQoS.AT_MOST_ONCE);
client.publishAndWait(DEVICE_RPC_REQUESTS_TOPIC + "1", "{\"method\":\"getSessionLimits\",\"params\":{}}".getBytes());
assertThat(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.as("await callback").isTrue();
var payload = callback.getPayloadBytes();
SessionLimits actualLimits = JacksonUtil.fromBytes(payload, SessionLimits.class);
assertEquals(expectedLimits, actualLimits);
client.disconnect();
}
@Test
public void getSessionLimitsRpcForGatewayTest() throws Exception {
loginSysAdmin();
TenantProfile tenantProfile = doGet("/api/tenantProfile/" + tenantProfileId, TenantProfile.class);
DefaultTenantProfileConfiguration profileConfiguration = tenantProfile.getDefaultProfileConfiguration();
profileConfiguration.setTransportGatewayMsgRateLimit("100:600");
profileConfiguration.setTransportGatewayTelemetryMsgRateLimit("50:600");
profileConfiguration.setTransportGatewayTelemetryDataPointsRateLimit("200:600");
profileConfiguration.setTransportGatewayDeviceMsgRateLimit("20:600");
profileConfiguration.setTransportGatewayDeviceTelemetryMsgRateLimit("10:600");
profileConfiguration.setTransportGatewayDeviceTelemetryDataPointsRateLimit("40:600");
doPost("/api/tenantProfile", tenantProfile);
var expectedLimits = new GatewaySessionLimits();
var gatewayLimits = new SessionLimits.SessionRateLimits(profileConfiguration.getTransportGatewayMsgRateLimit(),
profileConfiguration.getTransportGatewayTelemetryMsgRateLimit(),
profileConfiguration.getTransportGatewayTelemetryDataPointsRateLimit());
var gatewayDeviceLimits = new SessionLimits.SessionRateLimits(profileConfiguration.getTransportGatewayDeviceMsgRateLimit(),
profileConfiguration.getTransportGatewayDeviceTelemetryMsgRateLimit(),
profileConfiguration.getTransportGatewayDeviceTelemetryDataPointsRateLimit());
expectedLimits.setGatewayRateLimits(gatewayLimits);
expectedLimits.setRateLimits(gatewayDeviceLimits);
expectedLimits.setMaxPayloadSize(maxPayloadSize);
expectedLimits.setMaxInflightMessages(maxInflightMessages);
MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder()
.gatewayName("Test Get Service Configuration Gateway")
.transportPayloadType(TransportPayloadType.JSON)
.build();
processBeforeTest(configProperties);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(gatewayAccessToken);
MqttTestCallback callback = new MqttTestSubscribeOnTopicCallback(DEVICE_RPC_RESPONSE_TOPIC + "1");
client.setCallback(callback);
client.subscribeAndWait(DEVICE_RPC_RESPONSE_SUB_TOPIC, MqttQoS.AT_MOST_ONCE);
client.publishAndWait(DEVICE_RPC_REQUESTS_TOPIC + "1", "{\"method\":\"getSessionLimits\",\"params\":{}}".getBytes());
assertTrue(callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS));
var payload = callback.getPayloadBytes();
SessionLimits actualLimits = JacksonUtil.fromBytes(payload, GatewaySessionLimits.class);
assertEquals(expectedLimits, actualLimits);
client.disconnect();
}
}

32
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java

@ -20,6 +20,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.GetResponse;
import java.util.ArrayList;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -31,7 +33,6 @@ import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
@ -44,6 +45,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
private final TbQueueMsgDecoder<T> decoder;
private final Channel channel;
private final Connection connection;
private final int maxPollMessages;
private volatile Set<String> queues;
@ -51,6 +53,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
super(topic);
this.admin = admin;
this.decoder = decoder;
this.maxPollMessages = rabbitMqSettings.getMaxPollMessages();
try {
connection = rabbitMqSettings.getConnectionFactory().newConnection();
} catch (IOException | TimeoutException e) {
@ -70,13 +73,19 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
protected List<GetResponse> doPoll(long durationInMillis) {
List<GetResponse> result = queues.stream()
.map(queue -> {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
List<GetResponse> messages = new ArrayList<>();
for (int i = 0; i < maxPollMessages; i++) {
GetResponse response = doQueuePoll(queue);
if (response == null) {
break;
}
messages.add(response);
}
}).filter(Objects::nonNull).collect(Collectors.toList());
return messages;
})
.filter(r -> !r.isEmpty())
.flatMap(Collection::stream)
.collect(Collectors.toList());
if (result.size() > 0) {
return result;
} else {
@ -84,6 +93,15 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
}
}
protected GetResponse doQueuePoll(String queue) {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
}
}
@Override
protected void doSubscribe(List<String> topicNames) {
queues = partitions.stream()

2
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java

@ -46,6 +46,8 @@ public class TbRabbitMqSettings {
private int connectionTimeout;
@Value("${queue.rabbitmq.handshake_timeout:}")
private int handshakeTimeout;
@Value("${queue.rabbitmq.max_poll_messages:1}")
private int maxPollMessages;
private ConnectionFactory connectionFactory;

128
common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.rabbitmq;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.GetResponse;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
@ExtendWith(MockitoExtension.class)
class TbRabbitMqConsumerTemplateTest {
private static final String TOPIC = "some-topic";
@Mock
private TbQueueAdmin admin;
@Mock
private ConnectionFactory connectionFactory;
@Mock
private TbQueueMsgDecoder<DefaultTbQueueMsg> decoder;
@Mock
private Connection connection;
@Mock
private Channel channel;
@Mock
private TopicPartitionInfo partition;
@Mock
private GetResponse getResponse;
private TbRabbitMqConsumerTemplate<DefaultTbQueueMsg> consumer;
private void setUpConsumerWithMaxPollMessages(int maxPollMessages) throws Exception {
when(connectionFactory.newConnection()).thenReturn(connection);
when(connection.createChannel()).thenReturn(channel);
TbRabbitMqSettings settings = new TbRabbitMqSettings();
settings.setMaxPollMessages(maxPollMessages);
settings.setConnectionFactory(connectionFactory);
consumer = new TbRabbitMqConsumerTemplate<>(admin, settings, TOPIC, decoder);
when(partition.getFullTopicName()).thenReturn(TOPIC);
consumer.subscribe(Set.of(partition));
}
@Test
void pollWithMax5PollMessagesReturnsEmptyListIfNoMessages() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(null);
assertThat(consumer.poll(0L)).isEmpty();
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax5PollMessagesReturns5MessagesIfQueueContains5() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(5);
verify(channel, times(5)).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax1PollMessageReturns1MessageIfQueueContainsMore() throws Exception {
setUpConsumerWithMaxPollMessages(1);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(1);
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax3PollMessagesReturns2MessagesIfQueueContains2() throws Exception {
setUpConsumerWithMaxPollMessages(3);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse, getResponse, null);
assertThat(consumer.poll(0L)).hasSize(2);
verify(channel, times(3)).basicGet(anyString(), anyBoolean());
}
private byte[] newMessageBody() {
return ("{\"key\": \"" + UUID.randomUUID() + "\"}").getBytes(StandardCharsets.UTF_8);
}
}

37
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -273,13 +273,13 @@ public class TbUtils {
Long.class, boolean.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class, int.class, boolean.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToRadixString", new MethodStub(TbUtils.class.getMethod("intLongToRadixString",
Long.class, int.class, boolean.class, boolean.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class)));
@ -315,8 +315,6 @@ public class TbUtils {
String.class)));
parserConfig.addImport("decodeURI", new MethodStub(TbUtils.class.getMethod("decodeURI",
String.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class, Object.class)));
parserConfig.addImport("raiseError", new MethodStub(TbUtils.class.getMethod("raiseError",
String.class)));
parserConfig.addImport("isBinary", new MethodStub(TbUtils.class.getMethod("isBinary",
@ -327,7 +325,7 @@ public class TbUtils {
String.class)));
parserConfig.addImport("isHexadecimal", new MethodStub(TbUtils.class.getMethod("isHexadecimal",
String.class)));
parserConfig.addImport("byteArrayToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList",
parserConfig.addImport("bytesToExecutionArrayList", new MethodStub(TbUtils.class.getMethod("bytesToExecutionArrayList",
ExecutionContext.class, byte[].class)));
parserConfig.addImport("padStart", new MethodStub(TbUtils.class.getMethod("padStart",
String.class, int.class, char.class)));
@ -725,19 +723,19 @@ public class TbUtils {
return prepareNumberHexString(l, bigEndian, pref, len, HEX_LEN_LONG_MAX);
}
public static String intLongToString(Long number) {
return intLongToString(number, DEC_RADIX);
public static String intLongToRadixString(Long number) {
return intLongToRadixString(number, DEC_RADIX);
}
public static String intLongToString(Long number, int radix) {
return intLongToString(number, radix, true);
public static String intLongToRadixString(Long number, int radix) {
return intLongToRadixString(number, radix, true);
}
public static String intLongToString(Long number, int radix, boolean bigEndian) {
return intLongToString(number, radix, bigEndian, false);
public static String intLongToRadixString(Long number, int radix, boolean bigEndian) {
return intLongToRadixString(number, radix, bigEndian, false);
}
public static String intLongToString(Long number, int radix, boolean bigEndian, boolean pref) {
public static String intLongToRadixString(Long number, int radix, boolean bigEndian, boolean pref) {
if (radix >= 25 && radix <= MAX_RADIX) {
return Long.toString(number, radix);
}
@ -1150,12 +1148,7 @@ public class TbUtils {
}
public static void raiseError(String message) {
raiseError(message, null);
}
public static void raiseError(String message, Object value) {
String msg = value == null ? message : message + " for value " + value;
throw new RuntimeException(msg);
throw new RuntimeException(message);
}
private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) {
@ -1344,8 +1337,8 @@ public class TbUtils {
* Writes the bit value to the appropriate location in the bins array, starting at the end of the array,
* to ensure proper alignment (highest bit to low end).
*/
public static byte[] parseLongToBinaryArray(long longValue, int binsLength) {
int len = Math.min(binsLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX);
public static byte[] parseLongToBinaryArray(long longValue, int binLength) {
int len = Math.min(binLength, BYTES_LEN_LONG_MAX * BIN_LEN_MAX);
byte[] bins = new byte[len];
for (int i = 0; i < len; i++) {
bins[len - 1 - i] = (byte) ((longValue >> i) & 1);

94
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -19,6 +19,7 @@ import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ -330,8 +331,8 @@ public class TbUtilsTest {
@Test
public void parseBytesIntToFloat() {
byte[] intValByte = {0x00, 0x00, 0x00, 0x0A};
Float valueExpected = 10.0f;
Float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true);
float valueExpected = 10.0f;
float valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, true);
Assertions.assertEquals(valueExpected, valueActual);
valueActual = TbUtils.parseBytesIntToFloat(intValByte, 3, 1, false);
Assertions.assertEquals(valueExpected, valueActual);
@ -345,20 +346,23 @@ public class TbUtilsTest {
valueExpected = 10.0f;
valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, true);
Assertions.assertEquals(valueExpected, valueActual);
valueExpected = 1.6777216E8f;
valueActual = TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false);
valueExpected = 167772.16f;
double factor = 1e3;
valueActual = (float) (TbUtils.parseBytesIntToFloat(intValByte, 0, 4, false) / factor);
Assertions.assertEquals(valueExpected, valueActual);
String dataAT101 = "0x01756403671B01048836BF7701F000090722050000";
List<Byte> byteAT101 = TbUtils.hexToBytes(ctx, dataAT101);
float latitudeExpected = 24.62495f;
int offset = 9;
valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false);
Assertions.assertEquals(latitudeExpected, valueActual / 1000000);
factor = 1e6;
valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset, 4, false) / factor);
Assertions.assertEquals(latitudeExpected, valueActual);
float longitudeExpected = 118.030576f;
valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false);
Assertions.assertEquals(longitudeExpected, valueActual / 1000000);
valueActual = (float) (TbUtils.parseBytesIntToFloat(byteAT101, offset + 4, 4, false) / factor);
Assertions.assertEquals(longitudeExpected, valueActual);
valueExpected = 9.185175E8f;
valueActual = TbUtils.parseBytesIntToFloat(byteAT101, offset);
@ -520,6 +524,18 @@ public class TbUtilsTest {
valueExpected = 7.2057594037927936E17d;
valueActual = TbUtils.parseBytesLongToDouble(longValByte, 0, 8, false);
Assertions.assertEquals(valueExpected, valueActual);
String dataPalacKiyv = "0x32D009423F23B300B0106E08D96B6C00";
List<Byte> byteAT101 = TbUtils.hexToBytes(ctx, dataPalacKiyv);
double latitudeExpected = 50.422775429058610d;
int offset = 0;
double factor = 1e15;
valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset, 8, false);
Assertions.assertEquals(latitudeExpected, valueActual / factor);
double longitudeExpected = 30.517877378257072d;
valueActual = TbUtils.parseBytesLongToDouble(byteAT101, offset + 8, 8, false);
Assertions.assertEquals(longitudeExpected, valueActual / factor);
}
@Test
@ -720,27 +736,27 @@ public class TbUtilsTest {
@Test
public void numberToString_Test() {
Assertions.assertEquals("00111010", TbUtils.intLongToString(58L, MIN_RADIX));
Assertions.assertEquals("0000000010011110", TbUtils.intLongToString(158L, MIN_RADIX));
Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToString(131585L, MIN_RADIX));
Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToString(Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToString(-Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToString(-102L, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToString(-13158L, MIN_RADIX));
Assertions.assertEquals("777777777777777777777", TbUtils.intLongToString(Long.MAX_VALUE, 8));
Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToString(Long.MIN_VALUE, 8));
Assertions.assertEquals("9223372036854775807", TbUtils.intLongToString(Long.MAX_VALUE));
Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToString(Long.MIN_VALUE));
Assertions.assertEquals("3366", TbUtils.intLongToString(13158L, 16));
Assertions.assertEquals("FFCC9A", TbUtils.intLongToString(-13158L, 16));
Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToString(-13158L, 16, true, true));
Assertions.assertEquals("0x0400", TbUtils.intLongToString(1024L, 16, true, true));
Assertions.assertNotEquals("400", TbUtils.intLongToString(1024L, 16));
Assertions.assertEquals("0xFFFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertNotEquals("0xFC00", TbUtils.intLongToString(-1024L, 16, true, true));
Assertions.assertEquals("hazelnut", TbUtils.intLongToString(1356099454469L, MAX_RADIX));
Assertions.assertEquals("00111010", TbUtils.intLongToRadixString(58L, MIN_RADIX));
Assertions.assertEquals("0000000010011110", TbUtils.intLongToRadixString(158L, MIN_RADIX));
Assertions.assertEquals("00000000000000100000001000000001", TbUtils.intLongToRadixString(131585L, MIN_RADIX));
Assertions.assertEquals("0111111111111111111111111111111111111111111111111111111111111111", TbUtils.intLongToRadixString(Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1000000000000000000000000000000000000000000000000000000000000001", TbUtils.intLongToRadixString(-Long.MAX_VALUE, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111111111110011010", TbUtils.intLongToRadixString(-102L, MIN_RADIX));
Assertions.assertEquals("1111111111111111111111111111111111111111111111111100110010011010", TbUtils.intLongToRadixString(-13158L, MIN_RADIX));
Assertions.assertEquals("777777777777777777777", TbUtils.intLongToRadixString(Long.MAX_VALUE, 8));
Assertions.assertEquals("1000000000000000000000", TbUtils.intLongToRadixString(Long.MIN_VALUE, 8));
Assertions.assertEquals("9223372036854775807", TbUtils.intLongToRadixString(Long.MAX_VALUE));
Assertions.assertEquals("-9223372036854775808", TbUtils.intLongToRadixString(Long.MIN_VALUE));
Assertions.assertEquals("3366", TbUtils.intLongToRadixString(13158L, 16));
Assertions.assertEquals("FFCC9A", TbUtils.intLongToRadixString(-13158L, 16));
Assertions.assertEquals("0xFFCC9A", TbUtils.intLongToRadixString(-13158L, 16, true, true));
Assertions.assertEquals("0x0400", TbUtils.intLongToRadixString(1024L, 16, true, true));
Assertions.assertNotEquals("400", TbUtils.intLongToRadixString(1024L, 16));
Assertions.assertEquals("0xFFFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true));
Assertions.assertNotEquals("0xFC00", TbUtils.intLongToRadixString(-1024L, 16, true, true));
Assertions.assertEquals("hazelnut", TbUtils.intLongToRadixString(1356099454469L, MAX_RADIX));
}
@Test
@ -870,14 +886,16 @@ public class TbUtilsTest {
@Test
public void raiseError_Test() {
String message = "frequency_weighting_type must be 0, 1 or 2.";
Object value = 4;
String message = "frequency_weighting_type must be 0, 1 or 2. A value of " + value.toString() + " is invalid.";
try {
TbUtils.raiseError(message, value);
TbUtils.raiseError(message);
Assertions.fail("Should throw NumberFormatException");
} catch (RuntimeException e) {
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. for value 4"));
Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid."));
}
message = "frequency_weighting_type must be 0, 1 or 2.";
try {
TbUtils.raiseError(message);
Assertions.fail("Should throw NumberFormatException");
@ -1067,6 +1085,18 @@ public class TbUtilsTest {
String actual = TbUtils.hexToBase64(hex);
Assertions.assertEquals(expected, actual);
}
@Test
public void bytesToHex_Test() {
byte[] bb = {(byte) 0xBB, (byte) 0xAA};
String expected = "BBAA";
String actual = TbUtils.bytesToHex(bb);
Assertions.assertEquals(expected, actual);
ExecutionArrayList<Integer> expectedList = new ExecutionArrayList<>(ctx);
expectedList.addAll(List.of(-69, 83));
expected = "BB53";
actual = TbUtils.bytesToHex(expectedList);
Assertions.assertEquals(expected, actual);
}
private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length);

4
common/transport/coap/src/main/proto/efento/proto_config.proto

@ -31,7 +31,7 @@ message ProtoOutputControlState {
uint32 channel_state = 2;
}
/* Message containing request data for accesing calibration parameters */
/* Message containing request data for accessing calibration parameters */
message ProtoCalibrationParameters {
/* Request details. Bitmask: */
@ -349,4 +349,4 @@ message ProtoConfig {
/* BLE advertising period configuration. */
ProtoBleAdvertisingPeriod ble_advertising_period = 59;
}
}

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java

@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.TransportContext;
import org.thingsboard.server.common.transport.TransportTenantProfileCache;
import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor;
import org.thingsboard.server.transport.mqtt.gateway.GatewayLatencyService;
@ -51,6 +52,10 @@ public class MqttTransportContext extends TransportContext {
@Autowired
private ProtoMqttAdaptor protoMqttAdaptor;
@Getter
@Autowired
private TransportTenantProfileCache tenantProfileCache;
@Getter
@Autowired
private GatewayLatencyService gatewayLatencyService;

50
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -60,6 +60,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
@ -78,6 +79,8 @@ import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto;
import org.thingsboard.server.queue.scheduler.SchedulerComponent;
import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor;
import org.thingsboard.server.transport.mqtt.limits.GatewaySessionLimits;
import org.thingsboard.server.transport.mqtt.limits.SessionLimits;
import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx;
import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler;
import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher;
@ -130,6 +133,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private static final Pattern FW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN);
private static final Pattern SW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN);
private static final String SESSION_LIMITS = "getSessionLimits";
private static final String PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE";
@ -496,8 +500,12 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg));
} else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) {
TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC);
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg));
toServerRpcSubTopicType = TopicType.V1;
if (SESSION_LIMITS.equals(rpcRequestMsg.getMethodName())) {
onGetSessionLimitsRpc(deviceSessionCtx.getSessionInfo(), ctx, msgId, rpcRequestMsg);
} else {
transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg));
}
} else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) {
TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg);
transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg));
@ -931,7 +939,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
} else {
log.debug("[{}] Failed to process unsubscription [{}] to [{}] - Subscription not found", sessionId, mqttMsg.variableHeader().messageId(), topicName);
unSubResults.add((short)MqttReasonCodes.UnsubAck.NO_SUBSCRIPTION_EXISTED.byteValue());
unSubResults.add((short) MqttReasonCodes.UnsubAck.NO_SUBSCRIPTION_EXISTED.byteValue());
}
}
if (!activityReported) {
@ -1058,6 +1066,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.debug("[{}][{}][{}] IOException: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
cause.getMessage(),
cause);
} else if (log.isInfoEnabled()) {
log.info("[{}][{}][{}] IOException: {}", sessionId,
@ -1333,6 +1342,43 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
}
}
private void onGetSessionLimitsRpc(TransportProtos.SessionInfoProto sessionInfo, ChannelHandlerContext ctx, int msgId, TransportProtos.ToServerRpcRequestMsg rpcRequestMsg) {
var tenantProfile = context.getTenantProfileCache().get(deviceSessionCtx.getTenantId());
DefaultTenantProfileConfiguration profile = tenantProfile.getDefaultProfileConfiguration();
SessionLimits sessionLimits;
if (sessionInfo.getIsGateway()) {
var gatewaySessionLimits = new GatewaySessionLimits();
var gatewayLimits = new SessionLimits.SessionRateLimits(profile.getTransportGatewayMsgRateLimit(),
profile.getTransportGatewayTelemetryMsgRateLimit(),
profile.getTransportGatewayTelemetryDataPointsRateLimit());
var gatewayDeviceLimits = new SessionLimits.SessionRateLimits(profile.getTransportGatewayDeviceMsgRateLimit(),
profile.getTransportGatewayDeviceTelemetryMsgRateLimit(),
profile.getTransportGatewayDeviceTelemetryDataPointsRateLimit());
gatewaySessionLimits.setGatewayRateLimits(gatewayLimits);
gatewaySessionLimits.setRateLimits(gatewayDeviceLimits);
sessionLimits = gatewaySessionLimits;
} else {
var rateLimits = new SessionLimits.SessionRateLimits(profile.getTransportDeviceMsgRateLimit(),
profile.getTransportDeviceTelemetryMsgRateLimit(),
profile.getTransportDeviceTelemetryDataPointsRateLimit());
sessionLimits = new SessionLimits();
sessionLimits.setRateLimits(rateLimits);
}
sessionLimits.setMaxPayloadSize(context.getMaxPayloadSize());
sessionLimits.setMaxInflightMessages(context.getMessageQueueSizePerDeviceLimit());
ack(ctx, msgId, MqttReasonCodes.PubAck.SUCCESS);
TransportProtos.ToServerRpcResponseMsg responseMsg = TransportProtos.ToServerRpcResponseMsg.newBuilder()
.setRequestId(rpcRequestMsg.getRequestId())
.setPayload(JacksonUtil.toString(sessionLimits))
.build();
onToServerRpcResponse(responseMsg);
}
private void handleToSparkplugDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws ThingsboardException {
SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName());
SparkplugRpcRequestHeader header;

25
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/GatewaySessionLimits.java

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

29
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/limits/SessionLimits.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt.limits;
import lombok.Data;
import org.thingsboard.server.common.data.TransportPayloadType;
@Data
public class SessionLimits {
private int maxPayloadSize;
private int maxInflightMessages;
private SessionRateLimits rateLimits;
public record SessionRateLimits(String messages, String telemetryMessages, String telemetryDataPoints) {}
}

1
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java

@ -82,6 +82,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private volatile MqttTopicFilter telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter();
private volatile MqttTopicFilter attributesPublishTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
private volatile MqttTopicFilter attributesSubscribeTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
@Getter
private volatile TransportPayloadType payloadType = TransportPayloadType.JSON;
private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor;
private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor;

5
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -40,6 +41,8 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
@Getter
private volatile DeviceId deviceId;
@Getter
private volatile TenantId tenantId;
@Getter
protected volatile TransportDeviceInfo deviceInfo;
@Getter
@Setter
@ -58,6 +61,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
public void setDeviceInfo(TransportDeviceInfo deviceInfo) {
this.deviceInfo = deviceInfo;
this.deviceId = deviceInfo.getDeviceId();
this.tenantId = deviceInfo.getTenantId();
}
@Override
@ -65,7 +69,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext {
this.sessionInfo = sessionInfo;
this.deviceProfile = deviceProfile;
this.deviceInfo.setDeviceType(deviceProfile.getName());
}
@Override

7
dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java

@ -236,8 +236,9 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService<AssetP
log.trace("Executing findOrCreateAssetProfile");
AssetProfile assetProfile = findAssetProfileByName(tenantId, name, false);
if (assetProfile == null) {
boolean isDefault = "default".equals(name) && findDefaultAssetProfile(tenantId) == null;
try {
assetProfile = this.doCreateDefaultAssetProfile(tenantId, name, name.equals("default"), true);
assetProfile = this.doCreateAssetProfile(tenantId, name, isDefault, true);
} catch (DataValidationException e) {
if (ASSET_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS.equals(e.getMessage())) {
assetProfile = findAssetProfileByName(tenantId, name, false);
@ -252,10 +253,10 @@ public class AssetProfileServiceImpl extends CachedVersionedEntityService<AssetP
@Override
public AssetProfile createDefaultAssetProfile(TenantId tenantId) {
log.trace("Executing createDefaultAssetProfile tenantId [{}]", tenantId);
return doCreateDefaultAssetProfile(tenantId, "default", true, false);
return doCreateAssetProfile(tenantId, "default", true, false);
}
private AssetProfile doCreateDefaultAssetProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
private AssetProfile doCreateAssetProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
AssetProfile assetProfile = new AssetProfile();
assetProfile.setTenantId(tenantId);

7
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -271,8 +271,9 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
log.trace("Executing findOrCreateDefaultDeviceProfile");
DeviceProfile deviceProfile = findDeviceProfileByName(tenantId, name, false);
if (deviceProfile == null) {
boolean isDefault = "default".equals(name) && findDefaultDeviceProfile(tenantId) == null;
try {
deviceProfile = this.doCreateDefaultDeviceProfile(tenantId, name, name.equals("default"), true);
deviceProfile = this.doCreateDeviceProfile(tenantId, name, isDefault, true);
} catch (DataValidationException e) {
if (DEVICE_PROFILE_WITH_SUCH_NAME_ALREADY_EXISTS.equals(e.getMessage())) {
deviceProfile = findDeviceProfileByName(tenantId, name, false);
@ -287,10 +288,10 @@ public class DeviceProfileServiceImpl extends CachedVersionedEntityService<Devic
@Override
public DeviceProfile createDefaultDeviceProfile(TenantId tenantId) {
log.trace("Executing createDefaultDeviceProfile tenantId [{}]", tenantId);
return doCreateDefaultDeviceProfile(tenantId, "default", true, false);
return doCreateDeviceProfile(tenantId, "default", true, false);
}
private DeviceProfile doCreateDefaultDeviceProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
private DeviceProfile doCreateDeviceProfile(TenantId tenantId, String profileName, boolean defaultProfile, boolean publishSaveEvent) {
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setTenantId(tenantId);

2
dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -149,6 +150,7 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
}
List<OAuth2ClientInfo> clients = oauth2ClientDao.findByDomainId(domain.getUuidId()).stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
return new DomainInfo(domain, clients);
}

2
dao/src/main/java/org/thingsboard/server/dao/mobile/MobileAppServiceImpl.java

@ -36,6 +36,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -143,6 +144,7 @@ public class MobileAppServiceImpl extends AbstractEntityService implements Mobil
private MobileAppInfo getMobileAppInfo(MobileApp mobileApp) {
List<OAuth2ClientInfo> clients = oauth2ClientDao.findByMobileAppId(mobileApp.getUuidId()).stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
return new MobileAppInfo(mobileApp, clients);
}

4
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientServiceImpl.java

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -68,7 +69,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
}
@Override
@Transactional
public OAuth2Client saveOAuth2Client(TenantId tenantId, OAuth2Client oAuth2Client) {
log.trace("Executing saveOAuth2Client [{}]", oAuth2Client);
oAuth2ClientDataValidator.validate(oAuth2Client, OAuth2Client::getTenantId);
@ -96,7 +96,6 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
}
@Override
@Transactional
public void deleteOAuth2ClientById(TenantId tenantId, OAuth2ClientId oAuth2ClientId) {
log.trace("Executing deleteOAuth2ClientById [{}]", oAuth2ClientId);
oauth2ClientDao.removeById(tenantId, oAuth2ClientId.getId());
@ -126,6 +125,7 @@ public class OAuth2ClientServiceImpl extends AbstractEntityService implements OA
return oauth2ClientDao.findByIds(tenantId.getId(), oAuth2ClientIds)
.stream()
.map(OAuth2ClientInfo::new)
.sorted(Comparator.comparing(OAuth2ClientInfo::getTitle))
.collect(Collectors.toList());
}

12
dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java

@ -163,6 +163,18 @@ public class AssetProfileServiceTest extends AbstractServiceTest {
});
}
@Test
public void testSaveAssetProfileWithNameDefaultAndProfileWithIsDefaultAndDifferentNameAlreadyExists() {
AssetProfile defaultAssetProfile = assetProfileService.findDefaultAssetProfile(tenantId);
Assert.assertNotNull(defaultAssetProfile);
defaultAssetProfile.setName("Asset Profile 1");
assetProfileService.saveAssetProfile(defaultAssetProfile);
AssetProfile createdAssetProfile = assetProfileService.findOrCreateAssetProfile(tenantId, "default");
Assert.assertNotNull(createdAssetProfile);
Assert.assertEquals("default", createdAssetProfile.getName());
Assert.assertFalse(createdAssetProfile.isDefault());
}
@Test
public void testDeleteAssetProfileWithExistingAsset() {
AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile");

12
dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java

@ -209,6 +209,18 @@ public class DeviceProfileServiceTest extends AbstractServiceTest {
});
}
@Test
public void testSaveDeviceProfileWithNameDefaultAndProfileWithIsDefaultAndDifferentNameAlreadyExists() {
DeviceProfile defaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(tenantId);
Assert.assertNotNull(defaultDeviceProfile);
defaultDeviceProfile.setName("Device Profile 1");
deviceProfileService.saveDeviceProfile(defaultDeviceProfile);
DeviceProfile createdDeviceProfile = deviceProfileService.findOrCreateDeviceProfile(tenantId, "default");
Assert.assertNotNull(createdDeviceProfile);
Assert.assertEquals("default", createdDeviceProfile.getName());
Assert.assertFalse(createdDeviceProfile.isDefault());
}
@Ignore
@Test
public void testChangeDeviceProfileTypeWithExistingDevices() {

2
msa/js-executor/config/default.yml

@ -34,7 +34,7 @@ kafka:
partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency
requestTimeout: "30000" # The default value in kafkajs is: 30000
connectionTimeout: "1000" # The default value in kafkajs is: 1000
compression: "gzip" # gzip or uncompressed
compression: "none" # gzip or uncompressed
topic_properties: "retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1"
use_confluent_cloud: false
client_id: "kafkajs" #inject pod name to easy identify the client using /opt/kafka/bin/kafka-consumer-groups.sh

26
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java

@ -15,10 +15,30 @@
*/
package org.thingsboard.mqtt;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.Future;
import io.netty.buffer.ByteBuf;
public interface MqttHandler {
ListenableFuture<Void> onMessage(String topic, ByteBuf payload);
/**
* Changing ListenableFuture to Future allows you to choose CompletableFuture,
* which gives developers the freedom to choose the orchestration method.
* CompletableFuture is a newer, more evolved version that eliminates callback hell,
* is easier to use, and comes with the JDK. jdk 1.8 was previously used with the Before JDK1.8,
* use ListenableFuture, after that, it is recommended to use CompletableFuture.
* ListenableFuture It's still written that way.{@link MqttMessageListener#onMessage(topic, payload)}
* public ListenableFuture<Void> onMessage(String topic, ByteBuf message) {
* log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
* events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
* return Futures.immediateVoidFuture();
* }
* CompletableFuture It's like this.
* public CompletableFuture<Void> onMessage(String topic, ByteBuf message) {
* log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
* events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
* return CompletableFuture.completedFuture(null);
* }
* This change does not affect the system's current use of ListenableFuture so that it is free to choose between ListenableFuture or
* CompletableFuture in new development.
*/
Future<Void> onMessage(String topic, ByteBuf payload);
}

2
pom.xml

@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.63.0</grpc.version>
<tbel.version>1.2.2</tbel.version>
<tbel.version>1.2.3</tbel.version>
<lombok.version>1.18.32</lombok.version>
<paho.client.version>1.2.5</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

9
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -515,13 +515,16 @@ public class RestClient implements Closeable {
return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
}
public List<EntitySubtype> getAlarmTypes(PageLink pageLink) {
public PageData<EntitySubtype> getAlarmTypes(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/alarm/types?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<EntitySubtype>>() {
}).getBody();
new ParameterizedTypeReference<PageData<EntitySubtype>>() {
},
params).getBody();
}
public AlarmComment saveAlarmComment(AlarmId alarmId, AlarmComment alarmComment) {

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java

@ -35,10 +35,13 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
@ -62,10 +65,9 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
config = TbNodeUtils.convert(configuration, TbAwsLambdaNodeConfiguration.class);
if (StringUtils.isBlank(config.getFunctionName())) {
throw new TbNodeException("Function name must be set!", true);
}
String errorPrefix = "'" + ctx.getSelf().getName() + "' node configuration is invalid: ";
try {
validateFields(config, errorPrefix);
AWSCredentials awsCredentials = new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey());
client = AWSLambdaAsyncClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
@ -74,6 +76,8 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode {
.withConnectionTimeout((int) TimeUnit.SECONDS.toMillis(config.getConnectionTimeout()))
.withRequestTimeout((int) TimeUnit.SECONDS.toMillis(config.getRequestTimeout())))
.build();
} catch (DataValidationException e) {
throw new TbNodeException(e, true);
} catch (Exception e) {
throw new TbNodeException(e);
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeConfiguration.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.aws.lambda;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@ -23,12 +25,18 @@ public class TbAwsLambdaNodeConfiguration implements NodeConfiguration<TbAwsLamb
public static final String DEFAULT_QUALIFIER = "$LATEST";
@NotBlank
private String accessKey;
@NotBlank
private String secretKey;
@NotBlank
private String region;
@NotBlank
private String functionName;
private String qualifier;
@Min(0)
private int connectionTimeout;
@Min(0)
private int requestTimeout;
private boolean tellFailureIfFuncThrowsExc;

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -77,6 +77,8 @@ public class TbMqttNode extends TbAbstractExternalNode {
this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class);
try {
this.mqttClient = initClient(ctx);
} catch (TbNodeException e) {
throw e;
} catch (Exception e) {
throw new TbNodeException(e);
}
@ -119,8 +121,7 @@ public class TbMqttNode extends TbAbstractExternalNode {
MqttClientConfig config = new MqttClientConfig(getSslContext());
config.setOwnerId(getOwnerId(ctx));
if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) {
config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId());
config.setClientId(getClientId(ctx));
}
config.setCleanSession(this.mqttNodeConfiguration.isCleanSession());
@ -146,6 +147,17 @@ public class TbMqttNode extends TbAbstractExternalNode {
return client;
}
private String getClientId(TbContext ctx) throws TbNodeException {
String clientId = this.mqttNodeConfiguration.isAppendClientIdSuffix() ?
this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() :
this.mqttNodeConfiguration.getClientId();
if (clientId.length() > 23) {
throw new TbNodeException("Client ID is too long '" + clientId + "'. " +
"The length of Client ID cannot be longer than 23, but current length is " + clientId.length() + ".", true);
}
return clientId;
}
MqttClient getMqttClient(TbContext ctx, MqttClientConfig config) {
return MqttClient.create(config, null, ctx.getExternalCallExecutor());
}

46
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -80,6 +80,8 @@ public class TbHttpClient {
public static final String PROXY_USER = "tb.proxy.user";
public static final String PROXY_PASSWORD = "tb.proxy.password";
public static final String MAX_IN_MEMORY_BUFFER_SIZE_IN_KB = "tb.http.maxInMemoryBufferSizeInKb";
private final TbRestApiCallNodeConfiguration config;
private EventLoopGroup eventLoopGroup;
@ -120,7 +122,7 @@ public class TbHttpClient {
SslContext sslContext = config.getCredentials().initSslContext();
httpClient = httpClient.secure(t -> t.sslContext(sslContext));
}
} else if (!config.isUseSimpleClientHttpFactory()) {
} else if (config.isUseSimpleClientHttpFactory()) {
if (CredentialsType.CERT_PEM == config.getCredentials().getType()) {
throw new TbNodeException("Simple HTTP Factory does not support CERT PEM credentials!");
}
@ -129,15 +131,34 @@ public class TbHttpClient {
httpClient = httpClient.secure(t -> t.sslContext(sslContext));
}
validateMaxInMemoryBufferSize(config);
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONNECTION, "close") //In previous realization this header was present! (Added for hotfix "Connection reset")
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(
(config.getMaxInMemoryBufferSizeInKb() > 0 ? config.getMaxInMemoryBufferSizeInKb() : 256) * 1024))
.build();
} catch (SSLException e) {
throw new TbNodeException(e);
}
}
private void validateMaxInMemoryBufferSize(TbRestApiCallNodeConfiguration config) throws TbNodeException {
int systemMaxInMemoryBufferSizeInKb = 25000;
try {
Properties properties = System.getProperties();
if (properties.containsKey(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB)) {
systemMaxInMemoryBufferSizeInKb = Integer.parseInt(properties.getProperty(MAX_IN_MEMORY_BUFFER_SIZE_IN_KB));
}
} catch (Exception ignored) {}
if (config.getMaxInMemoryBufferSizeInKb() > systemMaxInMemoryBufferSizeInKb) {
throw new TbNodeException("The configured maximum in-memory buffer size (in KB) exceeds the system limit for this parameter.\n" +
"The system limit is " + systemMaxInMemoryBufferSizeInKb + " KB.\n" +
"Please use the system variable '" + MAX_IN_MEMORY_BUFFER_SIZE_IN_KB + "' to override the system limit.");
}
}
EventLoopGroup getSharedOrCreateEventLoopGroup(EventLoopGroup eventLoopGroupShared) {
if (eventLoopGroupShared != null) {
return eventLoopGroupShared;
@ -207,13 +228,23 @@ public class TbHttpClient {
semaphore.release();
}
onFailure.accept(processException(msg, throwable), throwable);
onFailure.accept(processException(msg, throwable), processThrowable(throwable));
});
} catch (InterruptedException e) {
log.warn("Timeout during waiting for reply!", e);
}
}
private Throwable processThrowable(Throwable origin) {
if (origin instanceof WebClientResponseException restClientResponseException
&& restClientResponseException.getStatusCode().is2xxSuccessful()) {
// return cause instead of original exception in case 2xx status code
// this will provide meaningful error message to the user
return new RuntimeException(restClientResponseException.getCause());
}
return origin;
}
public URI buildEncodedUri(String endpointUrl) {
if (endpointUrl == null) {
throw new RuntimeException("Url string cannot be null!");
@ -316,8 +347,7 @@ public class TbHttpClient {
Properties properties = System.getProperties();
if (properties.containsKey(HTTP_PROXY_HOST) || properties.containsKey(HTTPS_PROXY_HOST)) {
createHttpProxyFrom(option, properties);
}
if (properties.containsKey(SOCKS_PROXY_HOST)) {
} else if (properties.containsKey(SOCKS_PROXY_HOST)) {
createSocksProxyFrom(option, properties);
}
}
@ -336,8 +366,8 @@ public class TbHttpClient {
String hostname = properties.getProperty(hostProperty);
int port = Integer.parseInt(properties.getProperty(portProperty));
checkProxyHost(config.getProxyHost());
checkProxyPort(config.getProxyPort());
checkProxyHost(hostname);
checkProxyPort(port);
var proxy = option
.type(ProxyProvider.Proxy.HTTP)
@ -362,8 +392,8 @@ public class TbHttpClient {
ProxyProvider.Proxy type = SOCKS_VERSION_5.equals(version) ? ProxyProvider.Proxy.SOCKS5 : ProxyProvider.Proxy.SOCKS4;
int port = Integer.parseInt(properties.getProperty(SOCKS_PROXY_PORT));
checkProxyHost(config.getProxyHost());
checkProxyPort(config.getProxyPort());
checkProxyHost(hostname);
checkProxyPort(port);
ProxyProvider.Builder proxy = option
.type(type)

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java

@ -46,6 +46,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
private String proxyScheme;
private ClientCredentials credentials;
private boolean ignoreRequestBody;
private int maxInMemoryBufferSizeInKb;
@Override
public TbRestApiCallNodeConfiguration defaultConfiguration() {
@ -60,6 +61,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setEnableProxy(false);
configuration.setCredentials(new AnonymousCredentials());
configuration.setIgnoreRequestBody(false);
configuration.setMaxInMemoryBufferSizeInKb(256);
return configuration;
}

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

File diff suppressed because one or more lines are too long

65
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNodeTest.java

@ -41,6 +41,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -50,8 +51,8 @@ import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
@ -72,13 +73,17 @@ public class TbAwsLambdaNodeTest {
private AWSLambdaAsync clientMock;
@BeforeEach
void setUp() {
public void setUp() {
node = new TbAwsLambdaNode();
config = new TbAwsLambdaNodeConfiguration().defaultConfiguration();
config.setAccessKey("accessKey");
config.setSecretKey("secretKey");
config.setFunctionName("new-function");
}
@Test
public void verifyDefaultConfig() {
config = new TbAwsLambdaNodeConfiguration().defaultConfiguration();
assertThat(config.getAccessKey()).isNull();
assertThat(config.getSecretKey()).isNull();
assertThat(config.getRegion()).isEqualTo(("us-east-1"));
@ -94,10 +99,43 @@ public class TbAwsLambdaNodeTest {
@ValueSource(strings = " ")
public void givenInvalidFunctionName_whenInit_thenThrowsException(String funcName) {
config.setFunctionName(funcName);
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
assertThatThrownBy(() -> node.init(ctx, configuration))
.isInstanceOf(TbNodeException.class)
.hasMessage("Function name must be set!");
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = " ")
public void givenInvalidAccessKey_whenInit_thenThrowsException(String accessKey) {
config.setAccessKey(accessKey);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = " ")
public void givenInvalidSecretAccessKey_whenInit_thenThrowsException(String secretAccessKey) {
config.setSecretKey(secretAccessKey);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = " ")
public void givenInvalidRegion_whenInit_thenThrowsException(String region) {
config.setRegion(region);
verifyValidationExceptionOnInit();
}
@Test
public void givenInvalidConnectionTimeout_whenInit_thenThrowsException() {
config.setConnectionTimeout(-100);
verifyValidationExceptionOnInit();
}
@Test
public void givenInvalidRequestTimeout_whenInit_thenThrowsException() {
config.setRequestTimeout(-100);
verifyValidationExceptionOnInit();
}
@ParameterizedTest
@ -280,10 +318,19 @@ public class TbAwsLambdaNodeTest {
assertThat(throwableCaptor.getValue()).isInstanceOf(AWSLambdaException.class).hasMessageStartingWith(errorMsg);
}
private void verifyValidationExceptionOnInit() {
RuleNode ruleNode = new RuleNode();
ruleNode.setName("test");
when(ctx.getSelf()).thenReturn(ruleNode);
String errorPrefix = "'test' node configuration is invalid: ";
assertThatThrownBy(() -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.hasMessageContaining(errorPrefix)
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
private void init() {
config.setAccessKey("accessKey");
config.setSecretKey("secretKey");
config.setFunctionName("new-function");
ReflectionTestUtils.setField(node, "client", clientMock);
ReflectionTestUtils.setField(node, "config", config);
}

36
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java

@ -191,6 +191,42 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest {
assertThatNoException().isThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))));
}
@Test
public void givenClientIdIsTooLong_whenInit_thenThrowsException() {
String invalidClientId = "vhfrbeb38ygwfwrgfwefgterhytjytj";
mqttNodeConfig.setClientId(invalidClientId);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID));
assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Client ID is too long '" + invalidClientId + "'. " +
"The length of Client ID cannot be longer than 23, but current length is " + invalidClientId.length() + ".")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@Test
public void givenClientIdIsOkAndAppendClientIdSuffixIsTrue_whenInit_thenClientIdBecomesInvalidAndThrowsException() {
String validClientId = "fertjnhnjj4ge";
mqttNodeConfig.setClientId("fertjnhnjj4ge");
mqttNodeConfig.setAppendClientIdSuffix(true);
given(ctxMock.getTenantId()).willReturn(TENANT_ID);
given(ctxMock.getSelf()).willReturn(new RuleNode(RULE_NODE_ID));
String serviceId = "test-service";
given(ctxMock.getServiceId()).willReturn(serviceId);
String resultedClientId = validClientId + "_" + serviceId;
assertThatThrownBy(() -> mqttNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(mqttNodeConfig))))
.isInstanceOf(TbNodeException.class)
.hasMessage("Client ID is too long '" + resultedClientId + "'. " +
"The length of Client ID cannot be longer than 23, but current length is " + resultedClientId.length() + ".")
.extracting(e -> ((TbNodeException) e).isUnrecoverable())
.isEqualTo(true);
}
@Test
public void givenFailedByTimeoutConnectResult_whenInit_thenThrowsException() throws ExecutionException, InterruptedException, TimeoutException {
mqttNodeConfig.setHost("localhost");

5
ui-ngx/angular.json

@ -39,6 +39,11 @@
"input": "./node_modules/ace-builds/src-noconflict/",
"output": "/"
},
{
"glob": "worker-xml.js",
"input": "./node_modules/ace-builds/src-noconflict/",
"output": "/"
},
{
"glob": "worker-css.js",
"input": "./node_modules/ace-builds/src-noconflict/",

9
ui-ngx/src/app/core/api/alias-controller.ts

@ -68,10 +68,11 @@ export class AliasController implements IAliasController {
private translate: TranslateService,
private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases,
private origFilters: Filters) {
private origFilters: Filters,
private origUserFilters?: Filters) {
this.entityAliases = deepClone(this.origEntityAliases) || {};
this.filters = deepClone(this.origFilters) || {};
this.userFilters = {};
this.userFilters = deepClone(this.origUserFilters) || {};
}
updateEntityAliases(newEntityAliases: EntityAliases) {
@ -166,6 +167,10 @@ export class AliasController implements IAliasController {
return this.filters;
}
getUserFilters(): Filters {
return this.userFilters;
}
getFilterInfo(filterId: string): FilterInfo {
if (this.userFilters[filterId]) {
return this.userFilters[filterId];

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -137,6 +137,7 @@ export interface IAliasController {
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
getEntityAliases(): EntityAliases;
getFilters(): Filters;
getUserFilters(): Filters;
getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo): void;

2
ui-ngx/src/app/core/auth/auth.service.ts

@ -224,7 +224,7 @@ export class AuthService {
}
public loadOAuth2Clients(): Observable<Array<OAuth2ClientLoginInfo>> {
const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB;
const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB;
return this.http.post<Array<OAuth2ClientLoginInfo>>(url,
null, defaultHttpOptions()).pipe(
catchError(err => of([])),

20
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -31,7 +31,7 @@ import {
DashboardLayoutsInfo,
DashboardState,
DashboardStateLayouts,
GridSettings,
GridSettings, LayoutType,
WidgetLayout
} from '@shared/models/dashboard.models';
import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils';
@ -345,10 +345,12 @@ export class DashboardUtilsService {
return widgetConfig;
}
public prepareWidgetForScadaLayout(widget: Widget): Widget {
public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget {
const config = widget.config;
config.showTitle = false;
config.dropShadow = false;
config.resizable = !isScada;
config.preserveAspectRatio = isScada;
config.padding = '0';
config.margin = '0';
config.backgroundColor = 'rgba(0,0,0,0)';
@ -404,6 +406,7 @@ export class DashboardUtilsService {
private createDefaultGridSettings(): GridSettings {
return {
layoutType: LayoutType.default,
backgroundColor: '#eeeeee',
columns: 24,
margin: 10,
@ -489,12 +492,15 @@ export class DashboardUtilsService {
if (!layout.gridSettings) {
layout.gridSettings = this.createDefaultGridSettings();
}
if (layout.gridSettings.margins && layout.gridSettings.margins.length === 2) {
layout.gridSettings.margin = layout.gridSettings.margins[0];
delete layout.gridSettings.margins;
if ((layout.gridSettings as any).margins && (layout.gridSettings as any).margins.length === 2) {
layout.gridSettings.margin = (layout.gridSettings as any).margins[0];
delete (layout.gridSettings as any).margins;
}
layout.gridSettings.outerMargin = isDefined(layout.gridSettings.outerMargin) ? layout.gridSettings.outerMargin : true;
layout.gridSettings.margin = isDefined(layout.gridSettings.margin) ? layout.gridSettings.margin : 10;
if (!layout.gridSettings.layoutType) {
layout.gridSettings.layoutType = LayoutType.default;
}
}
public setLayouts(dashboard: Dashboard, targetState: string, newLayouts: DashboardStateLayouts) {
@ -654,7 +660,9 @@ export class DashboardUtilsService {
mobileOrder: widget.config.mobileOrder,
mobileHeight: widget.config.mobileHeight,
mobileHide: widget.config.mobileHide,
desktopHide: widget.config.desktopHide
desktopHide: widget.config.desktopHide,
preserveAspectRatio: widget.config.preserveAspectRatio,
resizable: widget.config.resizable
};
if (isUndefined(originalColumns)) {
originalColumns = 24;

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

@ -130,7 +130,14 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
layout: {},
layout: {
resizable: this.widget.config.resizable,
preserveAspectRatio: this.widget.config.preserveAspectRatio,
mobileHide: this.widget.config.mobileHide,
desktopHide: this.widget.config.desktopHide,
mobileOrder: this.widget.config.mobileOrder,
mobileHeight: this.widget.config.mobileHeight
},
widgetType: this.widget.type,
typeParameters,
actionSources,
@ -187,6 +194,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.widget.config.mobileHeight = widgetConfig.layout.mobileHeight;
this.widget.config.mobileHide = widgetConfig.layout.mobileHide;
this.widget.config.desktopHide = widgetConfig.layout.desktopHide;
this.widget.config.preserveAspectRatio = widgetConfig.layout.preserveAspectRatio;
this.widget.config.resizable = widgetConfig.layout.resizable;
this.dialogRef.close(this.widget);
}
}

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

@ -277,7 +277,7 @@
'tb-dashboard-toolbar-animated': isToolbarOpenedAnimate,
'tb-dashboard-toolbar-closed': !toolbarOpened }">
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill tb-dashboard-drawer-container">
<mat-drawer-content fxLayout="column" fxLayoutAlign="center start">
<mat-drawer-content #dashboardContent fxLayout="column" fxLayoutAlign="center start">
<section *ngIf="!widgetEditMode" class="tb-dashboard-title"
[ngStyle]="{'color': dashboard.configuration.settings.titleColor}">
<h3 [fxShow]="!isEdit && displayTitle()">{{ translatedDashboardTitle }}</h3>

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

@ -222,6 +222,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@ViewChild('dashboardContainer') dashboardContainer: ElementRef<HTMLElement>;
@ViewChild('dashboardContent', {read: ElementRef}) dashboardContent: ElementRef<HTMLElement>;
prevDashboard: Dashboard;
iframeMode = this.utils.iframeMode;
@ -520,7 +522,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.translate,
() => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters);
this.dashboardConfiguration.filters,
this.parentDashboard?.aliasController.getUserFilters());
this.updateDashboardCss();
@ -1166,8 +1169,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.filtersUpdated();
this.updateLayouts();
}
} else if (!this.widgetEditMode) {
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
}
}
}
@ -1214,6 +1215,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.setEditMode(false, false);
} else {
let reInitDashboard = false;
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
this.dashboardService.saveDashboard(this.dashboard).pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
@ -1351,7 +1353,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
const scada = this.isAddingToScadaLayout();
if (scada) {
newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget);
newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget, widgetTypeInfo.scada);
}
let showLayoutConfig = true;
if (scada || this.layouts.right.show || !this.showLayoutConfigInEdit(this.layouts.main.layoutCtx)) {

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts

@ -56,6 +56,7 @@ export interface DashboardContext {
export interface IDashboardController {
dashboardCtx: DashboardContext;
dashboardContainer: ElementRef;
dashboardContent: ElementRef;
elRef: ElementRef;
openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean);

85
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -119,20 +119,15 @@
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()"
appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field *ngIf="!isScada()" appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="columns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field *ngIf="isScada()"
class="medium-width" appearance="outline" subscriptSizing="dynamic">
@ -145,37 +140,29 @@
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.min-layout-width</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="minColumns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched"
class="tb-error">
warning
</mat-icon>
<span translate matSuffix>dashboard.columns-suffix</span>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.widgets-margins</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate">
<input matInput formControlName="margin" type="number" min="0" max="50" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate"
*ngIf="gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row">
@ -190,20 +177,16 @@
</div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>
@ -245,20 +228,16 @@
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.mobile-row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate">
<input matInput formControlName="mobileRowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>

426
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts

@ -43,7 +43,7 @@ import {
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog';
import { DeviceService } from '@core/http/device.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { take, takeUntil } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import {
GatewayLogLevel,
@ -61,7 +61,9 @@ import { CommonModule } from '@angular/common';
import { coerceBoolean } from '@shared/decorators/coercion';
import {
GatewayConfigCommand,
GatewayConfigValue, LogConfig
GatewayConfigSecurity,
GatewayConfigValue,
LogConfig
} from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models';
@Component({
@ -99,13 +101,14 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
initialCredentialsUpdated = new EventEmitter<DeviceCredentials>();
StorageTypes = StorageTypes;
storageTypes = Object.values(StorageTypes) as StorageTypes[];
storageTypes = Object.values(StorageTypes);
storageTypesTranslationMap = StorageTypesTranslationMap;
logSavingPeriods = LogSavingPeriodTranslations;
localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[];
localLogsConfigTranslateMap = LocalLogsConfigTranslateMap;
securityTypes = GecurityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
logSelector: FormControl;
basicFormGroup: FormGroup;
@ -119,6 +122,7 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
private cd: ChangeDetectorRef,
private dialog: MatDialog) {
this.initBasicFormGroup();
this.observeFormChanges();
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
@ -142,14 +146,12 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
writeValue(basicConfig: GatewayConfigValue): void {
this.basicFormGroup.patchValue(basicConfig, {emitEvent: false});
if (basicConfig?.thingsboard?.security) {
this.checkAndFetchCredentials(basicConfig.thingsboard.security);
}
this.checkAndFetchCredentials(basicConfig?.thingsboard?.security ?? {} as GatewayConfigSecurity);
if (basicConfig?.grpc) {
this.toggleRpcFields(basicConfig.grpc.enabled);
}
const commands = basicConfig?.thingsboard?.statistics?.commands || [];
commands.forEach(command => this.addCommand(command, false));
const commands = basicConfig?.thingsboard?.statistics?.commands ?? [];
commands.forEach((command: GatewayConfigCommand) => this.addCommand(command, false));
}
validate(): ValidationErrors | null {
@ -251,7 +253,7 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
data: {
gatewayName: gateway.name
}
}).afterClosed().subscribe(
}).afterClosed().pipe(take(1)).subscribe(
(res) => {
if (!res) {
this.basicFormGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false});
@ -273,214 +275,294 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
this.commandFormArray().push(commandFormGroup, { emitEvent });
}
private initBasicFormGroup(): void {
this.basicFormGroup = this.fb.group({
thingsboard: this.fb.group({
host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true, []],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([], [])
}),
maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
}),
security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null, []],
cert: [null, []],
privateKey: [null, []],
}),
qos: [1, [Validators.min(0), Validators.max(1), Validators.required, Validators.pattern(/^[^.\s]+$/)]]
}),
storage: this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [
100, [Validators.min(1),
Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]
],
max_records_count: [
100000,
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]
],
data_folder_path: ['./data/', [Validators.required]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', [Validators.required]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
thingsboard: this.initThingsboardFormGroup(),
storage: this.initStorageFormGroup(),
grpc: this.initGrpcFormGroup(),
connectors: this.fb.array([]),
logs: this.initLogsFormGroup(),
});
}
private initThingsboardFormGroup(): FormGroup {
return this.fb.group({
host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false],
remoteConfiguration: [true],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([])
}),
grpc: this.fb.group({
enabled: [false, []],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true, []],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true],
checkingDeviceActivity: this.initCheckingDeviceActivityFormGroup(),
security: this.initSecurityFormGroup(),
qos: [1, [Validators.required, Validators.min(0), Validators.max(1), Validators.pattern(/^[^.\s]+$/)]]
});
}
private initStorageFormGroup(): FormGroup {
return this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_count: [100000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_folder_path: ['./data/', [Validators.required]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', [Validators.required]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initGrpcFormGroup(): FormGroup {
return this.fb.group({
enabled: [false],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initLogsFormGroup(): FormGroup {
return this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: [
'%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]
],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.INFO, [Validators.required]]
}),
connectors: this.fb.array([]),
logs: this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}),
local: this.fb.group({})
})
local: this.fb.group({})
});
}
private initCheckingDeviceActivityFormGroup(): FormGroup {
return this.fb.group({
checkDeviceInactivity: [false],
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
this.basicFormGroup.get('thingsboard.security.password').valueChanges.subscribe(password => {
private initSecurityFormGroup(): FormGroup {
return this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null],
cert: [null],
privateKey: [null]
});
}
private observeFormChanges(): void {
this.observeSecurityPasswordChanges();
this.observeRemoteConfigurationChanges();
this.observeDeviceActivityChanges();
this.observeSecurityTypeChanges();
this.observeStorageTypeChanges();
}
private observeSecurityPasswordChanges(): void {
const securityUsername = this.basicFormGroup.get('thingsboard.security.username');
this.basicFormGroup.get('thingsboard.security.password').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(password => {
if (password && password !== '') {
this.basicFormGroup.get('thingsboard.security.username').setValidators([Validators.required]);
securityUsername.setValidators([Validators.required]);
} else {
this.basicFormGroup.get('thingsboard.security.username').clearValidators();
securityUsername.clearValidators();
}
this.basicFormGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false});
securityUsername.updateValueAndValidity({ emitEvent: false });
});
}
this.toggleRpcFields(false);
this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => {
private observeRemoteConfigurationChanges(): void {
this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(enabled => {
if (!enabled) {
this.openConfigurationConfirmDialog();
}
});
this.logSelector = this.fb.control(LocalLogsConfigs.service);
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(localLogsConfigsKey, {} as LogConfig);
for (const key of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(key, {} as LogConfig);
}
}
private observeDeviceActivityChanges(): void {
const checkingDeviceActivityGroup = this.basicFormGroup.get('thingsboard.checkingDeviceActivity') as FormGroup;
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => {
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(enabled => {
checkingDeviceActivityGroup.updateValueAndValidity();
const validators = [Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)];
if (enabled) {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds')
.setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds')
.setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]);
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators(validators);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators(validators);
} else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
}
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({ emitEvent: false });
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({ emitEvent: false });
});
this.basicFormGroup.get('grpc.enabled').valueChanges.subscribe(value => {
this.basicFormGroup.get('grpc.enabled').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
this.toggleRpcFields(value);
});
}
private observeSecurityTypeChanges(): void {
const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup;
securityGroup.get('type').valueChanges.subscribe(type => {
securityGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
securityGroup.get('privateKey').addValidators([Validators.required]);
securityGroup.get('privateKey').updateValueAndValidity();
securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity();
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
switch (type) {
case SecurityTypes.ACCESS_TOKEN:
this.addAccessTokenValidators(securityGroup);
break;
case SecurityTypes.TLS_PRIVATE_KEY:
this.addTlsPrivateKeyValidators(securityGroup);
break;
case SecurityTypes.TLS_ACCESS_TOKEN:
this.addTlsAccessTokenValidators(securityGroup);
break;
case SecurityTypes.USERNAME_PASSWORD:
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
break;
}
securityGroup.updateValueAndValidity();
});
securityGroup.get('caCert').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges());
['caCert', 'privateKey', 'cert'].forEach(field => {
securityGroup.get(field).valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => this.cd.detectChanges());
});
}
private observeStorageTypeChanges(): void {
const storageGroup = this.basicFormGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => {
storageGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.removeAllStorageValidators();
if (type === StorageTypes.MEMORY) {
storageGroup.get('read_records_count').addValidators(
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_folder_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_file_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_file_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_check_in_hours').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false});
switch (type) {
case StorageTypes.MEMORY:
this.addMemoryStorageValidators(storageGroup);
break;
case StorageTypes.FILE:
this.addFileStorageValidators(storageGroup);
break;
case StorageTypes.SQLITE:
this.addSqliteStorageValidators(storageGroup);
break;
}
});
}
private checkAndFetchCredentials(security: any = {}): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentialsUpdated.emit(credentials);
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.type')
.setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN
? SecurityTypes.TLS_ACCESS_TOKEN
: SecurityTypes.ACCESS_TOKEN, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, {emitEvent: false});
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, {emitEvent: false});
}
} else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.basicFormGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.password')
.setValue(parsedValue.password, {emitEvent: false});
} else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE) {
//if sertificate is present set sertificate as present
}
});
private addAccessTokenValidators(group: FormGroup): void {
group.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
group.get('accessToken').updateValueAndValidity();
}
private addTlsPrivateKeyValidators(group: FormGroup): void {
['caCert', 'privateKey', 'cert'].forEach(field => {
group.get(field).addValidators([Validators.required]);
group.get(field).updateValueAndValidity();
});
}
private addTlsAccessTokenValidators(group: FormGroup): void {
this.addAccessTokenValidators(group);
group.get('caCert').addValidators([Validators.required]);
group.get('caCert').updateValueAndValidity();
}
private addMemoryStorageValidators(group: FormGroup): void {
group.get('read_records_count').addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get('max_records_count').addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get('read_records_count').updateValueAndValidity({ emitEvent: false });
group.get('max_records_count').updateValueAndValidity({ emitEvent: false });
}
private addFileStorageValidators(group: FormGroup): void {
['data_folder_path', 'max_file_count', 'max_read_records_count', 'max_records_per_file'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});
}
private addSqliteStorageValidators(group: FormGroup): void {
['data_file_path', 'messages_ttl_check_in_hours', 'messages_ttl_in_days'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});
}
private checkAndFetchCredentials(security: GatewayConfigSecurity): void {
if (security.type === SecurityTypes.TLS_PRIVATE_KEY) {
return;
}
this.deviceService.getDeviceCredentials(this.device.id).pipe(takeUntil(this.destroy$)).subscribe(credentials => {
this.initialCredentialsUpdated.emit(credentials);
this.updateSecurityType(security, credentials);
this.updateCredentials(credentials, security);
});
}
private updateSecurityType(security, credentials: DeviceCredentials): void {
const isAccessToken = credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN
|| security.type === SecurityTypes.TLS_ACCESS_TOKEN;
const securityType = isAccessToken
? (security.type === SecurityTypes.TLS_ACCESS_TOKEN ? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN)
: (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC ? SecurityTypes.USERNAME_PASSWORD : null);
if (securityType) {
this.basicFormGroup.get('thingsboard.security.type').setValue(securityType, { emitEvent: false });
}
}
private updateCredentials(credentials: DeviceCredentials, security: GatewayConfigSecurity): void {
switch (credentials.credentialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.updateAccessTokenCredentials(credentials, security);
break;
case DeviceCredentialsType.MQTT_BASIC:
this.updateMqttBasicCredentials(credentials);
break;
case DeviceCredentialsType.X509_CERTIFICATE:
break;
}
}
private updateAccessTokenCredentials(credentials: DeviceCredentials, security: GatewayConfigSecurity): void {
this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, { emitEvent: false });
if (security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, { emitEvent: false });
}
}
private updateMqttBasicCredentials(credentials: DeviceCredentials): void {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, { emitEvent: false });
this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, { emitEvent: false });
this.basicFormGroup.get('thingsboard.security.password').setValue(parsedValue.password, { emitEvent: false });
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts

@ -69,8 +69,8 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
private cd: ChangeDetectorRef,
private dialog: MatDialog) {
private cd: ChangeDetectorRef
) {
this.gatewayConfigGroup = this.fb.group({
basicConfig: [],
@ -361,7 +361,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
}
private updateFormControls(thingsboard: GatewayGeneralConfig): void {
const { type, accessToken, ...securityConfig } = thingsboard.security || {};
const { type, accessToken, ...securityConfig } = thingsboard.security ?? {};
this.initialCredentials = {
deviceId: this.device as DeviceId,

23
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -289,13 +289,24 @@ const updateScadaSymbolMetadataInDom = (svgDoc: Document, metadata: ScadaSymbolM
metadataElement.appendChild(cdata);
};
const tbMetadataRegex = /<tb:metadata>.*<\/tb:metadata>/gs;
const tbMetadataRegex = /<tb:metadata[^>]*>.*<\/tb:metadata>/gs;
export interface ScadaSymbolContentData {
svgRootNode: string;
innerSvg: string;
}
export const removeScadaSymbolMetadata = (svgContent: string): string => {
let result = svgContent;
tbMetadataRegex.lastIndex = 0;
const metadataMatch = tbMetadataRegex.exec(svgContent);
if (metadataMatch !== null && metadataMatch.length) {
const metadata = metadataMatch[0];
result = result.replace(metadata, '');
}
return result;
};
export const scadaSymbolContentData = (svgContent: string): ScadaSymbolContentData => {
const result: ScadaSymbolContentData = {
svgRootNode: '',
@ -517,10 +528,12 @@ export class ScadaSymbolObject {
if (this.context) {
for (const tag of this.metadata.tags) {
const elements = this.context.tags[tag.tag];
elements.forEach(element => {
element.timeline().stop();
element.timeline(null);
});
if (elements) {
elements.forEach(element => {
element.timeline().stop();
element.timeline(null);
});
}
}
}
if (this.svgShape) {

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

@ -168,18 +168,32 @@
</tb-manage-widget-actions>
</div>
</div>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
<div [fxShow]="selectedOption === 'layout'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-form-panel" *ngIf="!showLayoutConfig || isDefaultBreakpoint">
<div class="tb-form-panel-title" translate>widget-config.resize-options</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="resizable">
{{ 'widget-config.resizable' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="preserveAspectRatio">
{{ 'widget-config.preserve-aspect-ratio' | translate }}
</mat-slide-toggle>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel" *ngIf="showLayoutConfig">
<div class="tb-form-panel-title">{{ (isDefaultBreakpoint ? 'widget-config.mobile' : 'widget-config.list-layout') | translate }}</div>
<div class="tb-form-row" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.order</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">

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

@ -63,7 +63,7 @@ import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { merge, Observable, of, Subject, Subscription } from 'rxjs';
import {
IBasicWidgetConfigComponent,
WidgetConfigCallbacks
@ -248,19 +248,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
noDataDisplayMessage: [null, []]
});
this.widgetSettings.get('showTitle').valueChanges.subscribe(() => {
this.updateWidgetSettingsEnabledState();
});
this.widgetSettings.get('showTitleIcon').valueChanges.subscribe(() => {
merge(this.widgetSettings.get('showTitle').valueChanges,
this.widgetSettings.get('showTitleIcon').valueChanges).subscribe(() => {
this.updateWidgetSettingsEnabledState();
});
this.layoutSettings = this.fb.group({
resizable: [true],
preserveAspectRatio: [false],
mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]],
mobileHeight: [null, [Validators.min(1), Validators.pattern(/^\d*$/)]],
mobileHide: [false],
desktopHide: [false]
});
this.layoutSettings.get('resizable').valueChanges.subscribe(() => {
this.updateLayoutEnabledState();
});
this.actionsSettings = this.fb.group({
actions: [null, []]
});
@ -349,16 +354,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'actions'
}
);
if (this.showLayoutConfig) {
this.headerOptions.push(
{
name: this.isDefaultBreakpoint
? this.translate.instant('widget-config.mobile')
: this.translate.instant('widget-config.list-layout'),
value: 'mobile'
}
);
}
this.headerOptions.push(
{
name: this.translate.instant('widget-config.layout'),
value: 'layout'
}
);
if (!this.selectedOption || !this.headerOptions.find(o => o.value === this.selectedOption)) {
this.selectedOption = this.headerOptions[0].value;
}
@ -569,6 +570,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
if (layout) {
this.layoutSettings.patchValue(
{
resizable: isDefined(layout.resizable) ? layout.resizable : true,
preserveAspectRatio: layout.preserveAspectRatio,
mobileOrder: layout.mobileOrder,
mobileHeight: layout.mobileHeight,
mobileHide: layout.mobileHide,
@ -579,6 +582,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} else {
this.layoutSettings.patchValue(
{
resizable: true,
preserveAspectRatio: false,
mobileOrder: null,
mobileHeight: null,
mobileHide: false,
@ -587,6 +592,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
{emitEvent: false}
);
}
this.updateLayoutEnabledState();
}
this.createChangeSubscriptions();
}
@ -620,6 +626,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
}
private updateLayoutEnabledState() {
const resizable: boolean = this.layoutSettings.get('resizable').value;
if (resizable) {
this.layoutSettings.get('preserveAspectRatio').enable({emitEvent: false});
} else {
this.layoutSettings.get('preserveAspectRatio').disable({emitEvent: false});
}
}
private updateSchemaForm(settings?: any) {
const widgetSettingsFormData: JsonFormComponentData = {};
if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {

1
ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss

@ -173,6 +173,7 @@ gridster-item:hover {
}
.tooltipster-sidetip.tb-widget-edit-actions-tooltip {
position: absolute !important;
.tooltipster-box {
user-select: none;
background: #fff;

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

@ -148,9 +148,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
$(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
$(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
const dashboardElement = this.widget.widgetContext.dashboardPageElement;
if (dashboardElement) {
this.initEditWidgetActionTooltip(dashboardElement);
const dashboardContentElement = this.widget.widgetContext.dashboardContentElement;
if (dashboardContentElement) {
this.initEditWidgetActionTooltip(dashboardContentElement);
}
}

176
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -343,6 +343,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private selectedValue = false;
private selectedCallback: (selected: boolean) => void = () => {};
resizableHandles = {} as any;
resizeEnabled = true;
isFullscreen = false;
isReference = false;
@ -387,6 +391,15 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
private gridsterItemComponentValue: GridsterItemComponentInterface;
private readonly aspectRatio: number;
private heightValue: number;
private widthValue: number;
private rowsValue: number;
private colsValue: number;
get mobileHide(): boolean {
return this.widgetLayout ? this.widgetLayout.mobileHide === true : false;
}
@ -397,10 +410,96 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
set gridsterItemComponent(item: GridsterItemComponentInterface) {
this.gridsterItemComponentValue = item;
if (this.widgetLayout?.preserveAspectRatio) {
this.applyPreserveAspectRatio(item);
}
this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue);
this.gridsterItemComponentSubject.complete();
}
private applyPreserveAspectRatio(item: GridsterItemComponentInterface) {
this.resizableHandles.ne = false;
this.resizableHandles.sw = false;
this.resizableHandles.nw = false;
const $item = item.$item;
this.rowsValue = $item.rows;
this.colsValue = $item.cols;
Object.defineProperty($item, 'rows', {
get: () => this.rowsValue,
set: v => {
if (this.rowsValue !== v) {
if (this.preserveAspectRatio) {
this.colsValue = v * this.aspectRatio;
}
this.rowsValue = v;
}
}
});
Object.defineProperty($item, 'cols', {
get: () => this.colsValue,
set: v => {
if (this.colsValue !== v) {
if (this.preserveAspectRatio) {
this.rowsValue = v / this.aspectRatio;
}
this.colsValue = v;
}
}
});
const resizable = item.resize;
this.heightValue = resizable.height;
this.widthValue = resizable.width;
const setItemHeight = resizable.setItemHeight.bind(resizable);
const setItemWidth = resizable.setItemWidth.bind(resizable);
resizable.setItemHeight = (height) => {
setItemHeight(height);
this.heightValue = height;
if (this.preserveAspectRatio) {
setItemWidth(height * this.aspectRatio);
}
};
resizable.setItemWidth = (width) => {
setItemWidth(width);
this.widthValue = width;
if (this.preserveAspectRatio) {
setItemHeight(width / this.aspectRatio);
}
};
Object.defineProperty(resizable, 'height', {
get: () => this.heightValue,
set: v => {
if (this.heightValue !== v) {
if (this.preserveAspectRatio) {
this.widthValue = v * this.aspectRatio;
}
this.heightValue = v;
}
}
});
Object.defineProperty(resizable, 'width', {
get: () => this.widthValue,
set: v => {
if (this.widthValue !== v) {
if (this.preserveAspectRatio) {
this.heightValue = v / this.aspectRatio;
}
this.widthValue = v;
}
}
});
}
get highlighted() {
return this.highlightedValue;
}
@ -434,6 +533,13 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
public widgetLayout?: WidgetLayout,
private parentDashboard?: IDashboardComponent,
private popoverComponent?: TbPopoverComponent) {
if (isDefined(widgetLayout?.resizable)) {
this.resizeEnabled = widgetLayout.resizable;
}
if (widgetLayout?.preserveAspectRatio) {
this.aspectRatio = this.widgetLayout.sizeX / this.widgetLayout.sizeY;
}
if (!widget.id) {
widget.id = guid();
}
@ -603,30 +709,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
}
}
@enumerable(true)
get cols(): number {
let res;
if (this.widgetLayout) {
res = this.widgetLayout.sizeX;
get preserveAspectRatio(): boolean {
if (!this.dashboard.isMobileSize && this.widgetLayout) {
return this.widgetLayout.preserveAspectRatio;
} else {
res = this.widget.sizeX;
return false;
}
return Math.floor(res);
}
@enumerable(true)
get cols(): number {
return Math.floor(this.sizeX);
}
set cols(cols: number) {
if (!this.dashboard.isMobileSize) {
if (this.widgetLayout) {
this.widgetLayout.sizeX = cols;
} else {
this.widget.sizeX = cols;
}
this.sizeX = cols;
}
}
@enumerable(true)
get rows(): number {
let res;
let res: number;
if (this.dashboard.isMobileSize) {
let mobileHeight;
if (this.widgetLayout) {
@ -638,26 +742,50 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
if (mobileHeight) {
res = mobileHeight;
} else {
const sizeY = this.widgetLayout ? this.widgetLayout.sizeY : this.widget.sizeY;
const sizeY = this.sizeY;
res = sizeY * 24 / this.dashboard.gridsterOpts.minCols;
}
} else {
if (this.widgetLayout) {
res = this.widgetLayout.sizeY;
} else {
res = this.widget.sizeY;
}
res = this.sizeY;
}
return Math.floor(res);
}
set rows(rows: number) {
if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) {
if (this.widgetLayout) {
this.widgetLayout.sizeY = rows;
} else {
this.widget.sizeY = rows;
}
this.sizeY = rows;
}
}
get sizeX(): number {
if (this.widgetLayout) {
return this.widgetLayout.sizeX;
} else {
return this.widget.sizeX;
}
}
set sizeX(sizeX: number) {
if (this.widgetLayout) {
this.widgetLayout.sizeX = sizeX;
} else {
this.widget.sizeX = sizeX;
}
}
get sizeY(): number {
if (this.widgetLayout) {
return this.widgetLayout.sizeY;
} else {
return this.widget.sizeY;
}
}
set sizeY(sizeY: number) {
if (this.widgetLayout) {
this.widgetLayout.sizeY = sizeY;
} else {
this.widget.sizeY = sizeY;
}
}

6
ui-ngx/src/app/modules/home/models/services.map.ts

@ -50,6 +50,8 @@ import { TenantService } from '@core/http/tenant.service';
import { TenantProfileService } from '@core/http/tenant-profile.service';
import { UiSettingsService } from '@core/http/ui-settings.service';
import { UsageInfoService } from '@core/http/usage-info.service';
import { EventService } from '@core/http/event.service';
import { AuditLogService } from '@core/http/audit-log.service';
export const ServicesMap = new Map<string, Type<any>>(
[
@ -87,6 +89,8 @@ export const ServicesMap = new Map<string, Type<any>>(
['userSettingsService', UserSettingsService],
['uiSettingsService', UiSettingsService],
['usageInfoService', UsageInfoService],
['notificationService', NotificationService]
['notificationService', NotificationService],
['eventService', EventService],
['auditLogService', AuditLogService]
]
);

4
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -184,6 +184,10 @@ export class WidgetContext {
return this.dashboard?.stateController?.dashboardCtrl?.elRef?.nativeElement;
}
get dashboardContentElement(): HTMLElement {
return this.dashboard?.stateController?.dashboardCtrl?.dashboardContent?.nativeElement;
}
authService: AuthService;
deviceService: DeviceService;
assetService: AssetService;

5
ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts

@ -64,9 +64,8 @@ export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAu
this.config.columns.push(
new DateEntityTableColumn<OAuth2ClientInfo>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<OAuth2ClientInfo>('title', 'admin.oauth2.title', '35%'),
new EntityTableColumn<OAuth2ClientInfo>('providerName', 'admin.oauth2.provider', '170px'),
new EntityTableColumn<OAuth2ClientInfo>('platforms', 'admin.oauth2.allowed-platforms', '170px',
new EntityTableColumn<OAuth2ClientInfo>('title', 'admin.oauth2.title', '350px'),
new EntityTableColumn<OAuth2ClientInfo>('platforms', 'admin.oauth2.allowed-platforms', '100%',
(clientInfo) => {
return clientInfo.platforms && clientInfo.platforms.length ?
clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') :

3
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html

@ -30,8 +30,7 @@
<tb-js-func #tagFunctionComponent
minHeight="300px"
formControlName="tagFunction"
[objectHighlightRules]="objectHighlightRules"
[propertyHighlightRules]="propertyHighlightRules"
[highlightRules]="highlightRules"
[editorCompleter]="completer"
[functionArgs]="tagFunctionArgs"
[helpId]="tagFunctionHelpId">

16
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts

@ -28,12 +28,10 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetService } from '@core/http/widget.service';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { TbHighlightRule } from '@shared/models/ace/ace.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import {
scadaSymbolClickActionHighlightRules,
scadaSymbolClickActionPropertiesHighlightRules,
scadaSymbolElementStateRenderHighlightRules,
scadaSymbolElementStateRenderPropertiesHighlightRules
scadaSymbolRenderFunctionHighlightRules
} from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { JsFuncComponent } from '@shared/components/js-func.component';
@ -79,9 +77,7 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft
tagFunctionHelpId: string;
objectHighlightRules: TbHighlightRule[];
propertyHighlightRules: TbHighlightRule[];
highlightRules: AceHighlightRules;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService) {
@ -99,14 +95,12 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft
if (this.tagFunctionType === 'renderFunction') {
this.panelTitle = 'scada.state-render-function';
this.tagFunctionArgs = ['ctx', 'element'];
this.objectHighlightRules = scadaSymbolElementStateRenderHighlightRules;
this.propertyHighlightRules = scadaSymbolElementStateRenderPropertiesHighlightRules;
this.highlightRules = scadaSymbolRenderFunctionHighlightRules;
this.tagFunctionHelpId = 'scada/tag_state_render_fn';
} else if (this.tagFunctionType === 'clickAction') {
this.panelTitle = 'scada.tag.on-click-action';
this.tagFunctionArgs = ['ctx', 'element', 'event'];
this.objectHighlightRules = scadaSymbolClickActionHighlightRules;
this.propertyHighlightRules = scadaSymbolClickActionPropertiesHighlightRules;
this.highlightRules = scadaSymbolClickActionHighlightRules;
this.tagFunctionHelpId = 'scada/tag_click_action_fn';
}
}

2
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component.html

@ -35,6 +35,6 @@
</div>
</div>
<ng-template #noTags>
<span fxLayoutAlign="start center"
<span fxLayoutAlign="center center"
class="tb-prompt">{{ 'scada.tag.no-tags' | translate }}</span>
</ng-template>

3
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html

@ -80,8 +80,7 @@
<tb-js-func formControlName="stateRenderFunction"
minHeight="300px"
[editorCompleter]="generalStateRenderFunctionCompleter"
[objectHighlightRules]="scadaSymbolGeneralStateRenderHighlightRules"
[propertyHighlightRules]="scadaSymbolGeneralStateRenderPropertiesHighlightRules"
[highlightRules]="highlightRules"
[functionArgs]="['ctx', 'svg']"
helpId="scada/symbol_state_render_fn">
</tb-js-func>

7
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts

@ -49,8 +49,7 @@ import {
elementStateRenderFunctionCompletions,
generalStateRenderFunctionCompletions,
scadaSymbolContextCompletion,
scadaSymbolGeneralStateRenderHighlightRules,
scadaSymbolGeneralStateRenderPropertiesHighlightRules
scadaSymbolGeneralStateHighlightRules
} from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { IAliasController } from '@core/api/widget-api.models';
@ -122,9 +121,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
elementStateRenderFunctionCompleter: TbEditorCompleter;
clickActionFunctionCompleter: TbEditorCompleter;
scadaSymbolGeneralStateRenderHighlightRules = scadaSymbolGeneralStateRenderHighlightRules;
scadaSymbolGeneralStateRenderPropertiesHighlightRules = scadaSymbolGeneralStateRenderPropertiesHighlightRules;
highlightRules = scadaSymbolGeneralStateHighlightRules;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,

25
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.html

@ -15,13 +15,19 @@
limitations under the License.
-->
<div class="tb-scada-symbol-editor-shape" #scadaSymbolShape></div>
<div class="tb-scada-symbol-editor-tooltips" #tooltipsContainer>
<div class="tb-scada-symbol-editor-shape" [fxShow]="editorMode === 'svg'" #scadaSymbolShape></div>
<div class="tb-scada-symbol-editor-tooltips" [fxShow]="editorMode === 'svg'" #tooltipsContainer>
<tb-anchor #tooltipsContainerComponent></tb-anchor>
</div>
<div class="tb-scada-symbol-editor-svg-xml" [fxShow]="editorMode === 'xml'">
<tb-svg-xml [formControl]="svgContentFormControl"
fillHeight
noLabel>
</tb-svg-xml>
</div>
<div class="tb-scada-symbol-editor-buttons">
<div class="tb-scada-symbol-editor-view-buttons">
<div class="tb-scada-symbol-editor-zoom-buttons tb-primary-fill">
<div class="tb-scada-symbol-editor-zoom-buttons tb-primary-fill" [fxShow]="editorMode === 'svg'">
<button mat-icon-button
[disabled]="zoomInDisabled"
(click)="zoomIn()"
@ -37,7 +43,7 @@
<mat-icon>remove</mat-icon>
</button>
</div>
<div *ngIf="displayShowHidden" class="tb-primary-fill">
<div *ngIf="displayShowHidden" [fxShow]="editorMode === 'svg'" class="tb-primary-fill">
<button mat-icon-button
(click)="toggleShowHidden()"
matTooltip="{{ (showHiddenElements ? 'scada.hide-hidden-elements' : 'scada.show-hidden-elements') | translate }}"
@ -46,7 +52,7 @@
</button>
</div>
</div>
<div class="tb-scada-symbol-editor-upload-buttons">
<div class="tb-scada-symbol-editor-right-buttons">
<div class="tb-primary-fill">
<button mat-icon-button
(click)="updateScadaSymbol.emit()"
@ -63,5 +69,14 @@
<mat-icon>download</mat-icon>
</button>
</div>
<tb-toggle-select appearance="fill"
[disabled]="!svgContentFormControl.valid"
fillHeight
extraPadding
primaryBackground
[(ngModel)]="editorMode">
<tb-toggle-option value="svg">{{ 'scada.mode-svg' | translate }}</tb-toggle-option>
<tb-toggle-option value="xml">{{ 'scada.mode-xml' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
</div>

10
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.scss

@ -25,6 +25,11 @@
inset: 0;
pointer-events: none;
}
.tb-scada-symbol-editor-svg-xml {
width: 100%;
height: 100%;
padding-top: 88px;
}
.tb-scada-symbol-editor-buttons {
pointer-events: none;
position: absolute;
@ -64,11 +69,14 @@
}
}
.tb-scada-symbol-editor-upload-buttons {
.tb-scada-symbol-editor-right-buttons {
display: flex;
flex-direction: row;
gap: 8px;
z-index: 101;
tb-toggle-select {
pointer-events: auto;
}
}
}

75
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.component.ts

@ -34,11 +34,15 @@ import {
ScadaSymbolEditObjectCallbacks
} from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { FormControl } from '@angular/forms';
import { removeScadaSymbolMetadata } from '@home/components/widget/lib/scada/scada-symbol.models';
export interface ScadaSymbolEditorData {
scadaSymbolContent: string;
}
type editorModeType = 'svg' | 'xml';
@Component({
selector: 'tb-scada-symbol-editor',
templateUrl: './scada-symbol-editor.component.html',
@ -84,10 +88,31 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
displayShowHidden = false;
svgContentFormControl = new FormControl();
svgContent: string;
private editorModeValue: editorModeType = 'svg';
get editorMode(): editorModeType {
return this.editorModeValue;
}
set editorMode(value: editorModeType) {
this.updateEditorMode(value);
}
constructor(private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.svgContentFormControl.valueChanges.subscribe((svgContent) => {
if (this.svgContent !== svgContent) {
this.svgContent = svgContent;
this.editObjectCallbacks.onSymbolEditObjectDirty(true);
}
this.editObjectCallbacks.onSymbolEditObjectValid(this.svgContentFormControl.valid);
});
}
ngAfterViewInit() {
@ -114,13 +139,16 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'data') {
if (this.scadaSymbolEditObject) {
setTimeout(() => {
this.updateContent(this.data.scadaSymbolContent);
});
}
setTimeout(() => {
this.updateContent(this.data.scadaSymbolContent);
});
} else if (propName === 'readonly') {
this.scadaSymbolEditObject.setReadOnly(this.readonly);
if (this.readonly) {
this.svgContentFormControl.disable({emitEvent: false});
} else {
this.svgContentFormControl.enable({emitEvent: false});
}
}
}
}
@ -131,7 +159,11 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
}
getContent(): string {
return this.scadaSymbolEditObject?.getContent();
if (this.editorMode === 'svg') {
return this.scadaSymbolEditObject?.getContent();
} else {
return this.svgContent;
}
}
zoomIn() {
@ -148,12 +180,33 @@ export class ScadaSymbolEditorComponent implements OnInit, OnDestroy, AfterViewI
this.scadaSymbolEditObject.showHiddenElements(this.showHiddenElements);
}
private updateEditorMode(mode: editorModeType) {
this.editorModeValue = mode;
if (mode === 'xml') {
this.svgContent = this.scadaSymbolEditObject.getContent();
this.svgContentFormControl.setValue(this.svgContent, {emitEvent: false});
} else {
this.updateEditObjectContent(this.svgContent);
}
}
private updateContent(content: string) {
this.displayShowHidden = false;
this.scadaSymbolEditObject.setContent(content);
setTimeout(() => {
this.updateZoomButtonsState();
});
this.svgContent = removeScadaSymbolMetadata(content);
if (this.editorMode === 'xml') {
this.svgContentFormControl.setValue(this.svgContent, {emitEvent: false});
} else {
this.updateEditObjectContent(this.svgContent);
}
}
private updateEditObjectContent(content: string) {
if (this.scadaSymbolEditObject) {
this.displayShowHidden = false;
this.scadaSymbolEditObject.setContent(content);
setTimeout(() => {
this.updateZoomButtonsState();
});
}
}
private updateZoomButtonsState() {

254
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -34,7 +34,7 @@ import {
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbHighlightRule } from '@shared/models/ace/ace.models';
import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models';
import { ValueType } from '@shared/models/constants';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
@ -54,6 +54,7 @@ export interface ScadaSymbolEditObjectCallbacks {
tagsUpdated: (tags: string[]) => void;
hasHiddenElements?: (hasHidden: boolean) => void;
onSymbolEditObjectDirty: (dirty: boolean) => void;
onSymbolEditObjectValid: (valid: boolean) => void;
onZoom?: () => void;
}
@ -915,91 +916,184 @@ export class ScadaSymbolElement {
}
const scadaSymbolCtxObjectHighlightRules: TbHighlightRule[] = [
{
class: 'scada-symbol-ctx',
regex: /(?<=\W|^)(ctx)(?=\W|$)\b/
}
];
const identifierRe = /[a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*/;
export const scadaSymbolGeneralStateRenderHighlightRules: TbHighlightRule[] =
scadaSymbolCtxObjectHighlightRules.concat({
class: 'scada-symbol-svg',
regex: /(?<=\W|^)(svg)(?=\W|$)\b/
});
const dotOperatorHighlightRule: AceHighlightRule = {
token: 'punctuation.operator',
regex: /[.](?![.])/,
};
export const scadaSymbolElementStateRenderHighlightRules: TbHighlightRule[] =
scadaSymbolCtxObjectHighlightRules.concat({
class: 'scada-symbol-element',
regex: /(?<=\W|^)(element)(?=\W|$)\b/
});
const endGroupHighlightRule: AceHighlightRule = {
regex: '',
token: 'empty',
next: 'no_regex'
};
export const scadaSymbolClickActionHighlightRules: TbHighlightRule[] =
scadaSymbolElementStateRenderHighlightRules.concat({
class: 'scada-symbol-event',
regex: /(?<=\W|^)(event)(?=\W|$)\b/
});
const scadaSymbolCtxObjectHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-ctx',
regex: /\bctx\b/,
next: 'scadaSymbolCtxApi'
};
const scadaSymbolCtxPropertyHighlightRules: TbHighlightRule[] = [
{
class: 'scada-symbol-ctx-properties',
regex: /(?<=ctx\.)(properties)\b/
},
{
class: 'scada-symbol-ctx-tags',
regex: /(?<=ctx\.)(tags)\b/
},
{
class: 'scada-symbol-ctx-values',
regex: /(?<=ctx\.)(values)\b/
},
{
class: 'scada-symbol-ctx-api',
regex: /(?<=ctx\.)(api)\b/
},
{
class: 'scada-symbol-ctx-svg',
regex: /(?<=ctx\.)(svg)\b/
},
{
class: 'scada-symbol-ctx-property',
regex: /(?<=ctx\.properties\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-tag',
regex: /(?<=ctx\.tags\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-value',
regex: /(?<=ctx\.values\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-api-method',
regex: /(?<=ctx\.api\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-svg-method',
regex: /(?<=ctx\.svg\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
}
];
export const scadaSymbolGeneralStateRenderPropertiesHighlightRules: TbHighlightRule[] =
scadaSymbolCtxPropertyHighlightRules.concat({
class: 'scada-symbol-svg-properties',
regex: /(?<=svg\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
});
const scadaSymbolSVGHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-svg',
regex: /\bsvg\b/,
next: 'scadaSymbolSVGApi'
};
export const scadaSymbolElementStateRenderPropertiesHighlightRules: TbHighlightRule[] =
scadaSymbolCtxPropertyHighlightRules.concat({
class: 'scada-symbol-element-properties',
regex: /(?<=element\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
});
const scadaSymbolElementHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-element',
regex: /\belement\b/,
next: 'scadaSymbolElementApi'
};
export const scadaSymbolClickActionPropertiesHighlightRules: TbHighlightRule[] =
scadaSymbolElementStateRenderPropertiesHighlightRules.concat({
class: 'scada-symbol-event-properties',
regex: /(?<=event\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
});
const scadaSymbolEventHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-event',
regex: /\bevent\b/,
next: 'scadaSymbolEventApi'
};
const scadaSymbolCtxApiHighlightRules: AceHighlightRules = {
scadaSymbolCtxApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-properties',
regex: /properties/,
next: 'scadaSymbolCtxPropertiesApi'
},
{
token: 'tb.scada-symbol-ctx-tags',
regex: /tags/,
next: 'scadaSymbolCtxTagsApi'
},
{
token: 'tb.scada-symbol-ctx-values',
regex: /values/,
next: 'scadaSymbolCtxValuesApi'
},
{
token: 'tb.scada-symbol-ctx-api',
regex: /api/,
next: 'scadaSymbolCtxApiMethodApi'
},
{
token: 'tb.scada-symbol-ctx-svg',
regex: /svg/,
next: 'scadaSymbolCtxSVGMethodApi'
},
endGroupHighlightRule
],
scadaSymbolCtxPropertiesApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-property',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxTagsApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-tag',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxValuesApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-value',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxApiMethodApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-api-method',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxSVGMethodApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-svg-method',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
const scadaSymbolSVGPropertyHighlightRules: AceHighlightRules = {
scadaSymbolSVGApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-svg-properties',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
const scadaSymbolElementPropertyHighlightRules: AceHighlightRules = {
scadaSymbolElementApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-element-properties',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
const scadaSymbolEventPropertyHighlightRules: AceHighlightRules = {
scadaSymbolEventApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-event-properties',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
export const scadaSymbolGeneralStateHighlightRules: AceHighlightRules = {
start: [
scadaSymbolCtxObjectHighlightRule,
scadaSymbolSVGHighlightRule
],
...scadaSymbolCtxApiHighlightRules,
...scadaSymbolSVGPropertyHighlightRules
};
export const scadaSymbolRenderFunctionHighlightRules: AceHighlightRules = {
no_regex: [
scadaSymbolCtxObjectHighlightRule,
scadaSymbolElementHighlightRule
],
...scadaSymbolCtxApiHighlightRules,
...scadaSymbolElementPropertyHighlightRules
};
export const scadaSymbolClickActionHighlightRules: AceHighlightRules = {
start: [
scadaSymbolCtxObjectHighlightRule,
scadaSymbolElementHighlightRule,
scadaSymbolEventHighlightRule
],
...scadaSymbolCtxApiHighlightRules,
...scadaSymbolElementPropertyHighlightRules,
...scadaSymbolEventPropertyHighlightRules
};
export const generalStateRenderFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => ({
ctx: ctxCompletion,

6
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html

@ -85,7 +85,7 @@
formControlName="metadata">
<div class="tb-scada-symbol-metadata-header-prefix">
<button fxHide.gt-sm
[disabled]="scadaSymbolFormGroup.invalid"
[disabled]="scadaSymbolFormGroup.invalid || !symbolEditorValid"
mat-button color="primary"
(click)="enterPreviewMode()">
<mat-icon>visibility</mat-icon>
@ -94,7 +94,7 @@
</div>
<div fxFlex.lt-md class="tb-scada-symbol-metadata-header-suffix" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="12px">
<button fxHide.lt-md
[disabled]="scadaSymbolFormGroup.invalid"
[disabled]="scadaSymbolFormGroup.invalid || !symbolEditorValid"
mat-button color="primary"
(click)="enterPreviewMode()">
<mat-icon>visibility</mat-icon>
@ -107,7 +107,7 @@
<mat-icon>close</mat-icon>
{{ 'action.decline' | translate }}
</button>
<button [disabled]="!scadaSymbolFormGroup.valid || !(scadaSymbolFormGroup.dirty || symbolEditorDirty)"
<button [disabled]="!scadaSymbolFormGroup.valid || !symbolEditorValid || !(scadaSymbolFormGroup.dirty || symbolEditorDirty)"
mat-flat-button color="primary"
(click)="onApplyScadaSymbolConfig()">
<mat-icon>done</mat-icon>

8
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts

@ -135,6 +135,8 @@ export class ScadaSymbolComponent extends PageComponent
symbolEditorDirty = false;
symbolEditorValid = true;
private previewScadaSymbolObjectSettings: ScadaSymbolObjectSettings;
private forcePristine = false;
@ -338,6 +340,10 @@ export class ScadaSymbolComponent extends PageComponent
this.symbolEditorDirty = dirty;
}
onSymbolEditObjectValid(valid: boolean) {
this.symbolEditorValid = valid;
}
updateScadaSymbol() {
this.dialog.open<UploadImageDialogComponent, UploadImageDialogData,
UploadImageDialogResult>(UploadImageDialogComponent, {
@ -354,6 +360,7 @@ export class ScadaSymbolComponent extends PageComponent
scadaSymbolContent: this.symbolData.scadaSymbolContent
};
this.symbolEditorDirty = true;
this.symbolEditorValid = true;
}
});
}
@ -471,6 +478,7 @@ export class ScadaSymbolComponent extends PageComponent
}
this.scadaSymbolFormGroup.markAsPristine();
this.symbolEditorDirty = false;
this.symbolEditorValid = true;
this.cd.markForCheck();
}
}

35
ui-ngx/src/app/shared/components/js-func.component.ts

@ -25,15 +25,15 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, Validator } from '@angular/forms';
import { Ace } from 'ace-builds';
import { getAce, Range, TbHighlightRule } from '@shared/models/ace/ace.models';
import { AceHighlightRules, getAce, Range } from '@shared/models/ace/ace.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, guid, isUndefined } from '@app/core/utils';
import { guid, isUndefined } from '@app/core/utils';
import { TranslateService } from '@ngx-translate/core';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
@ -90,9 +90,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
@Input() editorCompleter: TbEditorCompleter;
@Input() propertyHighlightRules: TbHighlightRule[];
@Input() objectHighlightRules: TbHighlightRule[];
@Input() highlightRules: AceHighlightRules;
@Input() globalVariables: Array<string>;
@ -220,27 +218,16 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
});
}
// @ts-ignore
if ((this.propertyHighlightRules?.length || this.objectHighlightRules?.length) && !!this.jsEditor.session.$mode) {
if (!!this.highlightRules && !!this.jsEditor.session.$mode) {
// @ts-ignore
const newMode = new this.jsEditor.session.$mode.constructor();
newMode.$highlightRules = new newMode.HighlightRules();
if (this.propertyHighlightRules?.length) {
const propertiesRules: { token: string; regex: RegExp }[] = newMode.$highlightRules.$rules.property;
const index = propertiesRules.findIndex(p => p.token === 'support.constant');
const additionalPropertyRules: { token: string; regex: RegExp }[] = this.propertyHighlightRules.map(r => ({
token: `tb.${r.class}`,
regex: r.regex
}));
propertiesRules.splice(index, 0, ...additionalPropertyRules);
}
if (this.objectHighlightRules?.length) {
const noRegexRules: { token: string; regex: RegExp }[] = newMode.$highlightRules.$rules.no_regex;
const index = noRegexRules.findIndex(p => Array.isArray(p.token) && p.token[0] === 'support.constant');
const additionalNoRegexRules: { token: string; regex: RegExp }[] = this.objectHighlightRules.map(r => ({
token: `tb.${r.class}`,
regex: r.regex
}));
noRegexRules.splice(index, 0, ...additionalNoRegexRules);
for(const group in this.highlightRules) {
if(!!newMode.$highlightRules.$rules[group]) {
newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]);
} else {
newMode.$highlightRules.$rules[group] = this.highlightRules[group];
}
}
// @ts-ignore
this.jsEditor.session.$onChangeMode(newMode);

38
ui-ngx/src/app/shared/components/svg-xml.component.html

@ -0,0 +1,38 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-svg-xml" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight, 'no-label': noLabel}"
tb-fullscreen
[fullscreen]="fullscreen" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;" class="tb-svg-xml-toolbar">
<label *ngIf="!noLabel" class="tb-title no-padding" [ngClass]="{'tb-error': !disabled && (hasErrors || required && !modelValue), 'tb-required': !disabled && required}">{{ label }}</label>
<span fxFlex></span>
<fieldset style="width: initial">
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"
style="border-radius: 50%"
(click)="fullscreen = !fullscreen">
<button type='button' mat-icon-button class="tb-mat-32">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
</fieldset>
</div>
<div id="tb-svg-xml-panel" class="tb-svg-xml-content-panel" fxLayout="column">
<div #svgXmlEditor id="tb-svg-xml-input" [ngStyle]="fillHeight ? {} : {minHeight: minHeight}" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

85
ui-ngx/src/app/shared/components/svg-xml.component.scss

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-svg-xml {
position: relative;
&.tb-disabled {
color: rgba(0, 0, 0, .38);
}
&.fill-height {
height: 100%;
}
&.no-label {
.tb-svg-xml-content-panel {
height: 100%;
}
.tb-svg-xml-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
pointer-events: none;
button {
pointer-events: auto;
}
}
}
.tb-svg-xml-content-panel {
height: calc(100% - 40px);
border: 1px solid #c0c0c0;
#tb-svg-xml-input {
width: 100%;
min-width: 200px;
height: 100%;
}
}
&:not(.tb-fullscreen) {
padding-bottom: 15px;
}
.tb-svg-xml-toolbar {
& > * {
&:not(:last-child) {
margin-right: 4px;
}
}
button.mat-mdc-button-base, button.mat-mdc-button-base.tb-mat-32 {
background: rgba(220, 220, 220, .35);
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
font-size: .8rem;
line-height: 15px;
&:not(.tb-help-popup-button) {
color: #7b7b7b;
}
}
button.mat-mdc-button-base:not(.mat-mdc-icon-button) {
height: 23px;
}
.tb-help-popup-button-loading {
background: #f3f3f3;
}
}
}

211
ui-ngx/src/app/shared/components/svg-xml.component.ts

@ -0,0 +1,211 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { Ace } from 'ace-builds';
import { getAce } from '@shared/models/ace/ace.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-svg-xml',
templateUrl: './svg-xml.component.html',
styleUrls: ['./svg-xml.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SvgXmlComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SvgXmlComponent),
multi: true,
}
],
encapsulation: ViewEncapsulation.None
})
export class SvgXmlComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
@ViewChild('svgXmlEditor', {static: true})
svgXmlEditorElmRef: ElementRef;
private svgXmlEditor: Ace.Editor;
private editorsResizeCaf: CancelAnimationFrame;
private editorResize$: ResizeObserver;
private ignoreChange = false;
@Input() label: string;
@Input() disabled: boolean;
@Input()
@coerceBoolean()
fillHeight = false;
@Input()
@coerceBoolean()
noLabel = false;
@Input() minHeight = '200px';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
fullscreen = false;
modelValue: string;
hasErrors = false;
private propagateChange = null;
constructor(public elementRef: ElementRef,
private utils: UtilsService,
private translate: TranslateService,
protected store: Store<AppState>,
private raf: RafService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
const editorElement = this.svgXmlEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: 'ace/mode/svg',
showGutter: true,
showPrintMargin: true,
readOnly: this.disabled
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
getAce().subscribe(
(ace) => {
this.svgXmlEditor = ace.edit(editorElement, editorOptions);
this.svgXmlEditor.session.setUseWrapMode(true);
this.svgXmlEditor.setValue(this.modelValue ? this.modelValue : '', -1);
this.svgXmlEditor.setReadOnly(this.disabled);
this.svgXmlEditor.on('change', () => {
if (!this.ignoreChange) {
this.updateView();
}
});
// @ts-ignore
this.svgXmlEditor.session.on('changeAnnotation', () => {
const annotations = this.svgXmlEditor.session.getAnnotations();
const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0;
if (this.hasErrors !== hasErrors) {
this.hasErrors = hasErrors;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
});
this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize();
});
this.editorResize$.observe(editorElement);
}
);
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
}
if (this.svgXmlEditor) {
this.svgXmlEditor.destroy();
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.svgXmlEditor.resize();
this.svgXmlEditor.renderer.updateFull();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.svgXmlEditor) {
this.svgXmlEditor.setReadOnly(this.disabled);
}
}
public validate(c: UntypedFormControl) {
return (!this.hasErrors) ? null : {
svgXml: {
valid: false,
},
};
}
writeValue(value: string): void {
this.modelValue = value;
if (this.svgXmlEditor) {
this.ignoreChange = true;
this.svgXmlEditor.setValue(this.modelValue ? this.modelValue : '', -1);
this.ignoreChange = false;
}
}
updateView() {
const editorValue = this.svgXmlEditor.getValue();
if (this.modelValue !== editorValue) {
this.modelValue = editorValue;
this.propagateChange(this.modelValue);
this.cd.markForCheck();
}
}
}

31
ui-ngx/src/app/shared/components/toggle-header.component.html

@ -20,16 +20,26 @@
[disabled]="!leftPaginationEnabled"
(click)="handlePaginatorClick('before', $event)"
(touchstart)="handlePaginatorTouchStart('before', $event)"
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': !isMdLg, 'tb-mat-24': isMdLg}">
class="tb-toggle-header-pagination-button"
[class.fill-height]="fillHeight"
[class.tb-mat-32]="!isMdLg"
[class.tb-mat-24]="isMdLg">
<mat-icon>chevron_left</mat-icon>
</button>
<div #toggleGroupContainer class="tb-toggle-container" [class]="{'tb-disable-pagination': disablePagination}" *ngIf="(useSelect$ | async) === false; else select" >
<div #toggleGroupContainer class="tb-toggle-container"
[class.tb-disable-pagination]="disablePagination"
[class.fill-height]="fillHeight"
[class.extra-padding]="extraPadding"
*ngIf="(useSelect$ | async) === false; else select" >
<mat-button-toggle-group #toggleGroup
class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize,
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value"
[class.tb-fill]="(appearance === 'fill' || appearance === 'fill-invert')"
[class.tb-invert]="appearance === 'fill-invert'"
[class.tb-primary-fill]="primaryBackground"
[class.tb-ignore-md-lg]="ignoreMdLgSize"
[class.tb-disabled]="disabled"
[name]="name"
[(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
@ -39,11 +49,16 @@
[disabled]="!rightPaginationEnabled"
(click)="handlePaginatorClick('after', $event)"
(touchstart)="handlePaginatorTouchStart('after', $event)"
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': !isMdLg, 'tb-mat-24': isMdLg}">
class="tb-toggle-header-pagination-button"
[class.fill-height]="fillHeight"
[class.tb-mat-32]="!isMdLg"
[class.tb-mat-24]="isMdLg">
<mat-icon>chevron_right</mat-icon>
</button>
<ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="tb-toggle-header-select"
[class.fill-height]="fillHeight"
subscriptSizing="dynamic">
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" [disabled]="disabled">
<mat-option *ngFor="let option of options" [value]="option.value"> {{ option.name }}</mat-option>
</mat-select>

38
ui-ngx/src/app/shared/components/toggle-header.component.scss

@ -21,6 +21,9 @@
grid-template-columns: min-content minmax(auto, 1fr) min-content;
.tb-toggle-header-pagination-button {
display: none;
&.fill-height {
height: 100%;
}
}
&.tb-toggle-header-pagination-controls-enabled {
.tb-toggle-header-pagination-button {
@ -43,6 +46,25 @@
}
:host ::ng-deep {
.tb-toggle-container {
&.fill-height {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
height: 100%;
.mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-button {
height: 100%;
}
}
}
}
&.extra-padding {
.mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-label-content {
padding: 0 20px;
}
}
}
}
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
overflow: visible;
width: 100%;
@ -51,6 +73,12 @@
padding: 2px;
border: none;
background: rgba(0, 0, 0, 0.06);
&.tb-primary-fill {
background: none;
&:before {
border-radius: 100px;
}
}
.mat-button-toggle + .mat-button-toggle {
border-left: none;
}
@ -161,6 +189,16 @@
&.mat-mdc-form-field {
line-height: 16px;
font-size: 12px;
&.fill-height {
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-flex {
margin: auto;
}
}
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
}
}
.mat-mdc-text-field-wrapper.mdc-text-field--outlined .mat-mdc-form-field-infix {
min-height: 0;

12
ui-ngx/src/app/shared/components/toggle-header.component.ts

@ -187,6 +187,18 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterV
@coerceBoolean()
disabled = false;
@Input()
@coerceBoolean()
fillHeight = false;
@Input()
@coerceBoolean()
extraPadding = false;
@Input()
@coerceBoolean()
primaryBackground = false;
get isMdLg(): boolean {
return !this.ignoreMdLgSize && this.isMdLgValue;
}

4
ui-ngx/src/app/shared/components/toggle-select.component.html

@ -16,10 +16,14 @@
-->
<tb-toggle-header
[class.fill-height]="fillHeight"
ignoreMdLgSize="true"
useSelectOnMdLg="false"
[disabled]="disabled"
[appearance]="appearance"
[fillHeight]="fillHeight"
[extraPadding]="extraPadding"
[primaryBackground]="primaryBackground"
[disablePagination]="disablePagination"
[selectMediaBreakpoint]="selectMediaBreakpoint"
[options]="options"

22
ui-ngx/src/app/shared/components/toggle-select.component.scss

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
tb-toggle-header {
&.fill-height {
height: 100%;
}
}
}

14
ui-ngx/src/app/shared/components/toggle-select.component.ts

@ -24,7 +24,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-toggle-select',
templateUrl: './toggle-select.component.html',
styleUrls: [],
styleUrls: ['./toggle-select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -52,6 +52,18 @@ export class ToggleSelectComponent extends _ToggleBase implements ControlValueAc
@coerceBoolean()
disablePagination = false;
@Input()
@coerceBoolean()
fillHeight = false;
@Input()
@coerceBoolean()
extraPadding = false;
@Input()
@coerceBoolean()
primaryBackground = false;
modelValue: any;
private propagateChange = null;

17
ui-ngx/src/app/shared/models/ace/ace.models.ts

@ -37,6 +37,8 @@ function loadAceDependencies(): Observable<any> {
aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-xml')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-svg')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
@ -47,6 +49,8 @@ function loadAceDependencies(): Observable<any> {
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/xml')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/svg')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf')));
aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
@ -348,8 +352,15 @@ export class Range implements Ace.Range {
}
export interface TbHighlightRule {
class: string;
regex: RegExp;
export interface AceHighlightRules {
[group: string]: Array<AceHighlightRule>;
}
export interface AceHighlightRule {
regex: RegExp | string;
token: string;
next?: string;
}

3
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -43,6 +43,8 @@ export interface WidgetLayout {
mobileOrder?: number;
col?: number;
row?: number;
resizable?: boolean;
preserveAspectRatio?: boolean;
}
export interface WidgetLayouts {
@ -95,7 +97,6 @@ export interface GridSettings {
mobileRowHeight?: number;
mobileDisplayLayoutFirst?: boolean;
layoutDimension?: LayoutDimension;
[key: string]: any;
}
export interface DashboardLayout {

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

@ -761,6 +761,8 @@ export interface WidgetConfig {
displayTimewindow?: boolean;
timewindow?: Timewindow;
timewindowStyle?: TimewindowStyle;
resizable?: boolean;
preserveAspectRatio?: boolean;
desktopHide?: boolean;
mobileHide?: boolean;
mobileHeight?: number;

3
ui-ngx/src/app/shared/shared.module.ts

@ -224,6 +224,7 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { ScadaSymbolInputComponent } from '@shared/components/image/scada-symbol-input.component';
import { CountryAutocompleteComponent } from '@shared/components/country-autocomplete.component';
import { CountryData } from '@shared/models/country.models';
import { SvgXmlComponent } from '@shared/components/svg-xml.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -338,6 +339,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
JsFuncComponent,
CssComponent,
HtmlComponent,
SvgXmlComponent,
FabTriggerDirective,
FabActionsDirective,
FabToolbarComponent,
@ -544,6 +546,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
JsFuncComponent,
CssComponent,
HtmlComponent,
SvgXmlComponent,
FabTriggerDirective,
FabActionsDirective,
TbJsonToStringDirective,

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

@ -3661,6 +3661,8 @@
"update-symbol": "Update SCADA symbol",
"edit-symbol": "Edit SCADA symbol",
"symbol-details": "SCADA symbol details",
"mode-svg": "SVG",
"mode-xml": "XML",
"no-symbols": "No symbols found",
"show-hidden-elements": "Show hidden elements",
"hide-hidden-elements": "Hide hidden elements",
@ -3835,6 +3837,8 @@
"open-hint": "Action invoked when user clicks to open component.",
"close": "Close",
"close-hint": "Action invoked when user clicks to close component.",
"close-state-animation": "Closed state animation",
"close-state-animation-hint": "Whether to enable blinking animation when component is in closed state.",
"opened-color": "Opened color",
"closed-color": "Closed color",
"opened-rotation-angle": "Opened rotation angle",
@ -3880,7 +3884,14 @@
"stand-filter-color": "Stand filter color",
"mode-box-background": "Mode box background",
"border-color": "Border color",
"label-color": "Label color"
"label-color": "Label color",
"water-leak-hint": "Indicates whether a leak.",
"default-color": "Default color",
"leak-color": "Leak color",
"full-value": "Full value",
"full-value-hint": "Double value indicating full value.",
"label": "Label",
"icon": "Icon"
}
},
"item": {
@ -6045,7 +6056,11 @@
"color": "Color",
"tooltip": "Tooltip",
"units-required": "Unit is required.",
"list-layout": "List layout"
"list-layout": "List layout",
"layout": "Layout",
"resize-options": "Resize options",
"resizable": "Resizable",
"preserve-aspect-ratio": "Preserve aspect ratio"
},
"widget-type": {
"import": "Import widget type",

Loading…
Cancel
Save