From 98f19e3162bff0a2b268027f5786898f744e21d1 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 20 May 2026 10:26:57 +0200 Subject: [PATCH 01/17] Fix CVE-2026-45149 --- ui-ngx/package.json | 3 ++- ui-ngx/yarn.lock | 26 ++++---------------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 076e2d316a..b343638845 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -142,6 +142,7 @@ "esbuild": "0.28.0", "rollup": "4.59.0", "jquery.terminal/**/form-data": ">=4.0.4", - "js-beautify/**/minimatch": "^9.0.7" + "js-beautify/**/minimatch": "^9.0.7", + "brace-expansion": "5.0.6" } } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index e9564f5279..258f852eea 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -4248,11 +4248,6 @@ babel-plugin-polyfill-regenerator@^0.6.5: dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - balanced-match@^4.0.2: version "4.0.4" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" @@ -4348,18 +4343,10 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -brace-expansion@^1.1.7: - version "1.1.13" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6" - integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" - integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== +brace-expansion@5.0.6, brace-expansion@^1.1.7, brace-expansion@^5.0.2: + version "5.0.6" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" + integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== dependencies: balanced-match "^4.0.2" @@ -4725,11 +4712,6 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - confbox@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" From 5343453cc22a56b42eeb87949b61573291376bd6 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 20 May 2026 10:29:01 +0200 Subject: [PATCH 02/17] Fix CVE-2026-6322 --- ui-ngx/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 258f852eea..289e71b943 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6095,9 +6095,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" - integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec" + integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ== fastq@^1.6.0: version "1.17.1" From 18bd3ac5f76387e7a8bc501d148d3deba162ea28 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 20 May 2026 10:31:34 +0200 Subject: [PATCH 03/17] Fix CVE-2026-44728 --- ui-ngx/yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 289e71b943..0c6bdf45a2 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -987,14 +987,14 @@ "@babel/helper-plugin-utils" "^7.28.6" "@babel/plugin-transform-modules-systemjs@^7.27.1": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz#7439e592a92d7670dfcb95d0cbc04bd3e64801d2" - integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== + version "7.29.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20" + integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w== dependencies: - "@babel/helper-module-transforms" "^7.28.3" - "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.28.5" + "@babel/traverse" "^7.29.0" "@babel/plugin-transform-modules-umd@^7.27.1": version "7.27.1" @@ -1298,7 +1298,7 @@ "@babel/parser" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6": +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== From 561d9dea5d6eeea3ca30b3462d7995875ec6ac21 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 20 May 2026 10:47:35 +0200 Subject: [PATCH 04/17] Minor fix --- ui-ngx/package.json | 3 +-- ui-ngx/yarn.lock | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b343638845..076e2d316a 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -142,7 +142,6 @@ "esbuild": "0.28.0", "rollup": "4.59.0", "jquery.terminal/**/form-data": ">=4.0.4", - "js-beautify/**/minimatch": "^9.0.7", - "brace-expansion": "5.0.6" + "js-beautify/**/minimatch": "^9.0.7" } } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 0c6bdf45a2..6b5f48283b 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -4248,6 +4248,11 @@ babel-plugin-polyfill-regenerator@^0.6.5: dependencies: "@babel/helper-define-polyfill-provider" "^0.6.5" +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + balanced-match@^4.0.2: version "4.0.4" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" @@ -4343,7 +4348,15 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -brace-expansion@5.0.6, brace-expansion@^1.1.7, brace-expansion@^5.0.2: +brace-expansion@^1.1.7: + version "1.1.14" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.14.tgz#d9de602370d91347cd9ddad1224d4fd701eb348b" + integrity sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^5.0.2: version "5.0.6" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.6.tgz#ec68fe0a641a29d8711579caf641d05bae1f2285" integrity sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g== @@ -4712,6 +4725,11 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + confbox@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" From 12905cb0d270b3d5f87a0d987d633e363b317d4d Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 25 May 2026 14:50:43 +0300 Subject: [PATCH 05/17] feat(iot-hub): single-step install dialog with rule chain profile toggle CF and Rule Chain installs now open directly into the selection form, skipping the prior "Install?" confirmation step. Rule Chain uses a single "Set as profile default rule chain" toggle that reveals the profile pickers when enabled, replacing the previous two-button design. The overwrite-confirmation step is preserved. Follow-up to #15645. --- .../iot-hub-install-dialog.component.html | 81 ++++++++++------ .../iot-hub-install-dialog.component.ts | 93 +++++++++++++------ .../assets/locale/locale.constant-en_US.json | 8 +- 3 files changed, 118 insertions(+), 64 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index 19a43fcb30..b144c3f35d 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -19,26 +19,55 @@ @switch (state) { @case ('confirm') {

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

- @if (item.type === ItemType.RULE_CHAIN) { -

{{ 'iot-hub.rule-chain-install-desc' | translate }}

- } @else { -

{{ 'iot-hub.install-desc' | translate }}

- } +

