Browse Source

Merge branch 'rc' of github.com:thingsboard/thingsboard into master-rc

pull/12289/head
ViacheslavKlimov 2 years ago
parent
commit
ead308470b
  1. 8
      application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg
  2. 8
      application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg
  3. 8
      application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg
  4. 8
      application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg
  5. 19
      application/src/main/data/upgrade/basic/schema_update.sql
  6. 2
      application/src/main/java/org/thingsboard/server/controller/QrCodeSettingsController.java
  7. 36
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  8. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  10. 24
      application/src/main/resources/thingsboard.yml
  11. 28
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java
  12. 4
      dao/src/main/java/org/thingsboard/server/dao/domain/DomainServiceImpl.java
  13. 15
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  14. 32
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DomainDataValidator.java
  15. 2
      dao/src/main/resources/sql/schema-entities.sql
  16. 2
      dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java
  17. 8
      docker/docker-upgrade-tb.sh
  18. 8
      msa/tb-node/docker/start-tb-node.sh
  19. 8
      msa/tb/docker/upgrade-tb.sh
  20. 8
      packaging/java/scripts/install/upgrade.sh
  21. 8
      packaging/java/scripts/install/upgrade_dev_db.sh
  22. 6
      packaging/java/scripts/windows/upgrade.bat
  23. 24
      transport/coap/src/main/resources/tb-coap-transport.yml
  24. 59
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  25. 6
      ui-ngx/src/app/core/services/item-buffer.service.ts
  26. 2
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts
  27. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  28. 13
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  29. 11
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html
  30. 68
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts
  31. 15
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html
  32. 20
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts
  33. 4
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  34. 2
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html
  35. 2
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  36. 1
      ui-ngx/src/app/modules/home/components/public-api.ts
  37. 3
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts
  38. 27
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/usage-info-widget.component.scss
  39. 6
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts
  40. 5
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  41. 6
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  42. 3
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  43. 1
      ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html
  44. 8
      ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts
  45. 20
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html
  46. 2
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts
  47. 2
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts
  48. 8
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html
  49. 6
      ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts
  50. 2
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html
  51. 19
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts
  52. 1
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss
  53. 12
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts
  54. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  55. 3
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  56. 6
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  57. 18
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  58. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts
  59. 36
      ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts
  60. 22
      ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html
  61. 6
      ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts
  62. 3
      ui-ngx/src/app/shared/components/time/timeinterval.component.ts
  63. 10
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html
  64. 16
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
  65. 16
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  66. 102
      ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts
  67. 4
      ui-ngx/src/app/shared/models/constants.ts
  68. 6
      ui-ngx/src/app/shared/models/entity.models.ts
  69. 6
      ui-ngx/src/app/shared/models/rule-node.models.ts
  70. 17
      ui-ngx/src/app/shared/models/tenant.model.ts
  71. 17
      ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts
  72. 52
      ui-ngx/src/assets/help/en_US/resource/js-resource-module_fn.md
  73. 18
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-bytes-expression_fn.md
  74. 0
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-expression_fn.md
  75. 78
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-json-expression_fn.md
  76. 6
      ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-json-key-expression_fn.md
  77. 1
      ui-ngx/src/assets/locale/locale.constant-ar_AE.json
  78. 3
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  79. 3
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  80. 3
      ui-ngx/src/assets/locale/locale.constant-da_DK.json
  81. 3
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  82. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  83. 1
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  84. 3
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  85. 3
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  86. 1
      ui-ngx/src/assets/locale/locale.constant-lt_LT.json
  87. 3
      ui-ngx/src/assets/locale/locale.constant-lv_LV.json
  88. 3
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  89. 1
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json
  90. 3
      ui-ngx/src/assets/locale/locale.constant-pt_BR.json
  91. 1
      ui-ngx/src/assets/locale/locale.constant-ro_RO.json
  92. 3
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  93. 3
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  94. 2
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  95. 1
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  96. 3
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json
  97. 3
      ui-ngx/tailwind.config.js

8
application/src/main/data/json/system/scada_symbols/dynamic-horizontal-scale-hp.svg

