Browse Source

Merge pull request #12289 from thingsboard/master-rc

rc
pull/12318/head
Viacheslav Klimov 2 years ago
committed by GitHub
parent
commit
87472fc6b0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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", "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 "actions": null
}, },
{ {
"tag": "highWarningScale", "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 "actions": null
}, },
{ {
@ -33,12 +33,12 @@
}, },
{ {
"tag": "lowCriticalScale", "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 "actions": null
}, },
{ {
"tag": "lowWarningScale", "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 "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", "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 "actions": null
}, },
{ {
"tag": "highWarningScale", "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 "actions": null
}, },
{ {
@ -33,12 +33,12 @@
}, },
{ {
"tag": "lowCriticalScale", "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 "actions": null
}, },
{ {
"tag": "lowWarningScale", "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 "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", "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 "actions": null
}, },
{ {
"tag": "highWarningScale", "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 "actions": null
}, },
{ {
@ -33,12 +33,12 @@
}, },
{ {
"tag": "lowCriticalScale", "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 "actions": null
}, },
{ {
"tag": "lowWarningScale", "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 "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", "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 "actions": null
}, },
{ {
"tag": "highWarningScale", "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 "actions": null
}, },
{ {
@ -33,12 +33,12 @@
}, },
{ {
"tag": "lowCriticalScale", "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 "actions": null
}, },
{ {
"tag": "lowWarningScale", "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 "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 LOOP
generatedBundleId := NULL; generatedBundleId := NULL;
-- migrate android config -- 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'; androidPkgName := qrCodeRecord.android_config::jsonb ->> 'appPackage';
SELECT id into androidAppId FROM mobile_app WHERE pkg_name = androidPkgName AND platform_type = 'ANDROID'; SELECT id into androidAppId FROM mobile_app WHERE pkg_name = androidPkgName AND platform_type = 'ANDROID';
IF androidAppId IS NULL THEN IF androidAppId IS NULL THEN
@ -154,17 +154,16 @@ $$
generatedBundleId := uuid_generate_v4(); generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, android_app_id) 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); VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, androidPkgName || ' (autogenerated)', androidAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId, UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId;
android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
ELSE ELSE
UPDATE mobile_app SET store_info = qrCodeRecord.android_config::jsonb - 'appPackage' - 'enabled' WHERE id = androidAppId; 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), 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;
END IF; END IF;
END IF; END IF;
UPDATE qr_code_settings SET android_enabled = (qrCodeRecord.android_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
-- migrate ios config -- 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); 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'; SELECT id INTO iosAppId FROM mobile_app WHERE pkg_name = iosPkgName AND platform_type = 'IOS';
IF iosAppId IS NULL THEN IF iosAppId IS NULL THEN
@ -176,19 +175,19 @@ $$
generatedBundleId := uuid_generate_v4(); generatedBundleId := uuid_generate_v4();
INSERT INTO mobile_app_bundle(id, created_time, tenant_id, title, ios_app_id) 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); VALUES (generatedBundleId, (extract(epoch from now()) * 1000), qrCodeRecord.tenant_id, iosPkgName || ' (autogenerated)', iosAppId);
UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId, UPDATE qr_code_settings SET mobile_app_bundle_id = generatedBundleId;
ios_enabled = (qrCodeRecord.ios_config::jsonb ->> 'enabled')::boolean WHERE id = qrCodeRecord.id;
ELSE ELSE
UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId; UPDATE mobile_app_bundle SET ios_app_id = iosAppId WHERE id = generatedBundleId;
END IF; END IF;
ELSE ELSE
UPDATE qr_code_settings SET mobile_app_bundle_id = (SELECT id FROM mobile_app_bundle WHERE mobile_app_bundle.ios_app_id = iosAppId), 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 mobile_app SET store_info = qrCodeRecord.ios_config::jsonb - 'enabled' WHERE id = iosAppId; UPDATE mobile_app SET store_info = qrCodeRecord.ios_config::jsonb - 'enabled' WHERE id = iosAppId;
END IF; END IF;
END IF; END IF;
UPDATE qr_code_settings SET ios_enabled = (qrCodeRecord.ios_config::jsonb -> 'enabled')::boolean WHERE id = qrCodeRecord.id;
END LOOP; 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_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; END IF;
ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config; ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config;
END; 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) return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", googlePlayLink) .header("Location", googlePlayLink)
.build(); .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(); String appStoreLink = qrCodeSettings.getAppStoreLink();
return ResponseEntity.status(HttpStatus.FOUND) return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", appStoreLink) .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))) { if (Boolean.TRUE.equals(sessionNewEvents.get(edgeId))) {
log.trace("[{}][{}] Set session new events flag to false", tenantId, edgeId.getId()); log.trace("[{}][{}] Set session new events flag to false", tenantId, edgeId.getId());
sessionNewEvents.put(edgeId, false); sessionNewEvents.put(edgeId, false);
processEdgeEventMigrationIfNeeded(session, edgeId);
session.processHighPriorityEvents(); session.processHighPriorityEvents();
Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() { processEdgeEventMigrationIfNeeded(session, edgeId);
@Override if (Boolean.TRUE.equals(edgeEventsMigrationProcessed.get(edgeId))) {
public void onSuccess(Boolean newEventsAdded) { Futures.addCallback(session.processEdgeEvents(), new FutureCallback<>() {
if (Boolean.TRUE.equals(newEventsAdded)) { @Override
sessionNewEvents.put(edgeId, true); 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); }, ctx.getGrpcCallbackExecutorService());
} } else {
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());
} else { } else {
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);
} }
@ -457,8 +461,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);
} else if (Boolean.FALSE.equals(eventsExist)) { } else if (Boolean.FALSE.equals(eventsExist)) {
edgeEventsMigrationProcessed.put(edgeId, true); 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_TS_ATTR_KEY = "queueStartTs";
private static final String QUEUE_START_SEQ_ID_ATTR_KEY = "queueStartSeqId"; 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"; private static final String RATE_LIMIT_REACHED = "Rate limit reached";
protected static final ConcurrentLinkedQueue<EdgeEvent> highPriorityQueue = new ConcurrentLinkedQueue<>(); 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 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))) { if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Recipients for " + deliveryMethod.getName() + " delivery method not chosen"); 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) NotificationTemplate notificationTemplate = notificationTemplateService.findTenantOrSystemNotificationTemplate(tenantId, type)
.orElseThrow(() -> new IllegalArgumentException("No notification template found for type " + type)); .orElseThrow(() -> new IllegalArgumentException("No notification template found for type " + type));
NotificationRequest notificationRequest = NotificationRequest.builder() NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(TenantId.SYS_TENANT_ID) .tenantId(tenantId)
.targets(List.of(targetId.getId())) .targets(List.of(targetId.getId()))
.templateId(notificationTemplate.getId()) .templateId(notificationTemplate.getId())
.info(info) .info(info)
.originatorEntityId(TenantId.SYS_TENANT_ID) .originatorEntityId(TenantId.SYS_TENANT_ID)
.build(); .build();
processNotificationRequest(TenantId.SYS_TENANT_ID, notificationRequest, null); processNotificationRequest(tenantId, notificationRequest, null);
} }
private void processNotificationRequestAsync(NotificationProcessingContext ctx, List<NotificationTarget> targets, FutureCallback<NotificationRequestStats> callback) { 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 between 0 and <= 4: SingleNodeConnectionIdGenerator is used
# - A value that are > 4: MultiNodeConnectionIdGenerator is used # - A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" 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 # Server DTLS credentials
credentials: credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) # 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.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType; 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.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; 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_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_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_NODE_ID; 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_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY; import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY;
@ -66,6 +69,12 @@ public class TbCoapDtlsSettings {
@Value("${coap.dtls.connection_id_length:}") @Value("${coap.dtls.connection_id_length:}")
private Integer cIdLength; private Integer cIdLength;
@Value("${coap.dtls.max_transmission_unit:1024}")
private Integer maxTransmissionUnit;
@Value("${coap.dtls.max_fragment_length:1024}")
private Integer maxFragmentLength;
@Bean @Bean
@ConfigurationProperties(prefix = "coap.dtls.credentials") @ConfigurationProperties(prefix = "coap.dtls.credentials")
public SslCredentialsConfig coapDtlsCredentials() { public SslCredentialsConfig coapDtlsCredentials() {
@ -108,6 +117,15 @@ public class TbCoapDtlsSettings {
configBuilder.set(DTLS_CONNECTION_ID_NODE_ID, null); 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( configBuilder.setAdvancedCertificateVerifier(
new TbCoapDtlsCertificateVerifier( new TbCoapDtlsCertificateVerifier(
transportService, transportService,
@ -127,4 +145,14 @@ public class TbCoapDtlsSettings {
return new InetSocketAddress(addr, port); 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.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.oauth2.OAuth2ClientDao; import org.thingsboard.server.dao.oauth2.OAuth2ClientDao;
import org.thingsboard.server.dao.service.validator.DomainDataValidator;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
@ -53,11 +54,14 @@ public class DomainServiceImpl extends AbstractEntityService implements DomainSe
private OAuth2ClientDao oauth2ClientDao; private OAuth2ClientDao oauth2ClientDao;
@Autowired @Autowired
private DomainDao domainDao; private DomainDao domainDao;
@Autowired
private DomainDataValidator domainDataValidator;
@Override @Override
public Domain saveDomain(TenantId tenantId, Domain domain) { public Domain saveDomain(TenantId tenantId, Domain domain) {
log.trace("Executing saveDomain [{}]", domain); log.trace("Executing saveDomain [{}]", domain);
try { try {
domainDataValidator.validate(domain, Domain::getTenantId);
Domain savedDomain = domainDao.save(tenantId, domain); Domain savedDomain = domainDao.save(tenantId, domain);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build()); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedDomain.getId()).entity(savedDomain).build());
return savedDomain; 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); 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 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 NAME = "name";
private static final String TOPIC = "topic"; 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 ( 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, created_time bigint NOT NULL,
tenant_id uuid NOT NULL, tenant_id uuid NOT NULL,
use_default_app boolean, 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() { public void testGetTenantDomains() {
List<Domain> domains = new ArrayList<>(); List<Domain> domains = new ArrayList<>();
for (int i = 0; i < 5; i++) { 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); Domain savedOauth2Client = domainService.saveDomain(SYSTEM_TENANT_ID, oAuth2Client);
domains.add(savedOauth2Client); domains.add(savedOauth2Client);
} }

8
docker/docker-upgrade-tb.sh

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

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

@ -47,12 +47,8 @@ elif [ "$UPGRADE_TB" == "true" ]; then
echo "Starting ThingsBoard upgrade ..." echo "Starting ThingsBoard upgrade ..."
if [[ -z "${FROM_VERSION// }" ]]; then
echo "FROM_VERSION variable is invalid or unspecified!" fromVersion="${FROM_VERSION// }"
exit 1
else
fromVersion="${FROM_VERSION// }"
fi
exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \ exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
-Dspring.jpa.hibernate.ddl-auto=none \ -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" CONF_FOLDER="${pkg.installFolder}/conf"
jarfile=${pkg.installFolder}/bin/${pkg.name}.jar jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
configfile=${pkg.name}.conf configfile=${pkg.name}.conf
upgradeversion=${DATA_FOLDER}/.upgradeversion
source "${CONF_FOLDER}/${configfile}" source "${CONF_FOLDER}/${configfile}"
FROM_VERSION=`cat ${upgradeversion}`
echo "Starting ThingsBoard upgrade ..." echo "Starting ThingsBoard upgrade ..."
fromVersion="${FROM_VERSION// }"
java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \ java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.ThingsboardInstallApplication \
-Dspring.jpa.hibernate.ddl-auto=none \ -Dspring.jpa.hibernate.ddl-auto=none \
-Dinstall.upgrade=true \ -Dinstall.upgrade=true \
-Dinstall.upgrade.from_version=${fromVersion} \
-Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \ -Dlogging.config=/usr/share/thingsboard/bin/install/logback.xml \
org.springframework.boot.loader.launch.PropertiesLauncher org.springframework.boot.loader.launch.PropertiesLauncher
echo "${pkg.upgradeVersion}" > ${upgradeversion}
stop-db.sh stop-db.sh

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

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

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

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

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

@ -15,12 +15,6 @@ IF NOT "%1"=="" (
GOTO :loop 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 LOADER_PATH=%BASE%\conf,%BASE%\extensions
SET SQL_DATA_FOLDER=%BASE%\data\sql SET SQL_DATA_FOLDER=%BASE%\data\sql
SET jarfile=%BASE%\lib\${pkg.name}.jar 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 between 0 and <= 4: SingleNodeConnectionIdGenerator is used
# - A value that are > 4: MultiNodeConnectionIdGenerator is used # - A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:}" 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 # Server DTLS credentials
credentials: credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) # 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 { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models'; import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models';
import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -682,6 +683,10 @@ export class DashboardUtilsService {
if (row > -1 && column > - 1) { if (row > -1 && column > - 1) {
widgetLayout.row = row; widgetLayout.row = row;
widgetLayout.col = column; widgetLayout.col = column;
if (this.hasWidgetCollision(widgetLayout.row, widgetLayout.col,
widgetLayout.sizeX, widgetLayout.sizeY, Object.values(layout.widgets))) {
this.widgetPossiblePosition(widgetLayout, layout);
}
} else { } else {
row = 0; row = 0;
for (const w of Object.keys(layout.widgets)) { for (const w of Object.keys(layout.widgets)) {
@ -703,6 +708,60 @@ export class DashboardUtilsService {
layout.widgets[widget.id] = widgetLayout; 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, public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string, targetState: string,
targetLayout: DashboardLayoutId, targetLayout: DashboardLayoutId,

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

@ -305,7 +305,11 @@ export class ItemBufferService {
connectors: [], connectors: [],
additionalInfo: origNode.additionalInfo, additionalInfo: origNode.additionalInfo,
configuration: origNode.configuration, 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, x: origNode.x,
y: origNode.y, y: origNode.y,
name: origNode.name, 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; let newWidth = 600;
if (content && content.length > 0) { if (content && content.length > 0) {
const lines = content.split('\n'); const lines = content.split('\n');
newHeight = 17 * lines.length + 16; newHeight = 18 * lines.length + 16;
let maxLineLength = 0; let maxLineLength = 0;
lines.forEach((row) => { lines.forEach((row) => {
const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); 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) { public exportDashboard($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.preventDefault();
} }
this.importExport.exportDashboard(this.currentDashboardId); this.importExport.exportDashboard(this.currentDashboardId);
} }

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

@ -41,7 +41,9 @@ import {
DashboardCallbacks, DashboardCallbacks,
DashboardWidget, DashboardWidget,
DashboardWidgets, DashboardWidgets,
IDashboardComponent IDashboardComponent,
maxGridsterCol,
maxGridsterRow
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs'; import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
@ -231,10 +233,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
disableAutoPositionOnConflict: false, disableAutoPositionOnConflict: false,
pushItems: false, pushItems: false,
swap: false, swap: false,
maxRows: 3000, maxRows: maxGridsterRow,
minCols: this.columns ? this.columns : 24, minCols: this.columns ? this.columns : 24,
setGridSize: this.setGridSize, setGridSize: this.setGridSize,
maxCols: 3000, maxCols: maxGridsterCol,
maxItemCols: 1000, maxItemCols: 1000,
maxItemRows: 1000, maxItemRows: 1000,
maxItemArea: 1000000, maxItemArea: 1000000,
@ -253,7 +255,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
draggable: { draggable: {
enabled: this.isEdit && !this.isEditingWidget, enabled: this.isEdit && !this.isEditingWidget,
delayStart: 100, 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(), itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => { 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" [disabled]="disabled"
(click)="openDebugStrategyPanel($event, matButton)"> (click)="openDebugStrategyPanel($event, matButton)">
<mat-icon [color]="debugSettingsFormGroup.disabled ? 'inherit' : 'primary'">bug_report</mat-icon> <mat-icon [color]="debugSettingsFormGroup.disabled ? 'inherit' : 'primary'">bug_report</mat-icon>
<span *ngIf="!failuresEnabled && !(isDebugAllActive$ | async)" translate>common.disabled</span> @if (isDebugAllActive$ | async) {
<span *ngIf="(isDebugAllActive$ | async) && failuresEnabled" translate>debug-config.all</span> {{ (allEnabled$ | async) === false ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) }}
<span *ngIf="(isDebugAllActive$ | async) && !failuresEnabled"> } @else {
{{ !allEnabled ? (allEnabledUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) }} {{ (failuresEnabled ? 'debug-settings.failures' : 'common.disabled') | translate }}
</span> }
<span *ngIf="!(isDebugAllActive$ | async) && failuresEnabled" translate>debug-config.failures</span>
</button> </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. /// 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 { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { MatButton } from '@angular/material/button'; 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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { of, shareReplay, timer } from 'rxjs'; import { BehaviorSubject, of, shareReplay, timer } from 'rxjs';
import { SECOND } from '@shared/models/time/time.models'; import { SECOND, MINUTE } from '@shared/models/time/time.models';
import { DebugSettings } from '@shared/models/entity.models'; import { EntityDebugSettings } from '@shared/models/entity.models';
import { map, startWith, switchMap, takeWhile } from 'rxjs/operators'; import { map, switchMap, takeWhile } from 'rxjs/operators';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({ @Component({
selector: 'tb-debug-settings-button', selector: 'tb-entity-debug-settings-button',
templateUrl: './debug-settings-button.component.html', templateUrl: './entity-debug-settings-button.component.html',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
@ -43,15 +51,16 @@ import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/f
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DebugSettingsButtonComponent), useExisting: forwardRef(() => EntityDebugSettingsButtonComponent),
multi: true multi: true
}, },
], ],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DebugSettingsButtonComponent implements ControlValueAccessor { export class EntityDebugSettingsButtonComponent implements ControlValueAccessor {
@Input() debugLimitsConfiguration: string; @Input() debugLimitsConfiguration: string;
@Input() entityLabel: string;
debugSettingsFormGroup = this.fb.group({ debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false], failuresEnabled: [false],
@ -60,11 +69,12 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor {
}); });
disabled = false; disabled = false;
private allEnabledSubject = new BehaviorSubject(false);
allEnabled$ = this.allEnabledSubject.asObservable();
isDebugAllActive$ = this.debugSettingsFormGroup.get('allEnabled').valueChanges.pipe( isDebugAllActive$ = this.allEnabled$.pipe(
startWith(null), switchMap((value) => {
switchMap(() => { if (value) {
if (this.allEnabled) {
return of(true); return of(true);
} else { } else {
return timer(0, SECOND).pipe( return timer(0, SECOND).pipe(
@ -77,31 +87,32 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor {
shareReplay(1) 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, constructor(private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private store: Store<AppState>, private store: Store<AppState>,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private fb: FormBuilder, private fb: FormBuilder,
private cd : ChangeDetectorRef,
) { ) {
this.debugSettingsFormGroup.valueChanges.pipe( this.debugSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe(value => { ).subscribe(value => {
this.propagateChange(value); this.propagateChange(value);
}) });
this.debugSettingsFormGroup.get('allEnabled').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => this.allEnabledSubject.next(value));
} }
get failuresEnabled(): boolean { get failuresEnabled(): boolean {
return this.debugSettingsFormGroup.get('failuresEnabled').value; return this.debugSettingsFormGroup.get('failuresEnabled').value;
} }
get allEnabled(): boolean {
return this.debugSettingsFormGroup.get('allEnabled').value;
}
get allEnabledUntil(): number { get allEnabledUntil(): number {
return this.debugSettingsFormGroup.get('allEnabledUntil').value; return this.debugSettingsFormGroup.get('allEnabledUntil').value;
} }
@ -117,30 +128,33 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor {
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DebugSettingsPanelComponent, 'bottom', true, null, this.viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null,
{ {
...debugSettings, ...debugSettings,
maxDebugModeDurationMinutes: this.maxDebugModeDurationMinutes, maxDebugModeDuration: this.maxDebugModeDuration,
debugLimitsConfiguration: this.debugLimitsConfiguration debugLimitsConfiguration: this.debugLimitsConfiguration,
entityLabel: this.entityLabel
}, },
{}, {},
{}, {}, true); {}, {}, true);
debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover; debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: DebugSettings) => { debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: EntityDebugSettings) => {
this.debugSettingsFormGroup.patchValue(settings); this.debugSettingsFormGroup.patchValue(settings);
this.cd.markForCheck();
debugStrategyPopover.hide(); debugStrategyPopover.hide();
}); });
} }
} }
registerOnChange(fn: (settings: DebugSettings) => void): void { registerOnChange(fn: (settings: EntityDebugSettings) => void): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(_: () => void): void {} registerOnTouched(_: () => void): void {}
writeValue(settings: DebugSettings): void { writeValue(settings: EntityDebugSettings): void {
this.debugSettingsFormGroup.patchValue(settings, {emitEvent: false}); this.debugSettingsFormGroup.patchValue(settings, {emitEvent: false});
this.allEnabledSubject.next(settings?.allEnabled);
this.debugSettingsFormGroup.get('allEnabled').updateValueAndValidity({onlySelf: true}); 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="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="hint-container">
<div class="tb-form-hint tb-primary-fill tb-flex center"> <div class="tb-form-hint tb-primary-fill tb-flex center">
@if (debugLimitsConfiguration) { @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 { } @else {
{{ 'debug-config.hint.main' | translate }} {{ 'debug-settings.hint.main' | translate }}
} }
</div> </div>
</div> </div>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<mat-slide-toggle class="mat-slide" [formControl]="onFailuresControl"> <mat-slide-toggle class="mat-slide" [formControl]="onFailuresControl">
<div tb-hint-tooltip-icon="{{ 'debug-config.hint.on-failure' | translate }}"> <div tb-hint-tooltip-icon="{{ 'debug-settings.hint.on-failure' | translate }}">
{{ 'debug-config.on-failure' | translate }} {{ 'debug-settings.on-failure' | translate }}
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
<div class="align-center flex justify-between"> <div class="align-center flex justify-between">
<mat-slide-toggle class="mat-slide" [formControl]="debugAllControl"> <mat-slide-toggle class="mat-slide" [formControl]="debugAllControl">
<div tb-hint-tooltip-icon="{{ 'debug-config.hint.all-messages' | translate }}"> <div tb-hint-tooltip-icon="{{ 'debug-settings.hint.all-messages' | translate }}">
{{ 'debug-config.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) } }} {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
<button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !allEnabled" <button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !allEnabled"
@ -58,6 +58,7 @@
<button mat-raised-button <button mat-raised-button
color="primary" color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async) || !onFailuresControl.dirty && !debugAllControl.dirty"
(click)="onApply()"> (click)="onApply()">
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}
</button> </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 { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { of, shareReplay, timer } from 'rxjs'; import { of, shareReplay, timer } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 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'; import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-debug-settings-panel', selector: 'tb-entity-debug-settings-panel',
templateUrl: './debug-settings-panel.component.html', templateUrl: './entity-debug-settings-panel.component.html',
standalone: true, standalone: true,
imports: [ imports: [
SharedModule, SharedModule,
@ -46,20 +46,21 @@ import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs
], ],
changeDetection: ChangeDetectionStrategy.OnPush 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 }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false;
@Input() entityLabel: string;
@Input() allEnabledUntil = 0; @Input() allEnabledUntil = 0;
@Input() maxDebugModeDurationMinutes: number; @Input() maxDebugModeDuration: number;
@Input() debugLimitsConfiguration: string; @Input() debugLimitsConfiguration: string;
onFailuresControl = this.fb.control(false); onFailuresControl = this.fb.control(false);
debugAllControl = this.fb.control(false); debugAllControl = this.fb.control(false);
maxMessagesCount: string; maxMessagesCount: string;
maxTimeFrameSec: string; maxTimeFrameDuration: number;
initialAllEnabled: boolean; initialAllEnabled: boolean;
isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( isDebugAllActive$ = this.debugAllControl.valueChanges.pipe(
@ -78,7 +79,7 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit
shareReplay(1), shareReplay(1),
); );
onSettingsApplied = new EventEmitter<DebugSettings>(); onSettingsApplied = new EventEmitter<EntityDebugSettings>();
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
@ -99,7 +100,7 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit
ngOnInit(): void { ngOnInit(): void {
this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0]; 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.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled); this.debugAllControl.patchValue(this.allEnabled);
this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime();
@ -128,6 +129,7 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit
onReset(): void { onReset(): void {
this.debugAllControl.patchValue(true); this.debugAllControl.patchValue(true);
this.debugAllControl.markAsDirty();
this.allEnabledUntil = 0; this.allEnabledUntil = 0;
this.cd.markForCheck(); this.cd.markForCheck();
} }

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

@ -40,13 +40,13 @@
</div> </div>
<mat-divider style="padding-bottom: 5px;"></mat-divider> <mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list"> <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" formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray.controls; let $index = index"> *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"> <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> <span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div> </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"> <div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-filter-predicate <tb-filter-predicate
class="flex-1" 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 class="tb-hint">{{ hintText | translate }}</div>
</div> </div>
<div class="flex flex-col flex-1" [class.!hidden]="!dynamicMode && !onlyUserDynamicSource"> <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"> <div class="flex max-w-35% flex-full flex-col">
<mat-form-field hideRequiredMarker class="mat-block"> <mat-form-field hideRequiredMarker class="mat-block">
<mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}"> <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, []], tenantNotificationRequestsRateLimit: [null, []],
tenantNotificationRequestsPerRuleRateLimit: [null, []], tenantNotificationRequestsPerRuleRateLimit: [null, []],
maxTransportMessages: [null, [Validators.required, Validators.min(0)]], maxTransportMessages: [null, [Validators.required, Validators.min(0)]],
maxDebugModeDurationMinutes: [null, [Validators.min(0)]],
maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]],
maxREExecutions: [null, [Validators.required, Validators.min(0)]], maxREExecutions: [null, [Validators.required, Validators.min(0)]],
maxJSExecutions: [null, [Validators.required, Validators.min(0)]], maxJSExecutions: [null, [Validators.required, Validators.min(0)]],
@ -97,6 +96,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
maxSms: [null, []], maxSms: [null, []],
smsEnabled: [null, []], smsEnabled: [null, []],
maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]], maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]],
maxDebugModeDurationMinutes: [null, [Validators.min(0)]],
defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]], defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]],
alarmsTtlDays: [null, [Validators.required, Validators.min(0)]], alarmsTtlDays: [null, [Validators.required, Validators.min(0)]],
rpcTtlDays: [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/widget-components.module';
export * from './widget/config/widget-config-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/config/widget-config.component.models';
export * from './widget/lib/table-widget.models'; export * from './widget/lib/table-widget.models';
export * from './widget/lib/flot-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, legendKey.dataKey.settings = mergeDeep<TimeSeriesChartKeySettings>({} as TimeSeriesChartKeySettings,
timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings); timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings);
legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault; 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); this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend);
if (!this.legendKeys.length) { 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 { .tb-usage-items-progress {
--mdc-linear-progress-track-height: 8px;
--mdc-linear-progress-active-indicator-height: 8px;
width: 34px; width: 34px;
@media #{$mat-md} { @media #{$mat-md} {
display: none; 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 { .mdc-linear-progress {
height: 8px;
margin-top: 6px; margin-top: 6px;
margin-bottom: 6px; margin-bottom: 6px;
border-radius: 2px; border-radius: 2px;
@ -93,21 +100,3 @@
color: rgba(0, 0, 0, 0.76); 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, PosFunction,
WidgetUnitedMapSettings WidgetUnitedMapSettings
} from '../map-models'; } 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 { catchError } from 'rxjs/operators';
import { calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps/common-maps-utils'; import { calculateNewPointCoordinate, loadImageWithAspect } from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models'; import { DataSet, DatasourceType, FormattedData, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.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 { EntityDataPageLink } from '@shared/models/query/query.models';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { CompiledTbFunction } from '@shared/models/js-function.models'; import { CompiledTbFunction } from '@shared/models/js-function.models';
@ -55,7 +55,7 @@ export class ImageMap extends LeafletMap {
mapImage: this.mapImage(options) mapImage: this.mapImage(options)
}; };
forkJoin(initData).subscribe(inited => { combineLatest(initData).subscribe(inited => {
this.posFunction = inited.posFunction; this.posFunction = inited.posFunction;
const mapImage = inited.mapImage; const mapImage = inited.mapImage;
this.imageUrl = mapImage.imageUrl; 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"> <div class="tb-widget-content" [class.tb-no-interaction]="disableWidgetInteraction">
<tb-widget #widgetComponent <tb-widget #widgetComponent
[dashboardWidget]="widget" [dashboardWidget]="widget"
[widget]="widget.widget"
[isEdit]="isEdit" [isEdit]="isEdit"
[isPreview]="isPreview" [isPreview]="isPreview"
[isMobile]="isMobile" [isMobile]="isMobile"
@ -96,8 +97,8 @@
noMargin noMargin
noPadding noPadding
[isEdit]="isEdit" [isEdit]="isEdit"
[timewindowStyle]="widgetComponent.widget.config.timewindowStyle" [timewindowStyle]="widget.widget.config.timewindowStyle"
[(ngModel)]="widgetComponent.widget.config.timewindow" [(ngModel)]="widget.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)"> (ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow> </tb-timewindow>
</div> </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() @Input()
dashboardWidget: DashboardWidget; dashboardWidget: DashboardWidget;
@Input()
widget: Widget;
@ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef; @ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef;
widget: Widget;
widgetInfo: WidgetInfo; widgetInfo: WidgetInfo;
errorMessages: string[]; errorMessages: string[];
widgetContext: WidgetContext; widgetContext: WidgetContext;
@ -223,8 +225,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.loadingData = true; this.loadingData = true;
this.widget = this.dashboardWidget.widget;
const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {}; const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
if (this.widget.config.actions) { if (this.widget.config.actions) {
for (const actionSourceId of Object.keys(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; operation: DashboardWidgetUpdateOperation;
} }
export const maxGridsterCol = 3000;
export const maxGridsterRow = 3000;
export class DashboardWidgets implements Iterable<DashboardWidget> { export class DashboardWidgets implements Iterable<DashboardWidget> {
highlightedMode = false; 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)"> (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
</tb-file-input> </tb-file-input>
<tb-js-func *ngIf="entityForm.get('resourceSubType').value === ResourceSubType.MODULE" <tb-js-func *ngIf="entityForm.get('resourceSubType').value === ResourceSubType.MODULE"
helpId="resource/js-resource-module_fn"
formControlName="content" formControlName="content"
required required
hideBrackets 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({ storeInfo: this.fb.group({
storeLink: [entity?.storeInfo?.storeLink ? entity.storeInfo.storeLink : '', 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 : '', 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]+)*$/)], 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) { if (value === PlatformType.ANDROID) {
form.get('storeInfo.sha256CertFingerprints').enable({emitEvent: false}); form.get('storeInfo.sha256CertFingerprints').enable({emitEvent: false});
form.get('storeInfo.appId').disable({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) { } else if (value === PlatformType.IOS) {
form.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false}); form.get('storeInfo.sha256CertFingerprints').disable({emitEvent: false});
form.get('storeInfo.appId').enable({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}); 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-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>mobile.page-name</mat-label> <mat-label translate>mobile.page-name</mat-label>
<input required matInput formControlName="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> </mat-form-field>
</div> </div>
<mat-form-field appearance="outline" subscriptSizing="dynamic"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
@ -59,7 +75,7 @@
<input required matInput formControlName="url"> <input required matInput formControlName="url">
<mat-hint></mat-hint> <mat-hint></mat-hint>
<mat-error *ngIf="customMobilePageForm.get('url').hasError('pattern')"> <mat-error *ngIf="customMobilePageForm.get('url').hasError('pattern')">
{{ 'mobile.url-pattern' | translate }} {{ 'mobile.invalid-url-format' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
@ -69,7 +85,7 @@
<input required matInput formControlName="path"> <input required matInput formControlName="path">
<mat-hint></mat-hint> <mat-hint></mat-hint>
<mat-error *ngIf="customMobilePageForm.get('path').hasError('pattern')"> <mat-error *ngIf="customMobilePageForm.get('path').hasError('pattern')">
{{ 'mobile.path-pattern' | translate }} {{ 'mobile.invalid-path-format' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</ng-container> </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({ customMobilePageForm = this.fb.group({
visible: [true], visible: [true],
icon: ['star'], icon: ['star'],
label: ['', Validators.required], label: ['', [Validators.required, Validators.pattern(/\S/)]],
type: [MobilePageType.DASHBOARD], type: [MobilePageType.DASHBOARD],
dashboardId: this.fb.control<string>(null, Validators.required), dashboardId: this.fb.control<string>(null, Validators.required),
url: [{value:'', disabled: true}, [Validators.required, Validators.pattern(/^(https?:\/\/)?(localhost|([\w\-]+\.)+[\w\-]+)(:\d+)?(\/[\w\-._~:\/?#[\]@!$&'()*+,;=%]*)?$/)]], 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() { private updateModel() {
if (isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) { if (isDefaultMobilePagesConfig(this.pagesForm.value.pages as MobilePage[])) {
this.propagateChange({pages: []}); this.propagateChange(null);
} else { } else {
this.propagateChange(this.pagesForm.value); 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"> class="tb-error">
warning warning
</mat-icon> </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>
<mat-form-field *ngIf="isCustomMenuItem" <mat-form-field *ngIf="isCustomMenuItem"
class="tb-mobile-page-item-info flex" appearance="outline" subscriptSizing="dynamic"> 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({ mobilePageRowForm = this.fb.group({
visible: [true, []], visible: [true, []],
icon: ['', []], icon: ['', []],
label: ['', []], label: ['', [Validators.pattern(/\S/)]],
type: [MobilePageType.DEFAULT] type: [MobilePageType.DEFAULT]
}); });
@ -187,7 +187,7 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit,
} }
} else { } else {
this.isCustomMenuItem = true; 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.mobilePageRowForm.get('label').updateValueAndValidity({emitEvent: false});
} }
this.updateCleanupState(); this.updateCleanupState();
@ -271,7 +271,7 @@ export class MobilePageItemRowComponent implements ControlValueAccessor, OnInit,
private updateModel() { private updateModel() {
this.modelValue.visible = this.mobilePageRowForm.get('visible').value; this.modelValue.visible = this.mobilePageRowForm.get('visible').value;
const label = this.mobilePageRowForm.get('label').value; const label = this.mobilePageRowForm.get('label').value;
if (label) { if (label?.trim()) {
this.modelValue.label = label; this.modelValue.label = label;
} else { } else {
delete this.modelValue.label; 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> <div class="flex-1" translate>mobile.configuration-step.more-information</div>
<a mat-stroked-button <a mat-stroked-button
color="primary" color="primary"
href="https://docs.flutter.dev/learn-flutter" href="https://docs.flutter.dev/get-started/learn-flutter"
target="_blank"> target="_blank">
<mat-icon class="tb-mat-24">rocket_launch</mat-icon>{{ 'mobile.configuration-step.getting-started' | translate }} <mat-icon class="tb-mat-24">rocket_launch</mat-icon>{{ 'mobile.configuration-step.getting-started' | translate }}
</a> </a>

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

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

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

@ -32,6 +32,7 @@
} }
.tb-editor { .tb-editor {
height: 400px; height: 400px;
overflow-y: auto;
} }
.tb-editor-buttons { .tb-editor-buttons {
height: 40px; 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 { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { EditorOptions } from 'tinymce';
@Component({ @Component({
selector: 'tb-release-notes-panel', selector: 'tb-release-notes-panel',
@ -43,7 +44,7 @@ export class EditorPanelComponent implements OnInit {
editorControl: FormControl<string>; editorControl: FormControl<string>;
tinyMceOptions: Record<string, any> = { tinyMceOptions: Partial<EditorOptions> = {
base_url: '/assets/tinymce', base_url: '/assets/tinymce',
suffix: '.min', suffix: '.min',
plugins: ['lists'], plugins: ['lists'],
@ -55,7 +56,14 @@ export class EditorPanelComponent implements OnInit {
autofocus: false, autofocus: false,
branding: false, branding: false,
promotion: 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) { 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 { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EditorOptions } from 'tinymce';
@Component({ @Component({
selector: 'tb-template-configuration', selector: 'tb-template-configuration',
@ -81,7 +82,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
readonly NotificationDeliveryMethod = NotificationDeliveryMethod; readonly NotificationDeliveryMethod = NotificationDeliveryMethod;
readonly NotificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap; readonly NotificationTemplateTypeTranslateMap = NotificationTemplateTypeTranslateMap;
tinyMceOptions: Record<string, any> = { tinyMceOptions: Partial<EditorOptions> = {
base_url: '/assets/tinymce', base_url: '/assets/tinymce',
suffix: '.min', suffix: '.min',
plugins: ['link', 'table', 'image', 'lists', 'code', 'fullscreen'], plugins: ['link', 'table', 'image', 'lists', 'code', 'fullscreen'],
@ -93,7 +94,8 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
height: 400, height: 400,
autofocus: false, autofocus: false,
branding: false, branding: false,
promotion: false promotion: false,
relative_urls: false
}; };
private propagateChange = null; private propagateChange = null;

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

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

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

@ -239,10 +239,10 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
<button [disabled]="(isLoading$ | async) || !isDebugModeEnabled()" <button [disabled]="(isLoading$ | async) || !isDebugSettingsEnabled()"
mat-fab color="accent" class="tb-btn-footer" mat-fab color="accent" class="tb-btn-footer"
(click)="resetDebugModeInAllNodes()" (click)="resetDebugSettingsInAllNodes()"
matTooltip="{{ 'rulenode.reset-debug-mode' | translate }}" matTooltip="{{ 'rulenode.reset-debug-settings' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>bug_report</mat-icon> <mat-icon>bug_report</mat-icon>
</button> </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 { MatDrawer } from '@angular/material/sidenav';
import { HttpStatusCode } from '@angular/common/http'; import { HttpStatusCode } from '@angular/common/http';
import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models';
import { EntityDebugSettings } from '@shared/models/entity.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -1415,17 +1416,20 @@ export class RuleChainPageComponent extends PageComponent
this.ruleChainCanvas.modelService.deleteSelected(); this.ruleChainCanvas.modelService.deleteSelected();
} }
isDebugModeEnabled(): boolean { isDebugSettingsEnabled(): boolean {
const res = this.ruleChainModel.nodes.find((node) => node.debugMode); const res = this.ruleChainModel.nodes.find((node) => node?.debugSettings && this.isDebugSettingsActive(node.debugSettings));
return typeof res !== 'undefined'; return typeof res !== 'undefined';
} }
resetDebugModeInAllNodes() { resetDebugSettingsInAllNodes(): void {
let changed = false; let changed = false;
this.ruleChainModel.nodes.forEach((node) => { this.ruleChainModel.nodes.forEach((node) => {
if (node.component.type !== RuleNodeType.INPUT) { if (node.component.type !== RuleNodeType.INPUT) {
changed = changed || node.debugMode; const nodeHasActiveDebugSettings = node?.debugSettings && this.isDebugSettingsActive(node.debugSettings);
node.debugMode = false; changed = changed || nodeHasActiveDebugSettings;
if (nodeHasActiveDebugSettings) {
node.debugSettings = { allEnabled: false, failuresEnabled: false, allEnabledUntil: 0 };
}
} }
}); });
if (changed) { 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() { validate() {
setTimeout(() => { setTimeout(() => {
this.isInvalid = false; 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 { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component';
import { RuleNodeConfigComponent } from './rule-node-config.component'; import { RuleNodeConfigComponent } from './rule-node-config.component';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; 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({ @NgModule({
declarations: [ declarations: [
@ -63,7 +63,7 @@ import { DebugSettingsButtonComponent } from '@home/components/debug-settings/de
HomeComponentsModule, HomeComponentsModule,
RuleChainRoutingModule, RuleChainRoutingModule,
DurationLeftPipe, DurationLeftPipe,
DebugSettingsButtonComponent EntityDebugSettingsButtonComponent
] ]
}) })
export class RuleChainModule { } 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 { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData } from '@shared/models/page/page-data'; 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 { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
@ -64,19 +64,19 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
this.reset(); this.reset();
} }
private deviceProfile: string; private deviceProfileIdValue: string;
get deviceProfileId(): string { get deviceProfileId(): string {
return this.deviceProfile; return this.deviceProfileIdValue;
} }
@Input() @Input()
set deviceProfileId(value: string) { set deviceProfileId(value: string) {
if (this.deviceProfile !== value) { if (this.deviceProfileIdValue !== value) {
if (this.deviceProfile) { if (this.deviceProfileIdValue) {
this.reset(); this.reset();
} }
this.deviceProfile = value; this.deviceProfileIdValue = value;
} }
} }
@ -257,16 +257,20 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On
} }
fetchPackages(searchText?: string): Observable<Array<OtaPackageInfo>> { fetchPackages(searchText?: string): Observable<Array<OtaPackageInfo>> {
this.searchText = searchText; if (isDefinedAndNotNull(this.deviceProfileId)) {
const pageLink = new PageLink(50, 0, searchText, { this.searchText = searchText;
property: 'title', const pageLink = new PageLink(50, 0, searchText, {
direction: Direction.ASC property: 'title',
}); direction: Direction.ASC
return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type, });
{ignoreLoading: true}).pipe( return this.otaPackageService.getOtaPackagesInfoByDeviceProfileId(pageLink, this.deviceProfileId, this.type,
catchError(() => of(emptyPageData<OtaPackageInfo>())), {ignoreLoading: true}).pipe(
map((data) => data && data.data.length ? data.data : null) catchError(() => of(emptyPageData<OtaPackageInfo>())),
); map((data) => data && data.data.length ? data.data : null)
);
} else {
return of([]);
}
} }
clear() { 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"> <mat-option *ngFor="let resource of filteredResources$ | async" [value]="resource">
<span [innerHTML]="resource.title | highlight:searchText"></span> <span [innerHTML]="resource.title | highlight:searchText"></span>
</mat-option> </mat-option>
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="searchText"> <ng-container *ngIf="subType !== ResourceSubType.EXTENSION; else extensionNoText">
{{ searchText }} <mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="null">
</mat-option> <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-autocomplete>
</mat-form-field> </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() @Input()
subType = ResourceSubType.EXTENSION; subType = ResourceSubType.EXTENSION;
ResourceSubType = ResourceSubType;
resourceFormGroup = this.fb.group({ resourceFormGroup = this.fb.group({
resource: this.fb.control<string|ResourceInfo>(null) 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>> { private fetchResources(searchText?: string): Observable<Array<ResourceInfo>> {
this.searchText = searchText; this.searchText = searchText;
return this.resourceService.getResources(new PageLink(50, 0, searchText), ResourceType.JS_MODULE, this.subType, {ignoreLoading: true}).pipe( 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') { if (typeof this.modelValue !== 'undefined') {
const min = this.timeService.boundMinInterval(this.minValue); const min = this.timeService.boundMinInterval(this.minValue);
const max = this.timeService.boundMaxInterval(this.maxValue); const max = this.timeService.boundMaxInterval(this.maxValue);
if (this.allowedIntervals?.length || if (IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
IntervalMath.numberValue(this.modelValue) >= min && IntervalMath.numberValue(this.modelValue) <= max) {
const advanced = this.allowedIntervals?.length const advanced = this.allowedIntervals?.length
? !this.allowedIntervals.includes(this.modelValue) ? !this.allowedIntervals.includes(this.modelValue)
: !this.timeService.matchesExistingInterval(this.minValue, this.maxValue, 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"> timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL">
<button *ngIf="!(timewindowForm.get('realtime.hideLastInterval').value || <button *ngIf="!(timewindowForm.get('realtime.hideLastInterval').value ||
timewindowForm.get('realtime.hideInterval').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)"> (click)="configureRealtimeLastIntervalOptions($event)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
@ -95,7 +95,7 @@
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"> timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL">
<button *ngIf="!(timewindowForm.get('realtime.hideQuickInterval').value || <button *ngIf="!(timewindowForm.get('realtime.hideQuickInterval').value ||
timewindowForm.get('realtime.hideInterval').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)"> (click)="configureRealtimeQuickIntervalOptions($event)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
@ -137,7 +137,7 @@
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"> timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL">
<button *ngIf="!(timewindowForm.get('history.hideLastInterval').value || <button *ngIf="!(timewindowForm.get('history.hideLastInterval').value ||
timewindowForm.get('history.hideInterval').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)"> (click)="configureHistoryLastIntervalOptions($event)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
@ -177,7 +177,7 @@
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"> timewindowForm.get('history.historyType').value === historyTypes.INTERVAL">
<button *ngIf="!(timewindowForm.get('history.hideQuickInterval').value || <button *ngIf="!(timewindowForm.get('history.hideQuickInterval').value ||
timewindowForm.get('history.hideInterval').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)"> (click)="configureHistoryQuickIntervalOptions($event)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
@ -199,7 +199,7 @@
formControlName="type" formControlName="type"
[allowedAggregationTypes]="timewindowForm.get('allowedAggTypes').value"> [allowedAggregationTypes]="timewindowForm.get('allowedAggTypes').value">
<button *ngIf="!timewindowForm.get('hideAggregation').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)"> (click)="openAggregationOptionsConfig($event)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </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; this.timewindowForm.get('realtime.advancedParams.lastAggIntervalsConfig').value;
if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) && if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
lastAggIntervalsConfig[timewindowMs].defaultAggInterval) { lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue( setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(
lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false} lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
); ));
} }
}); });
this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe( 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; this.timewindowForm.get('realtime.advancedParams.quickAggIntervalsConfig').value;
if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) && if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
quickAggIntervalsConfig[quickInterval].defaultAggInterval) { quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('realtime.interval').patchValue( setTimeout(() => this.timewindowForm.get('realtime.interval').patchValue(
quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false} quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
); ));
} }
}); });
this.timewindowForm.get('history.timewindowMs').valueChanges.pipe( 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; this.timewindowForm.get('history.advancedParams.lastAggIntervalsConfig').value;
if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) && if (lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
lastAggIntervalsConfig[timewindowMs].defaultAggInterval) { lastAggIntervalsConfig[timewindowMs].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue( setTimeout(() => this.timewindowForm.get('history.interval').patchValue(
lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false} lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
); ));
} }
}); });
this.timewindowForm.get('history.quickInterval').valueChanges.pipe( 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; this.timewindowForm.get('history.advancedParams.quickAggIntervalsConfig').value;
if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) && if (quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
quickAggIntervalsConfig[quickInterval].defaultAggInterval) { quickAggIntervalsConfig[quickInterval].defaultAggInterval) {
this.timewindowForm.get('history.interval').patchValue( setTimeout(() => this.timewindowForm.get('history.interval').patchValue(
quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false} 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) => { ).subscribe((timewindowMs: number) => {
if (this.realtimeAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) && if (this.realtimeAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
this.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) { 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.realtimeAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
); ));
} }
}); });
this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe( this.timewindowForm.get('realtime.quickInterval').valueChanges.pipe(
@ -318,9 +318,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((quickInterval: number) => { ).subscribe((quickInterval: number) => {
if (this.realtimeAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) && if (this.realtimeAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
this.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) { 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.realtimeAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval, {emitEvent: false}
); ));
} }
}); });
this.timewindowForm.get('history.timewindowMs').valueChanges.pipe( this.timewindowForm.get('history.timewindowMs').valueChanges.pipe(
@ -328,9 +328,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((timewindowMs: number) => { ).subscribe((timewindowMs: number) => {
if (this.historyAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) && if (this.historyAdvancedParams?.lastAggIntervalsConfig?.hasOwnProperty(timewindowMs) &&
this.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval) { 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.historyAdvancedParams.lastAggIntervalsConfig[timewindowMs].defaultAggInterval, {emitEvent: false}
); ));
} }
}); });
this.timewindowForm.get('history.quickInterval').valueChanges.pipe( this.timewindowForm.get('history.quickInterval').valueChanges.pipe(
@ -338,9 +338,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
).subscribe((quickInterval: number) => { ).subscribe((quickInterval: number) => {
if (this.historyAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) && if (this.historyAdvancedParams?.quickAggIntervalsConfig?.hasOwnProperty(quickInterval) &&
this.historyAdvancedParams.quickAggIntervalsConfig[quickInterval].defaultAggInterval) { 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} 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. /// limitations under the License.
/// ///
import { import { booleanAttribute, Directive, ElementRef, input, OnInit, Renderer2 } from '@angular/core';
AfterViewInit,
Directive,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { filter, takeUntil, tap } from 'rxjs/operators';
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; 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({ @Directive({
selector: '[tbTruncateWithTooltip]', 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 = input<string>(undefined, {alias: 'tbTruncateWithTooltip'});
text: string;
@Input() tooltipEnabled = input(true, {transform: booleanAttribute});
@coerceBoolean()
tooltipEnabled = true;
@Input() position = input<TooltipPosition>('above');
position: TooltipPosition = 'above';
private destroy$ = new Subject<void>();
constructor( constructor(
private elementRef: ElementRef, private elementRef: ElementRef<HTMLElement>,
private renderer: Renderer2, 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 { ngOnInit(): void {
this.observeMouseEvents();
this.applyTruncationStyles(); this.applyTruncationStyles();
this.tooltip.position = this.position();
this.showTooltipOnOverflow(this);
} }
ngAfterViewInit(): void { private showTooltipOnOverflow(ctx: TruncateWithTooltipDirective) {
this.tooltip.position = this.position; ctx.tooltip.show = (function(old) {
} function extendsFunction() {
if (ctx.tooltipEnabled() && ctx.isOverflown()) {
ngOnDestroy(): void { old.apply(ctx.tooltip, arguments);
if (this.tooltip._isTooltipVisible()) { }
this.hideTooltip(); }
} return extendsFunction;
this.destroy$.next(); })(ctx.tooltip.show);
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 applyTruncationStyles(): void { private applyTruncationStyles(): void {
@ -94,16 +71,7 @@ export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDe
this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis'); this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis');
} }
private isOverflown(element: HTMLElement): boolean { private isOverflown(): boolean {
return element.clientWidth < element.scrollWidth; return this.elementRef.nativeElement.clientWidth < this.elementRef.nativeElement.scrollWidth;
}
private showTooltip(): void {
this.tooltip.message = this.text || this.elementRef.nativeElement.innerText;
this.tooltip.show();
}
private hideTooltip(): void {
this.tooltip.hide();
} }
} }

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/`, scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/`,
scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`, scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`,
mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`,
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/applications/`,
} }
}; };
/* eslint-enable max-len */ /* eslint-enable max-len */

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

@ -193,11 +193,11 @@ export interface HasVersion {
version?: number; version?: number;
} }
export interface HasDebugSettings { export interface HasEntityDebugSettings {
debugSettings?: DebugSettings; debugSettings?: EntityDebugSettings;
} }
export interface DebugSettings { export interface EntityDebugSettings {
failuresEnabled?: boolean; failuresEnabled?: boolean;
allEnabled?: boolean; allEnabled?: boolean;
allEnabledUntil?: number; 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 { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.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 { export interface RuleNodeConfiguration {
[key: string]: any; [key: string]: any;
} }
export interface RuleNode extends BaseData<RuleNodeId>, HasDebugSettings { export interface RuleNode extends BaseData<RuleNodeId>, HasEntityDebugSettings {
ruleChainId?: RuleChainId; ruleChainId?: RuleChainId;
type: string; type: string;
name: string; name: string;
@ -331,7 +331,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor {
configurationDescriptor?: RuleNodeConfigurationDescriptor; configurationDescriptor?: RuleNodeConfigurationDescriptor;
} }
export interface FcRuleNodeType extends FcNode, HasDebugSettings { export interface FcRuleNodeType extends FcNode, HasEntityDebugSettings {
component?: RuleNodeComponentDescriptor; component?: RuleNodeComponentDescriptor;
singletonMode?: boolean; singletonMode?: boolean;
queueName?: string; queueName?: string;

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

@ -31,6 +31,7 @@ export interface DefaultTenantProfileConfiguration {
maxUsers: number; maxUsers: number;
maxDashboards: number; maxDashboards: number;
maxRuleChains: number; maxRuleChains: number;
maxEdges: number;
maxResourcesInBytes: number; maxResourcesInBytes: number;
maxOtaPackagesInBytes: number; maxOtaPackagesInBytes: number;
maxResourceSize: number; maxResourceSize: number;
@ -42,6 +43,13 @@ export interface DefaultTenantProfileConfiguration {
transportDeviceTelemetryMsgRateLimit?: string; transportDeviceTelemetryMsgRateLimit?: string;
transportDeviceTelemetryDataPointsRateLimit?: string; transportDeviceTelemetryDataPointsRateLimit?: string;
transportGatewayMsgRateLimit?: string;
transportGatewayTelemetryMsgRateLimit?: string;
transportGatewayTelemetryDataPointsRateLimit?: string;
transportGatewayDeviceMsgRateLimit?: string;
transportGatewayDeviceTelemetryMsgRateLimit?: string;
transportGatewayDeviceTelemetryDataPointsRateLimit?: string;
tenantEntityExportRateLimit?: string; tenantEntityExportRateLimit?: string;
tenantEntityImportRateLimit?: string; tenantEntityImportRateLimit?: string;
tenantNotificationRequestsRateLimit?: string; tenantNotificationRequestsRateLimit?: string;
@ -59,6 +67,8 @@ export interface DefaultTenantProfileConfiguration {
smsEnabled: boolean; smsEnabled: boolean;
maxCreatedAlarms: number; maxCreatedAlarms: number;
maxDebugModeDurationMinutes: number;
tenantServerRestLimitsConfiguration: string; tenantServerRestLimitsConfiguration: string;
customerServerRestLimitsConfiguration: string; customerServerRestLimitsConfiguration: string;
@ -75,6 +85,11 @@ export interface DefaultTenantProfileConfiguration {
cassandraQueryTenantRateLimitsConfiguration: string; cassandraQueryTenantRateLimitsConfiguration: string;
edgeEventRateLimits?: string;
edgeEventRateLimitsPerEdge?: string;
edgeUplinkMessagesRateLimits?: string;
edgeUplinkMessagesRateLimitsPerEdge?: string;
defaultStorageTtlDays: number; defaultStorageTtlDays: number;
alarmsTtlDays: number; alarmsTtlDays: number;
rpcTtlDays: number; rpcTtlDays: number;
@ -100,6 +115,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxUsers: 0, maxUsers: 0,
maxDashboards: 0, maxDashboards: 0,
maxRuleChains: 0, maxRuleChains: 0,
maxEdges: 0,
maxResourcesInBytes: 0, maxResourcesInBytes: 0,
maxOtaPackagesInBytes: 0, maxOtaPackagesInBytes: 0,
maxResourceSize: 0, maxResourceSize: 0,
@ -114,6 +130,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxSms: 0, maxSms: 0,
smsEnabled: true, smsEnabled: true,
maxCreatedAlarms: 0, maxCreatedAlarms: 0,
maxDebugModeDurationMinutes: 0,
tenantServerRestLimitsConfiguration: '', tenantServerRestLimitsConfiguration: '',
customerServerRestLimitsConfiguration: '', customerServerRestLimitsConfiguration: '',
maxWsSessionsPerTenant: 0, 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 { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/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({ @Pipe({
name: 'milliSecondsToTimeString' name: 'milliSecondsToTimeString'
@ -27,19 +27,21 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
} }
transform(milliSeconds: number, shortFormat = false, onlyFirstDigit = false): string { transform(milliSeconds: number, shortFormat = false, onlyFirstDigit = false): string {
const { days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds); const { years, days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds);
return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit); return this.formatTimeString(years, days, hours, minutes, seconds, shortFormat, onlyFirstDigit);
} }
private extractTimeUnits(milliseconds: number): { days: number; hours: number; minutes: number; seconds: number } { private extractTimeUnits(milliseconds: number): { years: number; days: number; hours: number; minutes: number; seconds: number } {
const days = Math.floor(milliseconds / DAY); const years = Math.floor(milliseconds / YEAR);
const days = Math.floor((milliseconds % YEAR) / DAY);
const hours = Math.floor((milliseconds % DAY) / HOUR); const hours = Math.floor((milliseconds % DAY) / HOUR);
const minutes = Math.floor((milliseconds % HOUR) / MINUTE); const minutes = Math.floor((milliseconds % HOUR) / MINUTE);
const seconds = Math.floor((milliseconds % MINUTE) / SECOND); const seconds = Math.floor((milliseconds % MINUTE) / SECOND);
return { days, hours, minutes, seconds }; return { years, days, hours, minutes, seconds };
} }
private formatTimeString( private formatTimeString(
years: number,
days: number, days: number,
hours: number, hours: number,
minutes: number, minutes: number,
@ -48,6 +50,7 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
onlyFirstDigit: boolean onlyFirstDigit: boolean
): string { ): string {
const timeUnits = [ const timeUnits = [
{ value: years, key: 'years', shortKey: 'short.years' },
{ value: days, key: 'days', shortKey: 'short.days' }, { value: days, key: 'days', shortKey: 'short.days' },
{ value: hours, key: 'hours', shortKey: 'short.hours' }, { value: hours, key: 'hours', shortKey: 'short.hours' },
{ value: minutes, key: 'minutes', shortKey: 'short.minutes' }, { value: minutes, key: 'minutes', shortKey: 'short.minutes' },
@ -59,7 +62,7 @@ export class MillisecondsToTimeStringPipe implements PipeTransform {
if (value > 0) { if (value > 0) {
timeString += this.translate.instant(shortFormat ? `timewindow.${shortKey}` : `timewindow.${key}`, { [key]: value }); timeString += this.translate.instant(shortFormat ? `timewindow.${shortKey}` : `timewindow.${key}`, { [key]: value });
if (onlyFirstDigit) { 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 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: 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": "الإخراج", "output": "الإخراج",
"test": "اختبار", "test": "اختبار",
"help": "مساعدة", "help": "مساعدة",
"reset-debug-mode": "إعادة تعيين وضع التصحيح في جميع العقد",
"test-with-this-message": "{{test}} مع هذه الرسالة" "test-with-this-message": "{{test}} مع هذه الرسالة"
}, },
"role": { "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.", "metadata-required": "Les entrades de metadades no poden estar buides.",
"output": "Sortida", "output": "Sortida",
"test": "Test", "test": "Test",
"help": "Ajuda", "help": "Ajuda"
"reset-debug-mode": "Restablir el mode de depuració a tots els nodes"
}, },
"role": { "role": {
"role": "Rol", "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ý.", "metadata-required": "Záznam metadat nemůže být prázdný.",
"output": "Výstup", "output": "Výstup",
"test": "Test", "test": "Test",
"help": "Nápověda", "help": "Nápověda"
"reset-debug-mode": "Resetovat režim ladění na všech uzlech"
}, },
"timezone": { "timezone": {
"timezone": "Časová zóna", "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.", "metadata-required": "Metadataposter må ikke være tomme.",
"output": "Output", "output": "Output",
"test": "Test", "test": "Test",
"help": "Hjælp", "help": "Hjælp"
"reset-debug-mode": "Nulstil debug-tilstand i alle knuder"
}, },
"role": { "role": {
"role": "Rolle", "role": "Rolle",

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

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

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

@ -989,16 +989,16 @@
"type-timeseries-deleted": "Telemetry deleted", "type-timeseries-deleted": "Telemetry deleted",
"type-sms-sent": "SMS sent" "type-sms-sent": "SMS sent"
}, },
"debug-config": { "debug-settings": {
"min": "{{number}} min",
"label": "Debug configuration", "label": "Debug configuration",
"on-failure": "Failures only (24/7)", "on-failure": "Failures only (24/7)",
"all-messages": "All messages ({{time}})", "all-messages": "All messages ({{time}})",
"failures": "Failures", "failures": "Failures",
"all": "All", "entity": "entity",
"rule-node": "rule node",
"hint": { "hint": {
"main": "All node debug messages rate limited with:", "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.", "on-failure": "Save all failure debug events without time limit.",
"all-messages": "Save all debug events during time limit." "all-messages": "Save all debug events during time limit."
} }
@ -3332,7 +3332,9 @@
"module-no-members": "Module has no exported members", "module-no-members": "Module has no exported members",
"module-load-error": "Module load error", "module-load-error": "Module load error",
"source-code": "Source code", "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-val": {
"key": "Key", "key": "Key",
@ -3550,7 +3552,8 @@
"tablet-959": "Tablet (max 959px)", "tablet-959": "Tablet (max 959px)",
"max-element-number": "Max elements number", "max-element-number": "Max elements number",
"page-name": "Page name", "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", "page-type": "Page type",
"pages-types": { "pages-types": {
"dashboard": "Dashboard", "dashboard": "Dashboard",
@ -3558,9 +3561,9 @@
"custom": "Custom" "custom": "Custom"
}, },
"url": "URL", "url": "URL",
"url-pattern": "Invalid URL", "invalid-url-format": "Invalid URL format",
"path": "Path", "path": "Path",
"path-pattern": "Path pattern", "invalid-path-format": "Invalid path format",
"custom-page": "Custom page", "custom-page": "Custom page",
"edit-page": "Edit page", "edit-page": "Edit page",
"edit-custom-page": "Edit custom page", "edit-custom-page": "Edit custom page",
@ -4293,7 +4296,7 @@
"output": "Output", "output": "Output",
"test": "Test", "test": "Test",
"help": "Help", "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", "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-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." "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": "{{ sec }} sec",
"sec-short": "{{ sec }}s", "sec-short": "{{ sec }}s",
"short": { "short": {
"years": "{ years, plural, =1 {1 year } other {# years } }",
"days": "{ days, plural, =1 {1 day } other {# days } }", "days": "{ days, plural, =1 {1 day } other {# days } }",
"hours": "{ hours, plural, =1 {1 hour } other {# hours } }", "hours": "{ hours, plural, =1 {1 hour } other {# hours } }",
"minutes": "{{minutes}} min ", "minutes": "{{minutes}} min ",

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

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

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

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

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

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

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

@ -4504,7 +4504,6 @@
"output": "Output", "output": "Output",
"test": "Test", "test": "Test",
"help": "Help", "help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes",
"test-with-this-message": "{{test}} with this message" "test-with-this-message": "{{test}} with this message"
}, },
"role": { "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.", "metadata-required": "Metadatu ievadi nevar būt tukši.",
"output": "Izeja", "output": "Izeja",
"test": "Tests", "test": "Tests",
"help": "Palīdzība", "help": "Palīdzība"
"reset-debug-mode": "Atiestatīt atkļūdošanu visās nodēs"
}, },
"tenant": { "tenant": {
"tenant": "Īrnieks", "tenant": "Īrnieks",

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

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

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

@ -4520,7 +4520,6 @@
"output": "Wyjście", "output": "Wyjście",
"test": "Test", "test": "Test",
"help": "Pomoc", "help": "Pomoc",
"reset-debug-mode": "Zresetuj tryb debugowania we wszystkich węzłach",
"test-with-this-message": "{{test}} with this message", "test-with-this-message": "{{test}} with this message",
"description": "Opis" "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.", "metadata-required": "As entradas de metadados não podem estar em branco.",
"output": "Saída", "output": "Saída",
"test": "Teste", "test": "Teste",
"help": "Ajuda", "help": "Ajuda"
"reset-debug-mode": "Redefinir modo de depuração em todos os nós"
}, },
"timezone": { "timezone": {
"timezone": "Fuso horário", "timezone": "Fuso horário",

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

@ -1401,7 +1401,6 @@
"output": "Ieşire", "output": "Ieşire",
"test": "Test", "test": "Test",
"help": "Ajutor", "help": "Ajutor",
"reset-debug-mode": "Dezactivează modul depanare în toate nodurile"
}, },
"tenant": { "tenant": {
"tenant": "Locatar", "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.", "metadata-required": "Vnosi metapodatkov ne smejo biti prazni.",
"output": "Izdelek", "output": "Izdelek",
"test": "Test", "test": "Test",
"help": "Pomoč", "help": "Pomoč"
"reset-debug-mode": "Ponastavi način za odpravljanje napak v vseh vozliščih"
}, },
"timezone": { "timezone": {
"timezone": "Časovni pas", "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.", "metadata-required": "Meta veri girişleri boş bırakılamaz.",
"output": "Çıktı", "output": "Çıktı",
"test": "Ölçek", "test": "Ölçek",
"help": "Yardım et", "help": "Yardım et"
"reset-debug-mode": "Tüm düğümlerde hata ayıklama modunu sıfırla"
}, },
"timezone": { "timezone": {
"timezone": "Saat dilimi", "timezone": "Saat dilimi",

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

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

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

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

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

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

3
ui-ngx/tailwind.config.js

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

Loading…
Cancel
Save