{{ 'iot-hub.install-desc' | translate }}

} @case ('select-entity') {

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

- @if (activeSelectEntityConfig?.promptKey) { -

+ @if (item.type === ItemType.RULE_CHAIN) { +

{{ 'iot-hub.rule-chain-install-subtitle' | translate }}

+
+
+ +
+ iot-hub.rule-chain-set-as-profile-default-toggle +
+
+
+ @if (ruleChainInstallForm.controls.setAsDefault.value) { +
+ + + + +
+ } +
+ } @else { + @if (activeSelectEntityConfig?.promptKey) { +

+ } + + } - - } @case ('confirm-overwrite') {

{{ 'iot-hub.rule-chain-overwrite-title' | translate }}

@@ -59,7 +88,7 @@ @case ('error') {

{{ 'iot-hub.install-error-title' | translate }}

{{ 'iot-hub.install-error-message' | translate:{ name: item.name } }}

- @if (item?.type === ItemType.SOLUTION_TEMPLATE) { + @if (item.type === ItemType.SOLUTION_TEMPLATE) { } @else {
@@ -73,28 +102,20 @@ @switch (state) { @case ('confirm') { - @if (item.type === ItemType.RULE_CHAIN) { - - - } @else { - - } + } @case ('select-entity') { @if (item.type === ItemType.RULE_CHAIN) { - } @else { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts index be37e6e1bb..6484a32c4b 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts @@ -14,9 +14,11 @@ /// limitations under the License. /// -import { Component, Inject } from '@angular/core'; +import { Component, DestroyRef, Inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Router } from '@angular/router'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DialogComponent } from '@shared/components/dialog.component'; @@ -70,15 +72,21 @@ export type InstallState = export class TbIotHubInstallDialogComponent extends DialogComponent { ItemType = ItemType; + EntityType = EntityType; item: MpItemVersionView; typeTranslations = itemTypeTranslations; - state: InstallState = 'confirm'; + state!: InstallState; errorMessage = ''; entityDetailsUrl: string | null = null; selectedEntityId: EntityId | null = null; pendingOverwrite: PendingOverwrite | null = null; + ruleChainInstallForm!: FormGroup<{ + setAsDefault: FormControl; + entityType: FormControl; + entityId: FormControl; + }>; private readonly selectEntityConfig: Partial> = { [ItemType.CALCULATED_FIELD]: { @@ -87,12 +95,6 @@ export class TbIotHubInstallDialogComponent extends DialogComponent(false), + entityType: this.fb.nonNullable.control(EntityType.DEVICE_PROFILE), + entityId: this.fb.control(null) + }); + this.ruleChainInstallForm.controls.setAsDefault.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(setAsDefault => { + const entityIdCtrl = this.ruleChainInstallForm.controls.entityId; + if (setAsDefault) { + entityIdCtrl.setValidators(Validators.required); + } else { + entityIdCtrl.clearValidators(); + entityIdCtrl.setValue(null); + } + entityIdCtrl.updateValueAndValidity(); + }); + this.ruleChainInstallForm.controls.entityType.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.ruleChainInstallForm.controls.entityId.setValue(null); + }); } getTypeLabel(): string { @@ -120,39 +159,33 @@ export class TbIotHubInstallDialogComponent extends DialogComponent { if (pending) { this.pendingOverwrite = pending; this.state = 'confirm-overwrite'; } else { this.pendingOverwrite = null; - this.doInstall(); + this.install(); } }, error: (err) => { @@ -164,7 +197,7 @@ export class TbIotHubInstallDialogComponent extends DialogComponent{{profile}} currently uses {{existing}} as its default rule chain. Installing will replace it with this rule chain.", "rule-chain-overwrite-replace": "Replace", From af1b888cf01a6b563fb9c100d230cb834be19270 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 25 May 2026 15:03:08 +0300 Subject: [PATCH 06/17] fix(iot-hub): polish rule chain install dialog layout and copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Group the slide-toggle and the conditionally rendered profile pickers in a single bordered card via tb-form-panel.stroked, matching the 4.3 entity-aggregation 'Apply await timeout' pattern. - Set the toggle's class to 'mat-slide flex' to match the canonical TB idiom; switch the tooltip label to plain interpolation. - Restore the trailing '?' in install-confirm-title — the confirm step is still a real confirmation question for item types that don't go through the select-entity flow. --- .../iot-hub-install-dialog.component.html | 50 +++++++++---------- .../assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html index b144c3f35d..102a55ad5e 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.html @@ -25,34 +25,34 @@

{{ 'iot-hub.install-confirm-title' | translate:{ name: item.name } }}

@if (item.type === ItemType.RULE_CHAIN) {

{{ 'iot-hub.rule-chain-install-subtitle' | translate }}

-
-
- -
- iot-hub.rule-chain-set-as-profile-default-toggle + +
+ +
+ {{ 'iot-hub.rule-chain-set-as-profile-default-toggle' | translate }}
+ @if (ruleChainInstallForm.controls.setAsDefault.value) { +
+ + + + +
+ }
- @if (ruleChainInstallForm.controls.setAsDefault.value) { -
- - - - -
- } } @else { @if (activeSelectEntityConfig?.promptKey) { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 7444a27506..73a7dce751 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3802,7 +3802,7 @@ "changelog": "Changelog", "created-by": "Created by {{name}}", "install-confirm": "Install {{name}} v{{version}}?", - "install-confirm-title": "Install '{{name}}'", + "install-confirm-title": "Install '{{name}}'?", "install-desc": "Installing this will automatically create and configure the necessary resources within your tenant", "install-success-desc": "'{{name}}' has been installed successfully", "remove": "Remove", From 22043d126187110bd2ed820d859abe1951b93423 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 25 May 2026 18:35:40 +0300 Subject: [PATCH 07/17] fixed UI sorting to match api logic --- .../controller/SystemInfoController.java | 12 +++++++ .../server/common/data/SystemParams.java | 2 ++ ui-ngx/src/app/core/auth/auth.models.ts | 3 ++ ui-ngx/src/app/core/auth/auth.reducer.ts | 4 ++- ui-ngx/src/app/core/auth/auth.service.ts | 3 ++ .../src/app/shared/models/page/page-link.ts | 36 +++++++++++++++++++ 6 files changed, 59 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 61240d304e..1f7a3ab5f6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsType; +import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.mobile.QrCodeSettingService; import org.thingsboard.server.dao.trendz.TrendzSettingsService; @@ -52,6 +53,7 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Hidden @@ -76,6 +78,11 @@ public class SystemInfoController extends BaseController { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + @Value("${sql.entity_data_query_nulls_order_strategy:default}") + private String nullsOrderStrategy; + + private static final Set ACCEPTED_NULLS_ORDER_STRATEGIES = Set.of("default", "nulls_first", "nulls_last"); + @Autowired(required = false) private BuildProperties buildProperties; @@ -91,6 +98,9 @@ public class SystemInfoController extends BaseController { @Autowired private TrendzSettingsService trendzSettingsService; + @Autowired + private EdqsService edqsService; + @PostConstruct public void init() { JsonNode info = buildInfoObject(); @@ -150,6 +160,8 @@ public class SystemInfoController extends BaseController { } systemParams.setUserSettings(userSettingsNode); systemParams.setMaxDatapointsLimit(maxDatapointsLimit); + systemParams.setNullsOrderStrategy(ACCEPTED_NULLS_ORDER_STRATEGIES.contains(nullsOrderStrategy) ? nullsOrderStrategy : "default"); + systemParams.setEdqsEnabled(edqsService.isApiEnabled()); if (!currentUser.isSystemAdmin()) { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index c31b49325e..527ee2f18d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -39,4 +39,6 @@ public class SystemParams { long maxArgumentsPerCF; long maxDataPointsPerRollingArg; TrendzSettings trendzSettings; + String nullsOrderStrategy; + boolean edqsEnabled; } diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 344c44f6c7..2485783853 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -17,6 +17,7 @@ import { AuthUser, User } from '@shared/models/user.model'; import { UserSettings } from '@shared/models/user-settings.models'; import { TrendzSettings } from '@shared/models/trendz-settings.models'; +import { NullsOrderStrategy } from '@shared/models/page/page-link'; export interface SysParamsState { userTokenAccessEnabled: boolean; @@ -34,6 +35,8 @@ export interface SysParamsState { ruleChainDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string; trendzSettings: TrendzSettings; + nullsOrderStrategy: NullsOrderStrategy; + edqsEnabled: boolean; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index ca46344bbb..263b182cb9 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -36,7 +36,9 @@ const emptyUserAuthState: AuthPayload = { maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, userSettings: initialUserSettings, - trendzSettings: initialTrendzSettings + trendzSettings: initialTrendzSettings, + nullsOrderStrategy: 'default', + edqsEnabled: false }; export const initialState: AuthState = { diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 1c6ac90f3b..744cb92534 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -20,6 +20,7 @@ import { HttpClient } from '@angular/common/http'; import { Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { setEdqsEnabled, setNullsOrderStrategy } from '@shared/models/page/page-link'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; import { Router, UrlTree } from '@angular/router'; @@ -441,6 +442,8 @@ export class AuthService { return this.http.get('/api/system/params', defaultHttpOptions()).pipe( map((sysParams) => { this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit); + setNullsOrderStrategy(sysParams.nullsOrderStrategy); + setEdqsEnabled(sysParams.edqsEnabled); return sysParams; }), catchError(() => of({} as SysParamsState)) diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index eb2c059c2a..a42ce4db6d 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -22,6 +22,19 @@ import { EntitiesTableAction } from '@home/models/entity/entity-table-component. export const MAX_SAFE_PAGE_SIZE = 2147483647; +export type NullsOrderStrategy = 'default' | 'nulls_first' | 'nulls_last'; + +let nullsOrderStrategy: NullsOrderStrategy = 'default'; +let edqsEnabled = false; + +export function setNullsOrderStrategy(value: NullsOrderStrategy): void { + nullsOrderStrategy = value ?? 'default'; +} + +export function setEdqsEnabled(value: boolean): void { + edqsEnabled = !!value; +} + export type PageLinkSearchFunction = (entity: T, textSearch: string, searchProperty?: string) => boolean; export interface PageQueryParam extends Partial{ @@ -76,6 +89,29 @@ const defaultPageLinkSearch: PageLinkSearchFunction = export function sortItems(item1: any, item2: any, property: string, asc: boolean): number { const item1Value = getDescendantProp(item1, property); const item2Value = getDescendantProp(item2, property); + const item1Empty = item1Value === null || item1Value === undefined || item1Value === ''; + const item2Empty = item2Value === null || item2Value === undefined || item2Value === ''; + // Mirror backend's nulls ordering only for column types where the SQL ORDER BY sees real NULLs + // (boolean/numeric attribute values and entity-field columns). String attribute/telemetry + // values are coalesced to '' in SQL, so the backend ignores the strategy there and naive + // compare below already matches its order. EDQS has its own fixed null handling (ASC=NULLS + // FIRST, DESC=NULLS LAST) that doesn't honor the strategy at all, so skip this branch then + // — naive compare already matches EDQS's behavior for the values we care about. + if (!edqsEnabled && (item1Empty || item2Empty) && !(item1Empty && item2Empty)) { + const other = item1Empty ? item2Value : item1Value; + const otherIsBoolOrNum = + typeof other === 'boolean' || other === 'true' || other === 'false' || + (typeof other === 'number' && isFinite(other)) || + (typeof other === 'string' && other.trim() !== '' && !isNaN(Number(other))); + if (otherIsBoolOrNum) { + const nullsFirst = nullsOrderStrategy === 'nulls_first' + || (nullsOrderStrategy === 'default' && !asc); + if (item1Empty) { + return nullsFirst ? -1 : 1; + } + return nullsFirst ? 1 : -1; + } + } let result = 0; if (item1Value !== item2Value) { const item1Type = typeof item1Value; From 12daf918e5fead8eade775767fa98ce952334e17 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 26 May 2026 10:26:16 +0300 Subject: [PATCH 08/17] fixed sorting for string values --- .../alarm/alarms-table-widget.component.ts | 16 +++++++--- .../entity/entities-table-widget.component.ts | 15 ++++++--- .../src/app/shared/models/page/page-link.ts | 31 +++++++++++-------- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 31927e0cb3..711107895f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -47,7 +47,7 @@ import { isUndefined } from '@core/utils'; import cssjs from '@core/css/css'; -import { sortItems } from '@shared/models/page/page-link'; +import { SortColumnType, sortItems } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'; @@ -118,6 +118,7 @@ import { dataKeyToEntityKey, dataKeyTypeToEntityKeyType, entityDataPageLinkSortDirection, + EntityKeyType, KeyFilter } from '@app/shared/models/query/query.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -730,8 +731,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.pageLink.sortOrder = null; } const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); + const sortColumnType: SortColumnType = key + ? (key.type === EntityKeyType.ENTITY_FIELD || key.type === EntityKeyType.ALARM_FIELD ? 'entityField' + : key.type === EntityKeyType.TIME_SERIES ? 'timeseries' : 'attribute') + : 'entityField'; const keyFilters: KeyFilter[] = null; // TODO: - this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters); + this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, sortColumnType, keyFilters); this.ctx.detectChanges(); } @@ -1256,6 +1261,7 @@ class AlarmsDatasource implements DataSource { private appliedPageLink: AlarmDataPageLink; private appliedSortOrderLabel: string; + private appliedSortColumnType: SortColumnType = 'entityField'; private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; @@ -1294,11 +1300,13 @@ class AlarmsDatasource implements DataSource { this.pageDataSubject.complete(); } - loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) { + loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, + sortColumnType: SortColumnType, keyFilters: KeyFilter[]) { this.dataLoading = true; // this.clear(); this.appliedPageLink = pageLink; this.appliedSortOrderLabel = sortOrderLabel; + this.appliedSortColumnType = sortColumnType; this.subscription.subscribeForAlarms(pageLink, keyFilters); } @@ -1330,7 +1338,7 @@ class AlarmsDatasource implements DataSource { } if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; - alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); + alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc, this.appliedSortColumnType)); } if (this.selection.hasValue()) { const alarmIds = alarms.map((alarm) => alarm.id.id); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index b99ba01212..8bd5f07896 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -100,7 +100,7 @@ import { EntityKeyType, KeyFilter } from '@shared/models/query/query.models'; -import { sortItems } from '@shared/models/page/page-link'; +import { SortColumnType, sortItems } from '@shared/models/page/page-link'; import { entityFields } from '@shared/models/entity.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -617,8 +617,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.pageLink.sortOrder = null; } const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); + const sortColumnType: SortColumnType = key + ? (key.type === EntityKeyType.ENTITY_FIELD ? 'entityField' + : key.type === EntityKeyType.TIME_SERIES ? 'timeseries' : 'attribute') + : 'entityField'; const keyFilters: KeyFilter[] = null; // TODO: - this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, keyFilters); + this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, sortColumnType, keyFilters); this.ctx.detectChanges(); } @@ -865,6 +869,7 @@ class EntityDatasource implements DataSource { private appliedPageLink: EntityDataPageLink; private appliedSortOrderLabel: string; + private appliedSortColumnType: SortColumnType = 'entityField'; private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; @@ -905,11 +910,13 @@ class EntityDatasource implements DataSource { this.pageDataSubject.complete(); } - loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) { + loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string, + sortColumnType: SortColumnType, keyFilters: KeyFilter[]) { this.dataLoading = true; // this.clear(); this.appliedPageLink = pageLink; this.appliedSortOrderLabel = sortOrderLabel; + this.appliedSortColumnType = sortColumnType; this.subscription.subscribeForPaginatedData(0, pageLink, keyFilters); } @@ -934,7 +941,7 @@ class EntityDatasource implements DataSource { }); if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; - entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); + entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc, this.appliedSortColumnType)); } if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) { maxCellButtonAction = entities[0].actionCellButtons.length; diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index a42ce4db6d..f3837a5cc5 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -86,24 +86,29 @@ const defaultPageLinkSearch: PageLinkSearchFunction = return false; }; -export function sortItems(item1: any, item2: any, property: string, asc: boolean): number { +export type SortColumnType = 'entityField' | 'attribute' | 'timeseries'; + +export function sortItems(item1: any, item2: any, property: string, asc: boolean, + columnType: SortColumnType = 'entityField'): number { const item1Value = getDescendantProp(item1, property); const item2Value = getDescendantProp(item2, property); const item1Empty = item1Value === null || item1Value === undefined || item1Value === ''; const item2Empty = item2Value === null || item2Value === undefined || item2Value === ''; - // Mirror backend's nulls ordering only for column types where the SQL ORDER BY sees real NULLs - // (boolean/numeric attribute values and entity-field columns). String attribute/telemetry - // values are coalesced to '' in SQL, so the backend ignores the strategy there and naive - // compare below already matches its order. EDQS has its own fixed null handling (ASC=NULLS - // FIRST, DESC=NULLS LAST) that doesn't honor the strategy at all, so skip this branch then - // — naive compare already matches EDQS's behavior for the values we care about. + // Mirror backend's nulls ordering. EDQS uses fixed NULLS FIRST regardless of strategy and + // naive compare below already matches it, so skip this branch when EDQS is on. + // For entityField columns the ORDER BY hits a real nullable DB column → strategy always applies. + // For attribute/timeseries the strategy only applies to numeric/boolean values; string/json + // are coalesced to '' on the backend, so naive compare below already matches its order. if (!edqsEnabled && (item1Empty || item2Empty) && !(item1Empty && item2Empty)) { - const other = item1Empty ? item2Value : item1Value; - const otherIsBoolOrNum = - typeof other === 'boolean' || other === 'true' || other === 'false' || - (typeof other === 'number' && isFinite(other)) || - (typeof other === 'string' && other.trim() !== '' && !isNaN(Number(other))); - if (otherIsBoolOrNum) { + let applyStrategy = columnType === 'entityField'; + if (!applyStrategy) { + const other = item1Empty ? item2Value : item1Value; + applyStrategy = + typeof other === 'boolean' || other === 'true' || other === 'false' || + (typeof other === 'number' && isFinite(other)) || + (typeof other === 'string' && other.trim() !== '' && !isNaN(Number(other))); + } + if (applyStrategy) { const nullsFirst = nullsOrderStrategy === 'nulls_first' || (nullsOrderStrategy === 'default' && !asc); if (item1Empty) { From e998026de37a6ffeabda0fad519b6d69976205dd Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Tue, 26 May 2026 10:36:04 +0300 Subject: [PATCH 09/17] CertificateReloadManager: detect content changes via checksum, not mtime --- .../service/CertificateReloadManager.java | 74 ++++--------------- .../service/CertificateReloadManagerTest.java | 64 ++++++++++------ 2 files changed, 54 insertions(+), 84 deletions(-) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java index 63f2247aba..82b7981f8f 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/CertificateReloadManager.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.queue.util.TbTransportComponent; -import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -165,7 +164,6 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis static class CertificateWatcher { private final List paths; private final Runnable reloadCallback; - private final Map lastModifiedMap; private final Map lastChecksumMap; private int consecutiveFailures; private String failedCombinedChecksum; @@ -173,60 +171,23 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis CertificateWatcher(List paths, Runnable reloadCallback) { this.paths = paths; this.reloadCallback = reloadCallback; - this.lastModifiedMap = new HashMap<>(); this.lastChecksumMap = new HashMap<>(); for (Path path : paths) { - lastModifiedMap.put(path, getLastModifiedTime(path)); lastChecksumMap.put(path, calculateChecksum(path)); } this.consecutiveFailures = 0; } synchronized void checkAndReload(String name) { - boolean anyModifiedChanged = false; - for (Path path : paths) { - long currentModified = getLastModifiedTime(path); - Long lastModified = lastModifiedMap.getOrDefault(path, 0L); - if (currentModified != lastModified) { - anyModifiedChanged = true; - break; - } - } - if (!anyModifiedChanged) { - return; - } - - // Capture mtimes and checksums together before the callback runs. - // Pairing a post-callback mtime with a pre-callback checksum would let a write-during-reload be missed on the next poll. - Map currentModifiedTimes = new HashMap<>(); Map currentChecksums = new HashMap<>(); - StringBuilder combined = new StringBuilder(); for (Path path : paths) { - currentModifiedTimes.put(path, getLastModifiedTime(path)); - String checksum = calculateChecksum(path); - currentChecksums.put(path, checksum); - if (!combined.isEmpty()) { - combined.append("|"); - } - combined.append(path).append("=").append(checksum); + currentChecksums.put(path, calculateChecksum(path)); } - String combinedChecksum = combined.toString(); - - // Build old combined checksum for comparison - StringBuilder oldCombined = new StringBuilder(); - for (Path path : paths) { - if (!oldCombined.isEmpty()) { - oldCombined.append("|"); - } - oldCombined.append(path).append("=").append(lastChecksumMap.getOrDefault(path, "")); - } - String oldCombinedChecksum = oldCombined.toString(); + String combinedChecksum = combinedChecksum(currentChecksums); + String oldCombinedChecksum = combinedChecksum(lastChecksumMap); if (combinedChecksum.equals(oldCombinedChecksum)) { - // Content unchanged, just update modification times - for (Path path : paths) { - lastModifiedMap.put(path, currentModifiedTimes.get(path)); - } + // Content unchanged return; } @@ -237,41 +198,34 @@ public class CertificateReloadManager implements SmartInitializingSingleton, Dis } if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { - // Update modification times to avoid re-checking mtime and re-computing checksums every poll cycle - for (Path path : paths) { - lastModifiedMap.put(path, currentModifiedTimes.get(path)); - } return; } try { log.info("Certificate change detected for: {}. Triggering reload...", name); reloadCallback.run(); - for (Path path : paths) { - lastModifiedMap.put(path, currentModifiedTimes.get(path)); - lastChecksumMap.put(path, currentChecksums.get(path)); - } + lastChecksumMap.putAll(currentChecksums); consecutiveFailures = 0; failedCombinedChecksum = null; } catch (Exception e) { consecutiveFailures++; failedCombinedChecksum = combinedChecksum; - // Deliberately NOT updating the lastModifiedMap here, so the next poll cycle retries - // (mtime mismatch passes the early gate, checksum matches failedCombinedChecksum). + // Deliberately NOT updating lastChecksumMap here, so the next poll cycle still sees a differing + // checksum, re-enters this method, and retries the same content. log.error("Failed to reload certificate for {} (attempt {}/{}): {}", name, consecutiveFailures, MAX_CONSECUTIVE_FAILURES, e.getMessage(), e); } } - private long getLastModifiedTime(Path path) { - try { - if (!Files.exists(path)) { - return 0; + private String combinedChecksum(Map checksums) { + StringBuilder combined = new StringBuilder(); + for (Path path : paths) { + if (!combined.isEmpty()) { + combined.append("|"); } - return Files.getLastModifiedTime(path).toMillis(); - } catch (IOException e) { - return 0; + combined.append(path).append("=").append(checksums.getOrDefault(path, "")); } + return combined.toString(); } private String calculateChecksum(Path path) { diff --git a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java index 6f78eaefae..8894937177 100644 --- a/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java +++ b/common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/CertificateReloadManagerTest.java @@ -31,10 +31,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; public class CertificateReloadManagerTest { @@ -59,11 +56,10 @@ public class CertificateReloadManagerTest { } } - private void writeFileAndAwaitMtimeChange(Path path, String content, long baselineMtime) throws IOException { + private void writeFileAndBumpMtime(Path path, String content, long baselineMtime) throws IOException { Files.writeString(path, content); - await().atMost(2, SECONDS) - .pollInterval(10, MILLISECONDS) - .until(() -> Files.getLastModifiedTime(path).toMillis() != baselineMtime); + // Force a strictly newer mtime: back-to-back writes can share a millisecond, hiding the change from the watcher. + Files.setLastModifiedTime(path, FileTime.fromMillis(baselineMtime + 1000)); } private long mtime(Path path) throws IOException { @@ -77,7 +73,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nTEST_CERT_V2_MODIFIED\n-----END CERTIFICATE-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -85,7 +81,7 @@ public class CertificateReloadManagerTest { } @Test - public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() throws Exception { + public void givenCertificateFileUnchanged_whenCheckForChanges_thenShouldNotTriggerReload() { AtomicInteger reloadCount = new AtomicInteger(0); certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); @@ -147,7 +143,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-key", keyFile, keyReloadCount::incrementAndGet); long baseline = mtime(keyFile); - writeFileAndAwaitMtimeChange(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline); + writeFileAndBumpMtime(keyFile, "-----BEGIN PRIVATE KEY-----\nTEST_KEY_V2_MODIFIED\n-----END PRIVATE KEY-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -168,8 +164,8 @@ public class CertificateReloadManagerTest { long baseline1 = mtime(certFile); long baseline2 = mtime(cert2File); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); - writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); + writeFileAndBumpMtime(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -191,8 +187,8 @@ public class CertificateReloadManagerTest { long baseline1 = mtime(certFile); long baseline2 = mtime(cert2File); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); - writeFileAndAwaitMtimeChange(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED1\n-----END CERTIFICATE-----\n", baseline1); + writeFileAndBumpMtime(cert2File, "-----BEGIN CERTIFICATE-----\nMODIFIED2\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -224,9 +220,7 @@ public class CertificateReloadManagerTest { for (int i = 0; i < 5; i++) { Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nCERT_VERSION_" + i + "\n-----END CERTIFICATE-----\n"); } - await().atMost(2, SECONDS) - .pollInterval(10, MILLISECONDS) - .until(() -> mtime(certFile) != baseline); + Files.setLastModifiedTime(certFile, FileTime.fromMillis(baseline + 1000)); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -242,7 +236,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nMODIFIED\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 5; i++) { new Thread(() -> { @@ -272,7 +266,7 @@ public class CertificateReloadManagerTest { certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, originalContent, baseline); + writeFileAndBumpMtime(certFile, originalContent, baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -289,7 +283,7 @@ public class CertificateReloadManagerTest { }); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 15; i++) { ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -311,19 +305,41 @@ public class CertificateReloadManagerTest { }); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(1); shouldFail.set(0); long baseline2 = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nGOOD_CERT\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(2); } + @Test + public void givenContentChangedButMtimeUnchanged_whenCheckForChanges_thenShouldTriggerReload() throws Exception { + // Bug fingerprint: a cert-manager rotation that lands in the same wall-clock millisecond as the + // watcher's recorded baseline mtime. Files.getLastModifiedTime().toMillis() truncates to the ms, + // so the rotated content shares the baseline mtime and an mtime-only gate would never re-hash it. + AtomicInteger reloadCount = new AtomicInteger(0); + + certificateReloadManager.registerWatcher("test-cert", certFile, reloadCount::incrementAndGet); + + long baseline = mtime(certFile); + Files.writeString(certFile, "-----BEGIN CERTIFICATE-----\nROTATED_SAME_MS\n-----END CERTIFICATE-----\n"); + // Force the mtime back to the exact baseline millisecond — content changed, timestamp did not. + Files.setLastModifiedTime(certFile, FileTime.fromMillis(baseline)); + + // Sanity guard: the watcher observes a timestamp identical to its recorded baseline. + assertThat(mtime(certFile)).isEqualTo(baseline); + + ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); + + assertThat(reloadCount.get()).isEqualTo(1); + } + @Test public void givenCallbackHitMaxFailures_whenFileChangesToNewContent_thenShouldResetAndRetry() throws Exception { AtomicInteger reloadAttempts = new AtomicInteger(0); @@ -337,7 +353,7 @@ public class CertificateReloadManagerTest { }); long baseline = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nBAD_CERT\n-----END CERTIFICATE-----\n", baseline); for (int i = 0; i < 15; i++) { ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); @@ -346,7 +362,7 @@ public class CertificateReloadManagerTest { shouldFail.set(0); long baseline2 = mtime(certFile); - writeFileAndAwaitMtimeChange(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2); + writeFileAndBumpMtime(certFile, "-----BEGIN CERTIFICATE-----\nFIXED_CERT\n-----END CERTIFICATE-----\n", baseline2); ReflectionTestUtils.invokeMethod(certificateReloadManager, "checkCertificates"); assertThat(reloadAttempts.get()).isEqualTo(11); From 9c1e07968572c2a7070aee9ccebad8b8ee50d73d Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 26 May 2026 13:43:07 +0300 Subject: [PATCH 10/17] feat(iot-hub): listing-slug deep links + edition / version gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a public listing item-version endpoint to IotHubApiService (GET /api/listings/public/by-slug/{slug}/item-version?ce=true& tbVersion=N) returning MpItemVersionView, with the new ListingItemVersionNotFound shape capturing the documented 404 bodies (noMatchingVersions / peRequired / minTbVersionRequired). - Wire a tbVersionIntToString helper alongside the existing tbVersionToInt so the dialogs can render the resolved versions compactly (430 → "4.3", 421 → "4.2.1"). - Route /iot-hub/listing/:slug to TbIotHubItemResolverComponent. Resolver picks the slug path first, dispatches the 404 bodies to three handlers: noMatchingVersions reuses the existing deep-link-not-found notification, peRequired opens a new dialog, minTbVersionRequired opens another with the resolved min version. - New TbIotHubPeRequiredDialogComponent renders the "Professional Edition required" prompt and TbIotHubUpgradeRequiredDialogComponent renders the "ThingsBoard upgrade required" prompt. Both reuse a single upgrade-required.svg asset (Figma export) tinted via CSS mask + background-color so the PE accent inherits without touching the asset. The shared "Upgrade instance" button links to the right upgrade doc per dialog. - New TbIotHubAlarmRulesUnavailablePageComponent replaces TbIotHubItemsPageComponent on /iot-hub/alarm-rules. It mirrors the iot-hub-home glow-blob + dot-pattern background (alarm-rule accent), renders the alarm-rules hero, a 28px title with the primary "Thingsboard v4.3" span, the description, an Upgrade instance link (https://thingsboard.io/docs/installation/upgrade- instructions/), Back to IoT Hub, and the current platform version read from env.tbVersion. - Locale: pe-required-title / pe-required-message (bolded "Professional Edition" / "Community Edition"), upgrade-required-title / upgrade-required-message (bolded interpolations), and the alarm- rules-unavailable-* keys for the new page. --- .../src/app/core/http/iot-hub-api.service.ts | 33 ++++ .../iot-hub/iot-hub-components.module.ts | 10 +- .../iot-hub-pe-required-dialog.component.html | 40 +++++ .../iot-hub-pe-required-dialog.component.scss | 77 ++++++++++ .../iot-hub-pe-required-dialog.component.ts | 47 ++++++ ...hub-upgrade-required-dialog.component.html | 43 ++++++ ...hub-upgrade-required-dialog.component.scss | 76 ++++++++++ ...t-hub-upgrade-required-dialog.component.ts | 59 ++++++++ ...larm-rules-unavailable-page.component.html | 52 +++++++ ...larm-rules-unavailable-page.component.scss | 143 ++++++++++++++++++ ...-alarm-rules-unavailable-page.component.ts | 43 ++++++ .../iot-hub-item-resolver.component.ts | 73 +++++++-- .../pages/iot-hub/iot-hub-routing.module.ts | 12 +- .../home/pages/iot-hub/iot-hub.module.ts | 4 +- .../models/iot-hub/iot-hub-version.models.ts | 9 ++ .../src/assets/iot-hub/upgrade-required.svg | 15 ++ .../assets/locale/locale.constant-en_US.json | 9 ++ 17 files changed, 730 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts create mode 100644 ui-ngx/src/assets/iot-hub/upgrade-required.svg diff --git a/ui-ngx/src/app/core/http/iot-hub-api.service.ts b/ui-ngx/src/app/core/http/iot-hub-api.service.ts index 12878ac0fc..b0f90ddeef 100644 --- a/ui-ngx/src/app/core/http/iot-hub-api.service.ts +++ b/ui-ngx/src/app/core/http/iot-hub-api.service.ts @@ -39,6 +39,16 @@ export function tbVersionToInt(version: string): number { return major * 100 + minor * 10 + patch; } +// Inverse of tbVersionToInt — produces "{major}.{minor}" by default +// and appends ".{patch}" when the patch component is non-zero so the +// rendered version stays compact (e.g. 430 → "4.3", 421 → "4.2.1"). +export function tbVersionIntToString(version: number): string { + const major = Math.floor(version / 100); + const minor = Math.floor((version % 100) / 10); + const patch = version % 10; + return patch === 0 ? `${major}.${minor}` : `${major}.${minor}.${patch}`; +} + export function iotHubResourceUrl(baseUrl: string, path: string): string { if (!path) { return path; @@ -121,6 +131,29 @@ export class IotHubApiService { ); } + /** + * Resolves the listing item-version that matches the caller's edition + * and platform version. + * + * Backend: GET /api/listings/public/by-slug/{slug}/item-version + * + * On success → 200 with `MpItemVersionView`. + * On miss → 404 with a `ListingItemVersionNotFound` body shape + * (`noMatchingVersions`, `peRequired`, or + * `minTbVersionRequired`). Callers should inspect + * `HttpErrorResponse.error` to differentiate. + */ + public getListingItemVersion(slug: string, config?: IotHubRequestConfig): Observable { + const queryParams = [ + 'ce=true', + `tbVersion=${tbVersionToInt(env.tbVersion)}` + ]; + return this.http.get( + `${this.baseUrl}/api/listings/public/by-slug/${encodeURIComponent(slug)}/item-version?${queryParams.join('&')}`, + { params: this.buildParams(config) } + ); + } + public getVersionReadme(versionId: string, config?: IotHubRequestConfig): Observable { return this.http.get(`${this.baseUrl}/api/versions/${versionId}/readme`, { params: this.buildParams(config), diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts index cb37201432..2df12eedce 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts @@ -31,6 +31,8 @@ import { TbIotHubSearchComponent } from './iot-hub-search.component'; import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-table.component'; import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-dialog.component'; import { TbPeConnectivityMethodPromptComponent } from './pe-connectivity-method-prompt.component'; +import { TbIotHubPeRequiredDialogComponent } from './iot-hub-pe-required-dialog.component'; +import { TbIotHubUpgradeRequiredDialogComponent } from './iot-hub-upgrade-required-dialog.component'; import { TbIotHubMarkdownComponent } from './iot-hub-markdown.component'; import { SolutionInstallDialogComponent } from './solution-install-dialog.component'; import { IotHubActionsService } from './iot-hub-actions.service'; @@ -53,7 +55,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link TbPeConnectivityMethodPromptComponent, TbIotHubMarkdownComponent, SolutionInstallDialogComponent, - InstallFormRendererComponent + InstallFormRendererComponent, + TbIotHubPeRequiredDialogComponent, + TbIotHubUpgradeRequiredDialogComponent ], imports: [ CommonModule, @@ -78,7 +82,9 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link TbIotHubInstalledItemsDialogComponent, TbPeConnectivityMethodPromptComponent, TbIotHubMarkdownComponent, - SolutionInstallDialogComponent + SolutionInstallDialogComponent, + TbIotHubPeRequiredDialogComponent, + TbIotHubUpgradeRequiredDialogComponent ] }) export class IotHubComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html new file mode 100644 index 0000000000..57e98e4cbb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.html @@ -0,0 +1,40 @@ + +
+ + + + +

+ {{ 'iot-hub.pe-required-title' | translate }} +

+ +

+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss new file mode 100644 index 0000000000..8e5f0ad2e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.scss @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../scss/constants"; + +:host { + display: block; + width: 500px; + max-width: 100%; +} + +.tb-iot-hub-pe-required { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 20px; + padding: 40px; + background: white; + border-radius: 8px; +} + +.tb-iot-hub-pe-required-close { + position: absolute; + top: 8px; + right: 8px; +} + +// SVG illustration applied as a mask so the silhouette is filled +// with $tb-primary-color. PE builds inherit their own primary value +// without re-exporting the asset. +.tb-iot-hub-pe-required-icon { + display: block; + width: 100px; + height: 100px; + background-color: $tb-primary-color; + -webkit-mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain; + mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain; +} + +.tb-iot-hub-pe-required-title { + font-size: 24px; + font-weight: 500; + line-height: 32px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.87); + margin: 0; +} + +.tb-iot-hub-pe-required-message { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.76); + margin: 0; +} + +.tb-iot-hub-pe-required-actions { + display: flex; + align-items: center; + gap: 8px; +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts new file mode 100644 index 0000000000..640c8fad20 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-pe-required-dialog.component.ts @@ -0,0 +1,47 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DialogComponent } from '@shared/components/dialog.component'; + +@Component({ + selector: 'tb-iot-hub-pe-required-dialog', + standalone: false, + templateUrl: './iot-hub-pe-required-dialog.component.html', + styleUrls: ['./iot-hub-pe-required-dialog.component.scss'] +}) +export class TbIotHubPeRequiredDialogComponent extends DialogComponent { + + constructor( + protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef + ) { + super(store, router, dialogRef); + } + + close(): void { + this.dialogRef.close(); + } + + upgradeInstance(): void { + window.open('https://thingsboard.io/docs/pe/installation/upgrade-from-ce/', '_blank'); + } +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html new file mode 100644 index 0000000000..e1fce18133 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.html @@ -0,0 +1,43 @@ + +
+ + + + +

+ {{ 'iot-hub.upgrade-required-title' | translate }} +

+ +

+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss new file mode 100644 index 0000000000..3a4d7f8c08 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.scss @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../scss/constants"; + +:host { + display: block; + width: 500px; + max-width: 100%; +} + +.tb-iot-hub-upgrade-required { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 20px; + padding: 40px; + background: white; + border-radius: 8px; +} + +.tb-iot-hub-upgrade-required-close { + position: absolute; + top: 8px; + right: 8px; +} + +// Re-use the upgrade-required.svg silhouette as a CSS mask so the icon +// tints with $tb-primary-color (PE overrides apply automatically). +.tb-iot-hub-upgrade-required-icon { + display: block; + width: 100px; + height: 100px; + background-color: $tb-primary-color; + -webkit-mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain; + mask: url('/assets/iot-hub/upgrade-required.svg') no-repeat center / contain; +} + +.tb-iot-hub-upgrade-required-title { + font-size: 24px; + font-weight: 500; + line-height: 32px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.87); + margin: 0; +} + +.tb-iot-hub-upgrade-required-message { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.76); + margin: 0; +} + +.tb-iot-hub-upgrade-required-actions { + display: flex; + align-items: center; + gap: 8px; +} diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts new file mode 100644 index 0000000000..480d3109ba --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-upgrade-required-dialog.component.ts @@ -0,0 +1,59 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { tbVersionIntToString } from '@core/http/iot-hub-api.service'; +import { environment as env } from '@env/environment'; + +export interface IotHubUpgradeRequiredDialogData { + minTbVersion: number; +} + +@Component({ + selector: 'tb-iot-hub-upgrade-required-dialog', + standalone: false, + templateUrl: './iot-hub-upgrade-required-dialog.component.html', + styleUrls: ['./iot-hub-upgrade-required-dialog.component.scss'] +}) +export class TbIotHubUpgradeRequiredDialogComponent extends DialogComponent { + + readonly minVersion: string; + readonly currentVersion: string; + + constructor( + protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: IotHubUpgradeRequiredDialogData + ) { + super(store, router, dialogRef); + this.minVersion = `v${tbVersionIntToString(data.minTbVersion)}`; + this.currentVersion = `v${env.tbVersion}`; + } + + close(): void { + this.dialogRef.close(); + } + + upgradeInstance(): void { + window.open('https://thingsboard.io/docs/installation/upgrade-instructions/', '_blank'); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html new file mode 100644 index 0000000000..18a47441ff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.html @@ -0,0 +1,52 @@ + +
+ +
+
+ +
+
+ + +

+ {{ 'iot-hub.alarm-rules-unavailable-title-prefix' | translate }} + {{ 'iot-hub.alarm-rules-unavailable-tb-version' | translate }} +

+ +

+ {{ 'iot-hub.alarm-rules-unavailable-desc' | translate }} +

+ +
+ + +
+ +
+ {{ 'iot-hub.alarm-rules-unavailable-current' | translate:{ version: currentTbVersion } }} +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss new file mode 100644 index 0000000000..d958011810 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.scss @@ -0,0 +1,143 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../scss/constants"; + +// Outer page — 16px padding around the white container, matches +// iot-hub-home. +:host { + display: block; + padding: 16px; + height: 100%; +} + +// Page container — same white card / dot pattern / blob layering +// rules as the iot-hub home page. +.tb-iot-hub-alarm-rules-unavailable { + position: relative; + background: white; + border-radius: 8px; + overflow: hidden; + height: 100%; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 800px; + background-image: radial-gradient(circle, rgba(0, 0, 0, 0.1) 1px, transparent 1px); + background-size: 20px 20px; + mask-image: linear-gradient(to bottom, black 0%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%); + pointer-events: none; + z-index: 1; + } + + > *:not(.tb-iot-hub-blob) { + position: relative; + z-index: 2; + } +} + +// Soft colored glow blobs — copied verbatim from iot-hub-home so the +// page background matches. `color` is bound from the ALARM_RULE +// HeroTypeConfig entry. +.tb-iot-hub-blob { + position: absolute; + width: 568px; + height: 568px; + border-radius: 50%; + background: currentColor; + opacity: 0.16; + filter: blur(120px); + pointer-events: none; + z-index: 0; +} + +.tb-iot-hub-blob-left { + top: 56px; + left: -221px; +} + +.tb-iot-hub-blob-right { + top: 259px; + right: -196px; +} + +.tb-alarm-rules-banner-container { + height: 100%; + display: flex; + overflow-y: auto; + flex-direction: column; +} + +// Banner content stack — vertically centred in the card. +.tb-alarm-rules-banner { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 40px; + padding: 80px 40px; + margin: auto; +} + +.tb-alarm-rules-banner-image { + width: 446px; + max-width: 100%; + height: auto; +} + +.tb-alarm-rules-banner-title { + font-size: 28px; + font-weight: 500; + line-height: 36px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.87); + margin: 0; + max-width: 900px; +} + +.tb-alarm-rules-banner-version { + color: $tb-primary-color; +} + +.tb-alarm-rules-banner-desc { + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.76); + margin: -24px 0 0; + max-width: 900px; +} + +.tb-alarm-rules-banner-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.tb-alarm-rules-banner-current { + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 0, 0, 0.54); + margin-top: -28px; +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts new file mode 100644 index 0000000000..c44a46ce0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-alarm-rules-unavailable-page.component.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { environment as env } from '@env/environment'; + +@Component({ + selector: 'tb-iot-hub-alarm-rules-unavailable-page', + standalone: false, + templateUrl: './iot-hub-alarm-rules-unavailable-page.component.html', + styleUrls: ['./iot-hub-alarm-rules-unavailable-page.component.scss'] +}) +export class TbIotHubAlarmRulesUnavailablePageComponent { + + // Same colour as the ALARM_RULE HeroTypeConfig entry on the home page. + readonly alarmRulesColor = '#d66f2e'; + + readonly currentTbVersion: string = env.tbVersion; + + constructor(private router: Router) {} + + goBack(): void { + void this.router.navigate(['/iot-hub']); + } + + upgradeInstance(): void { + window.open('https://thingsboard.io/docs/installation/upgrade-instructions/', '_blank'); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts index 0239966f10..606d6a29ae 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-item-resolver.component.ts @@ -20,9 +20,10 @@ import { MatDialog } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { AppState } from '@core/core.state'; +import { Observable } from 'rxjs'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { IotHubApiService } from '@core/http/iot-hub-api.service'; -import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; +import { ListingItemVersionNotFound, MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models'; import { DeepLinkOpenItem, isPublished, @@ -33,6 +34,13 @@ import { IotHubUnpublishedWarningDialogData, TbIotHubUnpublishedWarningDialogComponent } from '@home/components/iot-hub/iot-hub-unpublished-warning-dialog.component'; +import { + TbIotHubPeRequiredDialogComponent +} from '@home/components/iot-hub/iot-hub-pe-required-dialog.component'; +import { + IotHubUpgradeRequiredDialogData, + TbIotHubUpgradeRequiredDialogComponent +} from '@home/components/iot-hub/iot-hub-upgrade-required-dialog.component'; @Component({ selector: 'tb-iot-hub-item-resolver', @@ -52,23 +60,44 @@ export class TbIotHubItemResolverComponent implements OnInit { ngOnInit(): void { const params = this.route.snapshot.paramMap; + const slug = params.get('slug'); const itemVersionId = params.get('itemVersionId'); const itemId = params.get('itemId'); - const byVersion = itemVersionId != null; - const id = byVersion ? itemVersionId : itemId; + const bySlug = slug != null; + const byVersion = !bySlug && itemVersionId != null; - if (!isUUID(id)) { - this.failTo('iot-hub.deep-link-invalid-id'); - return; + let fetch$: Observable; + if (bySlug) { + fetch$ = this.iotHubApi.getListingItemVersion(slug, { ignoreErrors: true }); + } else { + const id = byVersion ? itemVersionId : itemId; + if (!isUUID(id)) { + this.failTo('iot-hub.deep-link-invalid-id'); + return; + } + fetch$ = byVersion + ? this.iotHubApi.getVersionInfo(id, { ignoreErrors: true }) + : this.iotHubApi.getPublishedVersion(id, { ignoreErrors: true }); } - const fetch$ = byVersion - ? this.iotHubApi.getVersionInfo(id, { ignoreErrors: true }) - : this.iotHubApi.getPublishedVersion(id, { ignoreErrors: true }); - fetch$.subscribe({ next: v => this.handleResolved(v, byVersion), error: err => { + if (bySlug && err?.status === 404) { + const body = (err?.error ?? {}) as ListingItemVersionNotFound; + if (body.peRequired) { + this.showPeRequired(); + return; + } + if (typeof body.minTbVersionRequired === 'number') { + this.showMinTbVersionRequired(body.minTbVersionRequired); + return; + } + if (body.noMatchingVersions) { + this.failTo('iot-hub.deep-link-not-found'); + return; + } + } const key = err?.status === 404 ? 'iot-hub.deep-link-not-found' : 'iot-hub.deep-link-fetch-failed'; @@ -77,6 +106,30 @@ export class TbIotHubItemResolverComponent implements OnInit { }); } + private showPeRequired(): void { + this.router.navigate(['/iot-hub'], { replaceUrl: true }).then(() => { + this.dialog.open(TbIotHubPeRequiredDialogComponent, { + panelClass: ['tb-dialog'], + disableClose: true, + autoFocus: false + }); + }); + } + + private showMinTbVersionRequired(minTbVersion: number): void { + this.router.navigate(['/iot-hub'], { replaceUrl: true }).then(() => { + this.dialog.open( + TbIotHubUpgradeRequiredDialogComponent, + { + panelClass: ['tb-dialog'], + disableClose: true, + autoFocus: false, + data: { minTbVersion } + } + ); + }); + } + private handleResolved(version: MpItemVersionView, byVersion: boolean): void { const segment = typeSegment(version.type); if (!segment) { diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts index 4a6cf80c52..3e387e6c0a 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub-routing.module.ts @@ -20,6 +20,7 @@ import { RouterModule, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component'; +import { TbIotHubAlarmRulesUnavailablePageComponent } from './iot-hub-alarm-rules-unavailable-page.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; import { TbIotHubSearchPageComponent } from './iot-hub-search-page.component'; @@ -86,11 +87,10 @@ const routes: Routes = [ }, { path: 'alarm-rules', - component: TbIotHubItemsPageComponent, + component: TbIotHubAlarmRulesUnavailablePageComponent, data: { auth: [Authority.TENANT_ADMIN], title: 'item.type-alarm-rule-plural', - itemType: 'ALARM_RULE', breadcrumb: { label: 'item.type-alarm-rule-plural', icon: 'notification_important' } } }, @@ -150,6 +150,14 @@ const routes: Routes = [ } } }, + { + path: 'listing/:slug', + component: TbIotHubItemResolverComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'iot-hub.item-detail' + } + }, { path: 'version/:itemVersionId', component: TbIotHubItemResolverComponent, diff --git a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts index b586fc613c..23c4687ac0 100644 --- a/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts +++ b/ui-ngx/src/app/modules/home/pages/iot-hub/iot-hub.module.ts @@ -21,6 +21,7 @@ import { IotHubComponentsModule } from '@home/components/iot-hub/iot-hub-compone import { IotHubRoutingModule } from './iot-hub-routing.module'; import { TbIotHubHomeComponent } from './iot-hub-home.component'; import { TbIotHubItemsPageComponent } from './iot-hub-items-page.component'; +import { TbIotHubAlarmRulesUnavailablePageComponent } from './iot-hub-alarm-rules-unavailable-page.component'; import { TbIotHubCreatorProfileComponent } from './iot-hub-creator-profile.component'; import { TbIotHubInstalledItemsComponent } from './iot-hub-installed-items.component'; import { TbIotHubSearchPageComponent } from './iot-hub-search-page.component'; @@ -33,7 +34,8 @@ import { TbIotHubItemResolverComponent } from './iot-hub-item-resolver.component TbIotHubCreatorProfileComponent, TbIotHubInstalledItemsComponent, TbIotHubSearchPageComponent, - TbIotHubItemResolverComponent + TbIotHubItemResolverComponent, + TbIotHubAlarmRulesUnavailablePageComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts index 94bb971323..bd6f29cdb4 100644 --- a/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts +++ b/ui-ngx/src/app/shared/models/iot-hub/iot-hub-version.models.ts @@ -112,6 +112,15 @@ export interface MpItemVersionView { resources: MpItemVersionResource[]; } +// 404 body shapes returned by the public listing item-version endpoint +// (GET /api/listings/public/by-slug/{slug}/item-version) when no +// version matches the caller's edition / platform combination. +export interface ListingItemVersionNotFound { + noMatchingVersions?: boolean; + peRequired?: boolean; + minTbVersionRequired?: number; +} + export interface MpItemVersionQueryOptions { type?: string; peOnly?: boolean; diff --git a/ui-ngx/src/assets/iot-hub/upgrade-required.svg b/ui-ngx/src/assets/iot-hub/upgrade-required.svg new file mode 100644 index 0000000000..1c0973a83a --- /dev/null +++ b/ui-ngx/src/assets/iot-hub/upgrade-required.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e2cf1fd6e7..15ded4b87d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3742,6 +3742,15 @@ "most-popular-in-iot-hub": "Most popular in IoT Hub", "alarm-rule-install-update-required": "Update required", "alarm-rule-install-update-required-text": "Alarm Rules require ThingsBoard 4.3 or later. Please update your platform instance to install Alarm Rule packages.", + "alarm-rules-unavailable-title-prefix": "Alarm Rules are available starting from", + "alarm-rules-unavailable-tb-version": "Thingsboard v4.3", + "alarm-rules-unavailable-desc": "Use pre-built Alarm Rule templates to detect critical conditions like low battery, threshold breaches, or devices going offline. Skip writing the rule logic and start reacting to incidents the moment they happen.", + "alarm-rules-unavailable-upgrade": "Upgrade instance", + "alarm-rules-unavailable-current": "Current platform version: ThingsBoard v{{version}}", + "pe-required-title": "Professional Edition required", + "pe-required-message": "This item is available only on ThingsBoard Professional Edition. You are currently running Community Edition. To install it, upgrade to Professional Edition.", + "upgrade-required-title": "ThingsBoard upgrade required", + "upgrade-required-message": "This item requires ThingsBoard {{minVersion}} or newer. You are currently running {{currentVersion}}. Please upgrade your instance to install it.", "become-creator-text": "Submit your templates to the ThingsBoard IoT Hub to get featured and showcase your solutions to our global community.", "contribute": "Contribute", "creator-profile": "Creator Profile", From 3eab08db8bd1c911ae2fdc814dbe21181313152f Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 28 May 2026 17:09:06 +0300 Subject: [PATCH 11/17] Bump Netty to 4.1.134.Final to fix MQTT decoder regression Netty 4.1.133.Final introduced a regression in MqttDecoder while fixing CVE-2026-44248: when multiple MQTT packets are present in the same cumulation buffer, the per-message size check used the total buffer size instead of the current packet's declared remaining length. Valid in-limit packets get rejected with TooLongFrameException("message length exceeds 65536: "). Fixed upstream by netty/netty#16787 and ported to 4.1 as netty/netty@30f8f284db, released in 4.1.134.Final. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4a8958ffa3..396033c915 100755 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 3.5.13 3.18.0 42.7.11 - 4.1.133.Final + 4.1.134.Final 10.1.55 2.4.0-b180830.0359 0.12.5 From 9ff02dd7221003986927cdaefc1a4ab77d590f07 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 3 Jun 2026 12:30:25 +0300 Subject: [PATCH 12/17] Version set to 4.2.2.3-SNAPSHOT --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edge-api/src/main/proto/edge.proto | 1 + common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/package.json | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/package.json | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/package.json | 2 +- ui-ngx/pom.xml | 2 +- 64 files changed, 65 insertions(+), 64 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 8e5e6b2564..afb619574a 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 4d5fff8157..61ff41ce80 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index f3f4f47bb9..a9d43c9ec1 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index bd0c1e6016..987e6e2ffe 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 55f2ee4896..c1a83ee870 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index f2a1fabfbd..c9cfeae4e7 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index e14e7072ea..a9ca14a2d6 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 0d75e57b9a..1c3b34f6f4 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index bb0d5cb192..ff630f416c 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 70f90cc45b..520b4ac493 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -48,6 +48,7 @@ enum EdgeVersion { V_4_2_2 = 4220; V_4_2_2_1 = 4221; V_4_2_2_2 = 4222; + V_4_2_2_3 = 4223; V_LATEST = 99999; } diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 4b2cd36592..f107721524 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 69c4e72e2a..d9184ac548 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index ab2388eb9d..f63c12d7dc 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 023b2bc46a..5335df00b9 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 1cf8320468..8e684718a0 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index 3527a1c695..cdfe36cd3c 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 6746100ef2..9f179e9b1a 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 6a55f3cc71..5791fefc6a 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 75d15a89d6..0de5ffe6ea 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 3e63520ce2..a2681241c6 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 3a06449bd9..4d5b6bce44 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 9267d74dd2..5da25b24d0 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index b63ad8a1ac..9cc958cde9 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index d77303b40c..b069dbe413 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index f746b58b7d..2565016226 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 10ce5ede63..ca992646a9 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 4748d75d92..3c60d4f077 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 66c87eb5d1..09c4318a20 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index bc172eeb45..4449502857 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 9bf82c7a65..2d811909ec 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 2601ac47fe..fa8b83c6f7 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index c06e98b45e..e433d6bfb2 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index f85e9b7691..54a1cd91c7 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 9cd29e35d5..070bf829a5 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.2.2.2", + "version": "4.2.2.3", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 2f7434b472..7174947cbc 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index ab484e7df8..519c928b6e 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 84c98308cd..2552bf188c 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 3dd84135cb..ba83cbb211 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 5c10adf67e..a20505772e 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index f081e6fd46..309536b4ef 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 78e445fda8..f7ea4160d1 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index d65d07f9a9..d80b63217c 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 8dfe14b3a6..0bf990171b 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index aa10ff1184..a711411729 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index fb344e3242..f0996dc1bf 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 0bc90c6ac4..9d781e0854 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index a0c023b5f8..47fff0501d 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index aaac7f8b67..4c58d7a94f 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.2.2.2", + "version": "4.2.2.3", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 196311f880..bc40e149ff 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 4b9b9fa21e..6a2b55cd84 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard netty-mqtt - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 396033c915..639fc2ab17 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index d108c49cda..9051482016 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index de69670863..2c73670dde 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 3810c46a99..8d9b6e5cbc 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 1ac0938163..dcb0f975f2 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 6376db6d9f..137dbaa1a7 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 723f9a1dd2..4fef3b50e7 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index d4e19705c9..8d24e0d982 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index fac60f7edc..99b98fa7c9 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 7b61c03200..1d61b07440 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 3645cf7d73..3889d8d334 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 683c0e3e2d..a1f9d384f1 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 076e2d316a..ad50207047 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.2.2.2", + "version": "4.2.2.3", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 4bc4bfd6cd..863463e78c 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.2.2-SNAPSHOT + 4.2.2.3-SNAPSHOT thingsboard org.thingsboard From b84f58a8af080cc5added74b74bd98dc85571194 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 3 Jun 2026 12:43:08 +0300 Subject: [PATCH 13/17] feat(html-container): scoped error handler, action support, completion polish - Provide a scoped Angular ErrorHandler on the dynamic component's injector in HtmlContainerWidgetComponent so template-runtime exceptions raised inside user-authored Angular templates flow through handleWidgetException instead of crashing the global Angular ErrorHandler. - Add `ctx.invokeAction($event, actionName, additionalParams?)` to WidgetActionsApi / WidgetComponent and a `WidgetDestroyCallback` + ctx.registerDestroyCallback() API on WidgetContext (callbacks run in registration order on destroy, errors per-callback are caught so one bad cleanup doesn't break the rest). - Surface widget actions on the HTML Container basic config: render tb-widget-actions-panel with the new strokedPanel input below the html-container settings, and register a default "JavaScript" multi-action source on the html_container widget JSON. - Expand the widget-completion docs for `registerDestroyCallback` (when it fires, what to use it for, lifecycle semantics, exact callback signature `() => void`) and for `invokeAction`'s `additionalParams` arg (forwarded to the configured JS action handler, common payload examples). --- .../system/widget_types/html_container.json | 2 +- ui-ngx/src/app/core/api/widget-api.models.ts | 1 + .../widget-actions-panel.component.html | 2 +- .../common/widget-actions-panel.component.ts | 5 +++ ...html-container-basic-config.component.html | 4 +++ .../html-container-basic-config.component.ts | 4 ++- .../html/html-container-widget.component.ts | 35 +++++++++++++++---- .../components/widget/widget.component.ts | 11 ++++++ .../home/models/widget-component.models.ts | 15 ++++++++ .../models/ace/widget-completion.models.ts | 33 +++++++++++++++++ 10 files changed, 102 insertions(+), 10 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/html_container.json b/application/src/main/data/json/system/widget_types/html_container.json index 25b9e5ac5d..a27996593b 100644 --- a/application/src/main/data/json/system/widget_types/html_container.json +++ b/application/src/main/data/json/system/widget_types/html_container.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n", + "controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'javaScript': {\n name: 'JavaScript',\n multiple: true\n }\n };\n}", "settingsDirective": "tb-html-container-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-html-container-basic-config", diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 5572730923..9ef4a4461b 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -110,6 +110,7 @@ export interface WidgetActionsApi { elementClick: ($event: Event) => void; cardClick: ($event: Event) => void; click: ($event: Event) => void; + invokeAction: ($event: Event, actionName: string, additionalParams?: any) => void; getActiveEntityInfo: () => SubscriptionEntityInfo; openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => MatDialogRef; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html index 72f4b0f1d8..500d974726 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
widget-config.actions
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts index 0320dc7fd1..389c780e4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts @@ -26,6 +26,7 @@ import { import { deepClone } from '@core/utils'; import { MatDialog } from '@angular/material/dialog'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-widget-actions-panel', @@ -45,6 +46,10 @@ export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit @Input() disabled: boolean; + @Input() + @coerceBoolean() + strokedPanel = false; + actionsFormGroup: UntypedFormGroup; private propagateChange = (_val: any) => {}; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html index 298bdb6e61..907da3e14b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.html @@ -17,4 +17,8 @@ --> + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts index d057acf057..3872e07853 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/html/html-container-basic-config.component.ts @@ -51,12 +51,14 @@ export class HtmlContainerBasicConfigComponent extends BasicWidgetConfigComponen protected onConfigSet(configData: WidgetConfigComponentData) { const settings: HtmlContainerWidgetSettings = {...htmlContainerDefaultSettings, ...(configData.config.settings || {})}; this.htmlContainerWidgetConfigForm = this.fb.group({ - settings: [settings, []] + settings: [settings, []], + actions: [configData.config.actions || {}, []] }); } protected prepareOutputConfig(config: any): WidgetConfigComponentData { this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings}; + this.widgetConfig.config.actions = config.actions; return this.widgetConfig; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts index f7bae926bc..51ee9871f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.component.ts @@ -15,8 +15,10 @@ /// import { - Component, + ChangeDetectorRef, + Component, ComponentRef, ElementRef, + inject, Inject, Injector, Input, @@ -96,6 +98,7 @@ export class HtmlContainerWidgetComponent implements OnInit { @Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type, private dynamicComponentFactoryService: DynamicComponentFactoryService, private utils: UtilsService, + private cdr: ChangeDetectorRef, private resources: ResourcesService) {} ngOnInit(): void { @@ -160,11 +163,13 @@ export class HtmlContainerWidgetComponent implements OnInit { this.compileAngularFunction().subscribe( { next: (containerFunction) => { - try { - this.initAngularComponent(imports, containerFunction); - } catch (e) { - this.handleWidgetException(e); - } + setTimeout(() => { + try { + this.initAngularComponent(imports, containerFunction); + } catch (e) { + this.handleWidgetException(e); + } + }); }, error: (e) => { this.handleWidgetException(e); @@ -200,9 +205,16 @@ export class HtmlContainerWidgetComponent implements OnInit { compileModules = compileModules.concat(imports); } const self = () => this; + + let containerRef: ComponentRef; + this.dynamicComponentFactoryService.createDynamicComponent( class TbContainerInstance { + + private cdr = inject(ChangeDetectorRef); + ngOnInit(): void { + this.cdr.detach(); if (containerFunction) { const instance = self(); try { @@ -212,6 +224,15 @@ export class HtmlContainerWidgetComponent implements OnInit { } } } + ngDoCheck(): void { + const instance = self(); + try { + this.cdr.detectChanges() + } catch (error) { + containerRef.destroy(); + instance.handleWidgetException(error) + } + } ngOnDestroy(): void { destroyContainerInstanceResources(); } @@ -224,7 +245,7 @@ export class HtmlContainerWidgetComponent implements OnInit { this.containerInstanceComponentType = componentType; const injector: Injector = Injector.create({providers: [], parent: this.angularContainer.viewContainerRef.injector}); try { - this.angularContainer.viewContainerRef.createComponent(this.containerInstanceComponentType, + containerRef = this.angularContainer.viewContainerRef.createComponent(this.containerInstanceComponentType, {index: 0, injector}); } catch (error) { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index cb96a493a1..fc7ad9f64b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -276,6 +276,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, elementClick: this.elementClick.bind(this), cardClick: this.cardClick.bind(this), click: this.click.bind(this), + invokeAction: this.invokeAction.bind(this), getActiveEntityInfo: this.getActiveEntityInfo.bind(this), openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this), openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this), @@ -1590,6 +1591,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } + private invokeAction($event: Event, actionName: string, additionalParams?: any) { + const descriptors = this.getActionDescriptors('javaScript'); + if (descriptors?.length) { + const found = descriptors.find(d => d.name === actionName); + if (found) { + this.handleWidgetAction($event, found, null, null, additionalParams); + } + } + } + private onWidgetAction($event: Event, action: WidgetAction) { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 1840bcce9d..d72587812c 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -149,6 +149,8 @@ export interface IDashboardWidget { updateWidgetParams(): void; } +export type WidgetDestroyCallback = () => void; + export class WidgetContext { constructor(public dashboard: IDashboardComponent, @@ -343,6 +345,8 @@ export class WidgetContext { ...RxJSOperators }; + private destroyCallbacks: WidgetDestroyCallback[] = []; + registerPopoverComponent(popoverComponent: TbPopoverComponent) { this.popoverComponents.push(popoverComponent); popoverComponent.tbDestroy.subscribe(() => { @@ -382,6 +386,10 @@ export class WidgetContext { } } + registerDestroyCallback(destroyCallback: WidgetDestroyCallback) { + this.destroyCallbacks.push(destroyCallback); + } + showSuccessToast(message: string, duration: number = 1000, verticalPosition: NotificationVerticalPosition = 'bottom', horizontalPosition: NotificationHorizontalPosition = 'left', @@ -494,6 +502,13 @@ export class WidgetContext { labelPattern.destroy(); } this.labelPatterns.clear(); + this.destroyCallbacks.forEach((destroyCallback) => { + try { + destroyCallback() + } catch (_ignoredError) { /* empty */ } + } + ); + this.destroyCallbacks.length = 0; this.width = undefined; this.height = undefined; this.destroyed = true; diff --git a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts index e6ce4b3ef7..757677fadd 100644 --- a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts @@ -632,6 +632,28 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi optional: true } ] + }, + invokeAction: { + description: 'Invoke action with JavaScript action source.', + meta: 'function', + args: [ + { + name: '$event', + description: 'DOM event object associated with action.', + type: 'Event' + }, + { + name: 'actionName', + description: 'Name of the configured action with JavaScript action source.', + type: 'string' + }, + { + name: 'additionalParams', + description: 'Optional payload merged into the action context and forwarded to the configured JavaScript action function as its additionalParams argument. Use it to pass row data, button state, or any extra values the action handler should react to.', + type: 'object', + optional: true + } + ] } } }, @@ -736,6 +758,17 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi } } } + }, + registerDestroyCallback: { + description: 'Registers a teardown callback that will be invoked exactly once when the widget is about to be destroyed (dashboard navigation, edit/view switch, layout change, etc.). Use it to release resources acquired during widget setup so they don\'t leak across widget reloads — e.g. unsubscribe RxJS subscriptions, detach DOM/window event listeners, clear setInterval / setTimeout timers, abort outstanding HTTP requests, destroy third-party plugin instances. Multiple callbacks may be registered; they are executed in registration order.', + meta: 'function', + args: [ + { + description: 'Zero-argument function executed when the widget is destroyed. Should be idempotent — synchronously dispose of one specific resource (e.g. one subscription or one listener) and avoid throwing; throwing here may prevent later cleanup callbacks from running.', + name: 'destroyCallback', + type: '() => void', + } + ] } }, ...serviceCompletions From e54e94e469a3741044942269dcc7e85e13a3d5d3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 4 Jun 2026 17:38:11 +0300 Subject: [PATCH 14/17] refactor(iot-hub): unify dot-pattern previews + rename search results container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the DEVICE-only `tb-iot-hub-card-preview-device` / `dlg-preview-device` variants in the item card and item detail dialog. The previously-special white-background-with-border treatment is gone — device previews now reuse the same dot-pattern background as widgets / solution templates for a consistent look. - In the home-page iot-hub widget, fold the `tb-iot-hub-widget-card-image-pattern` modifier into the base `.tb-iot-hub-widget-card-image` so every preview card already paints the dot pattern (no opt-in class required). - Rename `.tb-search-categories` to `.tb-results-container` across the iot-hub-search component / search page / creator profile pages so the wrapper class better reflects what it actually contains (browse / search results) and stays consistent between the search component and the screens that embed it. - Pick up incidental browse / search component html / scss tweaks touched alongside the rename. --- .../iot-hub/iot-hub-browse.component.html | 153 ++++++++-------- .../iot-hub/iot-hub-browse.component.scss | 13 +- .../iot-hub/iot-hub-item-card.component.html | 2 +- .../iot-hub/iot-hub-item-card.component.scss | 5 - .../iot-hub-item-detail-dialog.component.html | 3 +- .../iot-hub-item-detail-dialog.component.scss | 5 - .../iot-hub/iot-hub-search.component.html | 165 +++++++++--------- .../iot-hub/iot-hub-search.component.scss | 25 ++- .../home-page/iot-hub-widget.component.html | 2 +- .../home-page/iot-hub-widget.component.scss | 9 +- .../iot-hub-creator-profile.component.scss | 9 +- .../iot-hub/iot-hub-items-page.component.scss | 1 - .../iot-hub-search-page.component.scss | 8 +- 13 files changed, 194 insertions(+), 206 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html index cb59c9f5e9..83b1764afd 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.html @@ -405,87 +405,92 @@
- - @if (isLoading) { -
- -
- } +
- - @if (hasError && !isLoading) { -
-
-

{{ 'iot-hub.unable-to-load' | translate }}

-

{{ 'iot-hub.unable-to-load-text' | translate }}

- -
- } - - - @if (!isLoading && !hasError && items.length === 0) { -
-
-

{{ 'iot-hub.no-items-found' | translate }}

-

{{ 'iot-hub.no-items-found-text' | translate }}

- @if (hasActiveFilters()) { - + + @if (hasError) { +
+
+

{{ 'iot-hub.unable-to-load' | translate }}

+

{{ 'iot-hub.unable-to-load-text' | translate }}

+ +
} -
- } - - @if (!isLoading && !hasError && items.length > 0) { -
- @for (item of items; track item.id) { - - + + @if (!hasError && items.length === 0) { +
+
+

{{ 'iot-hub.no-items-found' | translate }}

+

{{ 'iot-hub.no-items-found-text' | translate }}

+ @if (hasActiveFilters()) { + + } +
} -
- -
-
- - @for (p of getPageNumbers(); track p) { - - } - -
-
- {{ 'iot-hub.items-per-page' | translate }}: - - @for (size of pageSizeOptions; track size) { - {{ size }} + + @if (!hasError && items.length > 0) { +
+ @for (item of items; track item.id) { + + } - -
+
+ + +
+
+ + @for (p of getPageNumbers(); track p) { + + } + +
+
+ {{ 'iot-hub.items-per-page' | translate }}: + + @for (size of pageSizeOptions; track size) { + {{ size }} + } + +
+
+ } + + + @if (isLoading) { +
+
+ +
+
+ }
- }
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss index b377219bb7..4c058ef5d1 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-browse.component.scss @@ -112,7 +112,8 @@ } .tb-iot-hub-filters-wrapper { - position: absolute; + position: sticky; + max-height: calc(100vh - var(--mat-toolbar-standard-height, 64px)); inset: 0; overflow-y: auto; } @@ -146,10 +147,12 @@ width: 60%; max-width: 550px; min-width: 300px; - margin-bottom: -48px; - - .embedded & { - margin-bottom: 0; + &:not(.embedded &) { + position: fixed; + top: var(--mat-toolbar-standard-height, 64px); + @media #{$mat-xs} { + top: var(--mat-toolbar-mobile-height, 56px); + } } } diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html index 70dc7d986e..dc2030b00a 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.html @@ -80,7 +80,7 @@ } @else {
-
+
@if (getPreviewUrl(); as previewUrl) { } @else { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss index 77d363f4ad..d46124d91e 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-card.component.scss @@ -52,11 +52,6 @@ position: relative; overflow: hidden; padding: 12px; - - &.tb-iot-hub-card-preview-device { - background: white; - border-bottom: 1px solid rgba(0, 0, 0, 0.12); - } } .tb-iot-hub-preview-image { diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html index c72e209037..6c0318abbc 100644 --- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html @@ -94,8 +94,7 @@
+ [class.dlg-preview-carousel]="item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1"> @if (item.type === ItemType.SOLUTION_TEMPLATE && carouselImages.length > 1) {