@ -18,12 +18,12 @@
},
{
"tag": "highCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighCriticalScale && highCriticalState !== null) {\n element.show();\n var offset = calculateOffset(highCriticalState, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null && highCriticalState !== null) {\n if (value >= highCriticalState && value <= ctx.properties.maxValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighCriticalScale && highCriticalState !== null) {\n element.show();\n var offset = calculateOffset(highCriticalState, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null && highCriticalState !== null) {\n if (value >= highCriticalState) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "highWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar highWarningState = ctx.values.highWarningState;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighWarningScale && highWarningState !== null) {\n element.show();\n var offset = calculateOffset(highWarningState, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighWarningScale && value !== null) {\n if (!showHighCriticalScale) {\n highCriticalState = ctx.properties.maxValue;\n }\n \n if (highWarningState !== null && highCriticalState !== null) {\n if (value < highCriticalState && value >= highWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar highWarningState = ctx.values.highWarningState;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighWarningScale && highWarningState !== null) {\n element.show();\n var offset = calculateOffset(highWarningState, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighWarningScale && value !== null) {\n if (!showHighCriticalScale) {\n highCriticalState = Number.MAX_SAFE_INTEGER;\n }\n \n if (highWarningState !== null && highCriticalState !== null) {\n if (value < highCriticalState && value >= highWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"actions": null
},
{
@ -33,12 +33,12 @@
},
{
"tag": "lowCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalValue = ctx.values.lowCriticalState;\n\nif (showLowCriticalScale && lowCriticalValue !== null) {\n element.show();\n var offset = calculateOffset(lowCriticalValue, minValue, maxValue);\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowCriticalScale && value !== null) {\n var lowCriticalScale = ctx.values.lowCriticalState;\n if (value <= lowCriticalScale && value >= ctx.properties.minValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalValue = ctx.values.lowCriticalState;\n\nif (showLowCriticalScale && lowCriticalValue !== null) {\n element.show();\n var offset = calculateOffset(lowCriticalValue, minValue, maxValue);\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowCriticalScale && value !== null) {\n var lowCriticalScale = ctx.values.lowCriticalState;\n if (value <= lowCriticalScale) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "lowWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningState = ctx.values.lowWarningState;\nvar lowCriticalState = ctx.values.lowCriticalState;\nif (showLowWarningScale && lowWarningState !== null) {\n element.show();\n var offset = calculateOffset(lowWarningState, minValue, maxValue);\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowWarningScale && value !== null) {\n if (!showLowCriticalScale) {\n lowCriticalState = ctx.properties.minValue;\n }\n if (lowCriticalState !== null && lowWarningState !== null) {\n if (value > lowCriticalState && value <= lowWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningState = ctx.values.lowWarningState;\nvar lowCriticalState = ctx.values.lowCriticalState;\nif (showLowWarningScale && lowWarningState !== null) {\n element.show();\n var offset = calculateOffset(lowWarningState, minValue, maxValue);\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowWarningScale && value !== null) {\n if (!showLowCriticalScale) {\n lowCriticalState = Number.MIN_SAFE_INTEGER;\n }\n if (lowCriticalState !== null && lowWarningState !== null) {\n if (value > lowCriticalState && value <= lowWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

8
application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg

@ -18,12 +18,12 @@
},
{
"tag": "highCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighCriticalScale && highCriticalState !== null) {\n element.show();\n var offset = calculateOffset(highCriticalState, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null && highCriticalState !== null) {\n if (value >= highCriticalState && value <= ctx.properties.maxValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighCriticalScale && highCriticalState !== null) {\n element.show();\n var offset = calculateOffset(highCriticalState, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null && highCriticalState !== null) {\n if (value >= highCriticalState) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "highWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar highWarningState = ctx.values.highWarningState;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighWarningScale && highWarningState !== null) {\n element.show();\n var offset = calculateOffset(highWarningState, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\nif (showHighWarningScale && value !== null) {\n if (!showHighCriticalScale) {\n highCriticalState = ctx.properties.maxValue;\n }\n if (highWarningState !== null && highCriticalState !== null) {\n if (value < highCriticalState && value >= highWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar highWarningState = ctx.values.highWarningState;\nvar highCriticalState = ctx.values.highCriticalState;\nif (showHighWarningScale && highWarningState !== null) {\n element.show();\n var offset = calculateOffset(highWarningState, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\nif (showHighWarningScale && value !== null) {\n if (!showHighCriticalScale) {\n highCriticalState = Number.MAX_SAFE_INTEGER;\n }\n if (highWarningState !== null && highCriticalState !== null) {\n if (value < highCriticalState && value >= highWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"actions": null
},
{
@ -33,12 +33,12 @@
},
{
"tag": "lowCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalValue = ctx.values.lowCriticalState;\n\nif (showLowCriticalScale && lowCriticalValue !== null) {\n element.show();\n var offset = calculateOffset(lowCriticalValue, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\nif (showLowCriticalScale && value !== null) {\n var lowCriticalScale = ctx.values.lowCriticalState;\n if (value <= lowCriticalScale && value >= ctx.properties.minValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalValue = ctx.values.lowCriticalState;\n\nif (showLowCriticalScale && lowCriticalValue !== null) {\n element.show();\n var offset = calculateOffset(lowCriticalValue, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\nif (showLowCriticalScale && value !== null) {\n var lowCriticalScale = ctx.values.lowCriticalState;\n if (value <= lowCriticalScale) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "lowWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningState = ctx.values.lowWarningState;\nvar lowCriticalState = ctx.values.lowCriticalState;\nif (showLowWarningScale && lowWarningState !== null) {\n element.show();\n var offset = calculateOffset(lowWarningState, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\nif (showLowWarningScale && value !== null) {\n if (!showLowCriticalScale) {\n lowCriticalState = ctx.properties.minValue;\n }\n if (lowCriticalState !== null && lowWarningState !== null) {\n if (value > lowCriticalState && value <= lowWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningState = ctx.values.lowWarningState;\nvar lowCriticalState = ctx.values.lowCriticalState;\nif (showLowWarningScale && lowWarningState !== null) {\n element.show();\n var offset = calculateOffset(lowWarningState, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\nif (showLowWarningScale && value !== null) {\n if (!showLowCriticalScale) {\n lowCriticalState = Number.MIN_SAFE_INTEGER;\n }\n if (lowCriticalState !== null && lowWarningState !== null) {\n if (value > lowCriticalState && value <= lowWarningState) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n }\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

8
application/src/main/data/json/system/scada_symbols/simple-horizontal-scale-hp.svg

@ -18,12 +18,12 @@
},
{
"tag": "highCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalValue = ctx.properties.highCriticalScale;\nvar value = ctx.values.value;\n\nif (showHighCriticalScale) {\n element.show();\n var offset = calculateOffset(highCriticalValue, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (value >= highCriticalScale && value <= ctx.properties.maxValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalValue = ctx.properties.highCriticalScale;\nvar value = ctx.values.value;\n\nif (showHighCriticalScale) {\n element.show();\n var offset = calculateOffset(highCriticalValue, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\nif (showHighCriticalScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (value >= highCriticalScale) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "highWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highWarningValue = ctx.properties.highWarningScale;\nvar value = ctx.values.value;\n\nif (showHighWarningScale) {\n element.show();\n var offset = calculateOffset(highWarningValue, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighWarningScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (!showHighCriticalScale) {\n highCriticalScale = ctx.properties.maxValue;\n }\n var highWarningScale = ctx.properties.highWarningScale;\n if (value < highCriticalScale && value >= highWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highWarningValue = ctx.properties.highWarningScale;\nvar value = ctx.values.value;\n\nif (showHighWarningScale) {\n element.show();\n var offset = calculateOffset(highWarningValue, minValue, maxValue);\n element.width(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighWarningScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (!showHighCriticalScale) {\n highCriticalScale = Number.MAX_SAFE_INTEGER;\n }\n var highWarningScale = ctx.properties.highWarningScale;\n if (value < highCriticalScale && value >= highWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"actions": null
},
{
@ -33,12 +33,12 @@
},
{
"tag": "lowCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalScale = ctx.properties.lowCriticalScale;\n\nif (showLowCriticalScale) {\n element.show();\n var offset = calculateOffset(lowCriticalScale, minValue, maxValue);\n var childrenElement = element.children();\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowCriticalScale && value !== null) {\n if (value <= lowCriticalScale && value >= ctx.properties.minValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalScale = ctx.properties.lowCriticalScale;\n\nif (showLowCriticalScale) {\n element.show();\n var offset = calculateOffset(lowCriticalScale, minValue, maxValue);\n var childrenElement = element.children();\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowCriticalScale && value !== null) {\n if (value <= lowCriticalScale) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "lowWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningScale = ctx.properties.lowWarningScale;\nif (showLowWarningScale) {\n element.show();\n var offset = calculateOffset(lowWarningScale, minValue, maxValue);\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowWarningScale && value !== null) {\n var lowCriticalScale = ctx.properties.lowCriticalScale;\n if (!showLowCriticalScale) {\n lowCriticalScale = ctx.properties.minValue;\n }\n if (value > lowCriticalScale && value <= lowWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningScale = ctx.properties.lowWarningScale;\nif (showLowWarningScale) {\n element.show();\n var offset = calculateOffset(lowWarningScale, minValue, maxValue);\n element.width(offset);\n} else {\n element.hide();\n}\n\nif (showLowWarningScale && value !== null) {\n var lowCriticalScale = ctx.properties.lowCriticalScale;\n if (!showLowCriticalScale) {\n lowCriticalScale = Number.MIN_SAFE_INTEGER;\n }\n if (value > lowCriticalScale && value <= lowWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

8
application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg

@ -18,12 +18,12 @@
},
{
"tag": "highCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalScale = ctx.properties.highCriticalScale;\n\nif (showHighCriticalScale) {\n element.show();\n var offset = calculateOffset(highCriticalScale, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (value >= highCriticalScale && value <= ctx.properties.maxValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highCriticalScale = ctx.properties.highCriticalScale;\n\nif (showHighCriticalScale) {\n element.show();\n var offset = calculateOffset(highCriticalScale, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\n\nif (showHighCriticalScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (value >= highCriticalScale) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "highWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highWarningValue = ctx.properties.highWarningScale;\nif (showHighWarningScale) {\n element.show();\n var offset = calculateOffset(highWarningValue, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\nif (showHighWarningScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (!showHighCriticalScale) {\n highCriticalScale = ctx.properties.maxValue;\n }\n var highWarningScale = ctx.properties.highWarningScale;\n if (value < highCriticalScale && value >= highWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showHighWarningScale = ctx.properties.showHighWarningScale;\nvar showHighCriticalScale = ctx.properties.showHighCriticalScale;\nvar highWarningValue = ctx.properties.highWarningScale;\nif (showHighWarningScale) {\n element.show();\n var offset = calculateOffset(highWarningValue, minValue, maxValue);\n element.height(653-offset);\n} else {\n element.hide();\n}\nif (showHighWarningScale && value !== null) {\n var highCriticalScale = ctx.properties.highCriticalScale;\n if (!showHighCriticalScale) {\n highCriticalScale = Number.MAX_SAFE_INTEGER;\n }\n var highWarningScale = ctx.properties.highWarningScale;\n if (value < highCriticalScale && value >= highWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"actions": null
},
{
@ -33,12 +33,12 @@
},
{
"tag": "lowCriticalScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalScale = ctx.properties.lowCriticalScale;\nif (showLowCriticalScale) {\n element.show();\n var offset = calculateOffset(lowCriticalScale, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\n\nif (showLowCriticalScale && value !== null) {\n if (value <= lowCriticalScale && value >= ctx.properties.minValue) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowCriticalScale = ctx.properties.lowCriticalScale;\nif (showLowCriticalScale) {\n element.show();\n var offset = calculateOffset(lowCriticalScale, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\n\nif (showLowCriticalScale && value !== null) {\n if (value <= lowCriticalScale) {\n element.fill(ctx.properties.activeCriticalScaleColor);\n } else {\n element.fill(ctx.properties.defaultCriticalScaleColor)\n }\n}",
"actions": null
},
{
"tag": "lowWarningScale",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningScale = ctx.properties.lowWarningScale;\nif (showLowWarningScale) {\n element.show();\n var offset = calculateOffset(lowWarningScale, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\n\nif (showLowWarningScale && value !== null) {\n var lowCriticalScale = ctx.properties.lowCriticalScale;\n if (!showLowCriticalScale) {\n lowCriticalScale = ctx.properties.minValue;\n }\n if (value > lowCriticalScale && value <= lowWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"stateRenderFunction": "function calculateOffset(value, minValue, maxValue) {\n var clampedValue = Math.max(minValue, Math.min(value, maxValue));\n var normalizedValue = (clampedValue - minValue) / (maxValue - minValue);\n var offset = normalizedValue * 653;\n return offset;\n}\n\nvar value = ctx.values.value;\nvar minValue = ctx.properties.minValue;\nvar maxValue = ctx.properties.maxValue;\nvar showLowWarningScale = ctx.properties.showLowWarningScale;\nvar showLowCriticalScale = ctx.properties.showLowCriticalScale;\nvar lowWarningScale = ctx.properties.lowWarningScale;\nif (showLowWarningScale) {\n element.show();\n var offset = calculateOffset(lowWarningScale, minValue, maxValue);\n element.height(offset);\n} else {\n element.hide();\n}\nif (showLowWarningScale && value !== null) {\n var lowCriticalScale = ctx.properties.lowCriticalScale;\n if (!showLowCriticalScale) {\n lowCriticalScale = Number.MIN_SAFE_INTEGER;\n }\n if (value > lowCriticalScale && value <= lowWarningScale) {\n element.fill(ctx.properties.activeWarningScaleColor);\n } else {\n element.fill(ctx.properties.defaultWarningScaleColor);\n }\n}",
"actions": null
},
{

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

19
application/src/main/data/upgrade/basic/schema_update.sql

@ -143,7 +143,7 @@ $$
LOOP
generatedBundleId := NULL;
-- migrate android config
IF (qrCodeRecord.android_config IS NOT NULL AND qrCodeRecord.android_config::jsonb -> 'appPackage' IS NOT NULL) THEN
IF (qrCodeRecord.android_config::jsonb ->> 'appPackage' IS NOT NULL) THEN
androidPkgName := qrCodeRecord.android_config::jsonb ->> 'appPackage';
SELECT id into androidAppId FROM mobile_app WHERE pkg_name = androidPkgName AND platform_type = 'ANDROID';
IF androidAppId IS NULL THEN
@ -154,17 +154,16 @@ $$
generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, androidPkgName || ' (autogenerated)', androidAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId,
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId;
ELSE
UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled' WHERE id = androidAppId;
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId),
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.android_app_id = androidAppId);
END IF;
END IF;
UPDATE qr_code_settings SET android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
-- migrate ios config
IF (qrCodeRecord.ios_config IS NOT NULL AND qrCodeRecord.ios_config::jsonb -> 'appId' IS NOT NULL) THEN
IF (qrCodeRecord.ios_config::jsonb ->> 'appId' IS NOT NULL) THEN
iosPkgName := substring(qrCodeRecord.ios_config::jsonb ->> 'appId', strpos(qrCodeRecord.ios_config::jsonb ->> 'appId', '.') + 1);
SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS';
IF iosAppId IS NULL THEN
@ -176,19 +175,19 @@ $$
generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id)
VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName || ' (autogenerated)', iosAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId,
ios_enabled = (qrCodeRecord.ios_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId;
ELSE
UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId;
END IF;
ELSE
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId),
ios_enabled = (qrCodeRecord.ios_config::jsonb -> 'enabled')::boolean WHERE id = qrCodeRecord.id;
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId);
UPDATE mobile_app SET store_info = qrCodeRecord.ios_config::jsonb - 'enabled' WHERE id = iosAppId;
END IF;
END IF;
UPDATE qr_code_settings SET ios_enabled = (qrCodeRecord.ios_config::jsonb -> 'enabled')::boolean WHERE id = qrCodeRecord.id;
END LOOP;
ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_tenant_id_unq_key TO qr_code_settings_tenant_id_unq_key;
ALTER TABLE qr_code_settings RENAME CONSTRAINT mobile_app_settings_pkey TO qr_code_settings_pkey;
END IF;
ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config;
END;

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

@ -176,7 +176,7 @@ public class QrCodeSettingsController extends BaseController {
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", googlePlayLink)
.build();
} else if (userAgent.contains("iPhone") || userAgent.contains("iPad") && qrCodeSettings.isIosEnabled()) {
} else if ((userAgent.contains("iPhone") || userAgent.contains("iPad")) && qrCodeSettings.isIosEnabled()) {
String appStoreLink = qrCodeSettings.getAppStoreLink();
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", appStoreLink)

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

@ -413,23 +413,27 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) {
log.trace("[{}][{}] Set session new events flag to false", tenantId, edgeId.getId());
sessionNewEvents.put(edgeId, false);
processEdgeEventMigrationIfNeeded(session, edgeId);
session.processHighPriorityEvents();
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override
public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) {
sessionNewEvents.put(edgeId, true);
processEdgeEventMigrationIfNeeded(session, edgeId);
if (Boolean.TRUE.equals(edgeEventsMigrationProcessed.get(edgeId))) {
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
@Override
public void onSuccess(Boolean newEventsAdded) {
if (Boolean.TRUE.equals(newEventsAdded)) {
sessionNewEvents.put(edgeId, true);
}
scheduleEdgeEventsCheck(session);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), t);
scheduleEdgeEventsCheck(session);
}
scheduleEdgeEventsCheck(session);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}] Failed to process edge events for edge [{}]!", tenantId, session.getEdge().getId().getId(), t);
scheduleEdgeEventsCheck(session);
}
}, ctx.getGrpcCallbackExecutorService());
}, ctx.getGrpcCallbackExecutorService());
} else {
scheduleEdgeEventsCheck(session);
}
} else {
scheduleEdgeEventsCheck(session);
}
@ -457,8 +461,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
scheduleEdgeEventsCheck(session);
} else if (Boolean.FALSE.equals(eventsExist)) {
edgeEventsMigrationProcessed.put(edgeId, true);
} else {
scheduleEdgeEventsCheck(session);
}
}
}

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

@ -110,7 +110,7 @@ public abstract class EdgeGrpcSession implements Closeable {
private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId";
private static final int MAX_DOWNLINK_ATTEMPTS = 10;
private static final int MAX_DOWNLINK_ATTEMPTS = 3;
private static final String RATE_LIMIT_REACHED = "Rate limit reached";
protected static final ConcurrentLinkedQueue<EdgeEvent> highPriorityQueue = new ConcurrentLinkedQueue<>();

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

@ -149,7 +149,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return; // if originated by rule - just ignore delivery method
}
}
if (ruleId == null) {
if (ruleId == null && !notificationTemplate.getNotificationType().isSystem()) {
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen");
}
@ -225,13 +225,13 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationTemplate notificationTemplate = notificationTemplateService.findTenantOrSystemNotificationTemplate(tenantId, type)
.orElseThrow(() -> new IllegalArgumentException("No notification template found for type " + type));
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(TenantId.SYS_TENANT_ID)
.tenantId(tenantId)
.targets(List.of(targetId.getId()))
.templateId(notificationTemplate.getId())
.info(info)
.originatorEntityId(TenantId.SYS_TENANT_ID)
.build();
processNotificationRequest(TenantId.SYS_TENANT_ID, notificationRequest, null);
processNotificationRequest(tenantId, notificationRequest, null);
}
private void processNotificationRequestAsync(NotificationProcessingContext ctx, List<NotificationTarget> targets, FutureCallback<NotificationRequestStats> callback) {

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

@ -1302,6 +1302,30 @@ coap:
# - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
# - A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}"
# Specify the MTU (Maximum Transmission Unit).
# Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU.
# Default = 1024
# Minimum value = 64
# If set to 0 - LAN MTU is used.
max_transmission_unit: "${COAP_DTLS_MAX_TRANSMISSION_UNIT:1024}"
# DTLS maximum fragment length (RFC 6066, Section 4).
# Default = 1024
# Possible values: 512, 1024, 2048, 4096.
# If set to 0, the default maximum fragment size of 2^14 bytes (16,384 bytes) is used.
# Without this extension, TLS specifies a fixed maximum plaintext fragment length of 2^14 bytes.
# It may be desirable for constrained clients to negotiate a smaller maximum fragment length due to memory limitations or bandwidth limitations.
# In order to negotiate smaller maximum fragment lengths,
# clients MAY include an extension of type "max_fragment_length" in the (extended) client hello.
# The "extension_data" field of this extension SHALL contain:
# enum {
# 2^9(1) == 512,
# 2^10(2) == 1024,
# 2^11(3) == 2048,
# 2^12(4) == 4096,
# (255)
# } MaxFragmentLength;
# TLS already requires clients and servers to support fragmentation of handshake messages.
max_fragment_length: "${COAP_DTLS_MAX_FRAGMENT_LENGTH:1024}"
# Server DTLS credentials
credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)

28
common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java

@ -21,6 +21,7 @@ import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.MaxFragmentLengthExtension.Length;
import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -44,6 +45,8 @@ import static org.eclipse.californium.elements.config.CertificateAuthenticationM
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_NODE_ID;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_MAX_FRAGMENT_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_MAX_TRANSMISSION_UNIT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY;
@ -66,6 +69,12 @@ public class TbCoapDtlsSettings {
@Value("${coap.dtls.connection_id_length:}")
private Integer cIdLength;
@Value("${coap.dtls.max_transmission_unit:1024}")
private Integer maxTransmissionUnit;
@Value("${coap.dtls.max_fragment_length:1024}")
private Integer maxFragmentLength;
@Bean
@ConfigurationProperties(prefix = "coap.dtls.credentials")
public SslCredentialsConfig coapDtlsCredentials() {
@ -108,6 +117,15 @@ public class TbCoapDtlsSettings {
configBuilder.set(DTLS_CONNECTION_ID_NODE_ID, null);
}
}
if (maxTransmissionUnit > 0) {
configBuilder.set(DTLS_MAX_TRANSMISSION_UNIT, maxTransmissionUnit);
}
if (maxFragmentLength > 0) {
Length length = fromLength(maxFragmentLength);
if (length != null) {
configBuilder.set(DTLS_MAX_FRAGMENT_LENGTH, length);
}
}
configBuilder.setAdvancedCertificateVerifier(
new TbCoapDtlsCertificateVerifier(
transportService,
@ -127,4 +145,14 @@ public class TbCoapDtlsSettings {
return new InetSocketAddress(addr, port);
}
private static Length fromLength(int length) {
for (Length l : Length.values()) {
if (l.length() == length) {
return l;
}
}
return null;
}
}

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

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import org.thingsboard.server.dao.service.validator.DomainDataValidator;
import java.util.Comparator;
import java.util.List;
@ -53,11 +54,14 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
private OAuth2ClientDao oauth2ClientDao;
@Autowired
private DomainDao domainDao;
@Autowired
private DomainDataValidator domainDataValidator;
@Override
public Domain saveDomain(TenantId tenantId, Domain domain) {
log.trace("Executing saveDomain [{}]", domain);
try {
domainDataValidator.validate(domain, Domain::getTenantId);
Domain savedDomain = domainDao.save(tenantId, domain);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build());
return savedDomain;

15
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -43,7 +43,10 @@ public abstract class DataValidator<D extends BaseData<?>> {
Pattern.compile("^[A-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", Pattern.CASE_INSENSITIVE);
private static final Pattern QUEUE_PATTERN = Pattern.compile("^[a-zA-Z0-9_.\\-]+$");
private static final String DOMAIN_REGEX = "^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\\.)*(xn--)?([a-z0-9][a-z0-9\\-]{0,60}|[a-z0-9-]{1,30}\\.[a-z]{2,})$";
private static final Pattern DOMAIN_PATTERN = Pattern.compile(DOMAIN_REGEX);
private static final String LOCALHOST_REGEX = "^localhost(:\\d{1,5})?$";
private static final Pattern LOCALHOST_PATTERN = Pattern.compile(LOCALHOST_REGEX);
private static final String NAME = "name";
private static final String TOPIC = "topic";
@ -171,4 +174,14 @@ public abstract class DataValidator<D extends BaseData<?>> {
}
}
public static boolean isValidDomain(String domainName) {
if (domainName == null) {
return false;
}
if (LOCALHOST_PATTERN.matcher(domainName).matches()) {
return true;
}
return DOMAIN_PATTERN.matcher(domainName).matches();
}
}

32
dao/src/main/java/org/thingsboard/server/dao/service/validator/DomainDataValidator.java

@ -0,0 +1,32 @@
/**
* 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.dao.service.validator;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
@Component
public class DomainDataValidator extends AbstractHasOtaPackageValidator<Domain> {
@Override
protected void validateDataImpl(TenantId tenantId, Domain domain) {
if (!isValidDomain(domain.getName())) {
throw new IncorrectParameterException("Domain name " + domain.getName() + " is invalid");
}
}
}

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

@ -896,7 +896,7 @@ CREATE TABLE IF NOT EXISTS queue_stats (
);
CREATE TABLE IF NOT EXISTS qr_code_settings (
id uuid NOT NULL CONSTRAINT mobile_app_settings_pkey PRIMARY KEY,
id uuid NOT NULL CONSTRAINT qr_code_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
use_default_app boolean,

2
dao/src/test/java/org/thingsboard/server/dao/service/DomainServiceTest.java

@ -90,7 +90,7 @@ public class DomainServiceTest extends AbstractServiceTest {
public void testGetTenantDomains() {
List<Domain> domains = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Domain oAuth2Client = constructDomain(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5), true, false);
Domain oAuth2Client = constructDomain(TenantId.SYS_TENANT_ID, StringUtils.randomAlphabetic(5).toLowerCase(), true, false);
Domain savedOauth2Client = domainService.saveDomain(SYSTEM_TENANT_ID, oAuth2Client);
domains.add(savedOauth2Client);
}

8
docker/docker-upgrade-tb.sh

@ -28,13 +28,7 @@ case $i in
esac
done
if [[ -z "${FROM_VERSION// }" ]]; then
echo "--fromVersion parameter is invalid or unspecified!"
echo "Usage: docker-upgrade-tb.sh --fromVersion={VERSION}"
exit 1
else
fromVersion="${FROM_VERSION// }"
fi
fromVersion="${FROM_VERSION// }"
set -e

8
msa/tb-node/docker/start-tb-node.sh

@ -47,12 +47,8 @@ elif [ "$UPGRADE_TB" == "true" ]; then
echo "Starting ThingsBoard upgrade ..."
if [[ -z "${FROM_VERSION// }" ]]; then
echo "FROM_VERSION variable is invalid or unspecified!"
exit 1
else
fromVersion="${FROM_VERSION// }"
fi
fromVersion="${FROM_VERSION// }"
exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
-Dspring.jpa.hibernate.ddl-auto=none \

8
msa/tb/docker/upgrade-tb.sh

@ -20,15 +20,23 @@ start-db.sh
CONF_FOLDER="${pkg.installFolder}/conf"
jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
configfile=${pkg.name}.conf
upgradeversion=${DATA_FOLDER}/.upgradeversion
source "${CONF_FOLDER}/${configfile}"
FROM_VERSION=`cat ${upgradeversion}`
echo "Starting ThingsBoard upgrade ..."
fromVersion="${FROM_VERSION// }"
java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
-Dspring.jpa.hibernate.ddl-auto=none \
-Dinstall.upgrade=true \
-Dinstall.upgrade.from_version=${fromVersion} \
-Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \
org.springframework.boot.loader.launch.PropertiesLauncher
echo "${pkg.upgradeVersion}" > ${upgradeversion}
stop-db.sh

8
packaging/java/scripts/install/upgrade.sh

@ -28,13 +28,7 @@ case $i in
esac
done
if [[ -z "${FROM_VERSION// }" ]]; then
echo "--fromVersion parameter is invalid or unspecified!"
echo "Usage: upgrade.sh --fromVersion={VERSION}"
exit 1
else
fromVersion="${FROM_VERSION// }"
fi
fromVersion="${FROM_VERSION// }"
CONF_FOLDER=${pkg.installFolder}/conf
configfile=${pkg.name}.conf

8
packaging/java/scripts/install/upgrade_dev_db.sh

@ -28,13 +28,7 @@ case $i in
esac
done
if [[ -z "${FROM_VERSION// }" ]]; then
echo "--fromVersion parameter is invalid or unspecified!"
echo "Usage: upgrade_dev_db.sh --fromVersion={VERSION}"
exit 1
else
fromVersion="${FROM_VERSION// }"
fi
fromVersion="${FROM_VERSION// }"
BASE=${project.basedir}/target
CONF_FOLDER=${BASE}/conf

6
packaging/java/scripts/windows/upgrade.bat

@ -15,12 +15,6 @@ IF NOT "%1"=="" (
GOTO :loop
)
if not defined fromVersion (
echo "--fromVersion parameter is invalid or unspecified!"
echo "Usage: upgrade.bat --fromVersion {VERSION}"
exit /b 1
)
SET LOADER_PATH=%BASE%\conf,%BASE%\extensions
SET SQL_DATA_FOLDER=%BASE%\data\sql
SET jarfile=%BASE%\lib\${pkg.name}.jar

24
transport/coap/src/main/resources/tb-coap-transport.yml

@ -198,6 +198,30 @@ coap:
# - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
# - A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}"
# Specify the MTU (Maximum Transmission Unit).
# Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU.
# Default = 1024
# Minimum value = 64
# If set to 0 - LAN MTU is used.
max_transmission_unit: "${COAP_DTLS_MAX_TRANSMISSION_UNIT:1024}"
# DTLS maximum fragment length (RFC 6066, Section 4).
# Default = 1024
# Possible values: 512, 1024, 2048, 4096.
# If set to 0, the default maximum fragment size of 2^14 bytes (16,384 bytes) is used.
# Without this extension, TLS specifies a fixed maximum plaintext fragment length of 2^14 bytes.
# It may be desirable for constrained clients to negotiate a smaller maximum fragment length due to memory limitations or bandwidth limitations.
# In order to negotiate smaller maximum fragment lengths,
# clients MAY include an extension of type "max_fragment_length" in the (extended) client hello.
# The "extension_data" field of this extension SHALL contain:
# enum {
# 2^9(1) == 512,
# 2^10(2) == 1024,
# 2^11(3) == 2048,
# 2^12(4) == 4096,
# (255)
# } MaxFragmentLength;
# TLS already requires clients and servers to support fragmentation of handshake messages.
max_fragment_length: "${COAP_DTLS_MAX_FRAGMENT_LENGTH:1024}"
# Server DTLS credentials
credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)

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

@ -60,6 +60,7 @@ import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/m
import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models';
import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models';
@Injectable({
providedIn: 'root'
@ -682,6 +683,10 @@ export class DashboardUtilsService {
if (row > -1 && column > - 1) {
widgetLayout.row = row;
widgetLayout.col = column;
if (this.hasWidgetCollision(widgetLayout.row, widgetLayout.col,
widgetLayout.sizeX, widgetLayout.sizeY, Object.values(layout.widgets))) {
this.widgetPossiblePosition(widgetLayout, layout);
}
} else {
row = 0;
for (const w of Object.keys(layout.widgets)) {
@ -703,6 +708,60 @@ export class DashboardUtilsService {
layout.widgets[widget.id] = widgetLayout;
}
private widgetPossiblePosition(widgetLayout: WidgetLayout, layout: DashboardLayout) {
let bestRow = 0;
let bestCol = 0;
let maxCol = layout.gridSettings.minColumns || layout.gridSettings.columns || 0;
let maxRow = 0;
const widgetLayouts = Object.values(layout.widgets);
widgetLayouts.forEach(widget => {
maxCol = Math.max(maxCol, widget.col + widget.sizeX);
maxRow = Math.max(maxRow, widget.row + widget.sizeY);
})
for (; bestRow < maxRow; bestRow++) {
for (bestCol = 0; bestCol < maxCol; bestCol++) {
if (!this.hasWidgetCollision(bestRow, bestCol, widgetLayout.sizeX, widgetLayout.sizeY, widgetLayouts)) {
widgetLayout.row = bestRow;
widgetLayout.col = bestCol;
return;
}
}
}
const canAddToRows = maxGridsterRow >= maxRow + bestRow;
const canAddToColumns = maxGridsterCol >= maxCol + bestCol;
const addToRows = bestRow <= bestCol && canAddToRows;
if (!addToRows && canAddToColumns) {
widgetLayout.col = maxCol;
widgetLayout.row = 0;
} else if (canAddToRows) {
widgetLayout.row = maxRow;
widgetLayout.col = 0;
}
}
private hasWidgetCollision(row: number, col: number, sizeX: number, sizeY: number, widgetLayouts: WidgetLayout[]) {
const left = col;
const right = col + sizeX;
const top = row;
const bottom = row + sizeY;
for (const widget of widgetLayouts) {
const left2 = widget.col;
const right2 = widget.col + widget.sizeX;
const top2 = widget.row;
const bottom2 = widget.row + widget.sizeY;
if (left < right2 && right > left2 && top < bottom2 && bottom > top2) {
return true;
}
}
return false;
}
public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,

6
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -305,7 +305,11 @@ export class ItemBufferService {
connectors: [],
additionalInfo: origNode.additionalInfo,
configuration: origNode.configuration,
debugMode: origNode.debugMode,
debugSettings: {
failuresEnabled: origNode.debugSettings?.failuresEnabled,
allEnabled: origNode.debugSettings?.allEnabled || origNode.debugSettings?.allEnabledUntil > new Date().getTime(),
allEnabledUntil: 0
},
x: origNode.x,
y: origNode.y,
name: origNode.name,

2
ui-ngx/src/app/modules/home/components/audit-log/audit-log-details-dialog.component.ts

@ -105,7 +105,7 @@ export class AuditLogDetailsDialogComponent extends DialogComponent<AuditLogDeta
let newWidth = 600;
if (content && content.length > 0) {
const lines = content.split('\n');
newHeight = 17 * lines.length + 16;
newHeight = 18 * lines.length + 16;
let maxLineLength = 0;
lines.forEach((row) => {
const line = row.replace(/\t/g, ' ').replace(/\n/g, '');

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

@ -874,7 +874,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
public exportDashboard($event: Event) {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
this.importExport.exportDashboard(this.currentDashboardId);
}

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

@ -41,7 +41,9 @@ import {
DashboardCallbacks,
DashboardWidget,
DashboardWidgets,
IDashboardComponent
IDashboardComponent,
maxGridsterCol,
maxGridsterRow
} from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
@ -231,10 +233,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
disableAutoPositionOnConflict: false,
pushItems: false,
swap: false,
maxRows: 3000,
maxRows: maxGridsterRow,
minCols: this.columns ? this.columns : 24,
setGridSize: this.setGridSize,
maxCols: 3000,
maxCols: maxGridsterCol,
maxItemCols: 1000,
maxItemRows: 1000,
maxItemArea: 1000000,
@ -253,7 +255,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
draggable: {
enabled: this.isEdit && !this.isEditingWidget,
delayStart: 100,
stop: (_, itemComponent) => {(itemComponent.item as DashboardWidget).updatePosition(itemComponent.$item.x, itemComponent.$item.y);}
stop: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).updatePosition(itemComponent.$item.x, itemComponent.$item.y);
this.notifyGridsterOptionsChanged();
}
},
itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => {

11
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html → ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html

@ -23,10 +23,9 @@
[disabled]="disabled"
(click)="openDebugStrategyPanel($event, matButton)">
<mat-icon [color]="debugSettingsFormGroup.disabled ? 'inherit' : 'primary'">bug_report</mat-icon>
<span *ngIf="!failuresEnabled && !(isDebugAllActive$ | async)" translate>common.disabled</span>
<span *ngIf="(isDebugAllActive$ | async) && failuresEnabled" translate>debug-config.all</span>
<span *ngIf="(isDebugAllActive$ | async) && !failuresEnabled">
{{ !allEnabled ? (allEnabledUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) }}
</span>
<span *ngIf="!(isDebugAllActive$ | async) && failuresEnabled" translate>debug-config.failures</span>
@if (isDebugAllActive$ | async) {
{{ (allEnabled$ | async) === false ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) }}
} @else {
{{ (failuresEnabled ? 'debug-settings.failures' : 'common.disabled') | translate }}
}
</button>

68
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts → ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts

@ -14,26 +14,34 @@
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
Renderer2,
ViewContainerRef
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { TbPopoverService } from '@shared/components/popover.service';
import { MatButton } from '@angular/material/button';
import { DebugSettingsPanelComponent } from './debug-settings-panel.component';
import { EntityDebugSettingsPanelComponent } from './entity-debug-settings-panel.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { of, shareReplay, timer } from 'rxjs';
import { SECOND } from '@shared/models/time/time.models';
import { DebugSettings } from '@shared/models/entity.models';
import { map, startWith, switchMap, takeWhile } from 'rxjs/operators';
import { BehaviorSubject, of, shareReplay, timer } from 'rxjs';
import { SECOND, MINUTE } from '@shared/models/time/time.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { map, switchMap, takeWhile } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'tb-debug-settings-button',
templateUrl: './debug-settings-button.component.html',
selector: 'tb-entity-debug-settings-button',
templateUrl: './entity-debug-settings-button.component.html',
standalone: true,
imports: [
CommonModule,
@ -43,15 +51,16 @@ import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/f
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DebugSettingsButtonComponent),
useExisting: forwardRef(() => EntityDebugSettingsButtonComponent),
multi: true
},
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugSettingsButtonComponent implements ControlValueAccessor {
export class EntityDebugSettingsButtonComponent implements ControlValueAccessor {
@Input() debugLimitsConfiguration: string;
@Input() entityLabel: string;
debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false],
@ -60,11 +69,12 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor {
});
disabled = false;
private allEnabledSubject = new BehaviorSubject(false);
allEnabled$ = this.allEnabledSubject.asObservable();
isDebugAllActive$ = this.debugSettingsFormGroup.get('allEnabled').valueChanges.pipe(
startWith(null),
switchMap(() => {
if (this.allEnabled) {
isDebugAllActive$ = this.allEnabled$.pipe(
switchMap((value) => {
if (value) {
return of(true);
} else {
return timer(0, SECOND).pipe(
@ -77,31 +87,32 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor {
shareReplay(1)
);
readonly maxDebugModeDurationMinutes = getCurrentAuthState(this.store).maxDebugModeDurationMinutes;
readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
private propagateChange: (settings: DebugSettings) => void = () => {};
private propagateChange: (settings: EntityDebugSettings) => void = () => {};
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private store: Store<AppState>,
private viewContainerRef: ViewContainerRef,
private fb: FormBuilder,
private cd : ChangeDetectorRef,
) {
this.debugSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => {
this.propagateChange(value);
})
});
this.debugSettingsFormGroup.get('allEnabled').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => this.allEnabledSubject.next(value));
}
get failuresEnabled(): boolean {
return this.debugSettingsFormGroup.get('failuresEnabled').value;
}
get allEnabled(): boolean {
return this.debugSettingsFormGroup.get('allEnabled').value;
}
get allEnabledUntil(): number {
return this.debugSettingsFormGroup.get('allEnabledUntil').value;
}
@ -117,30 +128,33 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor {
this.popoverService.hidePopover(trigger);
} else {
const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DebugSettingsPanelComponent, 'bottom', true, null,
this.viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null,
{
...debugSettings,
maxDebugModeDurationMinutes: this.maxDebugModeDurationMinutes,
debugLimitsConfiguration: this.debugLimitsConfiguration
maxDebugModeDuration: this.maxDebugModeDuration,
debugLimitsConfiguration: this.debugLimitsConfiguration,
entityLabel: this.entityLabel
},
{},
{}, {}, true);
debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: DebugSettings) => {
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: EntityDebugSettings) => {
this.debugSettingsFormGroup.patchValue(settings);
this.cd.markForCheck();
debugStrategyPopover.hide();
});
}
}
registerOnChange(fn: (settings: DebugSettings) => void): void {
registerOnChange(fn: (settings: EntityDebugSettings) => void): void {
this.propagateChange = fn;
}
registerOnTouched(_: () => void): void {}
writeValue(settings: DebugSettings): void {
writeValue(settings: EntityDebugSettings): void {
this.debugSettingsFormGroup.patchValue(settings, {emitEvent: false});
this.allEnabledSubject.next(settings?.allEnabled);
this.debugSettingsFormGroup.get('allEnabled').updateValueAndValidity({onlySelf: true});
}

15
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.html → ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html

@ -16,26 +16,26 @@
-->
<div class="flex max-w-sm flex-col gap-3 p-2">
<div class="tb-form-panel-title" translate>debug-config.label</div>
<div class="tb-form-panel-title" translate>debug-settings.label</div>
<div class="hint-container">
<div class="tb-form-hint tb-primary-fill tb-flex center">
@if (debugLimitsConfiguration) {
{{ 'debug-config.hint.main-limited' | translate: { msg: maxMessagesCount, sec: maxTimeFrameSec } }}
{{ 'debug-settings.hint.main-limited' | translate: { entity: entityLabel ?? ('debug-settings.entity' | translate), msg: maxMessagesCount, time: (maxTimeFrameDuration | milliSecondsToTimeString: true : true) } }}
} @else {
{{ 'debug-config.hint.main' | translate }}
{{ 'debug-settings.hint.main' | translate }}
}
</div>
</div>
<div class="flex flex-col gap-3">
<mat-slide-toggle class="mat-slide" [formControl]="onFailuresControl">
<div tb-hint-tooltip-icon="{{ 'debug-config.hint.on-failure' | translate }}">
{{ 'debug-config.on-failure' | translate }}
<div tb-hint-tooltip-icon="{{ 'debug-settings.hint.on-failure' | translate }}">
{{ 'debug-settings.on-failure' | translate }}
</div>
</mat-slide-toggle>
<div class="align-center flex justify-between">
<mat-slide-toggle class="mat-slide" [formControl]="debugAllControl">
<div tb-hint-tooltip-icon="{{ 'debug-config.hint.all-messages' | translate }}">
{{ 'debug-config.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) } }}
<div tb-hint-tooltip-icon="{{ 'debug-settings.hint.all-messages' | translate }}">
{{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
</div>
</mat-slide-toggle>
<button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !allEnabled"
@ -58,6 +58,7 @@
<button mat-raised-button
color="primary"
type="button"
[disabled]="(isLoading$ | async) || !onFailuresControl.dirty && !debugAllControl.dirty"
(click)="onApply()">
{{ 'action.apply' | translate }}
</button>

20
ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts → ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts

@ -32,12 +32,12 @@ import { SECOND } from '@shared/models/time/time.models';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { of, shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DebugSettings } from '@shared/models/entity.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators';
@Component({
selector: 'tb-debug-settings-panel',
templateUrl: './debug-settings-panel.component.html',
selector: 'tb-entity-debug-settings-panel',
templateUrl: './entity-debug-settings-panel.component.html',
standalone: true,
imports: [
SharedModule,
@ -46,20 +46,21 @@ import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DebugSettingsPanelComponent extends PageComponent implements OnInit {
export class EntityDebugSettingsPanelComponent extends PageComponent implements OnInit {
@Input() popover: TbPopoverComponent<DebugSettingsPanelComponent>;
@Input() popover: TbPopoverComponent<EntityDebugSettingsPanelComponent>;
@Input({ transform: booleanAttribute }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false;
@Input() entityLabel: string;
@Input() allEnabledUntil = 0;
@Input() maxDebugModeDurationMinutes: number;
@Input() maxDebugModeDuration: number;
@Input() debugLimitsConfiguration: string;
onFailuresControl = this.fb.control(false);
debugAllControl = this.fb.control(false);
maxMessagesCount: string;
maxTimeFrameSec: string;
maxTimeFrameDuration: number;
initialAllEnabled: boolean;
isDebugAllActive$ = this.debugAllControl.valueChanges.pipe(
@ -78,7 +79,7 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit
shareReplay(1),
);
onSettingsApplied = new EventEmitter<DebugSettings>();
onSettingsApplied = new EventEmitter<EntityDebugSettings>();
constructor(private fb: FormBuilder,
private cd: ChangeDetectorRef) {
@ -99,7 +100,7 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit
ngOnInit(): void {
this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0];
this.maxTimeFrameSec = this.debugLimitsConfiguration?.split(':')[1];
this.maxTimeFrameDuration = parseInt(this.debugLimitsConfiguration?.split(':')[1]) * SECOND;
this.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled);
this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime();
@ -128,6 +129,7 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit
onReset(): void {
this.debugAllControl.patchValue(true);
this.debugAllControl.markAsDirty();
this.allEnabledUntil = 0;
this.cd.markForCheck();
}

4
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html

@ -40,13 +40,13 @@
</div>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
<div class="flex flex-row items-stretch justify-start" style="height: 76px;"
<div class="flex flex-row items-stretch justify-start h-full min-h-19 max-h-24"
formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray.controls; let $index = index">
<div class="filters-operation flex max-w-8% flex-full flex-row items-stretch justify-center">
<span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex max-w-92% min-w-147.5 flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-filter-predicate
class="flex-1"

2
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html

@ -44,7 +44,7 @@
<div class="tb-hint">{{ hintText | translate }}</div>
</div>
<div class="flex flex-col flex-1" [class.!hidden]="!dynamicMode && !onlyUserDynamicSource">
<div formGroupName="dynamicValue" class="flex flex-row items-center justify-start gap-2">
<div formGroupName="dynamicValue" class="flex flex-row justify-start gap-2">
<div class="flex max-w-35% flex-full flex-col">
<mat-form-field hideRequiredMarker class="mat-block">
<mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}">

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

@ -86,7 +86,6 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
tenantNotificationRequestsRateLimit: [null, []],
tenantNotificationRequestsPerRuleRateLimit: [null, []],
maxTransportMessages: [null, [Validators.required, Validators.min(0)]],
maxDebugModeDurationMinutes: [null, [Validators.min(0)]],
maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]],
maxREExecutions: [null, [Validators.required, Validators.min(0)]],
maxJSExecutions: [null, [Validators.required, Validators.min(0)]],
@ -97,6 +96,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
maxSms: [null, []],
smsEnabled: [null, []],
maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]],
maxDebugModeDurationMinutes: [null, [Validators.min(0)]],
defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]],
alarmsTtlDays: [null, [Validators.required, Validators.min(0)]],
rpcTtlDays: [null, [Validators.required, Validators.min(0)]],

1
ui-ngx/src/app/modules/home/components/public-api.ts

@ -20,6 +20,7 @@ export * from './widget/lib/settings/common/widget-settings-common.module';
export * from './widget/widget-components.module';
export * from './widget/config/widget-config-components.module';
export * from './widget/lib/chart/echarts-widget.models';
export * from './widget/config/widget-config.component.models';
export * from './widget/lib/table-widget.models';
export * from './widget/lib/flot-widget.models';

3
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts

@ -112,9 +112,6 @@ export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterV
legendKey.dataKey.settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings);
legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault;
if (this.settings.yAxes[legendKey.dataKey.settings.yAxisId]) {
this.settings.yAxes[legendKey.dataKey.settings.yAxisId].show = !legendKey.dataKey.settings.dataHiddenByDefault;
}
});
this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend);
if (!this.legendKeys.length) {

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

@ -44,12 +44,19 @@
}
.tb-usage-items-progress {
--mdc-linear-progress-track-height: 8px;
--mdc-linear-progress-active-indicator-height: 8px;
width: 34px;
@media #{$mat-md} {
display: none;
}
.mat-mdc-progress-bar {
&.critical {
--mdc-linear-progress-track-color: rgba(209, 39, 48, 0.06);
--mdc-linear-progress-active-indicator-color: #D12730;
}
}
.mdc-linear-progress {
height: 8px;
margin-top: 6px;
margin-bottom: 6px;
border-radius: 2px;
@ -93,21 +100,3 @@
color: rgba(0, 0, 0, 0.76);
}
}
:host ::ng-deep {
.tb-usage-items-progress {
.mat-mdc-progress-bar {
.mdc-linear-progress__bar-inner {
border-top-width: 8px;
}
&.critical {
.mdc-linear-progress__buffer-bar {
background: rgba(209, 39, 48, 0.06);
}
.mdc-linear-progress__bar-inner {
border-top-color: #D12730;
}
}
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts

@ -23,14 +23,14 @@ import {
PosFunction,
WidgetUnitedMapSettings
} from '../map-models';
import { forkJoin, Observable, of, ReplaySubject, switchMap } from 'rxjs';
import { combineLatest, Observable, of, ReplaySubject, switchMap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, parseFunction, parseTbFunction } from '@core/utils';
import { isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, parseTbFunction } from '@core/utils';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { CompiledTbFunction } from '@shared/models/js-function.models';
@ -55,7 +55,7 @@ export class ImageMap extends LeafletMap {
mapImage: this.mapImage(options)
};
forkJoin(initData).subscribe(inited => {
combineLatest(initData).subscribe(inited => {
this.posFunction = inited.posFunction;
const mapImage = inited.mapImage;
this.imageUrl = mapImage.imageUrl;

5
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -66,6 +66,7 @@
<div class="tb-widget-content" [class.tb-no-interaction]="disableWidgetInteraction">
<tb-widget #widgetComponent
[dashboardWidget]="widget"
[widget]="widget.widget"
[isEdit]="isEdit"
[isPreview]="isPreview"
[isMobile]="isMobile"
@ -96,8 +97,8 @@
noMargin
noPadding
[isEdit]="isEdit"
[timewindowStyle]="widgetComponent.widget.config.timewindowStyle"
[(ngModel)]="widgetComponent.widget.config.timewindow"
[timewindowStyle]="widget.widget.config.timewindowStyle"
[(ngModel)]="widget.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>

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

@ -150,9 +150,11 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
@Input()
dashboardWidget: DashboardWidget;
@Input()
widget: Widget;
@ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef;
widget: Widget;
widgetInfo: WidgetInfo;
errorMessages: string[];
widgetContext: WidgetContext;
@ -223,8 +225,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.loadingData = true;
this.widget = this.dashboardWidget.widget;
const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
if (this.widget.config.actions) {
for (const actionSourceId of Object.keys(this.widget.config.actions)) {

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

@ -111,6 +111,9 @@ interface DashboardWidgetUpdateRecord {
operation: DashboardWidgetUpdateOperation;
}
export const maxGridsterCol = 3000;
export const maxGridsterRow = 3000;
export class DashboardWidgets implements Iterable<DashboardWidget> {
highlightedMode = false;

1
ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html

@ -80,6 +80,7 @@
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
</tb-file-input>
<tb-js-func *ngIf="entityForm.get('resourceSubType').value === ResourceSubType.MODULE"
helpId="resource/js-resource-module_fn"
formControlName="content"
required
hideBrackets

8
ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts

@ -76,9 +76,9 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
}),
storeInfo: this.fb.group({
storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : '',
Validators.pattern(/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[a-zA-Z0-9._]+$/)],
Validators.pattern(/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[a-zA-Z0-9._]+(?:&[a-zA-Z0-9._-]+=[a-zA-Z0-9._%-]*)*$/)],
sha256CertFingerprints: [entity?.storeInfo?.sha256CertFingerprints ? entity.storeInfo.sha256CertFingerprints : '',
Validators.pattern(/^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){1,31}$/)],
Validators.pattern(/^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}$/)],
appId: [entity?.storeInfo?.appId ? entity.storeInfo.appId : '', Validators.pattern(/^[A-Z0-9]{10}\.[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*$/)],
}),
});
@ -89,11 +89,11 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
if (value === PlatformType.ANDROID) {
form.get('storeInfo.sha256CertFingerprints').enable({emitEvent: false});
form.get('storeInfo.appId').disable({emitEvent: false});
form.get('storeInfo.storeLink').setValidators(Validators.pattern(/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[a-zA-Z0-9._]+$/));
form.get('storeInfo.storeLink').setValidators(Validators.pattern(/^https?:\/\/play\.google\.com\/store\/apps\/details\?id=[a-zA-Z0-9._]+(?:&[a-zA-Z0-9._-]+=[a-zA-Z0-9._%-]*)*$/));
} else if (value === PlatformType.IOS) {
form.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false});
form.get('storeInfo.appId').enable({emitEvent: false});
form.get('storeInfo.storeLink').setValidators(Validators.pattern(/^https?:\/\/apps\.apple\.com\/[a-z]{2}\/app\/[\w-]+\/id\d{7,10}$/));
form.get('storeInfo.storeLink').setValidators(Validators.pattern(/^https?:\/\/apps\.apple\.com\/[a-z]{2}\/app\/[\w-]+\/id\d{7,10}(?:\?[^\s]*)?$/));
}
form.get('storeInfo.storeLink').setValue('', {emitEvent: false});
});

20
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html

@ -30,6 +30,22 @@
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>mobile.page-name</mat-label>
<input required matInput formControlName="label">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
matTooltip="{{ 'mobile.page-name-required' | translate }}"
*ngIf="customMobilePageForm.get('label').hasError('required') && customMobilePageForm.get('label').touched"
class="tb-error">
warning
</mat-icon>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
matTooltip="{{ 'mobile.page-name-cannot-contain-only-spaces' | translate }}"
*ngIf="customMobilePageForm.get('label').hasError('pattern') && customMobilePageForm.get('label').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
@ -59,7 +75,7 @@
<input required matInput formControlName="url">
<mat-hint></mat-hint>
<mat-error *ngIf="customMobilePageForm.get('url').hasError('pattern')">
{{ 'mobile.url-pattern' | translate }}
{{ 'mobile.invalid-url-format' | translate }}
</mat-error>
</mat-form-field>
</ng-container>
@ -69,7 +85,7 @@
<input required matInput formControlName="path">
<mat-hint></mat-hint>
<mat-error *ngIf="customMobilePageForm.get('path').hasError('pattern')">
{{ 'mobile.path-pattern' | translate }}
{{ 'mobile.invalid-path-format' | translate }}
</mat-error>
</mat-form-field>
</ng-container>

2
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts

@ -60,7 +60,7 @@ export class CustomMobilePageComponent implements ControlValueAccessor, Validato
customMobilePageForm = this.fb.group({
visible: [true],
icon: ['star'],
label: ['', Validators.required],
label: ['', [Validators.required, Validators.pattern(/\S/)]],
type: [MobilePageType.DASHBOARD],
dashboardId: this.fb.control<string>(null, Validators.required),
url: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^(https?:\/\/)?(localhost|([\w\-]+\.)+[\w\-]+)(:\d+)?(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?$/)]],

2
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-layout.component.ts

@ -207,7 +207,7 @@ export class MobileLayoutComponent implements ControlValueAccessor, Validator {
private updateModel() {
if (isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) {
this.propagateChange({pages: []});
this.propagateChange(null);
} else {
this.propagateChange(this.pagesForm.value);
}

8
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.html

@ -42,6 +42,14 @@
class="tb-error">
warning
</mat-icon>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
matTooltip="{{ 'mobile.page-name-cannot-contain-only-spaces' | translate }}"
*ngIf="mobilePageRowForm.get('label').hasError('pattern') && mobilePageRowForm.get('label').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field *ngIf="isCustomMenuItem"
class="tb-mobile-page-item-info flex" appearance="outline" subscriptSizing="dynamic">

6
ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/mobile-page-item-row.component.ts

@ -99,7 +99,7 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit,
mobilePageRowForm = this.fb.group({
visible: [true, []],
icon: ['', []],
label: ['', []],
label: ['', [Validators.pattern(/\S/)]],
type: [MobilePageType.DEFAULT]
});
@ -187,7 +187,7 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit,
}
} else {
this.isCustomMenuItem = true;
this.mobilePageRowForm.get('label').setValidators([Validators.required]);
this.mobilePageRowForm.get('label').addValidators([Validators.required]);
this.mobilePageRowForm.get('label').updateValueAndValidity({emitEvent: false});
}
this.updateCleanupState();
@ -271,7 +271,7 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit,
private updateModel() {
this.modelValue.visible = this.mobilePageRowForm.get('visible').value;
const label = this.mobilePageRowForm.get('label').value;
if (label) {
if (label?.trim()) {
this.modelValue.label = label;
} else {
delete this.modelValue.label;

2
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-app-configuration-dialog.component.html

@ -72,7 +72,7 @@
<div class="flex-1" translate>mobile.configuration-step.more-information</div>
<a mat-stroked-button
color="primary"
href="https://docs.flutter.dev/learn-flutter"
href="https://docs.flutter.dev/get-started/learn-flutter"
target="_blank">
<mat-icon class="tb-mat-24">rocket_launch</mat-icon>{{ 'mobile.configuration-step.getting-started' | translate }}
</a>

19
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts

@ -24,13 +24,13 @@ import {
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { MobileAppBundleInfo } from '@shared/models/mobile-app.models';
import { ActivatedRouteSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order';
import { MobileBundleTableHeaderComponent } from '@home/pages/mobile/bundes/mobile-bundle-table-header.component';
import { DatePipe } from '@angular/common';
import { MobileAppService } from '@core/http/mobile-app.service';
import { map, take } from 'rxjs/operators';
import { finalize, map, skip, take, takeUntil } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { MatDialog } from '@angular/material/dialog';
@ -52,11 +52,14 @@ export class MobileBundleTableConfigResolver {
private readonly config: EntityTableConfig<MobileAppBundleInfo> = new EntityTableConfig<MobileAppBundleInfo>();
private openingEditDialog = false;
constructor(
private datePipe: DatePipe,
private mobileAppService: MobileAppService,
private translate : TranslateService,
private dialog: MatDialog,
private router: Router,
private store: Store<AppState>
) {
this.config.selectionEnabled = false;
@ -108,9 +111,15 @@ export class MobileBundleTableConfigResolver {
this.config.handleRowClick = ($event, bundle) => {
$event?.stopPropagation();
this.mobileAppService.getMobileAppBundleInfoById(bundle.id.id).subscribe(appBundleInfo => {
this.editBundle($event, appBundleInfo);
})
if (!this.openingEditDialog) {
this.openingEditDialog = true;
this.mobileAppService.getMobileAppBundleInfoById(bundle.id.id).pipe(
takeUntil(this.router.events.pipe(skip(1))),
finalize(() => {this.openingEditDialog = false;})
).subscribe(
appBundleInfo => this.editBundle($event, appBundleInfo)
);
}
return true;
};

1
ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.scss

@ -32,6 +32,7 @@
}
.tb-editor {
height: 400px;
overflow-y: auto;
}
.tb-editor-buttons {
height: 40px;

12
ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts

@ -17,6 +17,7 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EditorOptions } from 'tinymce';
@Component({
selector: 'tb-release-notes-panel',
@ -43,7 +44,7 @@ export class EditorPanelComponent implements OnInit {
editorControl: FormControl<string>;
tinyMceOptions: Record<string, any> = {
tinyMceOptions: Partial<EditorOptions> = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['lists'],
@ -55,7 +56,14 @@ export class EditorPanelComponent implements OnInit {
autofocus: false,
branding: false,
promotion: false,
resize: false
resize: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = editor.getContainer().closest('.tb-popover-content');
const uiContainer = document.querySelector('.tox.tox-tinymce-aux');
container.parentNode.appendChild(uiContainer);
});
}
};
constructor(private fb: FormBuilder) {

6
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -37,6 +37,7 @@ import { Subject } from 'rxjs';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core';
import { EditorOptions } from 'tinymce';
@Component({
selector: 'tb-template-configuration',
@ -81,7 +82,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
readonly NotificationDeliveryMethod = NotificationDeliveryMethod;
readonly NotificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap;
tinyMceOptions: Record<string, any> = {
tinyMceOptions: Partial<EditorOptions> = {
base_url: '/assets/tinymce',
suffix: '.min',
plugins: ['link', 'table', 'image', 'lists', 'code', 'fullscreen'],
@ -93,7 +94,8 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
height: 400,
autofocus: false,
branding: false,
promotion: false
promotion: false,
relative_urls: false
};
private propagateChange = null;

3
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -35,9 +35,10 @@
</mat-error>
</mat-form-field>
<section class="mb-5 flex max-w-xs flex-row">
<tb-debug-settings-button
<tb-entity-debug-settings-button
class="mr-2"
formControlName="debugSettings"
[entityLabel]="'debug-settings.rule-node' | translate"
[debugLimitsConfiguration]="ruleChainDebugPerTenantLimitsConfiguration"
/>
<button mat-stroked-button

6
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -239,10 +239,10 @@
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDebugModeEnabled()"
<button [disabled]="(isLoading$ | async) || !isDebugSettingsEnabled()"
mat-fab color="accent" class="tb-btn-footer"
(click)="resetDebugModeInAllNodes()"
matTooltip="{{ 'rulenode.reset-debug-mode' | translate }}"
(click)="resetDebugSettingsInAllNodes()"
matTooltip="{{ 'rulenode.reset-debug-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>bug_report</mat-icon>
</button>

18
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -95,6 +95,7 @@ import { ComponentClusteringMode } from '@shared/models/component-descriptor.mod
import { MatDrawer } from '@angular/material/sidenav';
import { HttpStatusCode } from '@angular/common/http';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import Timeout = NodeJS.Timeout;
@Component({
@ -1415,17 +1416,20 @@ export class RuleChainPageComponent extends PageComponent
this.ruleChainCanvas.modelService.deleteSelected();
}
isDebugModeEnabled(): boolean {
const res = this.ruleChainModel.nodes.find((node) => node.debugMode);
isDebugSettingsEnabled(): boolean {
const res = this.ruleChainModel.nodes.find((node) => node?.debugSettings && this.isDebugSettingsActive(node.debugSettings));
return typeof res !== 'undefined';
}
resetDebugModeInAllNodes() {
resetDebugSettingsInAllNodes(): void {
let changed = false;
this.ruleChainModel.nodes.forEach((node) => {
if (node.component.type !== RuleNodeType.INPUT) {
changed = changed || node.debugMode;
node.debugMode = false;
const nodeHasActiveDebugSettings = node?.debugSettings && this.isDebugSettingsActive(node.debugSettings);
changed = changed || nodeHasActiveDebugSettings;
if (nodeHasActiveDebugSettings) {
node.debugSettings = { allEnabled: false, failuresEnabled: false, allEnabledUntil: 0 };
}
}
});
if (changed) {
@ -1433,6 +1437,10 @@ export class RuleChainPageComponent extends PageComponent
}
}
private isDebugSettingsActive(debugSettings: EntityDebugSettings): boolean {
return debugSettings.allEnabled || debugSettings.failuresEnabled || debugSettings.allEnabledUntil > new Date().getTime();
}
validate() {
setTimeout(() => {
this.isInvalid = false;

4
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts

@ -33,7 +33,7 @@ import { RuleNodeLinkComponent } from './rule-node-link.component';
import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component';
import { RuleNodeConfigComponent } from './rule-node-config.component';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { DebugSettingsButtonComponent } from '@home/components/debug-settings/debug-settings-button.component';
import { EntityDebugSettingsButtonComponent } from '@home/components/entity/debug/entity-debug-settings-button.component';
@NgModule({
declarations: [
@ -63,7 +63,7 @@ import { DebugSettingsButtonComponent } from '@home/components/debug-settings/de
HomeComponentsModule,
RuleChainRoutingModule,
DurationLeftPipe,
DebugSettingsButtonComponent
EntityDebugSettingsButtonComponent
]
})
export class RuleChainModule { }

36
ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts

@ -31,7 +31,7 @@ import { OtaPackageService } from '@core/http/ota-package.service';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData } from '@shared/models/page/page-data';
import { getEntityDetailsPageURL } from '@core/utils';
import { getEntityDetailsPageURL, isDefinedAndNotNull } from '@core/utils';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
@ -64,19 +64,19 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
this.reset();
}
private deviceProfile: string;
private deviceProfileIdValue: string;
get deviceProfileId(): string {
return this.deviceProfile;
return this.deviceProfileIdValue;
}
@Input()
set deviceProfileId(value: string) {
if (this.deviceProfile !== value) {
if (this.deviceProfile) {
if (this.deviceProfileIdValue !== value) {
if (this.deviceProfileIdValue) {
this.reset();
}
this.deviceProfile = value;
this.deviceProfileIdValue = value;
}
}
@ -257,16 +257,20 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
}
fetchPackages(searchText?: string): Observable<Array<OtaPackageInfo>> {
this.searchText = searchText;
const pageLink = new PageLink(50, 0, searchText, {
property: 'title',
direction: Direction.ASC
});
return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type,
{ignoreLoading: true}).pipe(
catchError(() => of(emptyPageData<OtaPackageInfo>())),
map((data) => data && data.data.length ? data.data : null)
);
if (isDefinedAndNotNull(this.deviceProfileId)) {
this.searchText = searchText;
const pageLink = new PageLink(50, 0, searchText, {
property: 'title',
direction: Direction.ASC
});
return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type,
{ignoreLoading: true}).pipe(
catchError(() => of(emptyPageData<OtaPackageInfo>())),
map((data) => data && data.data.length ? data.data : null)
);
} else {
return of([]);
}
}
clear() {

22
ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html

@ -41,8 +41,24 @@
<mat-option *ngFor="let resource of filteredResources$ | async" [value]="resource">
<span [innerHTML]="resource.title | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="searchText">
{{ searchText }}
</mat-option>
<ng-container *ngIf="subType !== ResourceSubType.EXTENSION; else extensionNoText">
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="null">
<div (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<span>{{ 'js-func.no-js-module-text' | translate }}</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ 'js-func.no-js-module-matching' | translate: {module: searchText | truncate: true: 6: &apos;...&apos;} }}
</span>
</ng-template>
</div>
</mat-option>
</ng-container>
<ng-template #extensionNoText>
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="searchText">
{{ searchText }}
</mat-option>
</ng-template>
</mat-autocomplete>
</mat-form-field>

6
ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts

@ -78,6 +78,8 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
@Input()
subType = ResourceSubType.EXTENSION;
ResourceSubType = ResourceSubType;
resourceFormGroup = this.fb.group({
resource: this.fb.control<string|ResourceInfo>(null)
});
@ -210,6 +212,10 @@ export class ResourceAutocompleteComponent implements ControlValueAccessor, OnIn
}
}
textIsNotEmpty(text: string): boolean {
return (text && text.length > 0);
}
private fetchResources(searchText?: string): Observable<Array<ResourceInfo>> {
this.searchText = searchText;
return this.resourceService.getResources(new PageLink(50, 0, searchText), ResourceType.JS_MODULE, this.subType, {ignoreLoading: true}).pipe(

3
ui-ngx/src/app/shared/components/time/timeinterval.component.ts

@ -191,8 +191,7 @@ export class TimeintervalComponent implements OnInit, ControlValueAccessor, OnCh
if (typeof this.modelValue !== 'undefined') {
const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue);
if (this.allowedIntervals?.length ||
IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
if (IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
const advanced = this.allowedIntervals?.length
? !this.allowedIntervals.includes(this.modelValue)
: !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, this.modelValue,

10
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.html

@ -68,7 +68,7 @@
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL">
<button *ngIf="!(timewindowForm.get('realtime.hideLastInterval').value ||
timewindowForm.get('realtime.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
matSuffix mat-icon-button type="button"
(click)="configureRealtimeLastIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
@ -95,7 +95,7 @@
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL">
<button *ngIf="!(timewindowForm.get('realtime.hideQuickInterval').value ||
timewindowForm.get('realtime.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
matSuffix mat-icon-button type="button"
(click)="configureRealtimeQuickIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
@ -137,7 +137,7 @@
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL">
<button *ngIf="!(timewindowForm.get('history.hideLastInterval').value ||
timewindowForm.get('history.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
matSuffix mat-icon-button type="button"
(click)="configureHistoryLastIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
@ -177,7 +177,7 @@
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL">
<button *ngIf="!(timewindowForm.get('history.hideQuickInterval').value ||
timewindowForm.get('history.hideInterval').value)"
matSuffix mat-icon-button type="button" class="tb-mat-24"
matSuffix mat-icon-button type="button"
(click)="configureHistoryQuickIntervalOptions($event)">
<mat-icon>edit</mat-icon>
</button>
@ -199,7 +199,7 @@
formControlName="type"
[allowedAggregationTypes]="timewindowForm.get('allowedAggTypes').value">
<button *ngIf="!timewindowForm.get('hideAggregation').value"
matSuffix mat-icon-button type="button" class="tb-mat-24"
matSuffix mat-icon-button type="button"
(click)="openAggregationOptionsConfig($event)">
<mat-icon>edit</mat-icon>
</button>

16
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts

@ -245,9 +245,9 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('realtime.advancedParams.lastAggIntervalsConfig').value;
if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(
lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
));
}
});
this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe(
@ -257,9 +257,9 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('realtime.advancedParams.quickAggIntervalsConfig').value;
if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(
quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
));
}
});
this.timewindowForm.get('history.timewindowMs').valueChanges.pipe(
@ -269,9 +269,9 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('history.advancedParams.lastAggIntervalsConfig').value;
if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
setTimeout(() => this.timewindowForm.get('history.interval').patchValue(
lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
));
}
});
this.timewindowForm.get('history.quickInterval').valueChanges.pipe(
@ -281,9 +281,9 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm.get('history.advancedParams.quickAggIntervalsConfig').value;
if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
setTimeout(() => this.timewindowForm.get('history.interval').patchValue(
quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
));
}
});

16
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -308,9 +308,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((timewindowMs: number) => {
if (this.realtimeAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(
this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
));
}
});
this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe(
@ -318,9 +318,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((quickInterval: number) => {
if (this.realtimeAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue(
setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(
this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
));
}
});
this.timewindowForm.get('history.timewindowMs').valueChanges.pipe(
@ -328,9 +328,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((timewindowMs: number) => {
if (this.historyAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
setTimeout(() => this.timewindowForm.get('history.interval').patchValue(
this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
);
));
}
});
this.timewindowForm.get('history.quickInterval').valueChanges.pipe(
@ -338,9 +338,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((quickInterval: number) => {
if (this.historyAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue(
setTimeout(() => this.timewindowForm.get('history.interval').patchValue(
this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
);
));
}
});

102
ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts

@ -14,78 +14,55 @@
/// limitations under the License.
///
import {
AfterViewInit,
Directive,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { filter, takeUntil, tap } from 'rxjs/operators';
import { booleanAttribute, Directive, ElementRef, input, OnInit, Renderer2 } from '@angular/core';
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ContentObserver } from '@angular/cdk/observers';
import { merge } from 'rxjs';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
@Directive({
selector: '[tbTruncateWithTooltip]',
providers: [MatTooltip],
hostDirectives: [{
directive: MatTooltip,
inputs: ['matTooltipClass', 'matTooltipTouchGestures'],
}]
})
export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDestroy {
export class TruncateWithTooltipDirective implements OnInit {
@Input('tbTruncateWithTooltip')
text: string;
text = input<string>(undefined, {alias: 'tbTruncateWithTooltip'});
@Input()
@coerceBoolean()
tooltipEnabled = true;
tooltipEnabled = input(true, {transform: booleanAttribute});
@Input()
position: TooltipPosition = 'above';
private destroy$ = new Subject<void>();
position = input<TooltipPosition>('above');
constructor(
private elementRef: ElementRef,
private elementRef: ElementRef<HTMLElement>,
private renderer: Renderer2,
private tooltip: MatTooltip
) {}
private tooltip: MatTooltip,
private contentObserver: ContentObserver
) {
merge(toObservable(this.text), this.contentObserver.observe(this.elementRef)).pipe(
takeUntilDestroyed()
).subscribe(() => {
this.tooltip.message = this.text() || this.elementRef.nativeElement.innerText
})
}
ngOnInit(): void {
this.observeMouseEvents();
this.applyTruncationStyles();
this.tooltip.position = this.position();
this.showTooltipOnOverflow(this);
}
ngAfterViewInit(): void {
this.tooltip.position = this.position;
}
ngOnDestroy(): void {
if (this.tooltip._isTooltipVisible()) {
this.hideTooltip();
}
this.destroy$.next();
this.destroy$.complete();
}
private observeMouseEvents(): void {
fromEvent(this.elementRef.nativeElement, 'mouseenter')
.pipe(
filter(() => this.tooltipEnabled),
filter(() => this.isOverflown(this.elementRef.nativeElement)),
tap(() => this.showTooltip()),
takeUntil(this.destroy$),
)
.subscribe();
fromEvent(this.elementRef.nativeElement, 'mouseleave')
.pipe(
filter(() => this.tooltipEnabled),
filter(() => this.tooltip._isTooltipVisible()),
tap(() => this.hideTooltip()),
takeUntil(this.destroy$),
)
.subscribe();
private showTooltipOnOverflow(ctx: TruncateWithTooltipDirective) {
ctx.tooltip.show = (function(old) {
function extendsFunction() {
if (ctx.tooltipEnabled() && ctx.isOverflown()) {
old.apply(ctx.tooltip, arguments);
}
}
return extendsFunction;
})(ctx.tooltip.show);
}
private applyTruncationStyles(): void {
@ -94,16 +71,7 @@ export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDe
this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis');
}
private isOverflown(element: HTMLElement): boolean {
return element.clientWidth < element.scrollWidth;
}
private showTooltip(): void {
this.tooltip.message = this.text || this.elementRef.nativeElement.innerText;
this.tooltip.show();
}
private hideTooltip(): void {
this.tooltip.hide();
private isOverflown(): boolean {
return this.elementRef.nativeElement.clientWidth < this.elementRef.nativeElement.scrollWidth;
}
}

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

@ -193,8 +193,8 @@ export const HelpLinks = {
scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/`,
scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`,
mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`,
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/applications/`,
}
};
/* eslint-enable max-len */

6
ui-ngx/src/app/shared/models/entity.models.ts

@ -193,11 +193,11 @@ export interface HasVersion {
version?: number;
}
export interface HasDebugSettings {
debugSettings?: DebugSettings;
export interface HasEntityDebugSettings {
debugSettings?: EntityDebugSettings;
}
export interface DebugSettings {
export interface EntityDebugSettings {
failuresEnabled?: boolean;
allEnabled?: boolean;
allEnabledUntil?: number;

6
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -27,13 +27,13 @@ import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { HasDebugSettings } from '@shared/models/entity.models';
import { HasEntityDebugSettings } from '@shared/models/entity.models';
export interface RuleNodeConfiguration {
[key: string]: any;
}
export interface RuleNode extends BaseData<RuleNodeId>, HasDebugSettings {
export interface RuleNode extends BaseData<RuleNodeId>, HasEntityDebugSettings {
ruleChainId?: RuleChainId;
type: string;
name: string;
@ -331,7 +331,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
configurationDescriptor?: RuleNodeConfigurationDescriptor;
}
export interface FcRuleNodeType extends FcNode, HasDebugSettings {
export interface FcRuleNodeType extends FcNode, HasEntityDebugSettings {
component?: RuleNodeComponentDescriptor;
singletonMode?: boolean;
queueName?: string;

17
ui-ngx/src/app/shared/models/tenant.model.ts

@ -31,6 +31,7 @@ export interface DefaultTenantProfileConfiguration {
maxUsers: number;
maxDashboards: number;
maxRuleChains: number;
maxEdges: number;
maxResourcesInBytes: number;
maxOtaPackagesInBytes: number;
maxResourceSize: number;
@ -42,6 +43,13 @@ export interface DefaultTenantProfileConfiguration {
transportDeviceTelemetryMsgRateLimit?: string;
transportDeviceTelemetryDataPointsRateLimit?: string;
transportGatewayMsgRateLimit?: string;
transportGatewayTelemetryMsgRateLimit?: string;
transportGatewayTelemetryDataPointsRateLimit?: string;
transportGatewayDeviceMsgRateLimit?: string;
transportGatewayDeviceTelemetryMsgRateLimit?: string;
transportGatewayDeviceTelemetryDataPointsRateLimit?: string;
tenantEntityExportRateLimit?: string;
tenantEntityImportRateLimit?: string;
tenantNotificationRequestsRateLimit?: string;
@ -59,6 +67,8 @@ export interface DefaultTenantProfileConfiguration {
smsEnabled: boolean;
maxCreatedAlarms: number;
maxDebugModeDurationMinutes: number;
tenantServerRestLimitsConfiguration: string;
customerServerRestLimitsConfiguration: string;
@ -75,6 +85,11 @@ export interface DefaultTenantProfileConfiguration {
cassandraQueryTenantRateLimitsConfiguration: string;
edgeEventRateLimits?: string;
edgeEventRateLimitsPerEdge?: string;
edgeUplinkMessagesRateLimits?: string;
edgeUplinkMessagesRateLimitsPerEdge?: string;
defaultStorageTtlDays: number;
alarmsTtlDays: number;
rpcTtlDays: number;
@ -100,6 +115,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxUsers: 0,
maxDashboards: 0,
maxRuleChains: 0,
maxEdges: 0,
maxResourcesInBytes: 0,
maxOtaPackagesInBytes: 0,
maxResourceSize: 0,
@ -114,6 +130,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxSms: 0,
smsEnabled: true,
maxCreatedAlarms: 0,
maxDebugModeDurationMinutes: 0,
tenantServerRestLimitsConfiguration: '',
customerServerRestLimitsConfiguration: '',
maxWsSessionsPerTenant: 0,

17
ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts

@ -16,7 +16,7 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models';
import { DAY, HOUR, MINUTE, SECOND, YEAR } from '@shared/models/time/time.models';
@Pipe({
name: 'milliSecondsToTimeString'
@ -27,19 +27,21 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
}
transform(milliSeconds: number, shortFormat = false, onlyFirstDigit = false): string {
const { days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds);
return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit);
const { years, days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds);
return this.formatTimeString(years, days, hours, minutes, seconds, shortFormat, onlyFirstDigit);
}
private extractTimeUnits(milliseconds: number): { days: number; hours: number; minutes: number; seconds: number } {
const days = Math.floor(milliseconds / DAY);
private extractTimeUnits(milliseconds: number): { years: number; days: number; hours: number; minutes: number; seconds: number } {
const years = Math.floor(milliseconds / YEAR);
const days = Math.floor((milliseconds % YEAR) / DAY);
const hours = Math.floor((milliseconds % DAY) / HOUR);
const minutes = Math.floor((milliseconds % HOUR) / MINUTE);
const seconds = Math.floor((milliseconds % MINUTE) / SECOND);
return { days, hours, minutes, seconds };
return { years, days, hours, minutes, seconds };
}
private formatTimeString(
years: number,
days: number,
hours: number,
minutes: number,
@ -48,6 +50,7 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
onlyFirstDigit: boolean
): string {
const timeUnits = [
{ value: years, key: 'years', shortKey: 'short.years' },
{ value: days, key: 'days', shortKey: 'short.days' },
{ value: hours, key: 'hours', shortKey: 'short.hours' },
{ value: minutes, key: 'minutes', shortKey: 'short.minutes' },
@ -59,7 +62,7 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
if (value > 0) {
timeString += this.translate.instant(shortFormat ? `timewindow.${shortKey}` : `timewindow.${key}`, { [key]: value });
if (onlyFirstDigit) {
return timeString;
return timeString.trim();
}
}
}

52
ui-ngx/src/assets/help/en_US/resource/js-resource-module_fn.md

@ -0,0 +1,52 @@
### JavaScript Resource Module
<div class="divider"></div>
<br/>
A JavaScript module is a self-contained piece of code that encapsulates a specific functionality or set of related functionalities.
#### Use Cases
JavaScript resource modules are advantageous for reusing custom logic. They can be utilized in:
- Widget controller script.
- Data post-processing functions.
- Markdown/HTML value functions.
- Cell style functions.
- Cell content functions.
- Custom actions.
These modules can contain any JavaScript code, facilitating the reuse of specific logic. This includes variables or functions that are exported for use in other parts of the application.
##### Examples
You can declare variables:
```javascript
export const circle = '&#11044';
{:copy-code}
```
```javascript
export const integerRegex = /^[-+]?\d+$/;
{:copy-code}
```
Or define functions such as:
```javascript
export const getStatusStyles = (value) => {
let color;
if (value) {
color = 'rgb(39, 134, 34)';
} else {
color = 'rgb(255, 0, 0)';
}
return {
color: color,
fontSize: '18px'
};
};
{:copy-code}
```
```javascript
export const formatDateToString = (date) => {
const options = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
return date.toLocaleTimeString('en-US', options);
};
{:copy-code}
```
**Hint**: *Remember to use the `export` keyword to make variables and functions accessible for use outside the module.*

18
ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-bytes-expression_fn.md

@ -0,0 +1,18 @@
### Expressions
#### Bytes converter:
For bytes converter, expression fields can use slices format only. A slice specifies how to slice a sequence, determining the start point, and the endpoint. Here's a basic overview of slice components:
- `start`: The starting index of the slice. It is included in the slice. If omitted, slicing starts at the beginning of the sequence. Indexing starts at 0, so the first element of the sequence is at index 0.
- `stop`: The ending index of the slice. It is excluded from the slice, meaning the slice will end just before this index. If omitted, slicing goes through the end of the sequence.
##### Bytes parsing examples:
| Message body | Slice | Output data | Description |
|:-----------------------|-----------------|--------------------------|------------------------------|
| AM123,mytype,12.2,45 | [:5] | AM123 | Extracting device name |
| AM123,mytype,12.2,45 | [:] | AM123,mytype,12.2,45 | Extracting all data |
| AM123,mytype,12.2,45 | [18:] | 45 | Extracting humidity value |
| AM123,mytype,12.2,45 | [13:17] | 12.2 | Extracting temperature value |

0
ui-ngx/src/assets/help/en_US/widget/lib/gateway/expressions_fn.md → ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-expression_fn.md

78
ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-json-expression_fn.md

@ -0,0 +1,78 @@
### Expressions
#### JSON Path:
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:
- `$`: The root element of the JSON document;
- `.`: Child operator used to select child elements. For example, $.store.book ;
- `[]`: Child operator used to select child elements. $['store']['book'] accesses the book array within a store object;
##### Examples:
For example, if we want to extract the device name from the following message, we can use the expression below:
MQTT message:
```
{
"sensorModelInfo": {
"sensorName": "AM-123",
"sensorType": "myDeviceType"
},
"data": {
"temp": 12.2,
"hum": 56,
"status": "ok"
}
}
{:copy-code}
```
Expression:
`${sensorModelInfo.sensorName}`
Converted data:
`AM-123`
If we want to extract all data from the message above, we can use the following expression:
`${data}`
Converted data:
`{"temp": 12.2, "hum": 56, "status": "ok"}`
Or if we want to extract specific data (for example “temperature”), you can use the following expression:
`${data.temp}`
And as a converted data we will get:
`12.2`
<br/>
#### Regular expressions for topic:
Device name or device profile can be parsed from the MQTT topic using regular expression. A regular expression, often abbreviated as regex or regexp, is a sequence of characters that forms a search pattern, primarily used for string matching and manipulation.
##### Regular expression for topic examples:
| Topic | Regular expression | Output data | Description |
|:---------------------------|----------------------------------|--------------------------|--------------------------------------|
| /devices/AM123/mytype/data | /devices/([^/]+)/mytype/data | AM123 | Getting device name from topic |
| /devices/AM123/mytype/data | /devices/[A-Z0-9]+/([^/]+)/data | mytype | Getting device profile from topic |
<br/>

6
ui-ngx/src/assets/help/en_US/widget/lib/gateway/attributes_timeseries_expressions_fn.md → ui-ngx/src/assets/help/en_US/widget/lib/gateway/mqtt-json-key-expression_fn.md

@ -3,11 +3,11 @@
The expression field is used to extract data from the MQTT message. There are various available options for different parts of the messages:
- The JSONPath format can be used to extract data from the message body.
- The JSONPath format can be used to extract data from the message body.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- The regular expression format can be used to extract data from the topic where the message will arrive.
- Slices can only be used in the expression fields of bytes converters.
- Slices can only be used in the expression fields of bytes converters.
JSONPath expressions specify the items within a JSON structure (which could be an object, array, or nested combination of both) that you want to access. These expressions can select elements from JSON data on specific criteria. Here's a basic overview of how JSONPath expressions are structured:

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

@ -4602,7 +4602,6 @@
"output": "الإخراج",
"test": "اختبار",
"help": "مساعدة",
"reset-debug-mode": "إعادة تعيين وضع التصحيح في جميع العقد",
"test-with-this-message": "{{test}} مع هذه الرسالة"
},
"role": {

3
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -3651,8 +3651,7 @@
"metadata-required": "Les entrades de metadades no poden estar buides.",
"output": "Sortida",
"test": "Test",
"help": "Ajuda",
"reset-debug-mode": "Restablir el mode de depuració a tots els nodes"
"help": "Ajuda"
},
"role": {
"role": "Rol",

3
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -2426,8 +2426,7 @@
"metadata-required": "Záznam metadat nemůže být prázdný.",
"output": "Výstup",
"test": "Test",
"help": "Nápověda",
"reset-debug-mode": "Resetovat režim ladění na všech uzlech"
"help": "Nápověda"
},
"timezone": {
"timezone": "Časová zóna",

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

@ -2642,8 +2642,7 @@
"metadata-required": "Metadataposter må ikke være tomme.",
"output": "Output",
"test": "Test",
"help": "Hjælp",
"reset-debug-mode": "Nulstil debug-tilstand i alle knuder"
"help": "Hjælp"
},
"role": {
"role": "Rolle",

3
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -1886,8 +1886,7 @@
"metadata-required": "Οι καταχωρίσεις μεταδεδομένων δεν μπορούν να είναι κενές.",
"output": "Απόδοση",
"test": "Τεστ",
"help": "Βοήθεια",
"reset-debug-mode": "Επαναφορά λειτουργίας εντοπισμού σφαλμάτων σε όλους τους κόμβους"
"help": "Βοήθεια"
},
"role": {
"role": "Ρόλος",

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

@ -989,16 +989,16 @@
"type-timeseries-deleted": "Telemetry deleted",
"type-sms-sent": "SMS sent"
},
"debug-config": {
"min": "{{number}} min",
"debug-settings": {
"label": "Debug configuration",
"on-failure": "Failures only (24/7)",
"all-messages": "All messages ({{time}})",
"failures": "Failures",
"all": "All",
"entity": "entity",
"rule-node": "rule node",
"hint": {
"main": "All node debug messages rate limited with:",
"main-limited": "All node debug messages will be rate-limited, with a maximum of {{msg}} messages allowed per {{sec}} seconds.",
"main-limited": "All {{entity}} debug messages will be rate-limited, with a maximum of {{msg}} messages allowed per {{time}}.",
"on-failure": "Save all failure debug events without time limit.",
"all-messages": "Save all debug events during time limit."
}
@ -3332,7 +3332,9 @@
"module-no-members": "Module has no exported members",
"module-load-error": "Module load error",
"source-code": "Source code",
"source-code-load-error": "Source code load error"
"source-code-load-error": "Source code load error",
"no-js-module-text": "No JS modules found",
"no-js-module-matching": "No JS modules matching '{{module}}' were found."
},
"key-val": {
"key": "Key",
@ -3550,7 +3552,8 @@
"tablet-959": "Tablet (max 959px)",
"max-element-number": "Max elements number",
"page-name": "Page name",
"page-nam-required": "Page name is required.",
"page-name-required": "Page name is required.",
"page-name-cannot-contain-only-spaces": "Page name cannot contain only spaces.",
"page-type": "Page type",
"pages-types": {
"dashboard": "Dashboard",
@ -3558,9 +3561,9 @@
"custom": "Custom"
},
"url": "URL",
"url-pattern": "Invalid URL",
"invalid-url-format": "Invalid URL format",
"path": "Path",
"path-pattern": "Path pattern",
"invalid-path-format": "Invalid path format",
"custom-page": "Custom page",
"edit-page": "Edit page",
"edit-custom-page": "Edit custom page",
@ -4293,7 +4296,7 @@
"output": "Output",
"test": "Test",
"help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes",
"reset-debug-settings": "Reset debug settings in all nodes",
"test-with-this-message": "{{test}} with this message",
"queue-hint": "Select a queue for message forwarding to another queue. 'Main' queue is used by default.",
"queue-singleton-hint": "Select a queue for message forwarding in multi-instance environments. 'Main' queue is used by default."
@ -4743,6 +4746,7 @@
"sec": "{{ sec }} sec",
"sec-short": "{{ sec }}s",
"short": {
"years": "{ years, plural, =1 {1 year } other {# years } }",
"days": "{ days, plural, =1 {1 day } other {# days } }",
"hours": "{ hours, plural, =1 {1 hour } other {# hours } }",
"minutes": "{{minutes}} min ",

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

@ -3529,7 +3529,6 @@
"output": "Salida",
"test": "Test",
"help": "Ayuda",
"reset-debug-mode": "Restablecer el modo de depuración en todos los nodos",
"test-with-this-message": "{{test}} con este mensaje"
},
"timezone": {

3
ui-ngx/src/assets/locale/locale.constant-ka_GE.json

@ -1413,8 +1413,7 @@
"metadata-required": "მეტამონაცემები ვერ იქნება ცარიელი",
"output": "რეზულტატი",
"test": "ტესტი",
"help": "დახმარება",
"reset-debug-mode": "Debug რეჟიმის გათიშვა ყველა ნოდისთვის"
"help": "დახმარება"
},
"tenant": {
"tenant": "ტენანტი",

3
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -1878,8 +1878,7 @@
"metadata-required": "메타데이터 엔트리를 입력하세요.",
"output": "출력",
"test": "테스트",
"help": "도움말",
"reset-debug-mode": "모든 노드에 대해 디버그 모드 초기화"
"help": "도움말"
},
"timezone": {
"timezone": "Timezone",

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

@ -4504,7 +4504,6 @@
"output": "Output",
"test": "Test",
"help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes",
"test-with-this-message": "{{test}} with this message"
},
"role": {

3
ui-ngx/src/assets/locale/locale.constant-lv_LV.json

@ -1338,8 +1338,7 @@
"metadata-required": "Metadatu ievadi nevar būt tukši.",
"output": "Izeja",
"test": "Tests",
"help": "Palīdzība",
"reset-debug-mode": "Atiestatīt atkļūdošanu visās nodēs"
"help": "Palīdzība"
},
"tenant": {
"tenant": "Īrnieks",

3
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -4560,8 +4560,7 @@
"metadata-required": "Metagegevensvermeldingen mogen niet leeg zijn.",
"output": "Uitvoer",
"test": "Test",
"help": "Help",
"reset-debug-mode": "Foutopsporingsmodus resetten in alle rule nodes"
"help": "Help"
},
"role": {
"role": "Rol",

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

@ -4520,7 +4520,6 @@
"output": "Wyjście",
"test": "Test",
"help": "Pomoc",
"reset-debug-mode": "Zresetuj tryb debugowania we wszystkich węzłach",
"test-with-this-message": "{{test}} with this message",
"description": "Opis"
},

3
ui-ngx/src/assets/locale/locale.constant-pt_BR.json

@ -1554,8 +1554,7 @@
"metadata-required": "As entradas de metadados não podem estar em branco.",
"output": "Saída",
"test": "Teste",
"help": "Ajuda",
"reset-debug-mode": "Redefinir modo de depuração em todos os nós"
"help": "Ajuda"
},
"timezone": {
"timezone": "Fuso horário",

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

@ -1401,7 +1401,6 @@
"output": "Ieşire",
"test": "Test",
"help": "Ajutor",
"reset-debug-mode": "Dezactivează modul depanare în toate nodurile"
},
"tenant": {
"tenant": "Locatar",

3
ui-ngx/src/assets/locale/locale.constant-sl_SI.json

@ -1879,8 +1879,7 @@
"metadata-required": "Vnosi metapodatkov ne smejo biti prazni.",
"output": "Izdelek",
"test": "Test",
"help": "Pomoč",
"reset-debug-mode": "Ponastavi način za odpravljanje napak v vseh vozliščih"
"help": "Pomoč"
},
"timezone": {
"timezone": "Časovni pas",

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

@ -2447,8 +2447,7 @@
"metadata-required": "Meta veri girişleri boş bırakılamaz.",
"output": "Çıktı",
"test": "Ölçek",
"help": "Yardım et",
"reset-debug-mode": "Tüm düğümlerde hata ayıklama modunu sıfırla"
"help": "Yardım et"
},
"timezone": {
"timezone": "Saat dilimi",

2
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -1979,7 +1979,7 @@
"output": "Вихід",
"test": "Тест",
"help": "Допомога",
"reset-debug-mode": "Вимкнути режим налагодження у всіх правилах"
"reset-debug-settings": "Вимкнути налаштування налагодження у всіх правилах"
},
"scheduler": {
"scheduler": "Планувальник",

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

@ -4012,7 +4012,6 @@
"output": "输出",
"test": "测试",
"help": "帮助",
"reset-debug-mode": "重置所有节点中的调试模式",
"test-with-this-message": "使用此消息进行{{test}}测试",
"queue-hint": "选择一个队列将消息转发到另一个队列,默认情况下使用'Main'队列。",
"queue-singleton-hint": "选择一个队列以在多实体中转发消息,默认情况下使用'Main'队列。"

3
ui-ngx/src/assets/locale/locale.constant-zh_TW.json

@ -2768,8 +2768,7 @@
"metadata-required": "元資料項不能為空。",
"output": "輸出",
"test": "測試",
"help": "幫助",
"reset-debug-mode": "重置所有節點中的調試模式"
"help": "幫助"
},
"timezone": {
"timezone": "時區",

3
ui-ngx/tailwind.config.js

@ -103,7 +103,8 @@ module.exports = {
'25': '6.25rem',
'37.5': '9.375rem',
'62.5': '15.625rem',
'72.5': '18.125rem'
'72.5': '18.125rem',
'147.5': '36.875rem'
},
maxWidth: {
'5%': '5%',

Loading…
Cancel
Save