From 3d9df20d455e576f70b6f1d25b8262d63fc103d1 Mon Sep 17 00:00:00 2001 From: AndriiD Date: Sat, 7 Jan 2023 18:00:49 +0200 Subject: [PATCH 001/112] resetPassword in AuthController now returns nothing instead of token pair, just updates password. Button 'reset password' in reset password form now redirects to login page --- .../thingsboard/server/controller/AuthController.java | 3 +-- ui-ngx/src/app/core/auth/auth.service.ts | 10 ++++------ .../login/pages/login/reset-password.component.ts | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 0cb3a3fc92..1d1410b05a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -278,7 +278,7 @@ public class AuthController extends BaseController { @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @ResponseBody - public JwtPair resetPassword( + public void resetPassword( @ApiParam(value = "Reset password request.") @RequestBody ResetPasswordRequest resetPasswordRequest, HttpServletRequest request) throws ThingsboardException { @@ -305,7 +305,6 @@ public class AuthController extends BaseController { eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(securityUser.getId())); - return tokenFactory.createTokenPair(securityUser); } else { throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index a62623260f..3eb596381d 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -156,12 +156,10 @@ export class AuthService { )); } - public resetPassword(resetToken: string, password: string): Observable { - return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).pipe( - tap((loginResponse: LoginResponse) => { - this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, true); - } - )); + public resetPassword(resetToken: string, password: string) { + this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).subscribe( + () => { this.router.navigateByUrl('login'); }, () => {} + ); } public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index ab0928ec66..73d7da65d4 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -71,7 +71,7 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD } else { this.authService.resetPassword( this.resetToken, - this.resetPassword.get('newPassword').value).subscribe(); + this.resetPassword.get('newPassword').value); } } } From bd8129082655e93908adff8a2ad292776cd10694 Mon Sep 17 00:00:00 2001 From: AndriiD Date: Mon, 16 Jan 2023 14:03:17 +0200 Subject: [PATCH 002/112] fix test --- .../server/controller/BaseUserControllerTest.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 6429574e38..90d74a9117 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -235,16 +235,8 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .put("resetToken", TestMailService.currentResetPasswordToken) .put("password", "testPassword2"); - JsonNode tokenInfo = readResponse( - doPost("/api/noauth/resetPassword", resetPasswordRequest) - .andExpect(status().isOk()), JsonNode.class); - validateAndSetJwtToken(tokenInfo, email); - - doGet("/api/auth/user") - .andExpect(status().isOk()) - .andExpect(jsonPath("$.authority", is(Authority.TENANT_ADMIN.name()))) - .andExpect(jsonPath("$.email", is(email))); + doPost("/api/noauth/resetPassword", resetPasswordRequest); resetTokens(); login(email, "testPassword2"); From 533f08464818f403728f54fcd3f2bbd2387a94bc Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 18 Apr 2023 16:41:29 +0300 Subject: [PATCH 003/112] UI: Fixed reset password service --- ui-ngx/src/app/core/auth/auth.service.ts | 6 ++---- .../modules/login/pages/login/reset-password.component.ts | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 3eb596381d..7b5a997804 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -156,10 +156,8 @@ export class AuthService { )); } - public resetPassword(resetToken: string, password: string) { - this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()).subscribe( - () => { this.router.navigateByUrl('login'); }, () => {} - ); + public resetPassword(resetToken: string, password: string): Observable { + return this.http.post('/api/noauth/resetPassword', {resetToken, password}, defaultHttpOptions()); } public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) { diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts index 73d7da65d4..2857241c27 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password.component.ts @@ -22,7 +22,7 @@ import { PageComponent } from '@shared/components/page.component'; import { FormBuilder } from '@angular/forms'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; @Component({ @@ -44,6 +44,7 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD constructor(protected store: Store, private route: ActivatedRoute, + private router: Router, private authService: AuthService, private translate: TranslateService, public fb: FormBuilder) { @@ -71,7 +72,9 @@ export class ResetPasswordComponent extends PageComponent implements OnInit, OnD } else { this.authService.resetPassword( this.resetToken, - this.resetPassword.get('newPassword').value); + this.resetPassword.get('newPassword').value).subscribe( + () => this.router.navigateByUrl('login') + ); } } } From 6708f4f8f7c740f9c8a3c9551835655c28f884b6 Mon Sep 17 00:00:00 2001 From: KosukeFUKUMORI Date: Tue, 15 Aug 2023 13:09:58 +0900 Subject: [PATCH 004/112] Update locale.constant-ja_JP.json --- .../assets/locale/locale.constant-ja_JP.json | 198 +++++++++--------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json index f63a6681a2..329303745c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ja_JP.json +++ b/ui-ngx/src/assets/locale/locale.constant-ja_JP.json @@ -20,45 +20,45 @@ "yes": "はい", "no": "いいえ", "update": "更新", - "remove": "削除する", - "search": "サーチ", - "clear-search": "検索をクリアする", - "assign": "割り当てます", + "remove": "削除", + "search": " 検索", + "clear-search": "検索をクリア", + "assign": "割り当て", "unassign": "割り当て解除", "share": "シェア", "make-private": "プライベートにする", "apply": "適用", - "apply-changes": "変更を適用する", + "apply-changes": "変更を適用", "edit-mode": "編集モード", "enter-edit-mode": "編集モードに入る", - "decline-changes": "変更を拒否する", + "decline-changes": "変更を拒否", "close": "閉じる", - "back": "バック", - "run": "走る", + "back": "戻る", + "run": "実行", "sign-in": "サインイン!", "edit": "編集", "view": "ビュー", - "create": "作成する", + "create": "作成", "drag": "ドラッグ", "refresh": "リフレッシュ", "undo": "元に戻す", "copy": "コピー", "paste": "ペースト", - "copy-reference": "コピーリファレンス", + "copy-reference": "参照コピー", "paste-reference": "参照貼り付け", "import": "インポート", - "export": "輸出する", + "export": "エクスポート", "share-via": "{{provider}}" }, "aggregation": { "aggregation": "集約", "function": "データ集約機能", - "limit": "最大値", + "limit": "上限", "group-interval": "グループ化の間隔", - "min": "分", + "min": "最小", "max": "最大", "avg": "平均", - "sum": "和", + "sum": "総和", "count": "カウント", "none": "なし" }, @@ -77,17 +77,17 @@ "smtp-host": "SMTPホスト", "smtp-host-required": "SMTPホストが必要です。", "smtp-port": "SMTPポート", - "smtp-port-required": "smtpポートを指定する必要があります。", - "smtp-port-invalid": "それは有効なsmtpポートのようには見えません。", + "smtp-port-required": "SMTPポートを指定してください。", + "smtp-port-invalid": "有効なSMTPポートではありません。", "timeout-msec": "タイムアウト(ミリ秒)", - "timeout-required": "タイムアウトが必要です。", - "timeout-invalid": "それは有効なタイムアウトのようには見えません。", + "timeout-required": "タイムアウト設定が必要です。", + "timeout-invalid": "有効なタイムアウト設定ではありません。", "enable-tls": "TLSを有効にする", "tls-version": "TLSバージョン", "send-test-mail": "テストメールを送信する" }, "alarm": { - "alarm": "警報", + "alarm": "アラーム", "alarms": "アラーム", "select-alarm": "アラームを選択", "no-alarms-matching": "'{{entity}}'発見されました。", @@ -110,12 +110,12 @@ "created-time": "作成時刻", "type": "タイプ", "severity": "重大度", - "originator": "創始者", - "originator-type": "発信者タイプ", + "originator": "発信元", + "originator-type": "発信元タイプ", "details": "詳細", "status": "状態", "alarm-details": "アラームの詳細", - "start-time": "始まる時間", + "start-time": "開始時間", "end-time": "終了時間", "ack-time": "確認された時間", "clear-time": "クリアされた時間", @@ -124,7 +124,7 @@ "severity-minor": "マイナー", "severity-warning": "警告", "severity-indeterminate": "不確定", - "acknowledge": "認める", + "acknowledge": "承認", "clear": "クリア", "search": "アラームの検索", "selected-alarms": "{ count, plural, =1 {1 alarm} other {# alarms} }選択された", @@ -148,7 +148,7 @@ "filter-type-entity-name": "エンティティ名", "filter-type-state-entity": "ダッシュボード状態からのエンティティ", "filter-type-state-entity-description": "ダッシュボードの状態パラメータから取得されたエンティティ", - "filter-type-asset-type": "資産の種類", + "filter-type-asset-type": "アセットの種類", "filter-type-asset-type-description": "'{{assetTypes}}'", "filter-type-asset-type-and-name-description": "'{{assetTypes}}''{{prefix}}'", "filter-type-device-type": "デバイスタイプ", @@ -156,7 +156,7 @@ "filter-type-device-type-and-name-description": "'{{deviceTypes}}''{{prefix}}'", "filter-type-relations-query": "関係クエリ", "filter-type-relations-query-description": "{{entities}}{{relationType}}{{direction}}{{rootEntity}}", - "filter-type-asset-search-query": "資産検索クエリ", + "filter-type-asset-search-query": "アセット検索クエリ", "filter-type-asset-search-query-description": "{{assetTypes}}{{relationType}}{{direction}}{{rootEntity}}", "filter-type-device-search-query": "デバイス検索クエリ", "filter-type-device-search-query-description": "{{deviceTypes}}{{relationType}}{{direction}}{{rootEntity}}", @@ -178,31 +178,31 @@ "any-relation": "どれか" }, "asset": { - "asset": "資産", - "assets": "資産", - "management": "資産運用管理", + "asset": "アセット", + "assets": "アセット", + "management": "アセット管理", "view-assets": "アセットの表示", "add": "アセットを追加", "assign-to-customer": "顧客に割り当てる", - "assign-asset-to-customer": "顧客に資産を割り当てる", - "assign-asset-to-customer-text": "顧客に割り当てる資産を選択してください", + "assign-asset-to-customer": "顧客にアセットを割り当てる", + "assign-asset-to-customer-text": "顧客に割り当てるアセットを選択してください", "no-assets-text": "アセットが見つかりません", - "assign-to-customer-text": "資産を割り当てる顧客を選択してください", - "public": "パブリック", + "assign-to-customer-text": "アセットを割り当てる顧客を選択してください", + "public": "公開", "assignedToCustomer": "顧客に割り当てられた", "make-public": "アセットを公開する", "make-private": "アセットをプライベートにする", "unassign-from-customer": "顧客からの割り当て解除", "delete": "アセットを削除", - "asset-public": "資産は公開されています", - "asset-type": "資産の種類", - "asset-type-required": "資産の種類が必要です。", + "asset-public": "アセットは公開されています", + "asset-type": "アセットの種類", + "asset-type-required": "アセットの種類が必要です。", "select-asset-type": "アセットタイプを選択", "enter-asset-type": "アセットタイプを入力", "any-asset": "すべてのアセット", "no-asset-types-matching": "'{{entitySubtype}}'発見されました。", "asset-type-list-empty": "選択されたアセットタイプはありません。", - "asset-types": "資産タイプ", + "asset-types": "アセットタイプ", "name": "名", "name-required": "名前は必須です。", "description": "説明", @@ -211,7 +211,7 @@ "details": "詳細", "events": "イベント", "add-asset-text": "新しいアセットを追加する", - "asset-details": "資産の詳細", + "asset-details": "アセットの詳細", "assign-assets": "アセットの割り当て", "assign-assets-text": "{ count, plural, =1 {1 asset} other {# assets} }顧客に", "delete-assets": "アセットを削除する", @@ -219,25 +219,25 @@ "unassign-assets-action-title": "{ count, plural, =1 {1 asset} other {# assets} }顧客から", "assign-new-asset": "新しいアセットを割り当てる", "delete-asset-title": "'{{assetName}}'?", - "delete-asset-text": "確認後、資産と関連するすべてのデータが回復不能になることに注意してください。", + "delete-asset-text": "確認後、アセットと関連するすべてのデータが回復不能になることに注意してください。", "delete-assets-title": "{ count, plural, =1 {1 asset} other {# assets} }?", "delete-assets-action-title": "{ count, plural, =1 {1 asset} other {# assets} }", - "delete-assets-text": "確認後、選択したすべての資産が削除され、関連するすべてのデータは回復不能になりますので注意してください。", + "delete-assets-text": "確認後、選択したすべてのアセットが削除され、関連するすべてのデータは回復不能になりますので注意してください。", "make-public-asset-title": "'{{assetName}}'パブリック?", - "make-public-asset-text": "確認後、資産とそのすべてのデータは公開され、他の人がアクセスできるようになります。", + "make-public-asset-text": "確認後、アセットとそのすべてのデータは公開され、他の人がアクセスできるようになります。", "make-private-asset-title": "'{{assetName}}'プライベート?", - "make-private-asset-text": "確認後、資産とそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。", + "make-private-asset-text": "確認後、アセットとそのすべてのデータは非公開にされ、他の人がアクセスすることはできません。", "unassign-asset-title": "'{{assetName}}'?", - "unassign-asset-text": "確認後、資産は割り当て解除され、顧客はアクセスできなくなります。", + "unassign-asset-text": "確認後、アセットは割り当て解除され、顧客はアクセスできなくなります。", "unassign-asset": "アセットの割り当てを解除する", "unassign-assets-title": "{ count, plural, =1 {1 asset} other {# assets} }?", - "unassign-assets-text": "確認後、選択されたすべての資産が割り当て解除され、顧客がアクセスできなくなります。", + "unassign-assets-text": "確認後、選択されたすべてのアセットが割り当て解除され、顧客がアクセスできなくなります。", "copyId": "アセットIDをコピーする", "idCopiedMessage": "アセットIDがクリップボードにコピーされました", "select-asset": "アセットを選択", "no-assets-matching": "'{{entity}}'発見されました。", - "asset-required": "資産が必要です", - "name-starts-with": "アセット名はで始まります", + "asset-required": "アセットが必要です", + "name-starts-with": "次で始まるアセット名", "label": "ラベル" }, "attribute": { @@ -311,12 +311,12 @@ }, "contact": { "country": "国", - "city": "シティ", - "state": "州/県", + "city": "市区町村", + "state": "都道府県", "postal-code": "郵便番号", "postal-code-invalid": "無効な郵便番号形式です。", "address": "住所", - "address2": "アドレス2", + "address2": "住所2", "phone": "電話", "email": "Eメール", "no-address": "住所がありません" @@ -324,8 +324,8 @@ "common": { "username": "ユーザー名", "password": "パスワード", - "enter-username": "ユーザーネームを入力してください", - "enter-password": "パスワードを入力する", + "enter-username": "ユーザー名を入力してください", + "enter-password": "パスワードを入力してください", "enter-search": "検索を入力", "created-time": "作成時刻" }, @@ -338,27 +338,27 @@ "customer": "顧客", "customers": "顧客", "management": "顧客管理", - "dashboard": "カスタマーダッシュボード", - "dashboards": "カスタマーダッシュボード", + "dashboard": "顧客ダッシュボード", + "dashboards": "顧客ダッシュボード", "devices": "顧客デバイス", - "assets": "顧客資産", + "assets": "顧客アセット", "public-dashboards": "パブリックダッシュボード", "public-devices": "パブリックデバイス", - "public-assets": "公的資産", + "public-assets": "パブリックアセット", "add": "顧客を追加", - "delete": "顧客を削除する", - "manage-customer-users": "顧客ユーザーを管理する", + "delete": "顧客を削除", + "manage-customer-users": "顧客を管理する", "manage-customer-devices": "顧客のデバイスを管理する", "manage-customer-dashboards": "顧客ダッシュボードの管理", "manage-public-devices": "パブリックデバイスを管理する", - "manage-public-dashboards": "公開ダッシュボードの管理", - "manage-customer-assets": "顧客資産の管理", - "manage-public-assets": "公的資産を管理する", + "manage-public-dashboards": "パブリックダッシュボードの管理", + "manage-customer-assets": "顧客アセットの管理", + "manage-public-assets": "パブリックアセットを管理する", "add-customer-text": "新規顧客を追加", "no-customers-text": "顧客が見つかりません", - "customer-details": "お客様情報", + "customer-details": "顧客情報", "delete-customer-title": "'{{customerTitle}}'?", - "delete-customer-text": "確認後、お客様および関連するすべてのデータが回復不能になるので注意してください。", + "delete-customer-text": "確認後、顧客および関連するすべてのデータが回復不能になるので注意してください。", "delete-customers-title": "{ count, plural, =1 {1 customer} other {# customers} }?", "delete-customers-action-title": "{ count, plural, =1 {1 customer} other {# customers} }", "delete-customers-text": "確認後、選択したすべての顧客は削除され、関連するすべてのデータは回復不能になります。", @@ -381,10 +381,10 @@ "default-customer-required": "テナントレベルのダッシュボードをデバッグするには、デフォルトの顧客が必要です" }, "datetime": { - "date-from": "デートから", - "time-from": "からの時間", - "date-to": "日付", - "time-to": "の時間" + "date-from": "開始日", + "time-from": "開始時刻", + "date-to": "終了日", + "time-to": "終了時刻" }, "dashboard": { "dashboard": "ダッシュボード", @@ -462,11 +462,11 @@ "max-columns-count-message": "最大1000の列カウントのみが許可されます。", "widgets-margins": "ウィジェット間のマージン", "horizontal-margin": "水平マージン", - "horizontal-margin-required": "水平余白値が必要です。", + "horizontal-margin-required": "水平マージン設定が必要です。", "min-horizontal-margin-message": "最小水平マージン値としては0だけが許容されます。", "max-horizontal-margin-message": "最大水平マージン値は50だけです。", "vertical-margin": "垂直マージン", - "vertical-margin-required": "垂直マージン値が必要です。", + "vertical-margin-required": "垂直マージン設定が必要です。", "min-vertical-margin-message": "最小の垂直マージン値として0のみが許可されます。", "max-vertical-margin-message": "最大垂直マージン値は50のみです。", "autofill-height": "自動レイアウトの高さ", @@ -482,8 +482,8 @@ "display-entities-selection": "エンティティの選択を表示する", "display-dashboard-timewindow": "タイムウィンドウを表示する", "display-dashboard-export": "エクスポートの表示", - "import": "インポートダッシュボード", - "export": "エクスポートダッシュボード", + "import": "ダッシュボードをインポート", + "export": "ダッシュボードをエクスポート", "export-failed-error": "{{error}}", "create-new-dashboard": "新しいダッシュボードを作成する", "dashboard-file": "ダッシュボードファイル", @@ -529,15 +529,15 @@ }, "datakey": { "settings": "設定", - "advanced": "上級", + "advanced": "カスタム", "label": "ラベル", "color": "色", - "units": "値の隣に表示する特別なシンボル", - "decimals": "浮動小数点の後の桁数", + "units": "単位", + "decimals": "小数点以下の桁数", "data-generation-func": "データ生成関数", "use-data-post-processing-func": "データ後処理機能を使用する", "configuration": "データキー設定", - "timeseries": "タイムズ", + "timeseries": "時系列", "attributes": "属性", "alarm": "アラームフィールド", "timeseries-required": "エンティティの時系列データが必要です。", @@ -703,8 +703,8 @@ "type-devices": "デバイス", "list-of-devices": "{ count, plural, =1 {One device} other {List of # devices} }", "device-name-starts-with": "'{{prefix}}'", - "type-asset": "資産", - "type-assets": "資産", + "type-asset": "アセット", + "type-assets": "アセット", "list-of-assets": "{ count, plural, =1 {One asset} other {List of # assets} }", "asset-name-starts-with": "'{{prefix}}'", "type-rule": "ルール", @@ -731,7 +731,7 @@ "type-dashboards": "ダッシュボード", "list-of-dashboards": "{ count, plural, =1 {One dashboard} other {List of # dashboards} }", "dashboard-name-starts-with": "'{{prefix}}'", - "type-alarm": "警報", + "type-alarm": "アラーム", "type-alarms": "アラーム", "list-of-alarms": "{ count, plural, =1 {One alarms} other {List of # alarms} }", "alarm-name-starts-with": "'{{prefix}}'", @@ -787,7 +787,7 @@ "type": "タイプ", "key": "キー", "value": "値", - "id": "イド", + "id": "ID", "extension-id": "内線番号", "extension-type": "拡張タイプ", "transformer-json": "JSON *", @@ -998,13 +998,13 @@ "select": "ターゲットレイアウトを選択" }, "legend": { - "position": "伝説の位置", + "position": "凡例の位置", "show-max": "最大値を表示", - "show-min": "最小値を表示する", + "show-min": "最小値を表示", "show-avg": "平均値を表示", "show-total": "合計値を表示", "settings": "凡例の設定", - "min": "分", + "min": "最小", "max": "最大", "avg": "平均", "total": "合計" @@ -1018,7 +1018,7 @@ "password-again": "パスワードをもう一度", "sign-in": "サインインしてください", "username": "ユーザー名(電子メール)", - "remember-me": "私を覚えてますか", + "remember-me": "ログイン情報を記憶", "forgot-password": "パスワードをお忘れですか?", "password-reset": "パスワードのリセット", "new-password": "新しいパスワード", @@ -1030,7 +1030,7 @@ }, "position": { "top": "上", - "bottom": "ボトム", + "bottom": "下", "left": "左", "right": "右" }, @@ -1213,11 +1213,11 @@ "minutes-interval": "{ minutes, plural, =1 {1 minute} other {# minutes} }", "hours-interval": "{ hours, plural, =1 {1 hour} other {# hours} }", "days-interval": "{ days, plural, =1 {1 day} other {# days} }", - "days": "日々", - "hours": "時間", + "days": "日", + "hours": "時", "minutes": "分", "seconds": "秒", - "advanced": "上級" + "advanced": "カスタム" }, "timewindow": { "days": "{ days, plural, =1 { day } other {# days } }", @@ -1225,12 +1225,12 @@ "minutes": "{ minutes, plural, =0 { minute } =1 {1 minute } other {# minutes } }", "seconds": "{ seconds, plural, =0 { second } =1 {1 second } other {# seconds } }", "realtime": "リアルタイム", - "history": "歴史", - "last-prefix": "最終", + "history": "履歴", + "last-prefix": "直近", "period": "{{ startTime }}{{ endTime }}", "edit": "タイムウィンドウを編集", "date-range": "期間", - "last": "最終", + "last": "直近", "time-period": "期間" }, "user": { @@ -1383,7 +1383,7 @@ "widget-config": { "data": "データ", "settings": "設定", - "advanced": "上級", + "advanced": "カスタム", "title": "タイトル", "general-settings": "一般設定", "display-title": "タイトルを表示", @@ -1396,13 +1396,13 @@ "widget-style": "ウィジェットスタイル", "title-style": "タイトルスタイル", "mobile-mode-settings": "モバイルモードの設定", - "order": "注文", + "order": "順番", "height": "高さ", - "units": "値の隣に表示する特別なシンボル", - "decimals": "浮動小数点の後の桁数", + "units": "単位", + "decimals": "小数点以下の桁数", "timewindow": "タイムウィンドウ", "use-dashboard-timewindow": "ダッシュボードのタイムウィンドウを使用する", - "display-legend": "伝説を表示", + "display-legend": "凡例を表示", "datasources": "データソース", "maximum-datasources": "{ count, plural, =1 {1 datasource is allowed.} other {# datasources are allowed} }", "datasource-type": "タイプ", @@ -1411,7 +1411,7 @@ "add-datasource": "データソースを追加", "target-device": "ターゲットデバイス", "alarm-source": "アラームソース", - "actions": "行動", + "actions": "アクション", "action": "アクション", "add-action": "アクションを追加", "search-actions": "検索アクション", @@ -1429,8 +1429,8 @@ "delete-action-text": "'{{actionName}}'?" }, "widget-type": { - "import": "インポートウィジェットタイプ", - "export": "ウィジェットのタイプをエクスポートする", + "import": "ウィジェットタイプをインポート", + "export": "ウィジェットのタイプをエクスポート", "export-failed-error": "{{error}}", "create-new-widget-type": "新しいウィジェットタイプを作成する", "widget-type-file": "ウィジェットタイプファイル", @@ -1460,9 +1460,9 @@ "Dec": "12月", "January": "1月", "February": "2月", - "March": "行進", + "March": "3月", "April": "4月", - "June": "六月", + "June": "6月", "July": "7月", "August": "8月", "September": "9月", @@ -1480,7 +1480,7 @@ "Year": "年", "This Year": "今年", "Last Year": "昨年", - "Date picker": "日付ピッカー", + "Date picker": "日付選択", "Hour": "時", "Day": "日", "Week": "週間", From 0cdba40a10bb1c6fb382c8c45b2f6e8fd76799cf Mon Sep 17 00:00:00 2001 From: fline <286665119@qq.com> Date: Sat, 28 Dec 2024 23:10:19 +0800 Subject: [PATCH 005/112] Update locale.constant-zh_CN.json --- .../assets/locale/locale.constant-zh_CN.json | 998 ++++++++++++++++-- 1 file changed, 887 insertions(+), 111 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 54172aa1ac..9819c65dba 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -80,7 +80,8 @@ "clear": "清除", "upload": "上传", "delete-anyway": "仍要删除", - "delete-selected": "删除所选" + "delete-selected": "删除所选", + "set": "设置" }, "aggregation": { "aggregation": "聚合", @@ -110,6 +111,15 @@ "prohibit-different-url-hint": "应为生产环境启用此设置。禁用时可能会导致安全问题", "device-connectivity": { "device-connectivity": "设备连接", + "http-s": "HTTP(s)", + "mqtt-s": "MQTT(s)", + "coap-s": "COAP(s)", + "http": "HTTP", + "https": "HTTPs", + "mqtt": "MQTT", + "mqtts": "MQTTs", + "coap": "COAP", + "coaps": "COAPs", "hint": "如果主机或端口字段为空,将使用默认的协议值。", "host": "主机", "port": "端口", @@ -144,6 +154,8 @@ "sms-provider-type": "SMS 服务商类型", "sms-provider-type-required": "SMS 服务商类型必填。", "sms-provider-type-aws-sns": "亚马逊社交网站", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", "aws-access-key-id": "AWS访问密钥ID", "aws-access-key-id-required": "需要访问AWS密钥ID", "aws-secret-access-key": "AWS秘密访问密钥", @@ -187,6 +199,7 @@ "password-expiration-period-days-range": "密码过期期限(天)不能为负", "password-reuse-frequency-days": "密码重用频率(天)", "password-reuse-frequency-days-range": "天内密码重用频率不能为负", + "allow-whitespace": "允许空格", "force-reset-password-if-no-valid": "如果密码不可用则强制重置密码", "force-reset-password-if-no-valid-hint": "启用此功能时请小心:它会要求使用不可用密码的用户通过电子邮件重置其密码。", "general-policy": "基本策略", @@ -271,6 +284,7 @@ "login-provider": "登录提供商", "mapper": "映射", "new-domain": "新建", + "oauth2": "OAuth2.0", "password-max-length": "密码应该小于256个字符。", "redirect-uri-template": "重定向URI模板", "copy-redirect-uri": "复制重定向URI", @@ -295,8 +309,12 @@ "user-name-attribute-name": "用户名属性键", "user-name-attribute-name-required": "用户名属性密钥必填", "protocol": "协议", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", "enable": "启用OAuth2.0设置", "disable": "禁用OAuth2.0设置", + "edge": "传播到Edge", "edge-enable": "启用Edge", "edge-disable": "禁用Edge", "domains": "域名", @@ -305,6 +323,7 @@ "mobile-package-placeholder": "例如: my.example.app", "mobile-package-hint": "Android:应用程序ID或iOS:产品标识符", "mobile-package-unique": "应用程序包必须是唯一的。", + "mobile-package-required": "应用程序包必填。", "mobile-package-max-length": "应用程序包应小于256。", "mobile-package-spaces": "应用程序包不应包含空间。", "mobile-app-secret": "应用程序密钥", @@ -316,6 +335,9 @@ "copy-mobile-app-secret": "复制应用程序密钥", "delete-mobile-app": "删除应用程序信息", "providers": "供应商", + "platform-web": "Web", + "platform-android": "Android", + "platform-ios": "iOS", "all-platforms": "所有平台", "smtp-provider": "SMTP提供商", "allowed-platforms": "允许的平台", @@ -332,6 +354,8 @@ "token-uri-required": "需要令牌URI", "redirect-uri": "重定向URI", "google-provider": "谷歌", + "microsoft-provider": "Office 365", + "sendgrid-provider": "Sendgrid", "custom-provider": "自定义", "generate-access-token": "生成访问令牌", "update-access-token": "更新访问令牌", @@ -388,6 +412,10 @@ "scheme-octet-unspecified-2": "2 - 八进制未指定 (8 位二进制)", "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", "scheme-octet-unspecified-4": "4 - 八进制未指定 (8 位二进制)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", "scheme-pictogram-encoding": "9 - 图标编码", "scheme-music-codes": "10 - 音乐编码 (ISO-2022-JP)", "scheme-extended-kanji-jis": "13 - 扩展汉字 JIS (X 0212-1990)", @@ -436,15 +464,8 @@ "applications": "应用程序", "default": "默认", "custom": "自定义", - "app-package-name": "应用程序包名称", - "app-package-name-required": "应用程序包名称必填", - "sha256-certificate-fingerprints": "SHA256证书指纹", - "sha256-certificate-fingerprints-required": "SHA256证书指纹必填", - "app-id-required": "App ID必填", - "google-play-link": "Google Play链接", - "google-play-link-required": "Google Play链接必填", - "app-store-link": "App Store链接", - "app-store-link-required": "App Store链接", + "android": "Android", + "ios": "iOS", "appearance": "外观", "appearance-on-home-page": "外观显示在首页", "enabled": "启用", @@ -504,9 +525,11 @@ "signings-key-base64": "签名密钥必须是Base64格式。", "expiration-time": "令牌过期时间(秒)", "expiration-time-required": "令牌过期时间是必填。", + "expiration-time-max": "最大允许时间为2147483647秒(68年)。", "expiration-time-min": "最小时间为60秒(1分钟)。", "refresh-expiration-time": "刷新令牌过期时间(秒)", "refresh-expiration-time-required": "刷新令牌过期时间必填。", + "refresh-expiration-time-max": "最大允许时间为2147483647秒(68年)。", "refresh-expiration-time-min": "最小时间为900秒(15分钟)。", "refresh-expiration-time-less-token": "刷新令牌时间必须大于令牌过期时间。", "generate-key": "生成密钥", @@ -517,6 +540,7 @@ "notifications": "通知", "notifications-settings": "通知设置", "slack-api-token": "Slack API令牌", + "slack": "Slack", "slack-settings": "Slack设置", "mobile-settings": "移动设置", "firebase-service-account-file": "Firebase服务帐户JSON凭据文件", @@ -563,6 +587,7 @@ "assignee-last-name": "受托人名字", "assignee-email": "受托人邮箱", "unassigned": "未分配", + "user-deleted": "用户已删除", "assignee-not-set": "所有", "status": "状态", "alarm-details": "告警详细信息", @@ -572,7 +597,8 @@ "ack-time": "确认时间", "clear-time": "清除时间", "duration": "持续时间", - "alarm-severity-list": "警报严重性列表", + "alarm-severity": "警报严重程度", + "alarm-severity-list": "警报严重程度列表", "any-severity": "任何严重程度", "severity-critical": "危险", "severity-major": "重要", @@ -610,6 +636,7 @@ "fetch-size": "获取大小", "fetch-size-required": "获取大小必填。", "fetch-size-error-min": "最小值为10。", + "alarm-types": "告警类型", "alarm-type-list": "告警类型列表", "any-type": "任何类型", "assigned-to-current-user": "分配给当前用户", @@ -831,6 +858,9 @@ "api-usage": "Api 使用统计", "alarm": "告警", "alarms-created": "创建告警数", + "queue-stats": "队列统计信息", + "processing-failures-and-timeouts": "处理失败和超时", + "exceptions": "异常", "alarms-created-daily-activity": "每天产生的告警", "alarms-created-hourly-activity": "每小时产生的告警", "alarms-created-monthly-activity": "每月产生的告警", @@ -841,13 +871,15 @@ "email-messages": "邮件消息", "email-messages-daily-activity": "每天产生的邮件消息", "email-messages-monthly-activity": "每月产生的邮件消息", - "exceptions": "异常", "executions": "执行数", + "scripts": "脚本", "scripts-hourly-activity": "脚本每小时活动", "scripts-daily-activity": "脚本每日活动", "scripts-monthly-activity": "脚本每月活动", - "javascript-executions": "JavaScript 执行数", - "tbel-executions": "TBEL 执行数", + "javascript": "JavaScript", + "javascript-executions": "JavaScript执行数", + "tbel": "TBEL", + "tbel-executions": "TBEL执行数", "latest-error": "最新错误", "messages": "消息", "notifications": "通知", @@ -856,9 +888,7 @@ "permanent-failures": "${entityName}永久性故障", "permanent-timeouts": "${entityName}永久超时", "processing-failures": "${entityName}处理失败", - "processing-failures-and-timeouts": "处理失败和超时", "processing-timeouts": "${entityName}处理超时", - "queue-stats": "队列统计信息", "rule-chain": "规则链", "rule-engine": "规则引擎", "rule-engine-daily-activity": "每天的规则引擎活动", @@ -959,6 +989,20 @@ "type-timeseries-deleted": "遥测数据已删除", "type-sms-sent": "短信发送" }, + "debug-settings": { + "label": "调试配置", + "on-failure": "仅失败(24/7)", + "all-messages": "所有消息({{time}})", + "failures": "失败", + "entity": "实体", + "rule-node": "规则节点", + "hint": { + "main": "所有节点调试消息将被限速,限制为:", + "main-limited": "所有{{entity}}的调试消息将受到速率限制,每{{time}}允许最多{{msg}}条消息。", + "on-failure": "保存所有失败调试事件,无时间限制。", + "all-messages": "在时间限制内保存所有调试事件。" + } + }, "confirm-on-exit": { "message": "有未保存的更改,确定要离开此页吗?", "html-message": "有未保存的更改。
确定要离开此页面吗?", @@ -989,11 +1033,13 @@ "enter-password": "输入密码", "enter-search": "输入检索条件", "created-time": "创建时间", + "disabled": "已禁用", "loading": "正在加载中...", "proceed": "继续", "open-details-page": "打开详情页", "not-found": "未找到", - "documentation": "文档" + "documentation": "文档", + "time-left": "剩余{{time}}" }, "content-type": { "json": "Json", @@ -1226,6 +1272,7 @@ "import": "导入仪表板", "export": "导出仪表板", "export-failed-error": "无法导出仪表板: {{error}}", + "export-prompt": "嵌入仪表板图像和资源", "create-new-dashboard": "创建仪表板", "dashboard-file": "仪表板文件", "invalid-dashboard-file-error": "无法导入仪表板: 仪表板数据结构无效。", @@ -1478,6 +1525,10 @@ "client-public-key-tooltip": "X509 公钥必须采用 DER 编码的 X509v3 格式,并且仅支持 EC 算法,然后编码为 Base64 格式!", "mode": "安全配置模式", "client-tab": "客户端安全配置", + "client-certificate": "客户端证书", + "bootstrap-tab": "启动客户端", + "bootstrap-server": "启动服务器", + "lwm2m-server": "LwM2M服务器", "client-publicKey-or-id": "客户端公钥或ID", "client-publicKey-or-id-required": "客户端公钥或ID必填。", "client-publicKey-or-id-tooltip-psk": "SK标识符是最多128字节,如标准[RFC7925]所述。", @@ -1566,6 +1617,92 @@ "lwm2m-command": "使用以下文档通过 LWM2M 连接设备。" } }, + "dynamic-form": { + "property": { + "properties": "属性", + "property": "属性", + "id": "ID", + "name": "名称", + "type": "类型", + "type-text": "文本", + "type-password": "密码", + "type-textarea": "文本区域", + "type-number": "数字", + "type-switch": "开关", + "type-select": "选择框", + "type-radios": "单选按钮", + "type-datetime": "日期/时间", + "type-image": "图片", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", + "type-color": "颜色", + "type-color-settings": "颜色设置", + "type-font": "字体", + "type-units": "单位", + "type-icon": "图标", + "type-fieldset": "字段集", + "type-array": "数组", + "type-html-section": "HTML区域", + "group-title": "分组标题", + "no-properties": "没有配置属性", + "add-property": "添加属性", + "property-settings": "属性设置", + "remove-property": "移除属性", + "default-value": "默认值", + "value-required": "值是必需的", + "number-settings": "数字设置", + "min": "最小值", + "max": "最大值", + "step": "步长", + "selected-options-limit": "选项限制", + "advanced-ui-settings": "高级UI设置", + "disable-on-property": "基于属性禁用", + "display-condition-function": "显示条件函数", + "sub-label": "子标签", + "vertical-divider-after": "垂直分隔符后", + "input-field-suffix": "输入框后缀", + "property-row-classes": "属性行类", + "property-field-classes": "属性字段类", + "not-unique-property-ids-error": "属性ID必须唯一!", + "enable-multiple-select": "启用多选", + "allow-empty-select-option": "允许空选项", + "select-options": "选择项", + "not-unique-select-option-value-error": "选择项值必须唯一!", + "value": "值", + "label": "标签", + "add-option": "添加选项", + "no-options": "没有配置选项", + "remove-option": "移除选项", + "textarea-rows": "文本区行数", + "help-id": "帮助ID", + "buttons-direction": "按钮方向", + "direction-row": "行", + "direction-column": "列", + "radio-button-options": "单选按钮选项", + "datetime-type": "日期/时间字段类型", + "datetime-type-date": "日期", + "datetime-type-time": "时间", + "datetime-type-datetime": "日期/时间", + "enable-clear-button": "启用清除按钮", + "html-section-settings": "HTML区域设置", + "html-section-classes": "HTML区域类", + "html-section-content": "HTML区域内容", + "array-item": "数组项", + "item-type": "项类型", + "item-name": "项名称", + "no-items": "没有项" + }, + "clear-form": "清除表单", + "clear-form-prompt": "确定要移除所有表单属性吗?", + "import-form": "从JSON导入表单", + "export-form": "导出表单为JSON", + "json-file": "JSON文件", + "json-content": "JSON内容", + "invalid-form-json-file-error": "无法从JSON导入表单:无效的表单JSON数据结构。" + }, "asset-profile": { "asset-profile": "资产配置", "asset-profiles": "资产配置", @@ -1665,52 +1802,59 @@ "set-default-device-profile-text": "确认后,设备配置将被标记为默认,并将用于未指定配置的新设备。", "no-device-profiles-found": "未找到设备配置。", "create-new-device-profile": "创建设备配置", - "mqtt-device-topic-filters": "MQTT 设备 Topic 筛选器", - "mqtt-device-topic-filters-unique": "MQTT设备 Topic 筛选器必须唯一。", + "mqtt-device-topic-filters": "MQTT设备主题筛选器", + "mqtt-device-topic-filters-unique": "MQTT设备主题筛选器必须唯一。", "mqtt-device-topic-filters-spark-plug": "MQTT Sparkplug B边缘网络(EoN)节点", - "mqtt-device-topic-filters-spark-plug-hint": "允许来自具备Sparkplug B负载和Topic格式的边缘网络(EoN)节点的连接。", + "mqtt-device-topic-filters-spark-plug-hint": "允许来自具备Sparkplug B负载和主题格式的边缘网络(EoN)节点的连接。", "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "将SparkPlug指标存储为属性。", "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "将作为设备属性存储的SparkPlug指标的名称。其他指标将作为设备遥测数据进行存储。", - "mqtt-device-payload-type": "MQTT 设备 Payload", - "mqtt-enable-compatibility-with-json-payload-format": "启用与其他payload格式兼容。", - "mqtt-enable-compatibility-with-json-payload-format-hint": "启用后平台将默认使用Protobuf的payload格式,如果解析失败平台将尝试使用JSON的payload格式。对于固件更新期间的向后兼容性很有用,例如固件的初始版本使用Json而新版本使用Protobuf在设备队列的固件更新过程中,需要同时支持Protobuf和JSON。兼容模式会导致一点的性能下降,因此建议在所有设备更新后禁用此模式。", - "mqtt-use-json-format-for-default-downlink-topics": "缺省下行主题采用json格式", - "mqtt-use-json-format-for-default-downlink-topics-hint": "启用后平台将使用Json的playload格式通过以下主题推送属性和RPC:v1/devices/me/attributes/response/$request_id、v1/devices/me/attributes 、v1/devices/me/rpc/request/$request_id、v1/devices/me/rpc/response/$request_id。此设置不会影响使用新(v2)主题发送的属性和rpc订阅:v2/a/res/$request_id、v2/a、v2/r /req/$request_id,v2/r/res/$request_id。其中$request_id是一个整数请求标识符。", + "mqtt-device-payload-type": "MQTT设备Payload", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "启用与其他Payload格式兼容。", + "mqtt-enable-compatibility-with-json-payload-format-hint": "启用后平台将默认使用Protobuf的Payload格式,如果解析失败平台将尝试使用JSON的Payload格式。对于固件更新期间的向后兼容性很有用,例如固件的初始版本使用Json而新版本使用Protobuf在设备队列的固件更新过程中,需要同时支持Protobuf和JSON。兼容模式会导致一点的性能下降,因此建议在所有设备更新后禁用此模式。", + "mqtt-use-json-format-for-default-downlink-topics": "缺省下行主题采用Json格式", + "mqtt-use-json-format-for-default-downlink-topics-hint": "启用后平台将使用Json的Playload格式通过以下主题推送属性和RPC:v1/devices/me/attributes/response/$request_id、v1/devices/me/attributes 、v1/devices/me/rpc/request/$request_id、v1/devices/me/rpc/response/$request_id。此设置不会影响使用新(v2)主题发送的属性和rpc订阅:v2/a/res/$request_id、v2/a、v2/r /req/$request_id,v2/r/res/$request_id。其中$request_id是一个整数请求标识符。", "mqtt-send-ack-on-validation-exception": "发布消息验证失败时发送PUBACK", "mqtt-send-ack-on-validation-exception-hint": "默认情况下平台将关闭相关消息验证失败的MQTT会话,启用后平台将发布确认而不是关闭会话。", "snmp-add-mapping": "添加SNMP映射", "snmp-mapping-not-configured": "OID到时间序列/遥测的映射未配置", "snmp-timseries-or-attribute-name": "用于映射的时间序列/属性名称", "snmp-timseries-or-attribute-type": "用于映射的时间序列/属性类型", - "mqtt-payload-type-required": "Payload 类型必填。", - "coap-device-type": "CoAP 设备类型", - "coap-device-payload-type": "CoAP 设备消息 Payload", - "coap-device-type-required": "CoAP 设备类型必填。", + "snmp-method-pdu-type-get-request": "GetRequest", + "snmp-method-pdu-type-get-next-request": "GetNextRequest", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Payload类型必填。", + "coap-device-type": "CoAP设备类型", + "coap-device-payload-type": "CoAP设备消息Payload", + "coap-device-type-required": "CoAP设备类型必填。", "coap-device-type-default": "默认", "coap-device-type-efento": "Efento NB-IoT", "support-level-wildcards": "支持单[+]和多级[#]通配符。", - "telemetry-topic-filter": "遥测数据Topic筛选器", - "telemetry-topic-filter-required": "遥测数据Topic筛选器必填。", - "attributes-topic-filter": "属性Topic筛选器", - "attributes-subscribe-topic-filter": "订阅属性的Topic筛选器", - "attributes-topic-filter-required": "属性的Topic筛选器必填。", - "attributes-subscribe-topic-filter-required": "订阅属性的Topic筛选器必填。", - "telemetry-proto-schema": "遥测数据 proto schema", - "telemetry-proto-schema-required": "遥测数据 proto schema 必填。", - "attributes-proto-schema": "属性 proto schema", - "attributes-proto-schema-required": "属性 proto schema 必填。", - "rpc-response-proto-schema": "RPC 响应 proto schema", - "rpc-response-proto-schema-required": "RPC 响应 proto schema 必填。", - "rpc-response-topic-filter": "RPC响应 Topic 筛选器", - "rpc-response-topic-filter-required": "RPC响应 Topic 筛选器必填。", - "rpc-request-proto-schema": "RPC 请求 proto schema", - "rpc-request-proto-schema-required": "RPC 请求 proto schema 必填。", + "telemetry-topic-filter": "遥测数据主题筛选器", + "telemetry-topic-filter-required": "遥测数据主题筛选器必填。", + "attributes-topic-filter": "属性主题筛选器", + "attributes-subscribe-topic-filter": "订阅属性的主题筛选器", + "attributes-topic-filter-required": "属性的主题筛选器必填。", + "attributes-subscribe-topic-filter-required": "订阅属性的主题筛选器必填。", + "telemetry-proto-schema": "遥测数据 Proto Schema", + "telemetry-proto-schema-required": "遥测数据 Proto Schema 必填。", + "attributes-proto-schema": "属性 Proto Schema", + "attributes-proto-schema-required": "属性 Proto Schema 必填。", + "rpc-response-proto-schema": "RPC 响应 Proto Schema", + "rpc-response-proto-schema-required": "RPC 响应 Proto Schema 必填。", + "rpc-response-topic-filter": "RPC响应主题筛选器", + "rpc-response-topic-filter-required": "RPC响应主题筛选器必填。", + "rpc-request-proto-schema": "RPC 请求 Proto Schema", + "rpc-request-proto-schema-required": "RPC 请求 Proto Schema 必填。", "rpc-request-proto-schema-hint": "RPC 请求消息应始终包含字段:string method = 1; int32 requestId = 2; 和params = 3的任何数据类型。", - "not-valid-pattern-topic-filter": "无效的 Topic 筛选器模式", + "not-valid-pattern-topic-filter": "无效的主题筛选器模式", "not-valid-single-character": "单级通配符的使用无效", "not-valid-multi-character": "多级通配符的使用无效", - "single-level-wildcards-hint": "[+] 适用于任何Topic过滤级别。例如:v1/devices/+/telemetry or +/devices/+/attributes。", - "multi-level-wildcards-hint": "[#]可以替换Topic筛选器本身,并且必须是Topic的最后一个符号。例如:# or v1/devices/me/#。", + "single-level-wildcards-hint": "[+] 适用于任何主题过滤级别。例如:v1/devices/+/telemetry or +/devices/+/attributes。", + "multi-level-wildcards-hint": "[#]可以替换主题筛选器本身,并且必须是主题的最后一个符号。例如:# or v1/devices/me/#。", "alarm-rules": "告警规则", "alarm-rules-with-count": "告警规则({{count}})", "no-alarm-rules": "未配置告警规则", @@ -2118,8 +2262,10 @@ "sync-process-started-successfully": "同步处理开始成功!", "missing-related-rule-chains-title": "边缘缺少关联规则链", "missing-related-rule-chains-text": "分配给边缘的规则链使用规则节点将消息转发给未分配给当前边缘的规则链。

缺少的规则链列表:
{{missingRuleChains}}", + "upgrade-instructions": "升级说明", "widget-datasource-error": "组件只支持边缘实体数据源", - "upgrade-instructions": "升级说明" + "connected": "已连接", + "disconnected": "已断开连接" }, "edge-event": { "type-dashboard": "仪表板", @@ -2326,15 +2472,18 @@ "type-notification-templates": "通知模板", "list-of-notification-templates": "{ count, plural, =1 {1 个通知模板} other {# 个通知模板} }", "link": "连接", - "type-oauth2-client": "OAuth 2.0客户端", - "type-oauth2-clients": "OAuth 2.0客户端", - "list-of-oauth2-clients": "{ count, plural, =1 {One OAuth 2.0 client} other {List of # OAuth 2.0 clients} }", + "type-oauth2-client": "OAuth2.0客户端", + "type-oauth2-clients": "OAuth2.0客户端", + "list-of-oauth2-clients": "{ count, plural, =1 {一个 OAuth2.0客户端} other {# 个 OAuth2.0客户端列表} }", "type-domain": "域名", "type-domains": "域名", - "list-of-domains": "{ count, plural, =1 {One Domain} other {List of # Domains} }", + "list-of-domains": "{ count, plural, =1 {一个域名} other {# 个域名列表} }", "type-mobile-app": "移劝端", "type-mobile-apps": "移动端", - "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }" + "list-of-mobile-apps": "{ count, plural, =1 {一个移动应用} other {# 个移动应用列表} }", + "type-mobile-app-bundle": "移动应用包", + "type-mobile-app-bundles": "移动应用包", + "list-of-mobile-app-bundles": "{ count, plural, =1 {一个移动应用包} other {# 个移动应用包列表} }" }, "entity-field": { "created-time": "创建时间", @@ -2438,9 +2587,7 @@ "select-entity-view": "选择实体视图", "make-public": "实体视图设为公开", "make-private": "实体视图设为私有", - "start-date": "开始日期", "start-ts": "开始时间", - "end-date": "结束日期", "end-ts": "结束时间", "date-limits": "日期限制", "client-attributes": "客户端属性", @@ -2549,7 +2696,7 @@ "custom": "定制", "to-double": "加倍", "transformer": "转换器", - "json-required": "Transformer JSON 必填。", + "json-required": "转换器JSON必填。", "json-parse": "无法解析转换器JSON。", "attributes": "属性", "add-attribute": "添加属性", @@ -2576,7 +2723,7 @@ "no-file": "没有选择文件。", "drop-file": "删除文件或单击以选择要上载的文件。", "mapping": "映射", - "topic-filter": "Topic筛选器", + "topic-filter": "主题筛选器", "converter-type": "转换类型", "converter-json": "Json", "json-name-expression": "设备名称JSON表达式", @@ -2585,7 +2732,7 @@ "topic-type-expression": "设备类型主题表达式", "attribute-key-expression": "属性键名表达式", "attr-json-key-expression": "属性键JSON表达式", - "attr-topic-key-expression": "属性键名Topic表达式", + "attr-topic-key-expression": "属性键名主题表达式", "request-id-expression": "请求ID表达式", "request-id-json-expression": "请求ID JSON表达式", "request-id-topic-expression": "请求ID主题表达式", @@ -2788,16 +2935,14 @@ "function": "函数" }, "gateway": { - "gateway-exists": "同名设备已存在。", "gateway-name": "网关名称", "gateway-name-required": "网关名称必填。", - "gateway-saved": "已成功保存网关配置。", - "gateway": "网关", + "gateways": "网关", "create-new-gateway": "创建网关", "create-new-gateway-text": "确定要创建名为 '{{gatewayName}}' 的新网关?", + "launch-command": "启动命令", "no-gateway-found": "未找到网关。", - "no-gateway-matching": "未找到 '{{item}}' 。", - "launch-gateway": "启动网关" + "no-gateway-matching": "未找到 '{{item}}' 。" }, "grid": { "delete-item-title": "确定要删除此项吗?", @@ -3038,38 +3183,6 @@ "not-unique-behavior-ids-error": "行为ID必须是唯一的!", "default-settings": "默认设置" }, - "property": { - "property": "属性", - "id": "序号", - "name": "名称", - "type": "类型", - "type-text": "文本", - "type-number": "数字", - "type-switch": "开关", - "type-color": "颜色", - "type-color-settings": "颜色设置", - "type-font": "字体", - "type-units": "单位", - "type-icon": "图标", - "no-properties": "无任何属性配置", - "add-property": "添加属性", - "property-settings": "属性设置", - "remove-property": "删除属性", - "default-value": "默认数据", - "value-required": "值必填", - "number-settings": "数字设置", - "min": "最小值", - "max": "最大值", - "step": "步长", - "advanced-ui-settings": "UI高级设置", - "disable-on-property": "禁用属性", - "sub-label": "子级标签", - "vertical-divider-after": "垂直分隔线", - "input-field-suffix": "输入字段后缀", - "property-row-classes": "行类名称", - "property-field-classes": "字段类名称", - "not-unique-property-ids-error": "属性ID必须是唯一的!" - }, "symbol": { "symbol": "图形", "fluid-presence": "液体", @@ -3107,16 +3220,21 @@ "value": "数值", "units": "单位", "flow-meter-value-hint": "在流量计上显示数值", + "value-hint": "浮点数表示当前值", "running": "运行", "running-hint": "指示组件是否处于运行状态。", "warning-state": "警告状态", "warning": "警告", + "warning-click": "警告点击", "warning-state-hint": "指示组件是否处于警告状态。", "critical-state": "严重状态", "critical": "严重", + "critical-click": "严重状态点击", "critical-state-hint": "指示组件是否处于严重状态。", "critical-state-animation": "状态动画", "critical-state-animation-hint": "当组件处于严重状态时显示闪烁动画。", + "warning-critical-state-animation": "警告/严重状态动画", + "warning-critical-state-animation-hint": "当组件处于警告或严重状态时,是否启用闪烁动画。", "animation": "动画", "broken": "裂纹", "broken-hint": "指示组件是否存在裂纹。", @@ -3140,6 +3258,7 @@ "top-right-connector": "右左连接", "running-color": "运行颜色", "stopped-color": "停止颜色", + "stopped": "已停止", "warning-color": "警告颜色", "critical-color": "严重颜色", "opened": "已经打开", @@ -3192,7 +3311,7 @@ "recirculate-mode": "循环", "rinse-mode": "冲洗", "closed-mode": "关闭", - "stand-filter-color": "沙箱背景色", + "sand-filter-color": "沙滤器颜色", "mode-box-background": "按钮颜色", "border-color": "状态颜色", "label-color": "文本颜色", @@ -3202,7 +3321,47 @@ "full-value": "全部数值", "full-value-hint": "表示全部数值。", "label": "标签", - "icon": "图标" + "icon": "图标", + "button-color": "按钮颜色", + "on-label": "'开启' 标签文本", + "off-label": "'关闭' 标签文本", + "arrow-presence": "箭头存在", + "arrow-presence-hint": "指示连接器中是否存在箭头。", + "arrow-present": "箭头存在", + "arrow-direction": "箭头方向", + "arrow-direction-hint": "指示流动方向。", + "main-line": "主线", + "line": "线", + "line-color": "线颜色", + "arrow-color": "箭头颜色", + "target-value": "目标值", + "target-value-hint": "指示刻度上的目标点。", + "min-max-value": "最小值和最大值", + "min-value": "最小值", + "max-value": "最大值", + "warning-scale-color": "警告刻度颜色", + "critical-scale-color": "关键刻度颜色", + "scale-color": "刻度颜色", + "target": "目标", + "high-warning-state": "高警告状态", + "show-high-warning-scale": "显示高警告刻度", + "high-warning-scale": "高警告刻度", + "high-warning-state-hint": "双重值表示高警告范围,直到高关键或最大值。", + "low-warning-state": "低警告状态", + "show-low-warning-scale": "显示低警告刻度", + "low-warning-scale": "低警告刻度", + "low-warning-state-hint": "双重值表示低警告范围,直到低关键或最小值。", + "high-critical-state": "高关键状态", + "show-high-critical-scale": "显示高关键刻度", + "high-critical-scale": "高关键刻度", + "high-critical-state-hint": "双重值表示高关键范围,直到最大值刻度。", + "low-critical-state": "低关键状态", + "show-low-critical-scale": "显示低关键刻度", + "low-critical-scale": "低关键刻度", + "low-critical-state-hint": "双重值表示低关键范围,直到最小值刻度。", + "filter-color": "滤色器颜色", + "colors": "颜色", + "alarm-colors": "警报颜色" } }, "item": { @@ -3212,7 +3371,23 @@ "no-return-error": "函数必须返回值!", "return-type-mismatch": "函数必须返回'{{type}}'类型的值!", "tidy": "整理", - "mini": "迷你" + "mini": "迷你", + "modules": "模块", + "remove-module": "移除模块", + "no-modules": "未配置模块", + "add-module": "添加模块", + "module-alias": "别名", + "module-resource": "JS模块资源", + "not-unique-module-aliases-error": "模块别名必须是唯一的!", + "show-module-info": "显示模块信息", + "show-module-source-code": "显示模块源代码", + "module-members": "模块成员", + "module-no-members": "模块没有导出的成员", + "module-load-error": "模块加载错误", + "source-code": "源代码", + "source-code-load-error": "源代码加载错误", + "no-js-module-text": "未找到JS模块", + "no-js-module-matching": "未找到与 '{{module}}' 匹配的JS模块" }, "key-val": { "key": "键名", @@ -3337,6 +3512,137 @@ "copy-code": "点击复制", "copied": "复制完成" }, + "mobile": { + "add-application": "添加应用", + "app-id": "应用ID", + "app-id-required": "应用ID必填", + "app-id-pattern": "应用ID格式无效", + "app-store-link": "应用商店链接", + "app-store-link-required": "应用商店链接必填", + "application-details": "应用详情", + "application-package": "应用包", + "application-secret": "应用密钥", + "application-secret-required": "应用密钥必填", + "application": "应用", + "applications": "应用列表", + "copy-app-id": "复制应用ID", + "copy-app-store-link": "复制应用商店链接", + "copy-application-package": "复制应用包", + "copy-application-secret": "复制应用密钥", + "copy-google-play-link": "复制Google Play链接", + "copy-sha256-certificate-fingerprints": "复制SHA256证书指纹", + "delete-application": "删除应用", + "delete-application-button-text": "我理解后果,删除应用", + "delete-application-text": "此操作无法撤销。删除应用将永久删除应用及其所有相关数据。
如果不想永久删除,可以暂时挂起应用。
若要删除应用,请输入\"{{phrase}}\"进行确认。", + "delete-application-title-short": "确定要删除应用 '{{name}}' 吗?", + "delete-application-text-short": "请注意:确认后应用及所有相关数据将无法恢复。", + "delete-application-phrase": "删除应用", + "delete-applications-bundle-text": "请注意:确认后移动应用包及所有相关数据将无法恢复。", + "delete-applications-bundle-title": "确定要删除移动应用包 '{{bundleName}}' 吗?", + "generate-application-secret": "生成应用密钥", + "google-play-link": "Google Play链接", + "google-play-link-required": "Google Play链接必填", + "latest-version": "最新版本", + "min-version": "最低版本", + "invalid-version-pattern": "版本格式无效。请使用格式:major.minor.patch(例如:1.0.0)。", + "mobile-center": "移动中心", + "mobile-package": "应用包", + "mobile-package-max-length": "应用包长度应少于256个字符", + "mobile-package-required": "应用包是必需的", + "mobile-package-pattern": "应用包格式无效", + "no-application": "未找到应用", + "no-bundles": "未找到移动包", + "platform-type": "平台类型", + "search-application": "搜索应用", + "search-bundles": "搜索移动包", + "set": "设置", + "sha256-certificate-fingerprints": "SHA256证书指纹", + "sha256-certificate-fingerprints-required": "SHA256证书指纹是必需的", + "sha256-certificate-fingerprints-pattern": "SHA256证书指纹格式无效", + "show-hidden-pages": "显示隐藏页面", + "status": "状态", + "status-type": { + "deprecated": "已弃用", + "draft": "草稿", + "published": "已发布", + "suspended": "已暂停" + }, + "store-information": "商店信息", + "version-information": "版本信息", + "min-version-release-notes": "最低版本发布说明", + "latest-version-release-notes": "最新版本发布说明", + "bundle": "捆绑包", + "bundles": "捆绑包列表", + "add-bundle": "添加捆绑包", + "title": "标题", + "title-required": "标题必填", + "oauth-clients": "OAuth2.0客户端", + "android-app": "Android应用", + "android-application": "Android应用程序", + "ios-app": "iOS应用", + "ios-application": "iOS应用程序", + "invalid-store-link": "无效的商店链接", + "enable-oauth": "启用OAuth2.0", + "enable-self-registration": "启用自助注册", + "edit-bundle": "编辑捆绑包", + "description": "描述", + "basic-settings": "基本设置", + "no-application-matching": "未找到与 '{{entity}}' 匹配的应用程序。", + "no-bundle-matching": "未找到与 '{{entity}}' 匹配的捆绑包。", + "application-required": "应用程序必填。", + "bundle-required": "捆绑包必填。", + "no-application-text": "未找到应用程序", + "no-bundle-text": "未找到捆绑包", + "layout": "布局", + "pages": "页面", + "hide-all-pages": "隐藏所有页面", + "reset-to-default-pages": "重置为默认页面", + "add-specific-page": "添加特定页面", + "visible": "可见", + "hidden": "隐藏", + "reset-to-page-default": "重置页面为默认", + "mobile-599": "移动端(最大 599px)", + "tablet-959": "平板(最大 959px)", + "max-element-number": "最大元素数量", + "page-name": "页面名称", + "page-name-required": "页面名称必填。", + "page-name-cannot-contain-only-spaces": "页面名称不能仅包含空格。", + "page-type": "页面类型", + "pages-types": { + "dashboard": "仪表盘", + "web-view": "网页视图", + "custom": "自定义" + }, + "url": "URL", + "invalid-url-format": "URL格式无效", + "path": "路径", + "invalid-path-format": "路径格式无效", + "custom-page": "自定义页面", + "edit-page": "编辑页面", + "edit-custom-page": "编辑自定义页面", + "delete-page": "删除页面", + "qr-code-widget": "二维码小部件", + "type-here": "在此输入", + "configuration-dialog": "配置对话框", + "configuration-app": "配置应用程序", + "configuration-step": { + "prepare-environment-title": "准备开发环境", + "prepare-environment-text": "Flutter ThingsBoard 移动应用程序需要 Flutter SDK。按照说明设置 Flutter SDK。", + "get-source-code-title": "获取应用程序源代码", + "get-source-code-text": "您可以通过从 GitHub 仓库克隆来获取 Flutter ThingsBoard 移动应用程序源代码:", + "configure-api-title": "配置 ThingsBoard API 端点", + "configure-api-text": "在编辑器/IDE 中打开 flutter_thingsboard_app 项目。编辑:", + "configure-api-hint": "将 thingsBoardApiEndpoint 常量的值设置为与您的 ThingsBoard 服务器实例的 API 端点匹配。请勿使用“localhost”或“127.0.0.1”主机名。", + "run-app-title": "运行应用程序", + "run-app-text": "按您的 IDE 中的说明运行应用程序。\n如果使用终端,请使用以下命令运行应用程序:", + "more-information": "详细信息请参阅我们的入门文档。", + "getting-started": "入门", + "configure-package-title": "配置应用程序包", + "configure-package-text": "您可以手动更改应用程序包,也可以使用第三方 CLI 工具。", + "configure-package-text-install": "要安装 Rename CLI 工具,请执行以下命令:", + "configure-package-run-commands": "在您的项目根目录下运行以下命令:" + } + }, "notification": { "action-button": "操作按钮", "action-type": "操作类型", @@ -3457,6 +3763,8 @@ "no-notification-templates": "未找到通知模板", "no-notifications-yet": "目前还没有通知", "no-recipients-notification": "没有收件人的通知", + "no-recipients-matching": "未找到与 '{{entity}}' 匹配的收件人。", + "no-recipients-text": "未找到收件人", "no-rule": "未配置规则", "no-rules-notification": "没有规则通知", "no-severity-found": "未找到严重性级别", @@ -3501,6 +3809,8 @@ "users-entity-owner": "实体所有者的用户" }, "recipients": "收件人", + "notification-recipient": "通知接收者", + "notification-recipient-required": "通知接收者必填。", "notification-recipients": "通知/收件人", "recipients-count": "{ count, plural, =1 {1 个收件人} other {# 个收件人} }", "recipients-required": "收件人必填。", @@ -3831,7 +4141,8 @@ "relation-filters": "关联筛选器", "additional-info": "附加信息 (JSON)", "invalid-additional-info": "无法解析附加信息JSON。", - "no-relations-text": "未找到关联" + "no-relations-text": "未找到关联", + "not": "不是" }, "resource": { "add": "添加资源", @@ -3869,8 +4180,37 @@ "js-module": "JS 模块", "lwm2m-model": "LWM2M 模型", "pkcs-12": "PKCS #12" + }, + "resource-sub-type": "子类型", + "sub-type": { + "image": "图像", + "scada-symbol": "SCADA符号", + "extension": "扩展", + "module": "模块" } }, + "javascript": { + "add": "添加JavaScript资源", + "delete": "删除JavaScript资源", + "delete-javascript-resource-text": "请注意:确认后该JavaScript资源将不可恢复。", + "delete-javascript-resource-title": "确定要删除JavaScript资源 '{{resourceTitle}}' 吗?", + "delete-javascript-resources-action-title": "删除JavaScript { count, plural, =1 {1 个资源} other {# 个资源} }", + "delete-javascript-resources-text": "请注意,即使这些JavaScript资源已被JavaScript函数使用,它们也将被删除。", + "delete-javascript-resources-title": "确定要删除JavaScript { count, plural, =1 {1 个资源} other {# 个资源} } 吗?", + "download": "下载JavaScript资源", + "upload-from-file": "从文件上传JavaScript", + "resource-file": "JavaScript资源文件", + "drop-file": "拖放一个JavaScript文件,或点击选择要上传的文件。", + "drop-resource-file-or": "拖放一个JavaScript文件或", + "javascript-library": "JavaScript库", + "javascript-type": "JavaScript类型", + "javascript-resource-details": "JavaScript资源详情", + "search": "搜索JavaScript资源", + "selected-javascript-resources": "{ count, plural, =1 {1 个 JavaScript 资源} other {# 个 JavaScript 资源} } 已选择", + "no-javascript-resource-text": "未找到JavaScript资源", + "all-types": "全部", + "module-script": "模块脚本" + }, "rpc": { "error": { "target-device-is-not-set": "目标设备未设置!", @@ -3966,6 +4306,7 @@ "deselect-all": "取消选择", "rulenode-details": "规则节点详情", "debug-mode": "调试模式", + "singleton": "单例", "configuration": "配置", "link": "链接", "link-details": "规则节点链接详情", @@ -4012,6 +4353,7 @@ "output": "输出", "test": "测试", "help": "帮助", + "reset-debug-settings": "重置所有节点的调试设置", "test-with-this-message": "使用此消息进行{{test}}测试", "queue-hint": "选择一个队列将消息转发到另一个队列,默认情况下使用'Main'队列。", "queue-singleton-hint": "选择一个队列以在多实体中转发消息,默认情况下使用'Main'队列。" @@ -4175,6 +4517,7 @@ "tenant-profiles": "租户配置", "add": "添加租户配置", "add-profile": "添加配置", + "debug": "调试", "edit": "编辑租户配置", "tenant-profile-details": "租户配置详细信息", "no-tenant-profiles-text": "未找到租户配置", @@ -4245,6 +4588,8 @@ "maximum-ota-packages-sum-data-size": "OTA包文件总大小", "maximum-ota-package-sum-data-size-required": "OTA包文件总大小必填。", "maximum-ota-package-sum-data-size-range": "OTA包文件总大小不能为负数", + "maximum-debug-duration-min": "最大调试时长(分钟)", + "maximum-debug-duration-min-range": "最大调试时长不能为负", "rest-requests-for-tenant": "租户REST请求", "transport-tenant-telemetry-msg-rate-limit": "租户遥测消息", "transport-tenant-telemetry-data-points-rate-limit": "租户遥测数据点", @@ -4458,6 +4803,7 @@ "sec": "{{ sec }} 秒", "sec-short": "{{ sec }}s", "short": { + "years": "{ years, plural, =1 {1 年} other {# 年} }", "days": "{ days, plural, =1 {1 天 } other {# 天 } }", "hours": "{ hours, plural, =1 {1 小时 } other {# 小时 } }", "minutes": "{{minutes}}分 ", @@ -4496,7 +4842,16 @@ "hide-group-interval": "隐藏用户的分组间隔", "hide-max-values": "隐藏用户的最大值", "hide-timezone": "隐藏用户的时区", - "disable-custom-interval": "禁用自定义间隔选择" + "disable-custom-interval": "禁用自定义间隔选择", + "edit-aggregation-functions-list": "编辑聚合函数列表", + "edit-aggregation-functions-list-hint": "可以指定可用选项的列表。", + "allowed-aggregation-functions": "允许的聚合函数", + "edit-intervals-list": "编辑时间间隔列表", + "allowed-agg-intervals": "分组时间间隔", + "default-agg-interval": "默认分组时间间隔", + "edit-intervals-list-hint": "可以指定可用的时间间隔选项列表。", + "edit-grouping-intervals-list-hint": "可以配置分组时间间隔列表和默认分组时间间隔。", + "all": "全部" }, "tooltip": { "trigger": "触发", @@ -4570,7 +4925,365 @@ "quart": "夸脱", "gallon": "加仑", "oil-barrels": "油桶", - "cubic-meter-per-kilogram": "每公斤立方米" + "cubic-meter-per-kilogram": "每公斤立方米", + "gill": "吉尔", + "hogshead": "大桶", + "teaspoon": "茶匙", + "tablespoon": "汤匙", + "cup": "杯", + "celsius": "摄氏度", + "kelvin": "开尔文", + "rankine": "兰金", + "fahrenheit": "华氏度", + "percent": "百分比", + "meter-per-second": "米每秒", + "kilometer-per-hour": "千米每小时", + "foot-per-second": "英尺每秒", + "mile-per-hour": "英里每小时", + "knot": "节", + "millimeters-per-minute": "毫米每分钟", + "kilometer-per-hour-squared": "千米每小时平方", + "foot-per-second-squared": "英尺每秒平方", + "pascal": "帕斯卡", + "kilopascal": "千帕", + "megapascal": "兆帕", + "gigapascal": "吉帕", + "millibar": "毫巴", + "bar": "巴", + "kilobar": "千巴", + "newton": "牛顿", + "newton-meter": "牛顿·米", + "foot-pounds": "英尺磅", + "inch-pounds": "英寸磅", + "newton-per-meter": "牛顿每米", + "atmospheres": "大气压", + "pounds-per-square-inch": "磅每平方英寸", + "torr": "托", + "inches-of-mercury": "英寸汞柱", + "pascal-per-square-meter": "帕斯卡每平方米", + "pound-per-square-inch": "磅每平方英寸", + "newton-per-square-meter": "牛顿每平方米", + "kilogram-force-per-square-meter": "千克力每平方米", + "pascal-per-square-centimeter": "帕斯卡每平方厘米", + "ton-force-per-square-inch": "吨力每平方英寸", + "kilonewton-per-square-meter": "千牛顿每平方米", + "newton-per-square-millimeter": "牛顿每平方毫米", + "microjoule": "微焦耳", + "millijoule": "毫焦耳", + "joule": "焦耳", + "kilojoule": "千焦耳", + "megajoule": "兆焦耳", + "gigajoule": "吉焦耳", + "watt-hour": "瓦时", + "kilowatt-hour": "千瓦时", + "electron-volts": "电子伏特", + "joules-per-coulomb": "焦耳每库仑", + "british-thermal-unit": "英热单位", + "foot-pound": "英尺磅", + "calorie": "卡路里", + "small-calorie": "小卡路里", + "kilocalorie": "千卡路里", + "joule-per-kelvin": "焦耳每开尔文", + "joule-per-kilogram-kelvin": "焦耳每千克开尔文", + "joule-per-kilogram": "焦耳每千克", + "watt-per-meter-kelvin": "瓦特每米开尔文", + "joule-per-cubic-meter": "焦耳每立方米", + "therm": "热单位", + "electric-dipole-moment": "电偶极矩", + "magnetic-dipole-moment": "磁偶极矩", + "debye": "德拜", + "coulomb-per-square-meter-per-volt": "库仑每平方米每伏特", + "milliwatt": "毫瓦", + "microwatt": "微瓦", + "watt": "瓦特", + "kilowatt": "千瓦", + "megawatt": "兆瓦", + "gigawatt": "吉瓦", + "metric-horsepower": "公制马力", + "milliwatt-per-square-centimeter": "毫瓦每平方厘米", + "watt-per-square-centimeter": "瓦特每平方厘米", + "kilowatt-per-square-centimeter": "千瓦每平方厘米", + "milliwatt-per-square-meter": "毫瓦每平方米", + "watt-per-square-meter": "瓦特每平方米", + "kilowatt-per-square-meter": "千瓦每平方米", + "watt-per-square-inch": "瓦特每平方英寸", + "kilowatt-per-square-inch": "千瓦每平方英寸", + "horsepower": "马力", + "btu-per-hour": "英热单位/小时", + "coulomb": "库仑", + "millicoulomb": "毫库仑", + "microcoulomb": "微库仑", + "picocoulomb": "皮库仑", + "coulomb-per-meter": "库仑每米", + "coulomb-per-cubic-meter": "库仑每立方米", + "coulomb-per-square-meter": "库仑每平方米", + "square-millimeter": "平方毫米", + "square-centimeter": "平方厘米", + "square-meter": "平方米", + "hectare": "公顷", + "square-kilometer": "平方千米", + "square-inch": "平方英寸", + "square-foot": "平方英尺", + "square-yard": "平方码", + "acre": "英亩", + "square-mile": "平方英里", + "are": "亚尔", + "barn": "巴恩", + "circular-inch": "圆英寸", + "milliampere-hour": "毫安时", + "milliampere-hour-tags": "电流, 电流流动, 电荷, 电流容量, 电流流, 电流流动, 毫安时, 毫安小时, mAh", + "ampere-hours": "安时", + "ampere-hours-tags": "电流, 电流流动, 电荷, 电流容量, 电流流, 电流流动, 安培, 安时, Ah", + "kiloampere-hours": "千安时", + "kiloampere-hours-tags": "电流, 电流流动, 电荷, 电流容量, 电流流, 电流流动, 千安时, 千安小时, kAh", + "nanoampere": "纳安培", + "nanoampere-tags": "电流, 安培, 纳安培, nA", + "picoampere": "皮安培", + "picoampere-tags": "电流, 安培, 皮安培, pA", + "microampere": "微安培", + "microampere-tags": "电流, 微安培, 微安培, μA", + "milliampere": "毫安培", + "milliampere-tags": "电流, 毫安培, 毫安培, mA", + "ampere": "安培", + "ampere-tags": "电流, 电流流动, 电流流, 电流流动, 安培, 安培, 电流强度, A", + "kiloamperes": "千安培", + "kiloamperes-tags": "电流, 电流流动, 千安培, kA", + "microampere-per-square-centimeter": "每平方厘米微安培", + "microampere-per-square-centimeter-tags": "电流密度, 每平方厘米微安培, µA/cm²", + "ampere-per-square-meter": "每平方米安培", + "ampere-per-square-meter-tags": "电流密度, 单位面积电流, 每平方米安培, A/m²", + "ampere-per-meter": "每米安培", + "ampere-per-meter-tags": "磁场强度, 磁场强度, 每米安培, A/m", + "oersted": "奥斯特", + "oersted-tags": "磁场, 奥斯特, Oe", + "bohr-magneton": "玻尔磁子", + "bohr-magneton-tags": "原子物理学, 磁矩, 玻尔磁子, μB", + "ampere-meter-squared": "安培·米平方", + "ampere-meter-squared-tags": "磁矩, 偶极矩, 安培·米平方, A·m²", + "ampere-meter": "安培·米", + "ampere-meter-tags": "磁场, 电流环, 安培·米, A·m", + "nanovolt": "纳伏特", + "picovolt": "皮伏特", + "millivolts": "毫伏特", + "microvolts": "微伏特", + "volt": "伏特", + "kilovolts": "千伏特", + "dbmV": "dBmV", + "dbm": "dBm", + "volt-meter": "伏特·米", + "kilovolt-meter": "千伏特·米", + "megavolt-meter": "兆伏特·米", + "microvolt-meter": "微伏特·米", + "millivolt-meter": "毫伏特·米", + "nanovolt-meter": "纳伏特·米", + "ohm": "欧姆", + "microohm": "微欧姆", + "milliohm": "毫欧姆", + "kilohm": "千欧姆", + "megohm": "兆欧姆", + "gigohm": "吉欧姆", + "hertz": "赫兹", + "kilohertz": "千赫兹", + "megahertz": "兆赫兹", + "gigahertz": "吉赫兹", + "rpm": "每分钟转速", + "candela-per-square-meter": "每平方米坎德拉", + "candela": "坎德拉", + "lumen": "流明", + "lux": "勒克斯", + "foot-candle": "英尺烛光", + "lumen-per-square-meter": "每平方米流明", + "lux-second": "勒克斯秒", + "lumen-second": "流明秒", + "lumens-per-watt": "每瓦流明", + "absorbance": "吸光度", + "mole": "摩尔", + "nanomole": "纳摩尔", + "micromole": "微摩尔", + "millimole": "毫摩尔", + "kilomole": "千摩尔", + "mole-per-cubic-meter": "每立方米摩尔", + "rssi": "接收信号强度指示", + "ppm": "百万分之一", + "ppb": "十亿分之一", + "micrograms-per-cubic-meter": "每立方米微克", + "aqi": "空气质量指数", + "gram-per-cubic-meter": "每立方米克", + "gram-per-kilogram": "比湿", + "millimeters-per-second": "每秒毫米", + "neper": "奈普尔", + "bel": "贝尔", + "decibel": "分贝", + "meters-per-second-squared": "每秒平方米", + "becquerel": "贝克勒尔", + "curie": "居里", + "gray": "戈瑞", + "sievert": "希沃特", + "roentgen": "伦琴", + "cps": "每秒计数", + "rad": "辐射剂量", + "rem": "辐射当量剂量", + "dps": "每秒衰变次数", + "rutherford": "卢瑟福", + "coulombs-per-kilogram": "每千克库仑", + "becquerels-per-cubic-meter": "每立方米贝可勒尔", + "curies-per-liter": "每升居里", + "becquerels-per-second": "每秒贝可勒尔", + "curies-per-second": "每秒居里", + "gy-per-second": "每秒戈瑞", + "watt-per-steradian": "每斯特拉迪安瓦特", + "watt-per-square-metre-steradian": "每平方米斯特拉迪安瓦特", + "ph-level": "pH值", + "turbidity": "浑浊度", + "mg-per-liter": "每升毫克", + "microsiemens-per-centimeter": "每厘米微西门子", + "millisiemens-per-meter": "每米毫西门子", + "siemens-per-meter": "每米西门子", + "kilogram-per-cubic-meter": "每立方米千克", + "gram-per-cubic-centimeter": "每立方厘米克", + "kilogram-per-square-meter": "每平方米千克", + "milligram-per-milliliter": "每毫升毫克", + "milligram-per-cubic-meter": "每立方米毫克", + "pound-per-cubic-foot": "每立方英尺磅", + "ounces-per-cubic-inch": "每立方英寸盎司", + "tons-per-cubic-yard": "每立方码吨", + "particle-density": "粒子密度", + "kilometers-per-liter": "每升公里", + "miles-per-gallon": "每加仑英里", + "liters-per-100-km": "每百公里升数", + "gallons-per-mile": "每英里加仑", + "liters-per-hour": "每小时升数", + "gallons-per-hour": "每小时加仑", + "beats-per-minute": "每分钟节拍数", + "millimeters-of-mercury": "毫米汞柱", + "milligrams-per-deciliter": "每分升毫克", + "g-force": "重力加速度", + "kilonewton": "千牛顿", + "kilogram-force": "千克力", + "pound-force": "磅力", + "kilopound-force": "千磅力", + "dyne": "达因", + "poundal": "磅顿", + "kip": "千磅力", + "gal": "加尔", + "gravity": "重力", + "hectopascal": "百帕斯卡", + "atmosphere": "大气压", + "millibars": "毫巴", + "inch-of-mercury": "一英寸汞柱", + "richter-scale": "里氏震级", + "second": "秒", + "minute": "分", + "hour": "小时", + "day": "天", + "week": "周", + "month": "月", + "year": "年", + "cubic-foot-per-minute": "每分钟立方英尺", + "cubic-meters-per-hour": "每小时立方米", + "cubic-meters-per-second": "每秒立方米", + "liter-per-second": "每秒升", + "liter-per-minute": "每分钟升", + "gallons-per-minute": "每分钟加仑", + "cubic-foot-per-second": "每秒立方英尺", + "milliliters-per-minute": "每分钟毫升", + "bit": "比特", + "byte": "字节", + "kilobyte": "千字节", + "megabyte": "兆字节", + "gigabyte": "吉字节", + "terabyte": "太字节", + "petabyte": "拍字节", + "exabyte": "艾字节", + "zettabyte": "泽字节", + "yottabyte": "尧字节", + "bit-per-second": "每秒比特", + "kilobit-per-second": "每秒千比特", + "megabit-per-second": "每秒兆比特", + "gigabit-per-second": "每秒吉比特", + "terabit-per-second": "每秒太比特", + "byte-per-second": "每秒字节", + "kilobyte-per-second": "每秒千字节", + "megabyte-per-second": "每秒兆字节", + "gigabyte-per-second": "每秒吉字节", + "degree": "度", + "radian": "弧度", + "gradian": "梯度", + "mil": "千分之一英寸", + "revolution": "转", + "siemens": "西门子", + "millisiemens": "毫西门子", + "microsiemens": "微西门子", + "kilosiemens": "千西门子", + "megasiemens": "兆西门子", + "gigasiemens": "吉西门子", + "farad": "法拉", + "millifarad": "毫法拉", + "microfarad": "微法拉", + "nanofarad": "纳法拉", + "picofarad": "皮法拉", + "kilofarad": "千法拉", + "megafarad": "兆法拉", + "gigafarad": "吉法拉", + "terfarad": "太法拉", + "farad-per-meter": "每米法拉", + "tesla": "特斯拉", + "gauss": "高斯", + "kilogauss": "千高斯", + "millitesla": "毫特斯拉", + "microtesla": "微特斯拉", + "nanotesla": "纳特斯拉", + "kilotesla": "千特斯拉", + "megatesla": "兆特斯拉", + "millitesla-square-meters": "毫特斯拉平方米", + "gamma": "伽马", + "lambda": "波长", + "square-meter-per-second": "每秒平方米", + "square-centimeter-per-second": "每秒平方厘米", + "stoke": "斯托克", + "centistokes": "厘斯托克", + "square-foot-per-second": "每秒平方英尺", + "square-inch-per-second": "每秒平方英寸", + "pascal-second": "帕斯卡秒", + "centipoise": "厘泊", + "poise": "泊", + "reynolds": "雷诺数", + "pound-per-foot-hour": "每英尺每小时磅", + "newton-second-per-square-meter": "每平方米牛顿秒", + "dyne-second-per-square-centimeter": "每平方厘米达因秒", + "kilogram-per-meter-second": "千克每米秒", + "tesla-square-meters": "特斯拉平方米", + "maxwell": "麦克斯韦", + "tesla-per-meter": "每米特斯拉", + "gauss-per-centimeter": "每厘米高斯", + "weber": "韦伯", + "microweber": "微韦伯", + "milliweber": "毫韦伯", + "gauss-square-centimeter": "高斯平方厘米", + "kilogauss-square-centimeter": "千高斯平方厘米", + "henry": "亨利", + "millihenry": "毫亨利", + "microhenry": "微亨利", + "nanohenry": "纳亨利", + "henry-per-meter": "每米亨利", + "tesla-meter-per-ampere": "每安培米特斯拉", + "gauss-per-oersted": "每欧斯特高斯", + "kilogram-per-mole": "每摩尔千克", + "gram-per-mole": "每摩尔克", + "milligram-per-mole": "每摩尔毫克", + "joule-per-mole": "每摩尔焦耳", + "joule-per-mole-kelvin": "每摩尔每开尔文焦耳", + "millivolts-per-meter": "每米毫伏", + "volts-per-meter": "每米伏特", + "kilovolts-per-meter": "每米千伏", + "radian-per-second": "每秒弧度", + "radian-per-second-squared": "每秒平方弧度", + "revolutions-per-minute-per-second": "角加速度", + "revolutions-per-minute-per-second-squared": "角加速度", + "deg-per-second": "度/秒", + "degrees-brix": "布里克度", + "katal": "卡塔尔", + "katal-per-cubic-metre": "每立方米卡塔尔" }, "user": { "user": "用户", @@ -4771,14 +5484,15 @@ "type": "类型", "resources": "资源", "resource-url": "JavaScript/CSS", + "resource-is-extension": "是否扩展", "remove-resource": "删除资源", "add-resource": "添加资源", "html": "HTML", "tidy": "整理", "css": "CSS", - "settings-schema": "设置模式", - "datakey-settings-schema": "数据键设置", - "latest-datakey-settings-schema": "最新数据键设置", + "settings-form": "设置表单", + "data-key-settings-form": "数据键设置表单", + "latest-data-key-settings-form": "最新数据键设置表单", "widget-settings": "设置", "description": "描述", "tags": "标签", @@ -4811,7 +5525,9 @@ "selected-widgets": "已选择{ count, plural, =1 {1 个部件} other {# 个部件} }", "undo": "撤销", "export": "导出", + "export-prompt": "嵌入部件图片和资源", "export-widgets": "导出部件", + "export-widgets-prompt": "嵌入部件图片和资源", "import": "导入部件", "no-data": "部件上没有要显示的数据", "data-overflow": "部件显示{{count}}条实体中的{{total}}条。", @@ -4870,6 +5586,7 @@ "popover-style": "样式", "open-new-browser-tab": "在选项卡中打开", "open-URL": "打开URL", + "URL": "URL", "url-required": "URL必填。", "mobile": { "action-type": "移动端动作类型", @@ -5051,7 +5768,12 @@ "action-button": { "behavior": "行为", "on-click": "单击", - "on-click-hint": "单击按钮时触发动作" + "on-click-hint": "单击按钮时触发动作", + "first-button-click": "第一次按钮单击", + "first-button-click-hint": "按下第一个按钮时触发的动作", + "second-button-click": "第二次按钮单击", + "second-button-click-hint": "按下第二个按钮时触发的动作", + "button-click-hint": "按下部件时触发的动作" }, "command-button": { "behavior": "行为", @@ -5096,6 +5818,18 @@ "vertical-fill": "垂直填充", "button-appearance": "外观" }, + "segmented-button": { + "layout": "布局", + "layout-squared": "方形", + "layout-rounded": "圆角", + "card-border": "卡片边框", + "button-appearance": "按钮外观", + "first": "第一个", + "second": "第二个", + "color-styles": "颜色样式", + "selected": "已选中", + "unselected": "未选中" + }, "button": { "layout": "布局", "outlined": "默认", @@ -5109,6 +5843,7 @@ "color-palette": "调色板", "main": "前景色", "background": "背景色", + "border": "边框", "custom-styles": "自定义样式", "clear-style": "清除样式", "shadow": "阴影", @@ -5122,11 +5857,16 @@ "activated-state-hint": "按钮在激活状态的触发条件。", "disabled-state": "禁用状态", "disabled-state-hint": "按钮在禁用状态的触发条件。", + "selected-state": "选中状态", + "selected-state-hint": "配置按钮选中状态的触发条件。", "enabled": "启用", "hovered": "悬停", "pressed": "按压", "activated": "激活", - "disabled": "禁用" + "disabled": "禁用", + "initial": "初始状态", + "first": "第一个", + "second": "第二个" }, "background": { "background": "背景颜色", @@ -5378,7 +6118,7 @@ "border-radius": "边框圆角", "bar-width": "宽度", "label": "标签", - "label-hint": "在bar上显示标签。", + "label-hint": "在条形图上显示标签。", "series-label-hint": "显示具有数值的标签。", "label-background": "标签背景" } @@ -5715,6 +6455,10 @@ "no-gpio-leds": "未配置GPIO LED", "add-gpio-led": "添加GPIO LED" }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, "input-widgets": { "attribute-not-allowed": "属性参数不能在此部件中使用", "blocked-location": "在浏览器中阻止地理位置", @@ -6780,11 +7524,13 @@ "get-attribute": "获取属性", "set-attribute": "设置属性", "get-time-series": "获取遥测", + "get-alarm-status": "获取告警状态", "get-dashboard-state": "获取仪表板状态", "add-time-series": "添加遥测", "execute-rpc-text": "执行RPC方法'{{methodName}}'", - "get-attribute-text": "使用属性'{{key}}'", "get-time-series-text": "使用遥测'{{key}}'", + "get-attribute-text": "使用属性'{{key}}'", + "get-alarm-status-text": "使用告警状态", "get-dashboard-state-text": "使用仪表板状态", "when-dashboard-state-is-text": "当仪表板状态为'{{state}}'", "when-dashboard-state-function-is-text": "当(仪表板状态)是'{{state}}'", @@ -7063,6 +7809,7 @@ "widget-action": { "action-cell-button": "动作单元格按钮", "row-click": "点击行时", + "cell-click": "点击单元格时", "polygon-click": "点击多边形时", "marker-click": "点击标记时", "circle-click": "点击圆圈时", @@ -7071,7 +7818,9 @@ "element-click": "点击HTML元素时", "pie-slice-click": "点击切片时", "row-double-click": "双击行时", - "card-click": "点击卡片时" + "cell-double-click": "双击单元格时", + "card-click": "点击卡片时", + "click": "点击时" } }, "paginator": { @@ -7083,6 +7832,33 @@ "items-per-page-separator": "至" }, "language": { - "language": "语言" + "language": "语言", + "locales": { + "ar_AE": "العربية (الإمارات العربية المتحدة)", + "ca_ES": "català (Espanya)", + "cs_CZ": "čeština (Česko)", + "da_DK": "dansk (Danmark)", + "de_DE": "Deutsch (Deutschland)", + "el_GR": "Ελληνικά (Ελλάδα)", + "en_US": "English (United States)", + "es_ES": "español (España)", + "fa_IR": "فارسی (ایران)", + "fr_FR": "français (France)", + "it_IT": "italiano (Italia)", + "ja_JP": "日本語 (日本)", + "ka_GE": "ქართული (საქართველო)", + "ko_KR": "한국어 (대한민국)", + "lt_LT": "lietuvių (Lietuva)", + "lv_LV": "latviešu (Latvija)", + "nl_BE": "Nederlands (België)", + "pl_PL": "polski (Polska)", + "pt_BR": "português (Brasil)", + "ro_RO": "română (România)", + "sl_SI": "slovenščina (Slovenija)", + "tr_TR": "Türkçe (Türkiye)", + "uk_UA": "українська (Україна)", + "zh_CN": "中文 (中国)", + "zh_TW": "中文 (台灣)" + } } } From 9f9319b791636651863f7c7b00d55c5c935f53c4 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 6 Mar 2025 18:07:20 +0200 Subject: [PATCH 006/112] Fix RuleChainMetadata for older Edge versions - Check for Edge version and removal of data introduced in later versions. --- .../service/edge/EdgeMsgConstructorUtils.java | 38 +++++++++++++++++-- .../service/edge/rpc/EdgeGrpcSession.java | 2 +- .../edge/rpc/processor/EdgeProcessor.java | 3 +- .../processor/alarm/AlarmEdgeProcessor.java | 3 +- .../comment/AlarmCommentEdgeProcessor.java | 3 +- .../processor/asset/AssetEdgeProcessor.java | 3 +- .../profile/AssetProfileEdgeProcessor.java | 3 +- .../customer/CustomerEdgeProcessor.java | 3 +- .../dashboard/DashboardEdgeProcessor.java | 3 +- .../processor/device/DeviceEdgeProcessor.java | 3 +- .../profile/DeviceProfileEdgeProcessor.java | 3 +- .../processor/edge/EdgeEntityProcessor.java | 3 +- .../entityview/EntityViewEdgeProcessor.java | 3 +- .../NotificationRuleEdgeProcessor.java | 3 +- .../NotificationTargetEdgeProcessor.java | 3 +- .../NotificationTemplateEdgeProcessor.java | 3 +- .../processor/oauth2/DomainEdgeProcessor.java | 3 +- .../oauth2/OAuth2ClientEdgeProcessor.java | 3 +- .../ota/OtaPackageEdgeProcessor.java | 3 +- .../processor/queue/QueueEdgeProcessor.java | 3 +- .../relation/RelationEdgeProcessor.java | 4 +- .../resource/ResourceEdgeProcessor.java | 3 +- .../rule/RuleChainEdgeProcessor.java | 5 ++- .../rule/RuleChainMetadataEdgeProcessor.java | 5 ++- .../settings/AdminSettingsEdgeProcessor.java | 3 +- .../processor/tenant/TenantEdgeProcessor.java | 3 +- .../tenant/TenantProfileEdgeProcessor.java | 3 +- .../rpc/processor/user/UserEdgeProcessor.java | 3 +- .../widget/WidgetBundleEdgeProcessor.java | 3 +- .../widget/WidgetTypeEdgeProcessor.java | 3 +- 30 files changed, 94 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index b59dc096ee..703d42abf5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -24,6 +25,7 @@ import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; @@ -417,8 +420,37 @@ public class EdgeMsgConstructorUtils { .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); } - public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) { - return RuleChainMetadataUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChainMetaData)).build(); + public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + String metaData = prepareMetaDataForEdgeVersion(ruleChainMetaData, edgeVersion); + + return RuleChainMetadataUpdateMsg.newBuilder() + .setMsgType(msgType) + .setEntity(metaData) + .build(); + } + + private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + if (edgeVersion == EdgeVersion.V_3_7_0 || edgeVersion == EdgeVersion.V_3_8_0) { + JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); + JsonNode nodes = jsonNode.get("nodes"); + + for (JsonNode node : nodes) { + if (node.isObject()) + prepareRuleNodeForOldEdgeVersion((ObjectNode) node); + } + return JacksonUtil.toString(jsonNode); + } else { + return JacksonUtil.toString(ruleChainMetaData); + } + } + + private static void prepareRuleNodeForOldEdgeVersion(ObjectNode node) { + if (TbMsgTimeseriesNode.class.getName().equals(node.get("type").asText())) { + JsonNode configurationNode = node.get("configuration"); + if (configurationNode != null && configurationNode.isObject()) { + ((ObjectNode) configurationNode).remove("processingSettings"); + } + } } public static EntityDataProto constructEntityDataMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { @@ -467,7 +499,7 @@ public class EdgeMsgConstructorUtils { AttributeDeleteMsg.Builder attributeDeleteMsg = AttributeDeleteMsg.newBuilder(); attributeDeleteMsg.setScope(entityData.getAsJsonObject().getAsJsonPrimitive("scope").getAsString()); JsonArray jsonArray = entityData.getAsJsonObject().getAsJsonArray("keys"); - List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>(){}.getType()); + List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>() {}.getType()); attributeDeleteMsg.addAllAttributeNames(keys); attributeDeleteMsg.build(); builder.setAttributeDeleteMsg(attributeDeleteMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index a970097548..ecd0c6d9f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -780,7 +780,7 @@ public abstract class EdgeGrpcSession implements Closeable { return null; } - return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent); + return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, edgeVersion); } public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java index 0f17949437..dd6221c62b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java @@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; public interface EdgeProcessor { ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto msg); - default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index df9af20f84..6c4cbe7a94 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -55,7 +56,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor implements AlarmProce } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AlarmUpdateMsg alarmUpdateMsg = convertAlarmEventToAlarmMsg(edgeEvent); if (alarmUpdateMsg != null) { return DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java index 449f4867d1..4a76a3527b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -57,7 +58,7 @@ public class AlarmCommentEdgeProcessor extends BaseAlarmProcessor implements Ala } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); switch (edgeEvent.getAction()) { case ADDED_COMMENT: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index e7a9720419..47f4c11362 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -107,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index 7a0ae0ccc6..e05b6f41c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -94,7 +95,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor impleme } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java index 40cbe3985f..784d10b26c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -48,7 +49,7 @@ import java.util.UUID; public class CustomerEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index f9a7311f31..e1259a7e0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -100,7 +101,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 08f6b294bf..ab01f83cd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -222,7 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 35857d8edb..3355bc819f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -94,7 +95,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java index 77fa31c028..5d4674ba1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -101,7 +102,7 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor { } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index a8844e7397..56785f9ac0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -106,7 +107,7 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java index 77f97e0775..918ca024cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationRuleEdgeProcessor extends BaseEdgeProcessor { private NotificationRuleService notificationRuleService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationRuleId notificationRuleId = new NotificationRuleId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java index 5dfad971b0..0f95248c74 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationTargetEdgeProcessor extends BaseEdgeProcessor { private NotificationTargetService notificationTargetService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationTargetId notificationTargetId = new NotificationTargetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java index b79f4ab4b8..f3ae939514 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationTemplateEdgeProcessor extends BaseEdgeProcessor { private NotificationTemplateService notificationTemplateService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationTemplateId notificationTemplateId = new NotificationTemplateId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java index 6b3d429a2f..07247d946c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -42,7 +43,7 @@ public class DomainEdgeProcessor extends BaseEdgeProcessor { private DomainService domainService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DomainId domainId = new DomainId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java index 20bd5b3f7e..08b0405702 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class OAuth2ClientEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index b2080e8176..e22ed3bafa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index ebea0c8255..f5fcac3342 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class QueueEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { QueueId queueId = new QueueId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java index da6f749269..70666e8b0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -30,6 +29,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; @@ -85,7 +85,7 @@ public class RelationEdgeProcessor extends BaseRelationProcessor implements Rela } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityRelation entityRelation = JacksonUtil.convertValue(edgeEvent.getBody(), EntityRelation.class); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RelationUpdateMsg relationUpdateMsg = EdgeMsgConstructorUtils.constructRelationUpdatedMsg(msgType, entityRelation); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index 8d2c06025a..de31bf6a9c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -73,7 +74,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor implements Reso } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 06fb4c37a2..b949660e70 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -134,7 +135,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -161,7 +162,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils - .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg); downlinkMsg = builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java index fe9a66c456..845d2a7204 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,14 +37,14 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class RuleChainMetadataEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); RuleChain ruleChain = edgeCtx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); if (ruleChain != null) { RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils - .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java index 6028273dc2..f2f82fe9de 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -33,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class); if (adminSettings == null) { return null; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java index e21ed0b4ea..f0f41b5cc6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -38,7 +39,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TenantId tenantId = TenantId.fromUUID(edgeEvent.getEntityId()); if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { Tenant tenant = edgeCtx.getTenantService().findTenantById(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java index bcbeeb82b3..a0bb32503e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId()); if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { TenantProfile tenantProfile = edgeCtx.getTenantProfileService().findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index c8c0b49124..fdd03d63f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class UserEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index 4c22ddf898..52d7370470 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -37,7 +38,7 @@ import java.util.List; public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index 4da508b22b..47f30afa93 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { From ab4ed8fa7489c3225e18f6b5d56859b270fb7dbc Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 6 Mar 2025 18:14:15 +0200 Subject: [PATCH 007/112] Fix RuleChainMetadata for older Edge versions - refactoring --- .../server/service/edge/EdgeMsgConstructorUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 703d42abf5..e0974df8f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -115,6 +115,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.List; import java.util.UUID; @@ -430,7 +431,7 @@ public class EdgeMsgConstructorUtils { } private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - if (edgeVersion == EdgeVersion.V_3_7_0 || edgeVersion == EdgeVersion.V_3_8_0) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); From c4a629fc9b5c5227af17ee9bfdede0374c871673 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 7 Mar 2025 13:26:54 +0200 Subject: [PATCH 008/112] Fix RuleChainMetadata for older Edge versions - fixed EdgeVersion --- .../server/service/edge/EdgeMsgConstructorUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index e0974df8f8..d3e8e3104e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -431,7 +431,7 @@ public class EdgeMsgConstructorUtils { } private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); From 80a7edc8f784465765ba710d1ab201187133e5f4 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 11 Mar 2025 17:58:27 +0200 Subject: [PATCH 009/112] Fix RuleChainMetadata for older Edge versions - added test - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 16 ++-- .../edge/EdgeMsgConstructorUtilsTest.java | 91 +++++++++++++++++++ 2 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index d3e8e3104e..11e3172168 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -422,7 +422,7 @@ public class EdgeMsgConstructorUtils { } public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - String metaData = prepareMetaDataForEdgeVersion(ruleChainMetaData, edgeVersion); + String metaData = filterMetadataForOldEdgeVersions(ruleChainMetaData, edgeVersion); return RuleChainMetadataUpdateMsg.newBuilder() .setMsgType(msgType) @@ -430,14 +430,15 @@ public class EdgeMsgConstructorUtils { .build(); } - private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + private static String filterMetadataForOldEdgeVersions(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); for (JsonNode node : nodes) { - if (node.isObject()) - prepareRuleNodeForOldEdgeVersion((ObjectNode) node); + if (node.isObject()) { + removeIncompatibleFields((ObjectNode) node); + } } return JacksonUtil.toString(jsonNode); } else { @@ -445,11 +446,10 @@ public class EdgeMsgConstructorUtils { } } - private static void prepareRuleNodeForOldEdgeVersion(ObjectNode node) { + private static void removeIncompatibleFields(ObjectNode node) { if (TbMsgTimeseriesNode.class.getName().equals(node.get("type").asText())) { - JsonNode configurationNode = node.get("configuration"); - if (configurationNode != null && configurationNode.isObject()) { - ((ObjectNode) configurationNode).remove("processingSettings"); + if (node.has("configuration") && node.get("configuration").isObject()) { + ((ObjectNode) node.get("configuration")).remove("processingSettings"); } } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java new file mode 100644 index 0000000000..5e7cfcfc28 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; + +import java.util.Collections; + +public class EdgeMsgConstructorUtilsTest { + private static final int CONFIGURATION_VERSION = 5; + + @Test + public void testRuleChainMetadataUpdateMsgForAllEdgeVersions() { + // GIVEN + RuleChainMetaData metaData = createIncompatibleRuleNodesForOldEdge(); + + // WHEN + RuleNode ruleNode_V_4_0_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_4_0_0); + RuleNode ruleNode_V_3_9_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_9_0); + RuleNode ruleNode_V_3_8_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_8_0); + RuleNode ruleNode_V_3_7_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_7_0); + + // THEN + assertRuleNodeConfiguration(ruleNode_V_4_0_0, EdgeVersion.V_4_0_0); + assertRuleNodeConfiguration(ruleNode_V_3_9_0, EdgeVersion.V_3_9_0); + assertRuleNodeConfiguration(ruleNode_V_3_8_0, EdgeVersion.V_3_8_0); + assertRuleNodeConfiguration(ruleNode_V_3_7_0, EdgeVersion.V_3_7_0); + } + + private RuleChainMetaData createIncompatibleRuleNodesForOldEdge() { + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + + RuleNode ruleNode1 = new RuleNode(); + ruleNode1.setName("TbMsgTimeseriesNode"); + ruleNode1.setType(org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode.class.getName()); + ruleNode1.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode1.setConfiguration(JacksonUtil.valueToTree(new TbMsgTimeseriesNodeConfiguration().defaultConfiguration())); + + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(Collections.singletonList(ruleNode1)); + + return ruleChainMetaData; + } + + + private RuleNode getRuleNodeFromMetadataUpdateMessage(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion); + + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); + Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); + + RuleNode ruleNode = ruleChainMetaData.getNodes().stream().findFirst().orElse(null); + Assert.assertNotNull("RuleNode is null for Edge version " + edgeVersion, ruleNode); + Assert.assertNotNull("Configuration is null for Edge version " + edgeVersion, ruleNode.getConfiguration()); + + return ruleNode; + } + + private void assertRuleNodeConfiguration(RuleNode ruleNode, EdgeVersion edgeVersion) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { + Assert.assertEquals("Unexpected config size", 2, ruleNode.getConfiguration().size()); + Assert.assertFalse("Unexpected field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); + }else{ + Assert.assertEquals("Unexpected config size", 3, ruleNode.getConfiguration().size()); + Assert.assertTrue("Missing field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); + } + } +} From 4e51398cc401415619b084ee382465c61d85da80 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 18 Mar 2025 12:24:14 +0200 Subject: [PATCH 010/112] Fix RuleChainMetadata for older Edge versions - added new problem node - added test - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 49 ++++-- .../edge/EdgeMsgConstructorUtilsTest.java | 146 +++++++++++++----- 2 files changed, 149 insertions(+), 46 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 11e3172168..61875f1277 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -25,6 +25,10 @@ import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.Customer; @@ -117,11 +121,25 @@ import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { + public static final Map NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION = Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ); + + //added in edge version 3.8.0 + public static final Set MISSING_NODES_IN_VERSION_37 = Set.of( + TbSendRestApiCallReplyNode.class.getName(), + TbAwsLambdaNode.class.getName() + ); public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { return AlarmUpdateMsg.newBuilder().setMsgType(msgType) @@ -431,25 +449,36 @@ public class EdgeMsgConstructorUtils { } private static String filterMetadataForOldEdgeVersions(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { - JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); - JsonNode nodes = jsonNode.get("nodes"); + JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); + JsonNode nodes = jsonNode.get("nodes"); + + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { + Iterator iterator = nodes.iterator(); + while (iterator.hasNext()) { + JsonNode node = iterator.next(); - for (JsonNode node : nodes) { - if (node.isObject()) { - removeIncompatibleFields((ObjectNode) node); + String type = node.get("type").asText(); + if (MISSING_NODES_IN_VERSION_37.contains(type)) { + iterator.remove(); } } + } + + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { + nodes.forEach(EdgeMsgConstructorUtils::changeRuleNodeConfigForOldEdgeVersion); + return JacksonUtil.toString(jsonNode); } else { return JacksonUtil.toString(ruleChainMetaData); } } - private static void removeIncompatibleFields(ObjectNode node) { - if (TbMsgTimeseriesNode.class.getName().equals(node.get("type").asText())) { - if (node.has("configuration") && node.get("configuration").isObject()) { - ((ObjectNode) node.get("configuration")).remove("processingSettings"); + private static void changeRuleNodeConfigForOldEdgeVersion(JsonNode node) { + if (node.isObject()) { + JsonNode configurationNode = node.get("configuration"); + if (configurationNode != null && configurationNode.isObject() && + NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.containsKey(node.get("type").asText())) { + ((ObjectNode) configurationNode).remove(NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(node.get("type").asText())); } } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 5e7cfcfc28..84ccfdb3ee 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -15,9 +15,20 @@ */ package org.thingsboard.server.service.edge; +import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNodeConfiguration; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNodeConfiguration; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNodeConfiguration; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNodeConfiguration; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -26,66 +37,129 @@ import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.MISSING_NODES_IN_VERSION_37; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION; + +@Slf4j public class EdgeMsgConstructorUtilsTest { private static final int CONFIGURATION_VERSION = 5; + public static final List TEST_SUPPORTED_EDGE_VERSIONS = Arrays.asList( + EdgeVersion.V_4_0_0, EdgeVersion.V_3_9_0, EdgeVersion.V_3_8_0, EdgeVersion.V_3_7_0 + ); + + private static final Map CONFIG_TO_NODE_NAME = Map.of( + new TbMsgTimeseriesNodeConfiguration(), TbMsgTimeseriesNode.class.getName(), + new TbMsgAttributesNodeConfiguration(), TbMsgAttributesNode.class.getName(), + new TbSaveToCustomCassandraTableNodeConfiguration(), TbSaveToCustomCassandraTableNode.class.getName() + ); + + private static final Map NODE_TO_CONFIG_PARAMS_COUNT = Map.of( + TbMsgTimeseriesNode.class.getName(), 3, + TbMsgAttributesNode.class.getName(), 5, + TbSaveToCustomCassandraTableNode.class.getName(), 3 + ); + + private static final Map CONFIG_TO_MISS_NODE_FOR_OLD_EDGE = Map.of( + new TbSendRestApiCallReplyNodeConfiguration(), TbSendRestApiCallReplyNode.class.getName(), + new TbAwsLambdaNodeConfiguration(), TbAwsLambdaNode.class.getName() + ); + @Test - public void testRuleChainMetadataUpdateMsgForAllEdgeVersions() { + public void testRuleChainMetadataUpdateMsgForOldEdgeVersions() { // GIVEN - RuleChainMetaData metaData = createIncompatibleRuleNodesForOldEdge(); - - // WHEN - RuleNode ruleNode_V_4_0_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_4_0_0); - RuleNode ruleNode_V_3_9_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_9_0); - RuleNode ruleNode_V_3_8_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_8_0); - RuleNode ruleNode_V_3_7_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_7_0); - - // THEN - assertRuleNodeConfiguration(ruleNode_V_4_0_0, EdgeVersion.V_4_0_0); - assertRuleNodeConfiguration(ruleNode_V_3_9_0, EdgeVersion.V_3_9_0); - assertRuleNodeConfiguration(ruleNode_V_3_8_0, EdgeVersion.V_3_8_0); - assertRuleNodeConfiguration(ruleNode_V_3_7_0, EdgeVersion.V_3_7_0); + RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_NODE_NAME); + + TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + // WHEN + List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + + // THEN + validateRuleNodeConfig(ruleNodes, edgeVersion); + }); } - private RuleChainMetaData createIncompatibleRuleNodesForOldEdge() { + @Test + public void testRuleChainMetadataWithMissingNodeForOldEdgeVersions() { + // GIVEN + RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_MISS_NODE_FOR_OLD_EDGE); + + TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + // WHEN + List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + + // THEN + boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0); + + if (isOldEdge) { + Assert.assertTrue("Rule Node must be empty", ruleNodes.isEmpty()); + } else { + Assert.assertEquals(MISSING_NODES_IN_VERSION_37.size(), ruleNodes.size()); + } + }); + } + + private RuleChainMetaData createMetadataWithProblemNodes(Map nodeMap) { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + List ruleNodes = new ArrayList<>(); + + nodeMap.entrySet().forEach(configToNodeName -> { + RuleNode ruleNode = new RuleNode(); - RuleNode ruleNode1 = new RuleNode(); - ruleNode1.setName("TbMsgTimeseriesNode"); - ruleNode1.setType(org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode.class.getName()); - ruleNode1.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode1.setConfiguration(JacksonUtil.valueToTree(new TbMsgTimeseriesNodeConfiguration().defaultConfiguration())); + ruleNode.setName(configToNodeName.getValue()); + ruleNode.setType(configToNodeName.getValue()); + ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode.setConfiguration(JacksonUtil.valueToTree(configToNodeName.getKey().defaultConfiguration())); + + ruleNodes.add(ruleNode); + }); ruleChainMetaData.setFirstNodeIndex(0); - ruleChainMetaData.setNodes(Collections.singletonList(ruleNode1)); + ruleChainMetaData.setNodes(ruleNodes); return ruleChainMetaData; } - - private RuleNode getRuleNodeFromMetadataUpdateMessage(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + private List getRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion); RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); - RuleNode ruleNode = ruleChainMetaData.getNodes().stream().findFirst().orElse(null); - Assert.assertNotNull("RuleNode is null for Edge version " + edgeVersion, ruleNode); - Assert.assertNotNull("Configuration is null for Edge version " + edgeVersion, ruleNode.getConfiguration()); + return ruleChainMetaData.getNodes(); + } - return ruleNode; + private void validateRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { + ruleNodes.forEach(ruleNode -> { + int ruleNodeConfigAmount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); + + boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0); + int expectedConfigAmount = isOldEdge ? ruleNodeConfigAmount - 1 : ruleNodeConfigAmount; + boolean includeConfigParam = !isOldEdge; + + validateParams(ruleNode, expectedConfigAmount, includeConfigParam); + }); } - private void assertRuleNodeConfiguration(RuleNode ruleNode, EdgeVersion edgeVersion) { - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { - Assert.assertEquals("Unexpected config size", 2, ruleNode.getConfiguration().size()); - Assert.assertFalse("Unexpected field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); - }else{ - Assert.assertEquals("Unexpected config size", 3, ruleNode.getConfiguration().size()); - Assert.assertTrue("Missing field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); - } + private void validateParams(RuleNode ruleNode, int expectedConfigAmount, boolean includeConfigParam) { + String ignoreConfigParam = NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); + + Assert.assertEquals( + String.format("Expected %d config params for ruleNode '%s', but found %d", expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), + expectedConfigAmount, ruleNode.getConfiguration().size() + ); + + boolean hasIgnoredField = ruleNode.getConfiguration().has(ignoreConfigParam); + Assert.assertEquals( + String.format("Field '%s' for ruleNode '%s' should %s be present", ignoreConfigParam, ruleNode.getName(), includeConfigParam ? "not" : ""), + includeConfigParam, hasIgnoredField + ); } + } From 487d3849b4c07cc1f9e2eb9f2869ae9464b39a6e Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 18 Mar 2025 19:27:57 +0200 Subject: [PATCH 011/112] Fix RuleChainMetadata for older Edge versions - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 14 +++--- .../edge/rpc/utils/EdgeVersionUtils.java | 9 ++++ .../edge/EdgeMsgConstructorUtilsTest.java | 45 +++++++++---------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 61875f1277..85471f6211 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -129,7 +129,7 @@ import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { - public static final Map NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION = Map.of( + public static final Map NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION = Map.of( TbMsgTimeseriesNode.class.getName(), "processingSettings", TbMsgAttributesNode.class.getName(), "processingSettings", TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" @@ -452,7 +452,7 @@ public class EdgeMsgConstructorUtils { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { + if (EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion)) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { JsonNode node = iterator.next(); @@ -464,7 +464,7 @@ public class EdgeMsgConstructorUtils { } } - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { + if (EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion)) { nodes.forEach(EdgeMsgConstructorUtils::changeRuleNodeConfigForOldEdgeVersion); return JacksonUtil.toString(jsonNode); @@ -475,10 +475,10 @@ public class EdgeMsgConstructorUtils { private static void changeRuleNodeConfigForOldEdgeVersion(JsonNode node) { if (node.isObject()) { - JsonNode configurationNode = node.get("configuration"); - if (configurationNode != null && configurationNode.isObject() && - NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.containsKey(node.get("type").asText())) { - ((ObjectNode) configurationNode).remove(NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(node.get("type").asText())); + String nodeType = node.get("type").asText(); + + if (node.isObject() && node.has("configuration") && NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.containsKey(nodeType)) { + ((ObjectNode) node.get("configuration")).remove(NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(nodeType)); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index 07e2024e1f..2ae6e91f34 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -24,4 +24,13 @@ public final class EdgeVersionUtils { public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) { return currentVersion.ordinal() < requiredVersion.ordinal(); } + + public static boolean isEdgeOlderThan_3_9_0(EdgeVersion currentVersion) { + return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_9_0); + } + + public static boolean isEdgeOlderThan_3_8_0(EdgeVersion currentVersion) { + return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_8_0); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 84ccfdb3ee..937a6709c5 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -33,7 +33,6 @@ import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; @@ -43,13 +42,13 @@ import java.util.List; import java.util.Map; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.MISSING_NODES_IN_VERSION_37; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION; @Slf4j public class EdgeMsgConstructorUtilsTest { private static final int CONFIGURATION_VERSION = 5; - public static final List TEST_SUPPORTED_EDGE_VERSIONS = Arrays.asList( + public static final List SUPPORTED_EDGE_VERSIONS_FOR_TESTS = Arrays.asList( EdgeVersion.V_4_0_0, EdgeVersion.V_3_9_0, EdgeVersion.V_3_8_0, EdgeVersion.V_3_7_0 ); @@ -75,12 +74,12 @@ public class EdgeMsgConstructorUtilsTest { // GIVEN RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_NODE_NAME); - TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { // WHEN - List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - validateRuleNodeConfig(ruleNodes, edgeVersion); + assertRuleNodeConfig(ruleNodes, edgeVersion); }); } @@ -89,12 +88,12 @@ public class EdgeMsgConstructorUtilsTest { // GIVEN RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_MISS_NODE_FOR_OLD_EDGE); - TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { // WHEN - List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0); + boolean isOldEdge = EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion); if (isOldEdge) { Assert.assertTrue("Rule Node must be empty", ruleNodes.isEmpty()); @@ -108,13 +107,13 @@ public class EdgeMsgConstructorUtilsTest { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); List ruleNodes = new ArrayList<>(); - nodeMap.entrySet().forEach(configToNodeName -> { + nodeMap.forEach((key, value) -> { RuleNode ruleNode = new RuleNode(); - ruleNode.setName(configToNodeName.getValue()); - ruleNode.setType(configToNodeName.getValue()); + ruleNode.setName(value); + ruleNode.setType(value); ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode.setConfiguration(JacksonUtil.valueToTree(configToNodeName.getKey().defaultConfiguration())); + ruleNode.setConfiguration(JacksonUtil.valueToTree(key.defaultConfiguration())); ruleNodes.add(ruleNode); }); @@ -125,30 +124,30 @@ public class EdgeMsgConstructorUtilsTest { return ruleChainMetaData; } - private List getRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion); + private List extractRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + String ruleChainMetadataUpdateMsg = + EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion).getEntity(); - RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg, RuleChainMetaData.class, true); Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); return ruleChainMetaData.getNodes(); } - private void validateRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { + private void assertRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { ruleNodes.forEach(ruleNode -> { - int ruleNodeConfigAmount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); + int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); - boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0); - int expectedConfigAmount = isOldEdge ? ruleNodeConfigAmount - 1 : ruleNodeConfigAmount; - boolean includeConfigParam = !isOldEdge; + boolean isLegacyEdgeVersion = EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion); + int expectedConfigAmount = isLegacyEdgeVersion ? configParamCount - 1 : configParamCount; + boolean includeConfigParam = !isLegacyEdgeVersion; validateParams(ruleNode, expectedConfigAmount, includeConfigParam); }); } private void validateParams(RuleNode ruleNode, int expectedConfigAmount, boolean includeConfigParam) { - String ignoreConfigParam = NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); + String ignoreConfigParam = NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); Assert.assertEquals( String.format("Expected %d config params for ruleNode '%s', but found %d", expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), From d602333d98a7347410a9077adc5e876b358893a1 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 19 Mar 2025 19:30:01 +0200 Subject: [PATCH 012/112] Fix RuleChainMetadata for older Edge versions - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 74 +++++++++++-------- .../edge/rpc/utils/EdgeVersionUtils.java | 8 -- .../edge/EdgeMsgConstructorUtilsTest.java | 45 ++++------- 3 files changed, 60 insertions(+), 67 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 85471f6211..d9c9bee9df 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -119,7 +119,6 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.Iterator; import java.util.List; @@ -128,17 +127,30 @@ import java.util.Set; import java.util.UUID; @Slf4j + public class EdgeMsgConstructorUtils { - public static final Map NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION = Map.of( - TbMsgTimeseriesNode.class.getName(), "processingSettings", - TbMsgAttributesNode.class.getName(), "processingSettings", - TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + public static final Map> VERSION_TO_IGNORED_PARAM = Map.of( + EdgeVersion.V_3_8_0, + Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ), + EdgeVersion.V_3_7_0, + Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ) ); - //added in edge version 3.8.0 - public static final Set MISSING_NODES_IN_VERSION_37 = Set.of( - TbSendRestApiCallReplyNode.class.getName(), - TbAwsLambdaNode.class.getName() + //these nodes added in edge version 3.8.0 + public static final Map> VERSION_TO_MISSING_NODES = Map.of( + EdgeVersion.V_3_7_0, + Set.of( + TbSendRestApiCallReplyNode.class.getName(), + TbAwsLambdaNode.class.getName() + ) ); public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { @@ -452,33 +464,35 @@ public class EdgeMsgConstructorUtils { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); - if (EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion)) { - Iterator iterator = nodes.iterator(); - while (iterator.hasNext()) { - JsonNode node = iterator.next(); + changeConfigForOldEdgeVersions(nodes, edgeVersion); + removeMissingNodeOldForEdge(nodes, edgeVersion); - String type = node.get("type").asText(); - if (MISSING_NODES_IN_VERSION_37.contains(type)) { - iterator.remove(); - } - } - } + return JacksonUtil.toString(jsonNode); + } - if (EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion)) { - nodes.forEach(EdgeMsgConstructorUtils::changeRuleNodeConfigForOldEdgeVersion); + private static void changeConfigForOldEdgeVersions(JsonNode nodes, EdgeVersion edgeVersion) { + nodes.forEach(node -> { + if (node.isObject() && node.has("configuration")) { + String nodeType = node.get("type").asText(); + Map ignoredParams = VERSION_TO_IGNORED_PARAM.get(edgeVersion); - return JacksonUtil.toString(jsonNode); - } else { - return JacksonUtil.toString(ruleChainMetaData); - } + if (ignoredParams != null && ignoredParams.containsKey(nodeType)) { + ((ObjectNode) node.get("configuration")).remove(ignoredParams.get(nodeType)); + } + } + }); } - private static void changeRuleNodeConfigForOldEdgeVersion(JsonNode node) { - if (node.isObject()) { - String nodeType = node.get("type").asText(); + private static void removeMissingNodeOldForEdge(JsonNode nodes, EdgeVersion edgeVersion) { + Iterator iterator = nodes.iterator(); + + while (iterator.hasNext()) { + JsonNode node = iterator.next(); + String type = node.get("type").asText(); + Set missNodes = VERSION_TO_MISSING_NODES.get(edgeVersion); - if (node.isObject() && node.has("configuration") && NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.containsKey(nodeType)) { - ((ObjectNode) node.get("configuration")).remove(NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(nodeType)); + if (missNodes != null && missNodes.contains(type)) { + iterator.remove(); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index 2ae6e91f34..9902118eb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -25,12 +25,4 @@ public final class EdgeVersionUtils { return currentVersion.ordinal() < requiredVersion.ordinal(); } - public static boolean isEdgeOlderThan_3_9_0(EdgeVersion currentVersion) { - return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_9_0); - } - - public static boolean isEdgeOlderThan_3_8_0(EdgeVersion currentVersion) { - return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_8_0); - } - } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 937a6709c5..690deac0ec 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -34,15 +34,14 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.MISSING_NODES_IN_VERSION_37; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_IGNORED_PARAM; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_MISSING_NODES; @Slf4j public class EdgeMsgConstructorUtilsTest { @@ -93,13 +92,11 @@ public class EdgeMsgConstructorUtilsTest { List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - boolean isOldEdge = EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion); + int leftNode = VERSION_TO_MISSING_NODES.containsKey(edgeVersion) ? + CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - VERSION_TO_MISSING_NODES.get(edgeVersion).size() : + CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size(); - if (isOldEdge) { - Assert.assertTrue("Rule Node must be empty", ruleNodes.isEmpty()); - } else { - Assert.assertEquals(MISSING_NODES_IN_VERSION_37.size(), ruleNodes.size()); - } + Assert.assertEquals(leftNode, ruleNodes.size()); }); } @@ -129,6 +126,7 @@ public class EdgeMsgConstructorUtilsTest { EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion).getEntity(); RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg, RuleChainMetaData.class, true); + Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); return ruleChainMetaData.getNodes(); @@ -136,29 +134,18 @@ public class EdgeMsgConstructorUtilsTest { private void assertRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { ruleNodes.forEach(ruleNode -> { - int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); + int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); - boolean isLegacyEdgeVersion = EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion); - int expectedConfigAmount = isLegacyEdgeVersion ? configParamCount - 1 : configParamCount; - boolean includeConfigParam = !isLegacyEdgeVersion; + boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.keySet().stream() + .anyMatch(version -> version.equals(edgeVersion)); + int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; - validateParams(ruleNode, expectedConfigAmount, includeConfigParam); + Assert.assertEquals( + String.format("Expected %d config params for ruleNode '%s', but found %d", + expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), + expectedConfigAmount, ruleNode.getConfiguration().size() + ); }); } - private void validateParams(RuleNode ruleNode, int expectedConfigAmount, boolean includeConfigParam) { - String ignoreConfigParam = NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); - - Assert.assertEquals( - String.format("Expected %d config params for ruleNode '%s', but found %d", expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), - expectedConfigAmount, ruleNode.getConfiguration().size() - ); - - boolean hasIgnoredField = ruleNode.getConfiguration().has(ignoreConfigParam); - Assert.assertEquals( - String.format("Field '%s' for ruleNode '%s' should %s be present", ignoreConfigParam, ruleNode.getName(), includeConfigParam ? "not" : ""), - includeConfigParam, hasIgnoredField - ); - } - } From 65f5cd30936cc01fbd022e1a1ae8a65a50193fad Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 20 Mar 2025 11:43:34 +0200 Subject: [PATCH 013/112] Fix RuleChainMetadata for older Edge versions - refactoring --- .../server/service/edge/EdgeMsgConstructorUtils.java | 5 ++--- .../server/service/edge/EdgeMsgConstructorUtilsTest.java | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index d9c9bee9df..fb6d8d7f92 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -144,7 +144,6 @@ public class EdgeMsgConstructorUtils { ) ); - //these nodes added in edge version 3.8.0 public static final Map> VERSION_TO_MISSING_NODES = Map.of( EdgeVersion.V_3_7_0, Set.of( @@ -465,7 +464,7 @@ public class EdgeMsgConstructorUtils { JsonNode nodes = jsonNode.get("nodes"); changeConfigForOldEdgeVersions(nodes, edgeVersion); - removeMissingNodeOldForEdge(nodes, edgeVersion); + removeMissingNodeForOldEdge(nodes, edgeVersion); return JacksonUtil.toString(jsonNode); } @@ -483,7 +482,7 @@ public class EdgeMsgConstructorUtils { }); } - private static void removeMissingNodeOldForEdge(JsonNode nodes, EdgeVersion edgeVersion) { + private static void removeMissingNodeForOldEdge(JsonNode nodes, EdgeVersion edgeVersion) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 690deac0ec..702eed26b5 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -136,13 +136,13 @@ public class EdgeMsgConstructorUtilsTest { ruleNodes.forEach(ruleNode -> { int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); - boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.keySet().stream() - .anyMatch(version -> version.equals(edgeVersion)); + boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.entrySet().stream() + .anyMatch(entry -> entry.getKey().equals(edgeVersion) && + entry.getValue().containsKey(ruleNode.getType())); int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; Assert.assertEquals( - String.format("Expected %d config params for ruleNode '%s', but found %d", - expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), + String.format("For ruleNode '%s', edgeVersion '%s", ruleNode.getName(), edgeVersion), expectedConfigAmount, ruleNode.getConfiguration().size() ); }); From 1675e3aa50610b2df7578d7dd209169cf419017d Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 21 Mar 2025 11:20:47 +0200 Subject: [PATCH 014/112] Fix RuleChainMetadata for older Edge versions - renaming --- .../service/edge/EdgeMsgConstructorUtils.java | 20 +++++++++---------- .../edge/EdgeMsgConstructorUtilsTest.java | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index fb6d8d7f92..309c24d699 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -129,7 +129,7 @@ import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { - public static final Map> VERSION_TO_IGNORED_PARAM = Map.of( + public static final Map> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of( EdgeVersion.V_3_8_0, Map.of( TbMsgTimeseriesNode.class.getName(), "processingSettings", @@ -144,7 +144,7 @@ public class EdgeMsgConstructorUtils { ) ); - public static final Map> VERSION_TO_MISSING_NODES = Map.of( + public static final Map> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of( EdgeVersion.V_3_7_0, Set.of( TbSendRestApiCallReplyNode.class.getName(), @@ -451,7 +451,7 @@ public class EdgeMsgConstructorUtils { } public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - String metaData = filterMetadataForOldEdgeVersions(ruleChainMetaData, edgeVersion); + String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion); return RuleChainMetadataUpdateMsg.newBuilder() .setMsgType(msgType) @@ -459,21 +459,21 @@ public class EdgeMsgConstructorUtils { .build(); } - private static String filterMetadataForOldEdgeVersions(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + private static String sanitizeMetadataForLegacyEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); - changeConfigForOldEdgeVersions(nodes, edgeVersion); - removeMissingNodeForOldEdge(nodes, edgeVersion); + updateNodeConfigurationsForLegacyEdge(nodes, edgeVersion); + removeExcludedNodesForLegacyEdge(nodes, edgeVersion); return JacksonUtil.toString(jsonNode); } - private static void changeConfigForOldEdgeVersions(JsonNode nodes, EdgeVersion edgeVersion) { + private static void updateNodeConfigurationsForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) { nodes.forEach(node -> { if (node.isObject() && node.has("configuration")) { String nodeType = node.get("type").asText(); - Map ignoredParams = VERSION_TO_IGNORED_PARAM.get(edgeVersion); + Map ignoredParams = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion); if (ignoredParams != null && ignoredParams.containsKey(nodeType)) { ((ObjectNode) node.get("configuration")).remove(ignoredParams.get(nodeType)); @@ -482,13 +482,13 @@ public class EdgeMsgConstructorUtils { }); } - private static void removeMissingNodeForOldEdge(JsonNode nodes, EdgeVersion edgeVersion) { + private static void removeExcludedNodesForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { JsonNode node = iterator.next(); String type = node.get("type").asText(); - Set missNodes = VERSION_TO_MISSING_NODES.get(edgeVersion); + Set missNodes = EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion); if (missNodes != null && missNodes.contains(type)) { iterator.remove(); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 702eed26b5..ea1f99d41a 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -40,8 +40,8 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_IGNORED_PARAM; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_MISSING_NODES; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; @Slf4j public class EdgeMsgConstructorUtilsTest { @@ -92,8 +92,8 @@ public class EdgeMsgConstructorUtilsTest { List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - int leftNode = VERSION_TO_MISSING_NODES.containsKey(edgeVersion) ? - CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - VERSION_TO_MISSING_NODES.get(edgeVersion).size() : + int leftNode = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? + CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size(); Assert.assertEquals(leftNode, ruleNodes.size()); @@ -136,7 +136,7 @@ public class EdgeMsgConstructorUtilsTest { ruleNodes.forEach(ruleNode -> { int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); - boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.entrySet().stream() + boolean isOldEdgeVersion = IGNORED_PARAMS_BY_EDGE_VERSION.entrySet().stream() .anyMatch(entry -> entry.getKey().equals(edgeVersion) && entry.getValue().containsKey(ruleNode.getType())); int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; From 6f11005095641fc7faaabefe54b09d0926f45623 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 21 Mar 2025 11:30:55 +0200 Subject: [PATCH 015/112] Fix RuleChainMetadata for older Edge versions - remove space --- .../thingsboard/server/service/edge/EdgeMsgConstructorUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 309c24d699..9bcd8fc373 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -127,7 +127,6 @@ import java.util.Set; import java.util.UUID; @Slf4j - public class EdgeMsgConstructorUtils { public static final Map> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of( EdgeVersion.V_3_8_0, From 8f1fff61dfd55fc5e757f61e968f14f79da15363 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 21 Mar 2025 16:31:30 +0200 Subject: [PATCH 016/112] Fix equals method for CF --- .../cf/DefaultCalculatedFieldCache.java | 3 + .../DefaultEntitiesExportImportService.java | 2 - .../service/sync/vc/VersionControlTest.java | 2 + .../common/data/cf/CalculatedField.java | 57 ++++++++++++++----- .../ScriptCalculatedFieldConfiguration.java | 3 + .../SimpleCalculatedFieldConfiguration.java | 2 + 6 files changed, 53 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index 64487d9b3e..219a261183 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -144,6 +144,9 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { calculatedFieldFetchLock.lock(); try { CalculatedField calculatedField = calculatedFieldService.findById(tenantId, calculatedFieldId); + if (calculatedField == null) { + return; + } EntityId cfEntityId = calculatedField.getEntityId(); calculatedFields.put(calculatedFieldId, calculatedField); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 06fe7f4036..db7e37b368 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -62,7 +61,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final Map> importServices = new HashMap<>(); private final RelationService relationService; - private final CalculatedFieldService calculatedFieldService; private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 06f61ca4e5..29c3af5387 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -621,6 +621,7 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); + assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); }); List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); @@ -629,6 +630,7 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); + assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); }); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index b86f30ca78..ea7f81f216 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; @@ -37,10 +36,10 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; +import java.util.Objects; @Schema @Data -@EqualsAndHashCode(callSuper = true) public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { @Serial @@ -112,6 +111,48 @@ public class CalculatedField extends BaseData implements HasN return super.getCreatedTime(); } + // Getter is ignored for serialization + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + // Setter is annotated for deserialization + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CalculatedField that)) return false; + if (!super.equals(o)) return false; + return Objects.equals(tenantId, that.tenantId) && + Objects.equals(entityId, that.entityId) && + Objects.equals(name, that.name) && + Objects.equals(debugSettings, that.debugSettings) && + Objects.equals(configuration, that.configuration) && + type == that.type && debugMode == that.debugMode && + configurationVersion == that.configurationVersion && + Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(tenantId); + result = 31 * result + Objects.hashCode(entityId); + result = 31 * result + Objects.hashCode(type); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + Boolean.hashCode(debugMode); + result = 31 * result + Objects.hashCode(debugSettings); + result = 31 * result + Integer.hashCode(configurationVersion); + result = 31 * result + Objects.hashCode(configuration); + result = 31 * result + Objects.hashCode(version); + return result; + } + @Override public String toString() { return new StringBuilder() @@ -128,16 +169,4 @@ public class CalculatedField extends BaseData implements HasN .toString(); } - // Getter is ignored for serialization - @JsonIgnore - public boolean isDebugMode() { - return debugMode; - } - - // Setter is annotated for deserialization - @JsonSetter - public void setDebugMode(boolean debugMode) { - this.debugMode = debugMode; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java index 0971217fdf..c2dde43b8e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java @@ -16,13 +16,16 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @Override public CalculatedFieldType getType() { return CalculatedFieldType.SCRIPT; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 79a0518ba0..0a422de175 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @Override From 77721d5684968e9e72c487a72659423106e1e57d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 24 Mar 2025 09:49:40 +0200 Subject: [PATCH 017/112] Fix CFControllerTest --- .../server/controller/CalculatedFieldControllerTest.java | 9 ++++----- .../SimpleCalculatedFieldConfiguration.java | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index ee66f664cc..af43b34558 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -85,7 +84,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig(testDevice.getId())); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -134,16 +133,16 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { calculatedField.setType(CalculatedFieldType.SIMPLE); calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig(null)); + calculatedField.setConfiguration(getCalculatedFieldConfig()); calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { + private CalculatedFieldConfiguration getCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); Argument argument = new Argument(); - argument.setRefEntityId(referencedEntityId); + argument.setRefEntityId(null); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 0a422de175..5c0ce71e86 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -27,4 +27,5 @@ public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfi public CalculatedFieldType getType() { return CalculatedFieldType.SIMPLE; } + } From 3f089da378ec5debdf44d3f777222aac56415f5e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 24 Mar 2025 11:57:46 +0200 Subject: [PATCH 018/112] Fix tbCalculatedFieldNotificationsTopic --- .../org/thingsboard/server/queue/discovery/TopicService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 5992083d85..cfd796e361 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -47,7 +47,7 @@ public class TopicService { @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; - @Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}") + @Value("${queue.calculated-fields.notifications-topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); From fabf6f32351193343c20a331f344a147ef56ccc1 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 25 Mar 2025 18:28:30 +0200 Subject: [PATCH 019/112] fixed telemetry update handling for timeseries subscription --- .../DefaultTbLocalSubscriptionService.java | 8 ++--- .../service/ws/DefaultWebSocketService.java | 3 ++ .../sub/TelemetrySubscriptionUpdate.java | 1 + .../controller/TbTestWebSocketClient.java | 14 +++++++++ .../server/controller/WebsocketApiTest.java | 29 +++++++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 59681d3daa..165b0824f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -343,11 +343,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer s -> { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; - if (sub.isAllKeys()) { - updateData = data; - } else { - for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { + for (TsKvEntry kv : data) { + if (sub.getKeyStates().containsKey((kv.getKey()))) { + if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { if (updateData == null) { updateData = new ArrayList<>(); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 7288a8bac9..c52a20754d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -123,6 +123,7 @@ public class DefaultWebSocketService implements WebSocketService { private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; + private static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); @@ -684,6 +685,7 @@ public class DefaultWebSocketService implements WebSocketService { .queryTs(queryTs) .allKeys(true) .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) .build(); subLock.lock(); @@ -739,6 +741,7 @@ public class DefaultWebSocketService implements WebSocketService { .queryTs(queryTs) .allKeys(false) .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) .build(); subLock.lock(); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java index b22b021a03..1a3046e301 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.ws.telemetry.sub; import lombok.AllArgsConstructor; +import net.minidev.json.annotate.JsonIgnore; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 959db43125..1c5b1dd154 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -31,6 +31,7 @@ import org.thingsboard.server.service.ws.AuthCmd; import org.thingsboard.server.service.ws.WsCmd; import org.thingsboard.server.service.ws.WsCommandsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; @@ -38,6 +39,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.net.URI; import java.nio.channels.NotYetConnectedException; @@ -271,6 +273,18 @@ public class TbTestWebSocketClient extends WebSocketClient { return sendEntityDataQuery(edq); } + public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) { + log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope); + TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); + cmd.setEntityId(entityId.getId().toString()); + cmd.setEntityType(entityId.getEntityType().toString()); + cmd.setCmdId(1); + cmd.setScope(scope); + send(cmd); + String msg = this.waitForReply(); + return JacksonUtil.fromString(msg, JsonNode.class); + } + public void send(WsCmd... cmds) { WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); cmdsWrapper.setCmds(List.of(cmds)); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java index 9801907d3b..3842cf917b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -68,11 +68,13 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -584,6 +586,33 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertNull(msg); } + @Test + public void testTimeseriesSubscriptionCmd() throws Exception { + long now = System.currentTimeMillis() - 100; + + long lastTs = now - TimeUnit.MINUTES.toMillis(1); + TsKvEntry dataPoint1 = new BasicTsKvEntry(lastTs, new LongDataEntry("temperature", 42L)); + sendTelemetry(device, List.of(dataPoint1)); + + JsonNode update = getWsClient().sendTimeseriesCmd(device.getId(), "LATEST_TELEMETRY"); + JsonNode data = update.get("data"); + Assert.assertEquals(1, data.size()); + Assert.assertEquals(JacksonUtil.newArrayNode().add(lastTs).add("42"), data.get("temperature").get(0)); + + //Sending update from the past, while latest value has new timestamp; + TsKvEntry dataPoint4 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(5), new LongDataEntry("temperature", 45L)); + getWsClient().registerWaitForUpdate(); + sendTelemetry(device, List.of(dataPoint4)); + String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + getWsClient().registerWaitForUpdate(); + sendTelemetry(device, List.of(dataPoint4)); + msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + } + @Test public void testEntityDataLatestTsWsCmd() throws Exception { long now = System.currentTimeMillis(); From 3f13fbd23f3df2679fb604ec7fbb391a4f6c511e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 26 Mar 2025 13:24:49 +0200 Subject: [PATCH 020/112] fixed telemetry update handling for all keys --- .../DefaultTbLocalSubscriptionService.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 165b0824f5..627b7f8b63 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -343,13 +343,28 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer s -> { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; - for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { - if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { - if (updateData == null) { - updateData = new ArrayList<>(); + if (sub.isAllKeys()) { + if (sub.isLatestValues()) { + for (TsKvEntry kv : data) { + if (!sub.getKeyStates().containsKey((kv.getKey())) || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (updateData == null) { + updateData = new ArrayList<>(); + } + updateData.add(kv); + } + } + } else { + updateData = data; + } + } else { + for (TsKvEntry kv : data) { + if (sub.getKeyStates().containsKey((kv.getKey()))) { + if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (updateData == null) { + updateData = new ArrayList<>(); + } + updateData.add(kv); } - updateData.add(kv); } } } From 8441a6bca28acc9157b456adf7ee391b445d0121 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 26 Mar 2025 17:29:06 +0200 Subject: [PATCH 021/112] code refactoring --- .../DefaultTbLocalSubscriptionService.java | 7 +- .../service/ws/DefaultWebSocketService.java | 65 ++++++++----------- .../sub/TelemetrySubscriptionUpdate.java | 1 - .../server/common/data/DataConstants.java | 2 + 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 627b7f8b63..d39c5fcb12 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -343,10 +343,11 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer s -> { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; + Map keyStates = sub.getKeyStates(); if (sub.isAllKeys()) { if (sub.isLatestValues()) { for (TsKvEntry kv : data) { - if (!sub.getKeyStates().containsKey((kv.getKey())) || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (!keyStates.containsKey((kv.getKey())) || kv.getTs() > keyStates.get(kv.getKey())) { if (updateData == null) { updateData = new ArrayList<>(); } @@ -358,8 +359,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } } else { for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { - if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (keyStates.containsKey((kv.getKey()))) { + if (!sub.isLatestValues() || kv.getTs() > keyStates.get(kv.getKey())) { if (updateData == null) { updateData = new ArrayList<>(); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index c52a20754d..11d962eb7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -105,6 +105,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.LATEST_TELEMETRY_SCOPE; + /** * Created by ashvayka on 27.03.18. */ @@ -123,7 +125,6 @@ public class DefaultWebSocketService implements WebSocketService { private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; - private static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); @@ -668,25 +669,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateProcessor((subscription, update) -> { - subLock.lock(); - try { - sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); - } finally { - subLock.unlock(); - } - }) - .queryTs(queryTs) - .allKeys(true) - .keyStates(subState) - .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) - .build(); + TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); subLock.lock(); try { @@ -714,6 +697,28 @@ public class DefaultWebSocketService implements WebSocketService { on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); } + private TbTimeSeriesSubscription createTbTimeSeriesSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { + return TbTimeSeriesSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); + } finally { + subLock.unlock(); + } + }) + .queryTs(queryTs) + .allKeys(allKeys) + .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) + .build(); + } + private FutureCallback> getSubscriptionCallback(final WebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long queryTs, final long startTs, final List keys) { return new FutureCallback<>() { @@ -724,25 +729,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateProcessor((subscription, update) -> { - subLock.lock(); - try { - sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); - } finally { - subLock.unlock(); - } - }) - .queryTs(queryTs) - .allKeys(false) - .keyStates(subState) - .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) - .build(); + TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); subLock.lock(); try { diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java index 1a3046e301..b22b021a03 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.ws.telemetry.sub; import lombok.AllArgsConstructor; -import net.minidev.json.annotate.JsonIgnore; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index b53d6daec2..b2d9d59cca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -148,4 +148,6 @@ public class DataConstants { public static final String CF_QUEUE_NAME = "CalculatedFields"; public static final String CF_STATES_QUEUE_NAME = "CalculatedFieldStates"; + public static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; + } From 78d59a99355554ff24ce1409a724f5445689c2d5 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 26 Mar 2025 17:35:36 +0200 Subject: [PATCH 022/112] method renamed --- .../server/service/ws/DefaultWebSocketService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 11d962eb7c..2e4fe9730a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -669,7 +669,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); + TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); subLock.lock(); try { @@ -697,7 +697,7 @@ public class DefaultWebSocketService implements WebSocketService { on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); } - private TbTimeSeriesSubscription createTbTimeSeriesSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { + private TbTimeSeriesSubscription getTsSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { return TbTimeSeriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) @@ -729,7 +729,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); + TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); subLock.lock(); try { From 0dfef548816632fd54a0daffa4173f5085191ce2 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 27 Mar 2025 14:41:01 +0100 Subject: [PATCH 023/112] Added ability to use query timeout for JdbcTemplates --- ...medParameterJdbcTemplateConfiguration.java | 43 +++++++++++++++++++ .../server/dao/sql/JdbcTemplateTest.java | 39 +++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java new file mode 100644 index 0000000000..ef8d0ed064 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +@Slf4j +public class NamedParameterJdbcTemplateConfiguration { + + @Value("${spring.jpa.properties.javax.persistence.query.timeout:30000}") + private int queryTimeout; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @PostConstruct + private void init() { + int timeout = Math.max(1, (int) TimeUnit.MILLISECONDS.toSeconds(queryTimeout)); + log.info("Set jdbcTemplate query timeout [{}] second(s)", timeout); + namedParameterJdbcTemplate.getJdbcTemplate().setQueryTimeout(timeout); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java new file mode 100644 index 0000000000..d0af99021a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.AbstractJpaDaoTest; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestPropertySource(properties = { + "spring.jpa.properties.javax.persistence.query.timeout=500" +}) +public class JdbcTemplateTest extends AbstractJpaDaoTest { + + @Autowired + private NamedParameterJdbcTemplate jdbcTemplate; + + @Test + public void queryTimeoutTest() { + assertThrows(DataAccessResourceFailureException.class, () -> jdbcTemplate.query("SELECT pg_sleep(10)", rs -> {})); + } +} From 665871c1d09ec03a465aeeb4f2798d82c36574c5 Mon Sep 17 00:00:00 2001 From: Tarnavskiy Date: Fri, 28 Mar 2025 16:20:29 +0200 Subject: [PATCH 024/112] PROD-5908: Fixed bug with empty step-increment field. Fixed adaptation for small screen view --- .../alarm/alarms-table-widget-settings.component.html | 4 ++-- .../alarm/alarms-table-widget-settings.component.ts | 11 +++++++---- .../timeseries-table-widget-settings.component.html | 4 ++-- .../timeseries-table-widget-settings.component.ts | 11 +++++++---- .../persistent-table-widget-settings.component.html | 4 ++-- .../persistent-table-widget-settings.component.ts | 11 +++++++---- .../entities-table-widget-settings.component.html | 4 ++-- .../entities-table-widget-settings.component.ts | 11 +++++++---- ui-ngx/src/form.scss | 3 ++- 9 files changed, 38 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index ba613ddbfe..eec36b90fa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -95,7 +95,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -113,7 +113,7 @@ warning - +
widgets.table.page-step-count
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -80,7 +80,7 @@ warning - +
widgets.table.page-step-count
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -67,7 +67,7 @@ warning - +
widgets.table.page-step-count
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
@@ -110,7 +110,7 @@ warning - +
widgets.table.page-step-count
Date: Mon, 31 Mar 2025 13:10:29 +0300 Subject: [PATCH 025/112] Fix RuleChainMetadata for older Edge versions - refactoring test --- .../edge/EdgeMsgConstructorUtilsTest.java | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index ea1f99d41a..ea82ec1780 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -17,7 +17,11 @@ package org.thingsboard.server.service.edge; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNodeConfiguration; @@ -32,11 +36,12 @@ import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,106 +49,109 @@ import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUD import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; @Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class EdgeMsgConstructorUtilsTest { private static final int CONFIGURATION_VERSION = 5; - public static final List SUPPORTED_EDGE_VERSIONS_FOR_TESTS = Arrays.asList( - EdgeVersion.V_4_0_0, EdgeVersion.V_3_9_0, EdgeVersion.V_3_8_0, EdgeVersion.V_3_7_0 - ); - - private static final Map CONFIG_TO_NODE_NAME = Map.of( + private static final Map NODE_CONFIG_TO_NAME_MAP = Map.of( new TbMsgTimeseriesNodeConfiguration(), TbMsgTimeseriesNode.class.getName(), new TbMsgAttributesNodeConfiguration(), TbMsgAttributesNode.class.getName(), new TbSaveToCustomCassandraTableNodeConfiguration(), TbSaveToCustomCassandraTableNode.class.getName() ); - private static final Map NODE_TO_CONFIG_PARAMS_COUNT = Map.of( + private static final Map NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP = Map.of( TbMsgTimeseriesNode.class.getName(), 3, TbMsgAttributesNode.class.getName(), 5, TbSaveToCustomCassandraTableNode.class.getName(), 3 ); - private static final Map CONFIG_TO_MISS_NODE_FOR_OLD_EDGE = Map.of( + + private static final Map MISSING_NODE_CONFIGS_FOR_OLD_EDGES = Map.of( new TbSendRestApiCallReplyNodeConfiguration(), TbSendRestApiCallReplyNode.class.getName(), new TbAwsLambdaNodeConfiguration(), TbAwsLambdaNode.class.getName() ); - @Test - public void testRuleChainMetadataUpdateMsgForOldEdgeVersions() { + @ParameterizedTest(name = "Testing metadata update for EdgeVersion: {0}") + @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) + @DisplayName("Test RuleChain Metadata Update for Supported Edge Versions") + public void testRuleChainMetadataUpdateForSupportedEdgeVersions(EdgeVersion edgeVersion) { // GIVEN - RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_NODE_NAME); + RuleChainMetaData metaData = createMetadataWithNodes(NODE_CONFIG_TO_NAME_MAP); - SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { - // WHEN - List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); + // WHEN + List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); - // THEN - assertRuleNodeConfig(ruleNodes, edgeVersion); - }); + // THEN + verifyRuleNodeConfigurations(ruleNodes, edgeVersion); } - @Test - public void testRuleChainMetadataWithMissingNodeForOldEdgeVersions() { + @ParameterizedTest(name = "Testing metadata with missing nodes for EdgeVersion: {0}") + @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) + @DisplayName("Test RuleChain Metadata with Missing Nodes for Old Edge Versions") + public void testRuleChainMetadataWithMissingNodesForOldEdgeVersions(EdgeVersion edgeVersion) { // GIVEN - RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_MISS_NODE_FOR_OLD_EDGE); - - SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { - // WHEN - List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); - - // THEN - int leftNode = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? - CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : - CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size(); - - Assert.assertEquals(leftNode, ruleNodes.size()); - }); + RuleChainMetaData metaData = createMetadataWithNodes(MISSING_NODE_CONFIGS_FOR_OLD_EDGES); + + // WHEN + List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); + + // THEN + int expectedNodeCount = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? + MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : + MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size(); + Assertions.assertEquals( + expectedNodeCount, + ruleNodes.size(), + String.format("EdgeVersion '%s' should have %d nodes, but found %d.", edgeVersion, expectedNodeCount, ruleNodes.size()) + ); } - private RuleChainMetaData createMetadataWithProblemNodes(Map nodeMap) { + private RuleChainMetaData createMetadataWithNodes(Map nodeConfigMap) { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); List ruleNodes = new ArrayList<>(); - nodeMap.forEach((key, value) -> { + nodeConfigMap.forEach((config, nodeName) -> { RuleNode ruleNode = new RuleNode(); - - ruleNode.setName(value); - ruleNode.setType(value); + ruleNode.setName(nodeName); + ruleNode.setType(nodeName); ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode.setConfiguration(JacksonUtil.valueToTree(key.defaultConfiguration())); - + ruleNode.setConfiguration(JacksonUtil.valueToTree(config.defaultConfiguration())); ruleNodes.add(ruleNode); }); ruleChainMetaData.setFirstNodeIndex(0); ruleChainMetaData.setNodes(ruleNodes); - return ruleChainMetaData; } - private List extractRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { - String ruleChainMetadataUpdateMsg = - EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion).getEntity(); + private List extractRuleNodesFromMetadata(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + String metadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg( + UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, + metaData, + edgeVersion + ).getEntity(); - RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg, RuleChainMetaData.class, true); - - Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); - - return ruleChainMetaData.getNodes(); + RuleChainMetaData updatedMetaData = JacksonUtil.fromString(metadataUpdateMsg, RuleChainMetaData.class, true); + Assertions.assertNotNull(updatedMetaData, "RuleChainMetaData should not be null after update."); + return updatedMetaData.getNodes(); } - private void assertRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { + private void verifyRuleNodeConfigurations(List ruleNodes, EdgeVersion edgeVersion) { ruleNodes.forEach(ruleNode -> { - int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); + String nodeType = ruleNode.getType(); + int expectedParamCount = NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP.getOrDefault(nodeType, 0); + + boolean isRuleNodeModified = IGNORED_PARAMS_BY_EDGE_VERSION + .getOrDefault(edgeVersion, Map.of()) + .containsKey(nodeType); - boolean isOldEdgeVersion = IGNORED_PARAMS_BY_EDGE_VERSION.entrySet().stream() - .anyMatch(entry -> entry.getKey().equals(edgeVersion) && - entry.getValue().containsKey(ruleNode.getType())); - int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; + int actualParamCount = isRuleNodeModified ? expectedParamCount - 1 : expectedParamCount; - Assert.assertEquals( - String.format("For ruleNode '%s', edgeVersion '%s", ruleNode.getName(), edgeVersion), - expectedConfigAmount, ruleNode.getConfiguration().size() + Assertions.assertEquals( + actualParamCount, + ruleNode.getConfiguration().size(), + String.format("RuleNode '%s' for EdgeVersion '%s' should have %d config parameters.", + ruleNode.getName(), edgeVersion, actualParamCount) ); }); } From bfb7532488e49b8f9f746807ef85f8127e0a6353 Mon Sep 17 00:00:00 2001 From: Tarnavskiy Date: Mon, 31 Mar 2025 13:33:27 +0300 Subject: [PATCH 026/112] Changed styling to tailwind class --- .../settings/alarm/alarms-table-widget-settings.component.html | 2 +- .../cards/timeseries-table-widget-settings.component.html | 2 +- .../control/persistent-table-widget-settings.component.html | 2 +- .../entity/entities-table-widget-settings.component.html | 2 +- ui-ngx/tailwind.config.js | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index eec36b90fa..af2c92b733 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -95,7 +95,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index 4f260c350a..52dbf0ba79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -62,7 +62,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html index 0b32a225d4..d370fde972 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/persistent-table-widget-settings.component.html @@ -49,7 +49,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html index 02f4be6b3d..d369c4a165 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html @@ -92,7 +92,7 @@ {{ 'widgets.table.display-pagination' | translate }}
-
{{ 'widgets.table.page-step-settings' | translate }}
+
{{ 'widgets.table.page-step-settings' | translate }}
widgets.table.page-step-increment
diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index e84ba47abc..d675da4cda 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -102,6 +102,7 @@ module.exports = { minWidth: { '7.5': '1.875rem', '25': '6.25rem', + '30': '7.5rem', '37.5': '9.375rem', '62.5': '15.625rem', '72.5': '18.125rem', From 1c11b30eacb43589eb258a3941a6f588d6b40765 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 28 Mar 2025 15:11:45 +0200 Subject: [PATCH 027/112] UI: Fix incorrect datasource type switching behavior in map settings when 'keys' field is not set --- .../map/map-data-layer-dialog.component.ts | 28 +++++-------------- .../map/map-data-layer-row.component.ts | 26 ++++------------- .../map/map-data-source-row.component.ts | 19 ++----------- .../shared/models/widget/maps/map.models.ts | 15 ++++++++++ 4 files changed, 30 insertions(+), 58 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index dc0123406a..ae752ebca7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -31,7 +31,8 @@ import { pathDecoratorSymbolTranslationMap, PolygonsDataLayerSettings, ShapeDataLayerSettings, - TripsDataLayerSettings + TripsDataLayerSettings, + updateDataKeyToNewDsType } from '@shared/models/widget/maps/map.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -293,23 +294,23 @@ export class MapDataLayerDialogComponent extends DialogComponent { + if (newDsType === DatasourceType.function) { + if (dataKey && dataKey.type !== DataKeyType.function) { + dataKey.type = DataKeyType.function; + return true; + } + } else { + if (dataKey?.type === DataKeyType.function) { + dataKey.type = timeSeries ? DataKeyType.timeseries : DataKeyType.attribute; + return true; + } + } + return false; +} + export const mergeMapDatasources = (target: TbMapDatasource[], source: TbMapDatasource[]): TbMapDatasource[] => { const appendDatasources: TbMapDatasource[] = []; for (const sourceDs of source) { From ba238d61f3d655362e2aaf7c30ec09dd9742ec2d Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 31 Mar 2025 15:23:16 +0300 Subject: [PATCH 028/112] fixed issue with comparing different data point types --- .../thingsboard/server/edqs/data/dp/BoolDataPoint.java | 7 ++++++- .../thingsboard/server/edqs/data/dp/DoubleDataPoint.java | 6 +++++- .../thingsboard/server/edqs/data/dp/LongDataPoint.java | 8 +++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java index 70a14917ba..97a0a265d5 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; @@ -46,6 +47,10 @@ public class BoolDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Boolean.compare(value, dataPoint.getBool()); + if (dataPoint.getType() == DataType.BOOLEAN) { + return StringUtils.compareIgnoreCase(valueToString(), dataPoint.valueToString()); + } else { + return super.compareTo(dataPoint); + } } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java index 2ca8f2c03a..4660b3eb56 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java @@ -46,6 +46,10 @@ public class DoubleDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Double.compare(value, dataPoint.getDouble()); + if (dataPoint.getType() == DataType.DOUBLE || dataPoint.getType() == DataType.LONG) { + return Double.compare(value, dataPoint.getDouble()); + } else { + return super.compareTo(dataPoint); + } } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java index 92c0a972e7..a1d33203b7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java @@ -51,6 +51,12 @@ public class LongDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Long.compare(value, dataPoint.getLong()); + if (dataPoint.getType() == DataType.DOUBLE) { + return Double.compare(getDouble(), dataPoint.getDouble()); + } else if (dataPoint.getType() == DataType.LONG) { + return Long.compare(value, dataPoint.getLong()); + } else { + return super.compareTo(dataPoint); + } } } From d28f372d932c72d5adc33ce6d8c3e58da26f0e3e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 31 Mar 2025 15:28:55 +0300 Subject: [PATCH 029/112] UI: Refactoring widget action use signal input additionalWidgetActionTypes --- .../action/widget-action.component.html | 2 +- .../common/action/widget-action.component.ts | 24 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index a2a0e8c8fa..5c735010f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html @@ -37,7 +37,7 @@
widget-config.action
- + {{ widgetActionTypeTranslations.get(widgetActionType[actionType]) | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts index bd0fe36910..698645bacf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts @@ -26,7 +26,7 @@ import { ValidatorFn, Validators } from '@angular/forms'; -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, computed, ElementRef, forwardRef, input, Input, OnInit, ViewChild } from '@angular/core'; import { MapItemType, mapItemTypeTranslationMap, @@ -102,19 +102,16 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali @Input() actionNames: string[]; - @Input() - set additionalWidgetActionTypes(value: WidgetActionType[]) { - if (this.widgetActionFormGroup && !widgetActionTypes.includes(this.widgetActionFormGroup.get('type').value)) { - this.widgetActionFormGroup.get('type').setValue(WidgetActionType.doNothing); - } - if (value?.length) { - this.widgetActionTypes = widgetActionTypes.concat(value); - } else { - this.widgetActionTypes = widgetActionTypes; + additionalWidgetActionTypes = input(null); + + actionTypes = computed(() => { + const predefinedActionTypes = widgetActionTypes; + if (this.additionalWidgetActionTypes()?.length) { + return predefinedActionTypes.concat(this.additionalWidgetActionTypes()); } - } + return predefinedActionTypes; + }); - widgetActionTypes = widgetActionTypes; widgetActionTypeTranslations = widgetActionTypeTranslationMap; widgetActionType = WidgetActionType; @@ -191,9 +188,6 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali ).subscribe(() => { this.widgetActionUpdated(); }); - if (this.additionalWidgetActionTypes) { - this.widgetActionTypes = this.widgetActionTypes.concat(this.additionalWidgetActionTypes); - } } writeValue(widgetAction?: WidgetAction): void { From c1d382a39b8fd3213b7e0083b81527f6825770ba Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 31 Mar 2025 16:52:22 +0300 Subject: [PATCH 030/112] UI: Fixed default action header button color --- .../widget/action/widget-action-dialog.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 7a74624774..5e071103b8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -192,6 +192,11 @@ export class WidgetActionDialogComponent extends DialogComponent Date: Mon, 31 Mar 2025 17:35:52 +0300 Subject: [PATCH 031/112] UI: Fixed AdditionalMapDataSourceRowComponent duplicate code --- ...dditional-map-data-source-row.component.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts index 0957d991f5..60adb60cb7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts @@ -33,7 +33,7 @@ import { Validators } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { AdditionalMapDataSourceSettings } from '@shared/models/widget/maps/map.models'; +import { AdditionalMapDataSourceSettings, updateDataKeyToNewDsType } from '@shared/models/widget/maps/map.models'; import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -156,7 +156,7 @@ export class AdditionalMapDataSourceRowComponent implements ControlValueAccessor const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value; if (dataKeys?.length) { for (const key of dataKeys) { - updateModel = this.updateDataKeyToNewDsType(key, newDsType) || updateModel; + updateModel = updateDataKeyToNewDsType(key, newDsType) || updateModel; } if (updateModel) { this.dataSourceFormGroup.get('dataKeys').patchValue(dataKeys, {emitEvent: false}); @@ -168,21 +168,6 @@ export class AdditionalMapDataSourceRowComponent implements ControlValueAccessor } } - private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean { - if (newDsType === DatasourceType.function) { - if (dataKey.type !== DataKeyType.function) { - dataKey.type = DataKeyType.function; - return true; - } - } else { - if (dataKey.type === DataKeyType.function) { - dataKey.type = DataKeyType.attribute; - return true; - } - } - return false; - } - private updateValidators() { const dsType: DatasourceType = this.dataSourceFormGroup.get('dsType').value; if (dsType === DatasourceType.function) { From fe0e20f785c6d3a72df33f79fbed474d3dfaed3f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 31 Mar 2025 18:18:10 +0300 Subject: [PATCH 032/112] UI: Fixed updated header button color --- .../action/widget-action-dialog.component.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 5e071103b8..ea49a8b442 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -135,8 +135,8 @@ export class WidgetActionDialogComponent extends DialogComponent Date: Tue, 1 Apr 2025 12:16:54 +0300 Subject: [PATCH 033/112] UI: Fixed positioning for color picker popover --- ui-ngx/src/app/shared/components/color-input.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index d0dc7235f7..77d02b47d3 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -201,7 +201,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro renderer: this.renderer, hostView: this.viewContainerRef, componentType: ColorPickerPanelComponent, - preferredPlacement: ['left'], + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], context: { color: this.colorFormGroup.get('color').value, colorClearButton: this.colorClearButton, From 665bb03a1fd7acf8dd80595c7cee607fa072a0df Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 12:19:11 +0300 Subject: [PATCH 034/112] non-exiting data point should return 0 ts --- .../java/org/thingsboard/server/edqs/util/RepositoryUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index 71b58d759a..f355d83e62 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -344,7 +344,7 @@ public class RepositoryUtils { if (dp != null) { return new TsValue(dp.getTs() > 0 ? dp.getTs() : ts, dp.valueToString()); } else { - return new TsValue(ts, ""); + return TsValue.EMPTY; } } From bca508ac00aade7b23e01875b7d8f71b32ed5832 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 13:23:28 +0300 Subject: [PATCH 035/112] added test --- .../server/controller/EntityQueryControllerTest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 90b7ea7c7b..29a208a805 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -499,7 +499,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { ); EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); - List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + List latestValues = List.of(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"), + new EntityKey(EntityKeyType.ATTRIBUTE, "non-existing-attribute")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); PageData data = findByQueryAndCheck(query, 67); @@ -517,6 +518,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); Assert.assertEquals(deviceTemperatures, loadedTemperatures); + // check ts value == 0, value is empty string for non-existing data points + List loadedNonExistingAttributes = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("non-existing-attribute")).toList(); + loadedNonExistingAttributes.forEach(tsValue -> { + assertThat(tsValue.getTs()).isEqualTo(0L); + assertThat(tsValue.getValue()).isEqualTo(""); + }); + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); From 3cefb634367dd3309760457a38e3ef2522cbca83 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Apr 2025 14:57:36 +0300 Subject: [PATCH 036/112] UI: Add to place map item action customize tooltip --- ui-ngx/src/app/core/services/utils.service.ts | 2 +- .../home/components/widget/lib/maps/map.ts | 41 ++++-- .../action/map-item-tooltips.component.html | 82 ++++++++++++ .../action/map-item-tooltips.component.ts | 123 ++++++++++++++++++ .../action/widget-action.component.html | 4 + .../common/action/widget-action.component.ts | 4 +- .../common/widget-settings-common.module.ts | 4 + ui-ngx/src/app/shared/models/widget.models.ts | 25 +++- .../assets/locale/locale.constant-en_US.json | 13 +- 9 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 61f16d1f67..bfaad1148d 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -208,7 +208,7 @@ export class UtilsService { return parseException(exception, lineOffset); } - public customTranslation(translationValue: string, defaultValue: string): string { + public customTranslation(translationValue: string, defaultValue: string = translationValue): string { if (translationValue && isString(translationValue)) { if (translationValue.includes(`{${i18nPrefix}`)) { const matches = translationValue.match(i18nRegExp); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index f1bb414878..24a02c6875 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -50,7 +50,14 @@ import { UnplacedMapDataItem, } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; import { IWidgetSubscription, PlaceMapItemActionData, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { FormattedData, MapItemType, WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models'; +import { + FormattedData, + mapItemTooltipsTranslation, + MapItemType, + WidgetAction, + WidgetActionType, + widgetType +} from '@shared/models/widget.models'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer'; @@ -661,29 +668,45 @@ export abstract class TbMap { private createMarker(actionData: PlaceMapItemActionData) { this.createItem(actionData, () => this.prepareDrawMode('Marker', { - placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint') + placeMarker: actionData.action.mapItemTooltips.placeMarker + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.placeMarker) + : this.ctx.translate.instant(mapItemTooltipsTranslation.placeMarker) })); } private createRectangle(actionData: PlaceMapItemActionData): void { this.createItem(actionData, () => this.prepareDrawMode('Rectangle', { - firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint'), - finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint') + firstVertex: actionData.action.mapItemTooltips.startRect + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startRect) + : this.ctx.translate.instant(mapItemTooltipsTranslation.startRect), + finishRect: actionData.action.mapItemTooltips.finishRect + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishRect) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishRect), })); } private createPolygon(actionData: PlaceMapItemActionData): void { this.createItem(actionData, () => this.prepareDrawMode('Polygon', { - firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint'), - continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint'), - finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint') + firstVertex: actionData.action.mapItemTooltips.firstVertex + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.firstVertex) + : this.ctx.translate.instant(mapItemTooltipsTranslation.firstVertex), + continueLine: actionData.action.mapItemTooltips.continueLine + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.continueLine) + : this.ctx.translate.instant(mapItemTooltipsTranslation.continueLine), + finishPoly: actionData.action.mapItemTooltips.finishPoly + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPoly) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishPoly), })); } private createCircle(actionData: PlaceMapItemActionData): void { this.createItem(actionData, () => this.prepareDrawMode('Circle', { - startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint'), - finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint') + startCircle: actionData.action.mapItemTooltips.startCircle + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startCircle) + : this.ctx.translate.instant(mapItemTooltipsTranslation.startCircle), + finishCircle: actionData.action.mapItemTooltips.finishCircle + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishCircle) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishCircle), })); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html new file mode 100644 index 0000000000..1ef16f9232 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html @@ -0,0 +1,82 @@ + +
+ + {{ 'widget-action.map-item-tooltip.tooltips' | translate }} + + @switch (mapItemType) { + @case (MapItemType.marker) { +
+
widget-action.map-item-tooltip.place-marker
+ + + +
+ } + @case (MapItemType.rectangle) { +
+
widget-action.map-item-tooltip.start-draw-rectangle
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-rectangle
+ + + +
+ } + @case (MapItemType.polygon) { +
+
widget-action.map-item-tooltip.start-draw-polygon
+ + + +
+
+
widget-action.map-item-tooltip.continue-draw-polygon
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-polygon
+ + + +
+ } + @case (MapItemType.circle) { +
+
widget-action.map-item-tooltip.start-draw-circle
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-circle
+ + + +
+ } + } +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts new file mode 100644 index 0000000000..830fd925b7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts @@ -0,0 +1,123 @@ +/// +/// Copyright © 2016-2025 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, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MapItemTooltips, MapItemType, mapItemTooltipsTranslation } from '@shared/models/widget.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { deepTrim, isEqual } from '@core/utils'; + +@Component({ + selector: 'tb-map-item-tooltips', + templateUrl: './map-item-tooltips.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapItemTooltipsComponent), + multi: true + } + ] +}) +export class MapItemTooltipsComponent implements ControlValueAccessor, OnChanges { + + @Input({required: true}) + mapItemType: MapItemType; + + tooltipsForm: FormGroup; + MapItemType = MapItemType; + readonly mapItemTooltipsDefaultTranslate = mapItemTooltipsTranslation; + + private modelValue: MapItemTooltips; + private propagateChange = (_val: any) => {}; + + constructor(private fd: FormBuilder) { + this.tooltipsForm = this.fd.group({ + placeMarker: [''], + firstVertex: [''], + continueLine: [''], + finishPoly: [''], + startRect: [''], + finishRect: [''], + startCircle: [''], + finishCircle: [''] + }); + + this.tooltipsForm.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(this.updatedModel.bind(this)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.mapItemType) { + const mapItemTypeChanges = changes.mapItemType; + if (!mapItemTypeChanges.firstChange && mapItemTypeChanges.currentValue !== mapItemTypeChanges.previousValue) { + this.updatedValidators(true); + } + } + } + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any) { + } + + setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.tooltipsForm.disable({emitEvent: false}); + } else { + this.tooltipsForm.enable({emitEvent: false}); + } + } + + writeValue(obj: MapItemTooltips) { + this.modelValue = obj; + this.tooltipsForm.patchValue(obj, {emitEvent: false}); + this.updatedValidators(); + } + + private updatedValidators(emitNewValue = false) { + this.tooltipsForm.disable({emitEvent: false}); + switch (this.mapItemType) { + case MapItemType.marker: + this.tooltipsForm.get('placeMarker').enable({emitEvent: false}); + break; + case MapItemType.rectangle: + this.tooltipsForm.get('startRect').enable({emitEvent: false}); + this.tooltipsForm.get('finishRect').enable({emitEvent: false}); + break; + case MapItemType.polygon: + this.tooltipsForm.get('firstVertex').enable({emitEvent: false}); + this.tooltipsForm.get('continueLine').enable({emitEvent: false}); + this.tooltipsForm.get('finishPoly').enable({emitEvent: false}); + break; + case MapItemType.circle: + this.tooltipsForm.get('startCircle').enable({emitEvent: false}); + this.tooltipsForm.get('finishCircle').enable({emitEvent: false}); + break; + } + this.tooltipsForm.updateValueAndValidity({emitEvent: emitNewValue}) + } + + private updatedModel(value: MapItemTooltips) { + const currentValue = Object.fromEntries(Object.entries(deepTrim(value)).filter(([_, v]) => v != '')); + if (!isEqual(currentValue, this.modelValue)) { + this.modelValue = currentValue; + this.propagateChange(currentValue); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index 5c735010f1..a58b58bb45 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html @@ -270,6 +270,10 @@
+ + Date: Tue, 1 Apr 2025 15:42:32 +0300 Subject: [PATCH 037/112] UI: Fixed translation --- .../lib/settings/common/action/map-item-tooltips.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html index 1ef16f9232..f0abe17509 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html @@ -17,7 +17,7 @@ -->
- {{ 'widget-action.map-item-tooltip.tooltips' | translate }} + {{ 'widget-action.map-item-tooltip.customize-map-item-tooltips' | translate }} @switch (mapItemType) { @case (MapItemType.marker) { 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 f312b7a8d2..813ab7709a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6594,7 +6594,7 @@ }, "place-map-item": "Place map item", "map-item-tooltip": { - "tooltips": "Map item tooltips", + "customize-map-item-tooltips": "Customize map item tooltips", "place-marker": "Place marker", "start-draw-rectangle": "Start draw rectangle", "finish-draw-rectangle": "Finish draw rectangle", From dbd74a8b4c54d1a9ed4e413d0ccc80a1972e3f57 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 16:20:50 +0300 Subject: [PATCH 038/112] added test --- .../service/entitiy/EntityServiceTest.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index e5c4009715..a2c3fc07c8 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -95,7 +95,6 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.sql.relation.RelationRepository; @@ -1664,7 +1663,7 @@ public class EntityServiceTest extends AbstractControllerTest { List> timeseriesFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); - timeseriesFutures.add(saveLongTimeseries(device.getId(), "temperature", temperatures.get(i))); + timeseriesFutures.add(saveTimeseries(device.getId(), "temperature", temperatures.get(i))); } Futures.allAsList(timeseriesFutures).get(); @@ -1711,6 +1710,13 @@ public class EntityServiceTest extends AbstractControllerTest { List entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); assertThat(entitiesTelemetry).containsExactlyElementsOf(expectedSortedList); + // update temperature to long value for one of device + long longTempValue = -100L; + saveTimeseries(devices.get(new Random().nextInt(66)).getId(), "temperature", longTempValue).get(); + loadedEntities = loadAllData(querySortByTemp, deviceTemperatures.size()); + entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(entitiesTelemetry.get(0)).isEqualTo(String.valueOf(longTempValue)); + deviceService.deleteDevicesByTenantId(tenantId); } @@ -2252,15 +2258,18 @@ public class EntityServiceTest extends AbstractControllerTest { return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); } - private ListenableFuture saveLongTimeseries(EntityId entityId, String key, Double value) { - TsKvEntity tsKv = new TsKvEntity(); - tsKv.setStrKey(key); - tsKv.setDoubleValue(value); + private ListenableFuture saveTimeseries(EntityId entityId, String key, Double value) { KvEntry telemetryValue = new DoubleDataEntry(key, value); BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); return timeseriesService.save(tenantId, entityId, timeseries); } + private ListenableFuture saveTimeseries(EntityId entityId, String key, Long value) { + KvEntry telemetryValue = new LongDataEntry(key, value); + BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); + return timeseriesService.save(tenantId, entityId, timeseries); + } + protected void createMultiRootHierarchy(List buildings, List apartments, Map> entityNameByTypeMap, Map childParentRelationMap) throws InterruptedException { From cf405c32e78215686341d553c2028bd6b65605fd Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 16:37:31 +0300 Subject: [PATCH 039/112] fixed BoolDataPoint compare method --- .../org/thingsboard/server/edqs/data/dp/BoolDataPoint.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java index 97a0a265d5..adb03a5cb1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -16,7 +16,6 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; @@ -48,7 +47,7 @@ public class BoolDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { if (dataPoint.getType() == DataType.BOOLEAN) { - return StringUtils.compareIgnoreCase(valueToString(), dataPoint.valueToString()); + return Boolean.compare(value, dataPoint.getBool()); } else { return super.compareTo(dataPoint); } From bfb475744069614f78ffed25c3ce25ab33eee86d Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 1 Apr 2025 16:50:04 +0300 Subject: [PATCH 040/112] Fix RuleChainMetadata for older Edge versions - refactoring test --- .../edge/EdgeMsgConstructorUtilsTest.java | 171 +++++++++--------- 1 file changed, 84 insertions(+), 87 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index ea82ec1780..a4c2133f1c 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -16,34 +16,34 @@ package org.thingsboard.server.service.edge; import lombok.extern.slf4j.Slf4j; -import org.junit.Assert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; -import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; -import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNodeConfiguration; +import org.thingsboard.rule.engine.filter.TbCheckRelationNode; +import org.thingsboard.rule.engine.flow.TbAckNode; +import org.thingsboard.rule.engine.math.TbMathNode; +import org.thingsboard.rule.engine.metadata.CalculateDeltaNode; +import org.thingsboard.rule.engine.metadata.TbGetTelemetryNode; import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; -import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNodeConfiguration; import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; -import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNodeConfiguration; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; -import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import java.util.ArrayList; -import java.util.HashMap; +import java.lang.reflect.Constructor; import java.util.List; -import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; @@ -51,109 +51,106 @@ import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORE @Slf4j @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class EdgeMsgConstructorUtilsTest { - private static final int CONFIGURATION_VERSION = 5; - - private static final Map NODE_CONFIG_TO_NAME_MAP = Map.of( - new TbMsgTimeseriesNodeConfiguration(), TbMsgTimeseriesNode.class.getName(), - new TbMsgAttributesNodeConfiguration(), TbMsgAttributesNode.class.getName(), - new TbSaveToCustomCassandraTableNodeConfiguration(), TbSaveToCustomCassandraTableNode.class.getName() - ); - private static final Map NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP = Map.of( - TbMsgTimeseriesNode.class.getName(), 3, - TbMsgAttributesNode.class.getName(), 5, - TbSaveToCustomCassandraTableNode.class.getName(), 3 - ); + private static final int CONFIGURATION_VERSION = 5; + static Stream provideEdgeVersions() { + return Stream.of( + EdgeVersion.V_4_0_0, + EdgeVersion.V_3_9_0, + EdgeVersion.V_3_8_0, + EdgeVersion.V_3_7_0 + ); + } - private static final Map MISSING_NODE_CONFIGS_FOR_OLD_EDGES = Map.of( - new TbSendRestApiCallReplyNodeConfiguration(), TbSendRestApiCallReplyNode.class.getName(), - new TbAwsLambdaNodeConfiguration(), TbAwsLambdaNode.class.getName() - ); + private static final RuleChainMetaData RULE_CHAIN_META_DATA = new RuleChainMetaData(); + private static final List TEST_NODES = + List.of( + new TbSaveToCustomCassandraTableNode(), + new TbMsgAttributesNode(), + new TbMsgTimeseriesNode(), + new TbSendRestApiCallReplyNode(), + new TbAwsLambdaNode(), + + new TbMathNode(), + new CalculateDeltaNode(), + new TbAckNode(), + new TbCheckRelationNode(), + new TbGetTelemetryNode() + ); - @ParameterizedTest(name = "Testing metadata update for EdgeVersion: {0}") - @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) - @DisplayName("Test RuleChain Metadata Update for Supported Edge Versions") - public void testRuleChainMetadataUpdateForSupportedEdgeVersions(EdgeVersion edgeVersion) { - // GIVEN - RuleChainMetaData metaData = createMetadataWithNodes(NODE_CONFIG_TO_NAME_MAP); + @BeforeAll + static void setUp() { + List ruleNodes = TEST_NODES.stream() + .map(node -> { + RuleNode ruleNode = new RuleNode(); + ruleNode.setName(node.getClass().getName()); + ruleNode.setType(node.getClass().getName()); + ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode.setConfiguration(JacksonUtil.valueToTree(createDefaultConfiguration(node))); + return ruleNode; + }) + .toList(); + + RULE_CHAIN_META_DATA.setFirstNodeIndex(0); + RULE_CHAIN_META_DATA.setNodes(ruleNodes); + } - // WHEN - List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); + private static NodeConfiguration createDefaultConfiguration(TbNode node) { + try { + org.thingsboard.rule.engine.api.RuleNode annotation = node.getClass().getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class); + Constructor constructor = annotation.configClazz().getConstructor(); + NodeConfiguration configInstance = (NodeConfiguration) constructor.newInstance(); - // THEN - verifyRuleNodeConfigurations(ruleNodes, edgeVersion); + return configInstance.defaultConfiguration(); + } catch (Exception e) { + throw new RuntimeException("Exception during creating RuleNodeConfiguration for node - " + node, e); + } } - @ParameterizedTest(name = "Testing metadata with missing nodes for EdgeVersion: {0}") - @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) - @DisplayName("Test RuleChain Metadata with Missing Nodes for Old Edge Versions") - public void testRuleChainMetadataWithMissingNodesForOldEdgeVersions(EdgeVersion edgeVersion) { - // GIVEN - RuleChainMetaData metaData = createMetadataWithNodes(MISSING_NODE_CONFIGS_FOR_OLD_EDGES); - + @ParameterizedTest(name = "Test Sanitize Metadata For Edge: {0}") + @MethodSource("provideEdgeVersions") + @DisplayName("Test Sanitize Metadata For Legacy Edge Version") + public void testSanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) { // WHEN - List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); + List ruleNodes = sanitizeMetadataForLegacyEdgeVersion(edgeVersion); // THEN - int expectedNodeCount = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? - MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : - MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size(); - Assertions.assertEquals( - expectedNodeCount, - ruleNodes.size(), - String.format("EdgeVersion '%s' should have %d nodes, but found %d.", edgeVersion, expectedNodeCount, ruleNodes.size()) - ); - } - - private RuleChainMetaData createMetadataWithNodes(Map nodeConfigMap) { - RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); - List ruleNodes = new ArrayList<>(); - - nodeConfigMap.forEach((config, nodeName) -> { - RuleNode ruleNode = new RuleNode(); - ruleNode.setName(nodeName); - ruleNode.setType(nodeName); - ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode.setConfiguration(JacksonUtil.valueToTree(config.defaultConfiguration())); - ruleNodes.add(ruleNode); + ruleNodes.forEach(ruleNode -> { + checkUpdateNodeConfigurationsForLegacyEdge(ruleNode, edgeVersion); + checkRemoveExcludedNodesForLegacyEdge(ruleNode, edgeVersion); }); - - ruleChainMetaData.setFirstNodeIndex(0); - ruleChainMetaData.setNodes(ruleNodes); - return ruleChainMetaData; } - private List extractRuleNodesFromMetadata(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + private List sanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) { String metadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg( UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, - metaData, + RULE_CHAIN_META_DATA, edgeVersion ).getEntity(); RuleChainMetaData updatedMetaData = JacksonUtil.fromString(metadataUpdateMsg, RuleChainMetaData.class, true); Assertions.assertNotNull(updatedMetaData, "RuleChainMetaData should not be null after update."); + return updatedMetaData.getNodes(); } - private void verifyRuleNodeConfigurations(List ruleNodes, EdgeVersion edgeVersion) { - ruleNodes.forEach(ruleNode -> { - String nodeType = ruleNode.getType(); - int expectedParamCount = NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP.getOrDefault(nodeType, 0); + private void checkUpdateNodeConfigurationsForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) { + if (IGNORED_PARAMS_BY_EDGE_VERSION.containsKey(edgeVersion) && IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).containsKey(ruleNode.getType())) { + String ignoredParam = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).get(ruleNode.getType()); - boolean isRuleNodeModified = IGNORED_PARAMS_BY_EDGE_VERSION - .getOrDefault(edgeVersion, Map.of()) - .containsKey(nodeType); + Assertions.assertFalse(ruleNode.getConfiguration().has(ignoredParam), + String.format("RuleNode '%s' for EdgeVersion '%s' should ignore '%s' config parameter.", ruleNode.getName(), edgeVersion, ignoredParam)); + } + } - int actualParamCount = isRuleNodeModified ? expectedParamCount - 1 : expectedParamCount; + private void checkRemoveExcludedNodesForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) { + boolean isNodeExcluded = Optional.ofNullable(EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion)) + .map(excludedNodes -> !excludedNodes.contains(ruleNode.getType())) + .orElse(true); - Assertions.assertEquals( - actualParamCount, - ruleNode.getConfiguration().size(), - String.format("RuleNode '%s' for EdgeVersion '%s' should have %d config parameters.", - ruleNode.getName(), edgeVersion, actualParamCount) - ); - }); + Assertions.assertTrue(isNodeExcluded, + String.format("For EdgeVersion '%s', ruleNode '%s' should not be included.", edgeVersion, ruleNode.getType())); } } From a043ca68ad241c5fbd3182db2afa080db99cccda Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 1 Apr 2025 17:43:49 +0300 Subject: [PATCH 041/112] UI: New map widget - refactor create tooltip in order to control layer interaction (including leaflet-geoman ugly behavior: disable/enable popups on toggle global draw mode) --- .../lib/maps/data-layer/data-layer-utils.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts index d5329586be..4b7b933d74 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts @@ -29,35 +29,39 @@ export const createTooltip = (map: TbMap, settings: DataLayerTooltipSettings, data: FormattedData, canOpen: () => boolean): L.Popup => { - const tooltip = L.popup(); - layer.bindPopup(tooltip, {autoClose: settings.autoclose, closeOnClick: false}); - layer.off('click'); + const tooltip = L.popup({autoClose: settings.autoclose, closeOnClick: false}); + (tooltip as any)._source = layer; + layer.on('move', (e) => { + tooltip.setLatLng((e as any).latlng); + }); + layer.on('remove', () => { + tooltip.close(); + }); if (settings.trigger === DataLayerTooltipTrigger.click) { - layer.on('click', () => { + layer.on('click', (e) => { + L.DomEvent.stop(e); if (tooltip.isOpen()) { - layer.closePopup(); + tooltip.close(); } else if (canOpen()) { - layer.openPopup(); + if ((tooltip as any)._prepareOpen((layer as any)._latlng)) { + tooltip.openOn(map.getMap()); + } } }); } else if (settings.trigger === DataLayerTooltipTrigger.hover) { layer.on('mouseover', () => { if (canOpen()) { - layer.openPopup(); + if ((tooltip as any)._prepareOpen((layer as any)._latlng)) { + tooltip.openOn(map.getMap()); + } } }); - layer.on('mousemove', (e) => { - tooltip.setLatLng(e.latlng); - }); layer.on('mouseout', () => { - layer.closePopup(); + tooltip.close(); }); } layer.on('popupopen', () => { bindTooltipActions(map, tooltip, settings, data); - (layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { - event.preventDefault(); - }); }); return tooltip; } From 63649046ed31fee0a4839f5441e9f944226a89e8 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Apr 2025 18:20:30 +0300 Subject: [PATCH 042/112] UI: Fixed typo in 'Calculate Field' script section title --- .../components/dialog/calculated-field-dialog.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 813f6b1b4c..1688685523 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -75,7 +75,9 @@ />
-
{{ 'calculated-fields.expression' | translate }}
+
+ {{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }} +
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { From 390a10bdb744329365f0e22024c9937c522a1833 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Apr 2025 18:36:05 +0300 Subject: [PATCH 043/112] UI: Fixed time-unit component not correctly calculating selected time --- .../components/rule-node/common/time-unit-input.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts index 54bb7fa9b8..b0d0a97641 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts @@ -177,7 +177,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } private updatedModel(value: Partial, forceUpdated = false) { - const time = value.time * this.timeIntervalsInSec.get(value.timeUnit); + const time = isDefinedAndNotNull(value.time) ? value.time * this.timeIntervalsInSec.get(value.timeUnit) : null; if (this.modelValue !== time || forceUpdated) { this.modelValue = time; this.propagateChange(time); From bffa565842b38423dd65350bf97d1665a309ba4f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:04:23 +0300 Subject: [PATCH 044/112] UI: Add fetch function for string items list; fix double item addition on blur --- .../string-items-list.component.html | 16 +++---- .../components/string-items-list.component.ts | 46 +++++++++++++------ 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index b50e0ecce8..d467fbe5f0 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -30,7 +30,7 @@ close @@ -49,12 +48,11 @@ - - - - - {{ 'common.not-found' | translate }} - + @for (value of filteredValues | async; track value.value) { + + } @empty { + {{ 'common.not-found' | translate }} + } {{ hint }} diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index a2fa28d0ff..a4a8ebd49b 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -30,6 +30,7 @@ import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { isDefined } from '@core/utils'; export interface StringItemsOption { name: string; @@ -116,6 +117,9 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @coerceArray() predefinedValues: StringItemsOption[]; + @Input() + fetchOptionsFn: (searchText?: string) => Observable>; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -124,7 +128,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return this.stringItemsForm.get('item'); } - onTouched = () => {}; + private onTouched = () => {}; private propagateChange: (value: any) => void = () => {}; private dirty = false; @@ -136,7 +140,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } ngOnInit() { - if (this.predefinedValues) { + if (this.predefinedValues || isDefined(this.fetchOptionsFn)) { this.filteredValues = this.itemControl.valueChanges .pipe( tap((value) => { @@ -147,7 +151,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), - mergeMap(name => this.fetchValues(name)), + mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); } @@ -199,19 +203,16 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.dirty = true; } - addItem(event: MatChipInputEvent): void { - const item = event.value?.trim() ?? ''; - if (item) { - if (this.predefinedValues) { - const findItems = this.predefinedValues - .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); - if (findItems.length === 1) { - this.add(findItems[0]); - } - } else { - this.add({value: item, name: item}); - } + addOnBlur(event: FocusEvent) { + const target: HTMLElement = event.relatedTarget as HTMLElement; + if (target && target.tagName !== 'MAT-OPTION') { + this.addItem(this.stringItemInput.nativeElement.value ?? '') } + this.onTouched(); + } + + addOnEnd(event: MatChipInputEvent): void { + this.addItem(event.value ?? '') } removeItems(item: StringItemsOption) { @@ -239,6 +240,21 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return values ? values.name : undefined; } + private addItem(value: string) { + const item = value.trim(); + if (item) { + if (this.predefinedValues) { + const findItems = this.predefinedValues + .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); + if (findItems.length === 1) { + this.add(findItems[0]); + } + } else { + this.add({value: item, name: item}); + } + } + } + private add(item: StringItemsOption) { if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) { if (!this.modelValue) { From 44f1e139bde66aec55ba0fbace0e078fd84f3ea1 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:41:59 +0300 Subject: [PATCH 045/112] UI: Add allowUserValue for string items list --- .../components/string-items-list.component.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index a4a8ebd49b..5929bbe924 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -31,6 +40,7 @@ import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { isDefined } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface StringItemsOption { name: string; @@ -120,6 +130,10 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @Input() fetchOptionsFn: (searchText?: string) => Observable>; + @Input() + @coerceBoolean() + allowUserValue = false; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -132,7 +146,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private propagateChange: (value: any) => void = () => {}; private dirty = false; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { this.stringItemsForm = this.fb.group({ item: [null], items: [null] @@ -151,6 +166,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), + tap(name => this.searchText = name), mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); @@ -243,12 +259,20 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private addItem(value: string) { const item = value.trim(); if (item) { - if (this.predefinedValues) { + if (this.predefinedValues && !this.allowUserValue) { const findItems = this.predefinedValues .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); if (findItems.length === 1) { this.add(findItems[0]); } + } else if (isDefined(this.fetchOptionsFn) && !this.allowUserValue) { + this.fetchOptionsFn(item).pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((findItems) => { + if (findItems.length === 1) { + this.add(findItems[0]); + } + }) } else { this.add({value: item, name: item}); } @@ -272,7 +296,6 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { if (!this.predefinedValues?.length) { return of([]); } - this.searchText = searchText; let result = this.predefinedValues; if (searchText && searchText.length) { result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase())); From d3e20161c96ff744357deeacdc63ae48edd3c5d6 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 2 Apr 2025 14:08:28 +0300 Subject: [PATCH 046/112] UI: Error icon with tooltip for toggle select --- .../common/map/map-settings.component.html | 8 ++-- .../components/toggle-header.component.html | 11 ++++- .../components/toggle-header.component.scss | 6 +++ .../components/toggle-header.component.ts | 45 ++++++++++++++++--- .../assets/locale/locale.constant-en_US.json | 3 +- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index fd3af9feb8..de3a3aa66c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -41,10 +41,10 @@
- {{ 'widgets.maps.overlays.trips' | translate }} - {{ 'widgets.maps.overlays.markers' | translate }} - {{ 'widgets.maps.overlays.polygons' | translate }} - {{ 'widgets.maps.overlays.circles' | translate }} + {{ 'widgets.maps.overlays.trips' | translate }} + {{ 'widgets.maps.overlays.markers' | translate }} + {{ 'widgets.maps.overlays.polygons' | translate }} + {{ 'widgets.maps.overlays.circles' | translate }}
- {{ option.name }} + + {{ option.name }} + + warning + +
- {{ 'widgets.maps.overlays.trips' | translate }} - {{ 'widgets.maps.overlays.markers' | translate }} - {{ 'widgets.maps.overlays.polygons' | translate }} - {{ 'widgets.maps.overlays.circles' | translate }} + {{ 'widgets.maps.overlays.trips' | translate }} + {{ 'widgets.maps.overlays.markers' | translate }} + {{ 'widgets.maps.overlays.polygons' | translate }} + {{ 'widgets.maps.overlays.circles' | translate }}
warning diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index 4d3e56bd0f..3058ad285d 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -46,13 +46,12 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { startWith, takeUntil } from 'rxjs/operators'; import { Platform } from '@angular/cdk/platform'; import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefined } from '@core/utils'; export interface ToggleHeaderOption { name: string; value: any; - error?: boolean; - errorText?: any; + error?: string; } export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; @@ -70,11 +69,9 @@ export class ToggleOption implements OnChanges { @Input() value: any; - @Input() error: boolean; + @Input() error: string; - @Input() errorText: any; - - @Output() errorChange = new EventEmitter(); + @Output() errorChange = new EventEmitter(); get viewValue(): string { return (this._element?.nativeElement.textContent || '').trim(); @@ -85,8 +82,8 @@ export class ToggleOption implements OnChanges { ) {} ngOnChanges(changes: SimpleChanges) { - if (changes['error']) { - if (!changes['error'].firstChange && changes['error'].currentValue !== changes['error'].previousValue) { + if (changes?.error) { + if (!changes.error.firstChange && changes.error.currentValue !== changes.error.previousValue) { this.errorChange.emit(this.error); } } @@ -121,7 +118,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI private subscribeToToggleOptions() { this.toggleOptions.forEach(option => { - if (isDefinedAndNotNull(option.error) || isDefinedAndNotNull(option.errorText)) { + if (isDefined(option.error)) { option.errorChange.pipe(takeUntil(this._destroyed)).subscribe(() => { this.syncToggleHeaderOptions(); }); @@ -137,8 +134,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI { name: option.viewValue, value: option.value, - error: option.error, - errorText: option.errorText + error: option.error } ); }); 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 7c5eae3f75..608bb0917a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1137,7 +1137,8 @@ "key-required": "Key is required.", "key-pattern": "Key is invalid.", "key-max-length": "Key should be less than 256 characters." - } + }, + "required-fields": "Missing required fields" }, "content-type": { "json": "Json", @@ -7980,8 +7981,7 @@ "trips": "Trips", "markers": "Markers", "polygons": "Polygons", - "circles": "Circles", - "required-fields": "Required fields are not filled in." + "circles": "Circles" }, "data-layer": { "source": "Source", From 40179f0010935c483e76092629f29025aad257b0 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 2 Apr 2025 15:59:04 +0300 Subject: [PATCH 050/112] UI: Border error for toggle button --- .../src/app/shared/components/toggle-header.component.html | 2 +- .../src/app/shared/components/toggle-header.component.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index 1340ad7782..253749d7b9 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -41,7 +41,7 @@ [name]="name" [(ngModel)]="value" (ngModelChange)="valueChange.emit(value)"> - + {{ option.name }} Date: Wed, 2 Apr 2025 16:25:26 +0300 Subject: [PATCH 051/112] UI: Refactoring --- .../components/toggle-header.component.html | 6 ++-- .../components/toggle-header.component.ts | 30 +++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index 253749d7b9..fdfabd08b5 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -41,12 +41,12 @@ [name]="name" [(ngModel)]="value" (ngModelChange)="valueChange.emit(value)"> - + {{ option.name }} warning diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index 3058ad285d..9add580a73 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -39,19 +39,18 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { BehaviorSubject, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, ReplaySubject, Subject, Subscription } from 'rxjs'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; import { startWith, takeUntil } from 'rxjs/operators'; import { Platform } from '@angular/cdk/platform'; import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; -import { isDefined } from '@core/utils'; export interface ToggleHeaderOption { name: string; value: any; - error?: string; + error$?: Observable; } export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; @@ -65,13 +64,13 @@ export type ScrollDirection = 'after' | 'before'; } ) // eslint-disable-next-line @angular-eslint/directive-class-suffix -export class ToggleOption implements OnChanges { +export class ToggleOption implements OnChanges, OnDestroy { @Input() value: any; @Input() error: string; - @Output() errorChange = new EventEmitter(); + currentError = new ReplaySubject(1); get viewValue(): string { return (this._element?.nativeElement.textContent || '').trim(); @@ -83,11 +82,15 @@ export class ToggleOption implements OnChanges { ngOnChanges(changes: SimpleChanges) { if (changes?.error) { - if (!changes.error.firstChange && changes.error.currentValue !== changes.error.previousValue) { - this.errorChange.emit(this.error); + if (changes.error.currentValue !== changes.error.previousValue) { + this.currentError.next(this.error); } } } + + ngOnDestroy() { + this.currentError.complete(); + } } @Directive() @@ -106,7 +109,6 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI ngAfterContentInit(): void { this.toggleOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { - this.subscribeToToggleOptions(); this.syncToggleHeaderOptions(); }); } @@ -116,16 +118,6 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI this._destroyed.complete(); } - private subscribeToToggleOptions() { - this.toggleOptions.forEach(option => { - if (isDefined(option.error)) { - option.errorChange.pipe(takeUntil(this._destroyed)).subscribe(() => { - this.syncToggleHeaderOptions(); - }); - } - }); - } - private syncToggleHeaderOptions() { if (this.toggleOptions?.length) { this.options.length = 0; @@ -134,7 +126,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI { name: option.viewValue, value: option.value, - error: option.error + error$: option.currentError.asObservable() } ); }); From 5097d7276aea992e7b3b2e437d9cdb79efe41301 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 3 Apr 2025 09:12:22 +0300 Subject: [PATCH 052/112] UI: Fixed powered by typo on the dashboard footer --- .../components/dashboard-page/dashboard-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 5fd247e072..77f882d191 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -365,7 +365,7 @@ close Date: Thu, 3 Apr 2025 14:25:42 +0300 Subject: [PATCH 053/112] improved performance --- .../subscription/DefaultTbLocalSubscriptionService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index d39c5fcb12..5716976b6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -347,7 +347,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer if (sub.isAllKeys()) { if (sub.isLatestValues()) { for (TsKvEntry kv : data) { - if (!keyStates.containsKey((kv.getKey())) || kv.getTs() > keyStates.get(kv.getKey())) { + Long stateTs = keyStates.get(kv.getKey()); + if (stateTs == null || kv.getTs() > stateTs) { if (updateData == null) { updateData = new ArrayList<>(); } @@ -359,8 +360,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } } else { for (TsKvEntry kv : data) { - if (keyStates.containsKey((kv.getKey()))) { - if (!sub.isLatestValues() || kv.getTs() > keyStates.get(kv.getKey())) { + Long stateTs = keyStates.get(kv.getKey()); + if (stateTs != null) { + if (!sub.isLatestValues() || kv.getTs() > stateTs) { if (updateData == null) { updateData = new ArrayList<>(); } From 27352b7b7e977911604941986ea2c93e8be60bac Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 4 Apr 2025 09:19:17 +0300 Subject: [PATCH 054/112] Improve after review --- .../common/data/cf/CalculatedField.java | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index ea7f81f216..3b2ddf0627 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; @@ -36,10 +37,10 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; -import java.util.Objects; @Schema @Data +@EqualsAndHashCode(callSuper = true) public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { @Serial @@ -63,7 +64,7 @@ public class CalculatedField extends BaseData implements HasN @Schema(description = "Version of calculated field configuration.", example = "0") private int configurationVersion; @Schema(implementation = SimpleCalculatedFieldConfiguration.class) - private transient CalculatedFieldConfiguration configuration; + private CalculatedFieldConfiguration configuration; @Getter @Setter private Long version; @@ -123,36 +124,6 @@ public class CalculatedField extends BaseData implements HasN this.debugMode = debugMode; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof CalculatedField that)) return false; - if (!super.equals(o)) return false; - return Objects.equals(tenantId, that.tenantId) && - Objects.equals(entityId, that.entityId) && - Objects.equals(name, that.name) && - Objects.equals(debugSettings, that.debugSettings) && - Objects.equals(configuration, that.configuration) && - type == that.type && debugMode == that.debugMode && - configurationVersion == that.configurationVersion && - Objects.equals(version, that.version); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + Objects.hashCode(tenantId); - result = 31 * result + Objects.hashCode(entityId); - result = 31 * result + Objects.hashCode(type); - result = 31 * result + Objects.hashCode(name); - result = 31 * result + Boolean.hashCode(debugMode); - result = 31 * result + Objects.hashCode(debugSettings); - result = 31 * result + Integer.hashCode(configurationVersion); - result = 31 * result + Objects.hashCode(configuration); - result = 31 * result + Objects.hashCode(version); - return result; - } - @Override public String toString() { return new StringBuilder() From 9d40dd1931722082604b72979218cec1a05eeb70 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 4 Apr 2025 09:19:20 +0300 Subject: [PATCH 055/112] UI: Color settings panel tab validation --- .../common/color-settings-panel.component.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts index 2cba44d334..13fe9b42b9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts @@ -102,14 +102,28 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit { type: [this.colorSettings?.type || ColorType.constant, []], color: [this.colorSettings?.color, []], - gradient: [this.colorSettings?.gradient || defaultGradient(this.minValue, this.maxValue), []], - rangeList: [this.colorSettings?.rangeList || defaultRange(), []], - colorFunction: [this.colorSettings?.colorFunction, []] + gradient: [{value: this.colorSettings?.gradient || defaultGradient(this.minValue, this.maxValue), disabled: this.colorSettings?.type !== ColorType.gradient}, []], + rangeList: [{value: this.colorSettings?.rangeList || defaultRange(), disabled: this.colorSettings?.type !== ColorType.range}, []], + colorFunction: [{value: this.colorSettings?.colorFunction, disabled: this.colorSettings?.type !== ColorType.function}, []] } ); this.colorSettingsFormGroup.get('type').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) - ).subscribe(() => { + ).subscribe((type: ColorType) => { + this.colorSettingsFormGroup.get('gradient').disable({emitEvent: false}); + this.colorSettingsFormGroup.get('rangeList').disable({emitEvent: false}); + this.colorSettingsFormGroup.get('colorFunction').disable({emitEvent: false}); + switch (type) { + case ColorType.gradient: + this.colorSettingsFormGroup.get('gradient').enable({emitEvent: false}); + break; + case ColorType.range: + this.colorSettingsFormGroup.get('rangeList').enable({emitEvent: false}); + break; + case ColorType.function: + this.colorSettingsFormGroup.get('colorFunction').enable({emitEvent: false}); + break; + } setTimeout(() => {this.popover?.updatePosition();}, 0); }); } @@ -131,7 +145,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit } applyColorSettings() { - const colorSettings = this.colorSettingsFormGroup.value; + const colorSettings: ColorSettings = {...this.colorSettings, ...this.colorSettingsFormGroup.value}; this.colorSettingsApplied.emit(colorSettings); } From 8c8485d7a70e8057c52e929ad80f7981f9e13004 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 4 Apr 2025 10:38:42 +0300 Subject: [PATCH 056/112] UI: tab validation for data layer colors panel --- .../data-layer-color-settings-panel.component.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts index 895fb3e782..dfb049af19 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -82,9 +82,9 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen { type: [this.colorSettings?.type || DataLayerColorType.constant, []], color: [this.colorSettings?.color, []], - rangeKey: [this.colorSettings?.rangeKey, [Validators.required]], - range: [this.colorSettings?.range, []], - colorFunction: [this.colorSettings?.colorFunction, []] + rangeKey: [{value: this.colorSettings?.rangeKey, disabled: this.colorSettings.type !== DataLayerColorType.range}, [Validators.required]], + range: [{value: this.colorSettings?.range, disabled: this.colorSettings.type !== DataLayerColorType.range}, []], + colorFunction: [{value: this.colorSettings?.colorFunction, disabled: this.colorSettings.type !== DataLayerColorType.function}, []] } ); this.colorSettingsFormGroup.get('type').valueChanges.pipe( @@ -101,7 +101,7 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen } applyColorSettings() { - const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.value; + const colorSettings: DataLayerColorSettings = {...this.colorSettings ,...this.colorSettingsFormGroup.value}; this.colorSettingsApplied.emit(colorSettings); } @@ -122,8 +122,15 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen const type: DataLayerColorType = this.colorSettingsFormGroup.get('type').value; if (type === DataLayerColorType.range) { this.colorSettingsFormGroup.get('rangeKey').enable({emitEvent: false}); + this.colorSettingsFormGroup.get('range').enable({emitEvent: false}); } else { this.colorSettingsFormGroup.get('rangeKey').disable({emitEvent: false}); + this.colorSettingsFormGroup.get('range').disable({emitEvent: false}); + } + if (type === DataLayerColorType.function) { + this.colorSettingsFormGroup.get('colorFunction').enable({emitEvent: false}); + } else { + this.colorSettingsFormGroup.get('colorFunction').disable({emitEvent: false}); } } } From 3c50ab1d5e263286ad9ad4a7a726fc41c97edb4c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 4 Apr 2025 11:16:36 +0300 Subject: [PATCH 057/112] UI: Refactoring --- .../common/color-settings-panel.component.ts | 44 +++++++++++-------- ...ta-layer-color-settings-panel.component.ts | 8 ++-- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts index 13fe9b42b9..63d73bf578 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts @@ -102,30 +102,36 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit { type: [this.colorSettings?.type || ColorType.constant, []], color: [this.colorSettings?.color, []], - gradient: [{value: this.colorSettings?.gradient || defaultGradient(this.minValue, this.maxValue), disabled: this.colorSettings?.type !== ColorType.gradient}, []], - rangeList: [{value: this.colorSettings?.rangeList || defaultRange(), disabled: this.colorSettings?.type !== ColorType.range}, []], - colorFunction: [{value: this.colorSettings?.colorFunction, disabled: this.colorSettings?.type !== ColorType.function}, []] + gradient: [this.colorSettings?.gradient || defaultGradient(this.minValue, this.maxValue), []], + rangeList: [this.colorSettings?.rangeList || defaultRange(), []], + colorFunction: [this.colorSettings?.colorFunction, []] } ); this.colorSettingsFormGroup.get('type').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) - ).subscribe((type: ColorType) => { - this.colorSettingsFormGroup.get('gradient').disable({emitEvent: false}); - this.colorSettingsFormGroup.get('rangeList').disable({emitEvent: false}); - this.colorSettingsFormGroup.get('colorFunction').disable({emitEvent: false}); - switch (type) { - case ColorType.gradient: - this.colorSettingsFormGroup.get('gradient').enable({emitEvent: false}); - break; - case ColorType.range: - this.colorSettingsFormGroup.get('rangeList').enable({emitEvent: false}); - break; - case ColorType.function: - this.colorSettingsFormGroup.get('colorFunction').enable({emitEvent: false}); - break; - } + ).subscribe(() => { + this.updateValidators(); setTimeout(() => {this.popover?.updatePosition();}, 0); }); + this.updateValidators(); + } + + updateValidators() { + const type: ColorType = this.colorSettingsFormGroup.get('type').value; + this.colorSettingsFormGroup.get('gradient').disable({emitEvent: false}); + this.colorSettingsFormGroup.get('rangeList').disable({emitEvent: false}); + this.colorSettingsFormGroup.get('colorFunction').disable({emitEvent: false}); + switch (type) { + case ColorType.gradient: + this.colorSettingsFormGroup.get('gradient').enable({emitEvent: false}); + break; + case ColorType.range: + this.colorSettingsFormGroup.get('rangeList').enable({emitEvent: false}); + break; + case ColorType.function: + this.colorSettingsFormGroup.get('colorFunction').enable({emitEvent: false}); + break; + } } copyColorSettings(comp: ColorSettingsComponent) { @@ -145,7 +151,7 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit } applyColorSettings() { - const colorSettings: ColorSettings = {...this.colorSettings, ...this.colorSettingsFormGroup.value}; + const colorSettings: ColorSettings = this.colorSettingsFormGroup.getRawValue(); this.colorSettingsApplied.emit(colorSettings); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts index dfb049af19..f083b00723 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.ts @@ -82,9 +82,9 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen { type: [this.colorSettings?.type || DataLayerColorType.constant, []], color: [this.colorSettings?.color, []], - rangeKey: [{value: this.colorSettings?.rangeKey, disabled: this.colorSettings.type !== DataLayerColorType.range}, [Validators.required]], - range: [{value: this.colorSettings?.range, disabled: this.colorSettings.type !== DataLayerColorType.range}, []], - colorFunction: [{value: this.colorSettings?.colorFunction, disabled: this.colorSettings.type !== DataLayerColorType.function}, []] + rangeKey: [this.colorSettings?.rangeKey, [Validators.required]], + range: [this.colorSettings?.range, []], + colorFunction: [this.colorSettings?.colorFunction, []] } ); this.colorSettingsFormGroup.get('type').valueChanges.pipe( @@ -101,7 +101,7 @@ export class DataLayerColorSettingsPanelComponent extends PageComponent implemen } applyColorSettings() { - const colorSettings: DataLayerColorSettings = {...this.colorSettings ,...this.colorSettingsFormGroup.value}; + const colorSettings: DataLayerColorSettings = this.colorSettingsFormGroup.getRawValue(); this.colorSettingsApplied.emit(colorSettings); } From d293887c58fb59594375a468c5ddea7fc730301c Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 4 Apr 2025 12:24:23 +0300 Subject: [PATCH 058/112] UI: Fixed enter press trigger click handler in integration dialig --- .../entity/debug/entity-debug-settings-button.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html index 4243044fc8..f79abfe904 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.html @@ -16,6 +16,7 @@ -->
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts index 0fcac393f0..478447d1d9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts @@ -55,8 +55,8 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments); this.eventsTable.entitiesTable.updateData(); - this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments) } cancel(): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 1688685523..8ef923d3f5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -110,7 +110,7 @@
{{ 'api-usage.tbel' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 5d770fed3b..a00d07122e 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -462,7 +462,7 @@ export class EventTableConfig extends EntityTableConfig { break; case DebugEventType.DEBUG_CALCULATED_FIELD: this.cellActionDescriptors.push({ - name: this.translate.instant('common.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}), + name: this.translate.instant('calculated-fields.test-with-this-message'), icon: 'bug_report', isEnabled: () => true, onAction: (_, entity) => this.debugEventSelected.next(entity.body) 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 ec047f91ae..be8b95e6c3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1059,6 +1059,7 @@ "delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.", "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?", "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", + "test-with-this-message": "Test with this message", "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-empty": "Arguments should not be empty.", @@ -1121,8 +1122,6 @@ "documentation": "Documentation", "time-left": "{{time}} left", "output": "Output", - "test-function": "Test function", - "test-with-this-message": "{{test}} with this message", "suffix": { "s": "s", "ms": "ms" From 19e9145183f72e3fd17c4d5c522e95506bcba2be Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 7 Apr 2025 09:55:23 +0300 Subject: [PATCH 062/112] added test --- .../timeseries/BaseTimeseriesServiceTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 6a38159804..07df14ce03 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -60,6 +60,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -768,6 +769,27 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { testFindAllByQueriesWithAggregationAndInvalidInterval(-1); } + @Test + public void testRemoveLatestAndNoValuePresentInDB() throws ExecutionException, InterruptedException, TimeoutException { + TsKvEntry tsKvEntry = toTsEntry(TS, stringKvEntry); + tsService.save(tenantId, deviceId, tsKvEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + Optional tsKvEntryOpt = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + assertThat(tsKvEntryOpt).isPresent(); + equalsIgnoreVersion(tsKvEntry, tsKvEntryOpt.get()); + assertThat(tsKvEntryOpt.get().getVersion()).isNotNull(); + + tsService.removeLatest(tenantId, deviceId, List.of(STRING_KEY)); + + await().alias("Wait until ts last is removed from the cache").atMost(MAX_TIMEOUT, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + Optional tsKvEntryAfterRemoval = tsService.findLatest(tenantId, deviceId, STRING_KEY).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(tsKvEntryAfterRemoval).isNotPresent(); + }); + } + private void testFindAllByQueriesWithAggregationAndInvalidInterval(long interval) { BaseReadTsKvQuery query = new BaseReadTsKvQuery(STRING_KEY, TS, TS, interval, 1000, Aggregation.SUM, "DESC"); Assert.assertThrows(IncorrectParameterException.class, () -> findAndVerifyQueryId(deviceId, query)); From 76ed955f427efd6badeabe40d77e8ea0fce10e20 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 7 Apr 2025 11:47:57 +0300 Subject: [PATCH 063/112] UI: Fixed debug settings panel; Improved model data entity debug settings --- ui-ngx/src/app/core/auth/auth.models.ts | 1 + .../calculated-fields-table-config.ts | 14 +++-------- .../calculated-field-dialog.component.html | 3 +-- .../calculated-field-dialog.component.ts | 1 - .../entity-debug-settings-button.component.ts | 7 +++--- ...entity-debug-settings-panel.component.html | 4 +-- .../entity-debug-settings-panel.component.ts | 25 ++++++++++++++----- .../debug/entity-debug-settings.model.ts | 6 ++--- .../rule-node-details.component.html | 5 ++-- .../rulechain/rule-node-details.component.ts | 13 +++------- .../assets/locale/locale.constant-en_US.json | 2 -- 11 files changed, 38 insertions(+), 43 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 1944c693ac..e5cc1424ab 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -31,6 +31,7 @@ export interface SysParamsState { maxDataPointsPerRollingArg: number; maxArgumentsPerCF: number; ruleChainDebugPerTenantLimitsConfiguration?: string; + calculatedFieldDebugPerTenantLimitsConfiguration?: string; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index c5b256ea89..07f8f0293c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -27,10 +27,9 @@ import { PageLink } from '@shared/models/page/page-link'; import { Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; -import { MINUTE } from '@shared/models/time/time.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -47,7 +46,8 @@ import { getCalculatedFieldArgumentsHighlights, } from '@shared/models/calculated-field.models'; import { - CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, + CalculatedFieldDebugDialogComponent, + CalculatedFieldDebugDialogData, CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedFieldScriptTestDialogComponent, @@ -60,9 +60,6 @@ import { DatePipe } from '@angular/common'; export class CalculatedFieldsTableConfig extends EntityTableConfig { - readonly calculatedFieldsDebugPerTenantLimitsConfiguration = - getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration']; - readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('calculated-fields.see-debug-events'), @@ -189,9 +186,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.onDebugConfigChanged(id.id, settings) @@ -215,7 +210,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 169d6dff50..52051aa6f7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -48,7 +48,6 @@ export interface CalculatedFieldDialogData { value?: CalculatedField; buttonTitle: string; entityId: EntityId; - debugLimitsConfiguration: string; tenantId: string; entityName?: string; additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>; diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts index 90beed2f7e..7cb0ac7f58 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts @@ -36,6 +36,7 @@ import { Store } from '@ngrx/store'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { EntityType } from '@shared/models/entity-type.models'; @Component({ selector: 'tb-entity-debug-settings-button', @@ -58,9 +59,8 @@ import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entit }) export class EntityDebugSettingsButtonComponent implements ControlValueAccessor { - @Input() debugLimitsConfiguration: string; - @Input() entityLabel: string; @Input() additionalActionConfig: AdditionalDebugActionConfig; + @Input({required: true}) entityType: EntityType; debugSettingsFormGroup = this.fb.group({ failuresEnabled: [false], @@ -123,8 +123,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor debugSettings: this.debugSettingsFormGroup.value, debugConfig: { maxDebugModeDuration: this.maxDebugModeDuration, - debugLimitsConfiguration: this.debugLimitsConfiguration, - entityLabel: this.entityLabel, + entityType: this.entityType, additionalActionConfig: this.additionalActionConfig, }, onSettingsAppliedFn: settings => { diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html index 5863a295d6..337a452757 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html @@ -17,10 +17,10 @@ -->
debug-settings.label
- @if (debugLimitsConfiguration) { + @if (maxMessagesCount) {
- {{ 'debug-settings.hint.main-limited' | translate: { entity: entityLabel ?? ('debug-settings.entity' | translate), msg: maxMessagesCount, time: (maxTimeFrameDuration | milliSecondsToTimeString: true : true) } }} + {{ 'debug-settings.hint.main-limited' | translate: { entity: (entityLabel | translate | lowercase), msg: maxMessagesCount, time: (maxTimeFrameDuration | milliSecondsToTimeString: true : true) } }}
} diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts index 1f95a93084..e19407a687 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts @@ -28,13 +28,15 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { FormBuilder } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { SECOND } from '@shared/models/time/time.models'; +import { MINUTE, SECOND } from '@shared/models/time/time.models'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { of, shareReplay, timer } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs/operators'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; @Component({ selector: 'tb-entity-debug-settings-panel', @@ -51,10 +53,9 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements @Input({ transform: booleanAttribute }) failuresEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false; - @Input() entityLabel: string; + @Input() entityType: EntityType; @Input() allEnabledUntil = 0; - @Input() maxDebugModeDuration: number; - @Input() debugLimitsConfiguration: string; + @Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @Input() additionalActionConfig: AdditionalDebugActionConfig; onFailuresControl = this.fb.control(false); @@ -63,6 +64,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements maxMessagesCount: string; maxTimeFrameDuration: number; initialAllEnabled: boolean; + entityLabel: string; isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( startWith(this.debugAllControl.value), @@ -101,11 +103,13 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements } ngOnInit(): void { - this.maxMessagesCount = this.debugLimitsConfiguration?.split(':')[0]; - this.maxTimeFrameDuration = parseInt(this.debugLimitsConfiguration?.split(':')[1]) * SECOND; + const debugLimitsConfiguration = this.getByEntityTypeDebugLimit(this.entityType); + this.maxMessagesCount = debugLimitsConfiguration?.split(':')[0]; + this.maxTimeFrameDuration = parseInt(debugLimitsConfiguration?.split(':')[1]) * SECOND; this.onFailuresControl.patchValue(this.failuresEnabled); this.debugAllControl.patchValue(this.allEnabled); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); + this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; } onCancel(): void { @@ -135,4 +139,13 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements this.allEnabledUntil = 0; this.cd.markForCheck(); } + + private getByEntityTypeDebugLimit(entityType: EntityType): string { + switch (entityType) { + case EntityType.RULE_NODE: + return getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration; + case EntityType.CALCULATED_FIELD: + return getCurrentAuthState(this.store).calculatedFieldDebugPerTenantLimitsConfiguration; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts index 6560580502..0af9da52f2 100644 --- a/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts +++ b/ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts @@ -15,6 +15,7 @@ /// import { EntityDebugSettings } from '@shared/models/entity.models'; +import { EntityType } from '@shared/models/entity-type.models'; export interface AdditionalDebugActionConfig void> { action: Action; @@ -24,10 +25,9 @@ export interface AdditionalDebugActionConfig vo export interface EntityDebugSettingPanelConfig { debugSettings: EntityDebugSettings; debugConfig: { - maxDebugModeDuration: number; - debugLimitsConfiguration: string; - entityLabel?: string; + maxDebugModeDuration?: number; additionalActionConfig?: AdditionalDebugActionConfig; + entityType: EntityType; } onSettingsAppliedFn: (settings: EntityDebugSettings) => void; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index 1923d3ef91..b55dcdaa73 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -22,7 +22,7 @@
-
+
rulenode.name @@ -38,8 +38,7 @@
-
-
widgets.maps.data-layer.fill-color
- +
+
+
{{ 'widgets.maps.data-layer.shape.fill' | translate }}
+ + {{ 'widgets.maps.data-layer.shape.fill-type-color' | translate }} + {{ 'widgets.maps.data-layer.shape.fill-type-stripe' | translate }} + {{ 'widgets.maps.data-layer.shape.fill-type-image' | translate }} + +
+
+
widgets.maps.data-layer.shape.color
+ +
+
+
widgets.maps.data-layer.shape.stripe-pattern
+ + +
+
+
widgets.maps.data-layer.shape.image
+ +
-
widgets.maps.data-layer.stroke
+
widgets.maps.data-layer.shape.stroke
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts index e7e11284eb..48854adc44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts @@ -30,7 +30,7 @@ import { pathDecoratorSymbols, pathDecoratorSymbolTranslationMap, PolygonsDataLayerSettings, - ShapeDataLayerSettings, + ShapeDataLayerSettings, ShapeFillType, TripsDataLayerSettings, updateDataKeyToNewDsType } from '@shared/models/widget/maps/map.models'; @@ -78,6 +78,8 @@ export class MapDataLayerDialogComponent extends DialogComponent = []; datasourceTypesTranslations = datasourceTypeTranslationMap; @@ -266,7 +268,10 @@ export class MapDataLayerDialogComponent extends DialogComponent + this.updateValidators() + ); break; } this.dataLayerFormGroup.get('dsType').valueChanges.pipe( @@ -349,8 +359,9 @@ export class MapDataLayerDialogComponent extends DialogComponent +
+
widgets.maps.data-layer.shape.fill-image
+
+ + + {{ 'widgets.maps.data-layer.shape.fill-image-type-image' | translate }} + + + {{ 'widgets.maps.data-layer.shape.fill-image-type-function' | translate }} + + +
+
+
+ +
+
widgets.maps.data-layer.shape.opacity
+ + + +
+
+
widgets.maps.data-layer.shape.angle
+ + +
deg
+
+
+
+
widgets.maps.data-layer.shape.scale
+ + + +
+
+
+
+
+ + + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss new file mode 100644 index 0000000000..5c127549db --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2025 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'; + +.tb-shape-fill-image-settings-panel { + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-shape-fill-image-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + height: auto; + } + .tb-shape-fill-image-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: auto; + } + .tb-shape-fill-image-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts new file mode 100644 index 0000000000..0d14c5f81c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2025 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, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; + +@Component({ + selector: 'tb-shape-fill-image-settings-panel', + templateUrl: './shape-fill-image-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-image-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ShapeFillImageSettingsPanelComponent implements OnInit { + + @Input() + shapeFillImageSettings: ShapeFillImageSettings; + + @Output() + shapeFillImageSettingsApplied = new EventEmitter(); + + ShapeFillImageType = ShapeFillImageType; + + shapeFillImageSettingsFormGroup: UntypedFormGroup; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + constructor(private fb: UntypedFormBuilder, + private popover: TbPopoverComponent, + private widgetService: WidgetService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.shapeFillImageSettingsFormGroup = this.fb.group( + { + type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []], + image: [this.shapeFillImageSettings?.image, [Validators.required]], + opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]], + angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]], + scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]], + imageFunction: [this.shapeFillImageSettings?.imageFunction, [Validators.required]], + images: [this.shapeFillImageSettings?.images, []] + } + ); + this.shapeFillImageSettingsFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyShapeFillImageSettings() { + const shapeFillImageSettings: ShapeFillImageSettings = this.shapeFillImageSettingsFormGroup.value; + this.shapeFillImageSettingsApplied.emit(shapeFillImageSettings); + this.popover?.hide(); + } + + private updateValidators() { + const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value; + if (type === ShapeFillImageType.image) { + this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('imageFunction').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false}); + } else { + this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('imageFunction').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('images').enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html new file mode 100644 index 0000000000..da4feaccf2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html @@ -0,0 +1,29 @@ + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts new file mode 100644 index 0000000000..5f707abced --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; +import { + ShapeFillImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; + +@Component({ + selector: 'tb-shape-fill-image-settings', + templateUrl: './shape-fill-image-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillImageSettingsComponent), + multi: true + } + ] +}) +export class ShapeFillImageSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + ShapeFillImageType = ShapeFillImageType; + + modelValue: ShapeFillImageSettings; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ShapeFillImageSettings): void { + if (value) { + this.modelValue = value; + } + } + + openImageSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ShapeFillImageSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + shapeFillImageSettings: this.modelValue, + }, + isModal: true + }).tbComponentRef.instance.shapeFillImageSettingsApplied.subscribe((shapeFillImageSettings) => { + this.modelValue = shapeFillImageSettings; + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html new file mode 100644 index 0000000000..c4b931dafd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html @@ -0,0 +1,84 @@ + +
+
widgets.maps.data-layer.shape.stripe-pattern
+
+
+
+
widgets.maps.data-layer.shape.first-stripe
+
+ + + px + + +
+
+
+
widgets.maps.data-layer.shape.second-stripe
+
+ + + px + + +
+
+
+
widgets.maps.data-layer.shape.angle
+ + +
deg
+
+
+
+
+ widgets.background.preview +
+
+
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss new file mode 100644 index 0000000000..5f3af9894b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2025 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'; + +.tb-shape-fill-stripe-settings-panel { + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-shape-fill-stripe-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + height: auto; + } + .tb-shape-fill-stripe-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: auto; + } + .tb-shape-fill-stripe-settings-preview { + flex: 1; + background: #fff; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + padding: 12px 16px 24px 16px; + align-items: center; + gap: 12px; + } + .tb-shape-fill-stripe-settings-preview-title { + align-self: stretch; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + color: rgba(0, 0, 0, 0.38); + } + .tb-shape-fill-stripe-settings-preview-box { + position: relative; + width: 136px; + height: 118px; + border-radius: 2.666px; + } + + .tb-shape-fill-stripe-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts new file mode 100644 index 0000000000..c9df56338b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2025 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, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { + generateStripePreviewUrl +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component'; +import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-shape-fill-stripe-settings-panel', + templateUrl: './shape-fill-stripe-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-stripe-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ShapeFillStripeSettingsPanelComponent implements OnInit { + + @Input() + shapeFillStripeSettings: ShapeFillStripeSettings; + + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + + @Input() + dataLayerType: MapDataLayerType; + + @Output() + shapeFillStripeSettingsApplied = new EventEmitter(); + + stripePreviewStyle: ComponentStyle; + + shapeFillStripeSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private sanitizer: DomSanitizer, + private popover: TbPopoverComponent, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.shapeFillStripeSettingsFormGroup = this.fb.group( + { + weight: [this.shapeFillStripeSettings?.weight, [Validators.min(0)]], + color: [this.shapeFillStripeSettings?.color, []], + spaceWeight: [this.shapeFillStripeSettings?.spaceWeight, [Validators.min(0)]], + spaceColor: [this.shapeFillStripeSettings?.spaceColor, []], + angle: [this.shapeFillStripeSettings?.angle, [Validators.min(0), Validators.max(180)]] + } + ); + this.shapeFillStripeSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updatePreview(); + }); + this.updatePreview(); + } + + cancel() { + this.popover?.hide(); + } + + applyShapeFillStripeSettings() { + const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; + this.shapeFillStripeSettingsApplied.emit(shapeFillStripeSettings); + this.popover?.hide(); + } + + private updatePreview() { + const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; + const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings); + this.stripePreviewStyle = { + background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`) + }; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html new file mode 100644 index 0000000000..2188e97ae6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html @@ -0,0 +1,27 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts new file mode 100644 index 0000000000..63b2b4b212 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { isDefinedAndNotNull, stringToBase64 } from '@core/utils'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; +import { + ShapeFillStripeSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; + +@Component({ + selector: 'tb-shape-fill-stripe-settings', + templateUrl: './shape-fill-stripe-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillStripeSettingsComponent), + multi: true + } + ] +}) +export class ShapeFillStripeSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + + @Input() + dataLayerType: MapDataLayerType; + + modelValue: ShapeFillStripeSettings; + + stripePreviewUrl: SafeUrl; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ShapeFillStripeSettings): void { + if (value) { + this.modelValue = value; + } + this.updatePreview(); + } + + openStripeSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ShapeFillStripeSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + shapeFillStripeSettings: this.modelValue, + context: this.context, + dsType: this.dsType, + dsEntityAliasId: this.dsEntityAliasId, + dsDeviceId: this.dsDeviceId, + dataLayerType: this.dataLayerType + }, + isModal: true + }).tbComponentRef.instance.shapeFillStripeSettingsApplied.subscribe((shapeFillStripeSettings) => { + this.modelValue = shapeFillStripeSettings; + this.updatePreview(); + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } + + private updatePreview() { + this.stripePreviewUrl = this.sanitizer.bypassSecurityTrustUrl(generateStripePreviewUrl(this.modelValue)); + } +} + +export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings): string => { + const weight = isDefinedAndNotNull(settings?.weight) ? settings.weight : 3; + const spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9; + const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45; + const height = weight + spaceWeight; + const color = settings?.color?.color || '#8f8f8f'; + const spaceColor = settings?.spaceColor?.color || 'rgba(143,143,143,0)'; + const svgStr = ` + + + + + + + + `; + const encodedSvg = stringToBase64(svgStr); + return `data:image/svg+xml;base64,${encodedSvg}`; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index c00c816073..233b202684 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -255,6 +255,18 @@ import { MapDataSourcesComponent } from '@home/components/widget/lib/settings/co import { MapDataSourceRowComponent } from '@home/components/widget/lib/settings/common/map/map-data-source-row.component'; +import { + ShapeFillImageSettingsComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings.component'; +import { + ShapeFillImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; +import { + ShapeFillStripeSettingsComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component'; +import { + ShapeFillStripeSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; @NgModule({ declarations: [ @@ -342,6 +354,10 @@ import { MarkerImageSettingsComponent, MarkerImageSettingsPanelComponent, MarkerClusteringSettingsComponent, + ShapeFillStripeSettingsComponent, + ShapeFillStripeSettingsPanelComponent, + ShapeFillImageSettingsComponent, + ShapeFillImageSettingsPanelComponent, MapDataLayerDialogComponent, MapDataLayerRowComponent, MapDataLayersComponent, diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index bac8c8fb75..e09d5e5756 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -487,8 +487,40 @@ export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, fillColor: { type: DataLayerColorType.constant, color: 'rgba(51,136,255,0.2)', }, + fillImage: { + type: ShapeFillImageType.image, + image: '/assets/widget-preview-empty.svg', + opacity: 1, + angle: 0, + scale: 1 + }, + fillStripe: { + weight: 3, + color: { + type: DataLayerColorType.constant, + color: '#8f8f8f' + }, + spaceWeight: 9, + spaceColor: { + type: DataLayerColorType.constant, + color: 'rgba(143,143,143,0)', + }, + angle: 45 + }, strokeColor: { type: DataLayerColorType.constant, color: '#3388ff', @@ -539,10 +592,31 @@ export const defaultCirclesDataLayerSettings = (mapType: MapType, functionsOnly } as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings(mapType) as CirclesDataLayerSettings); export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, fillColor: { type: DataLayerColorType.constant, color: 'rgba(51,136,255,0.2)', }, + fillImage: { + type: ShapeFillImageType.image, + image: '/assets/widget-preview-empty.svg', + opacity: 1, + angle: 0, + scale: 1 + }, + fillStripe: { + weight: 3, + color: { + type: DataLayerColorType.constant, + color: '#8f8f8f' + }, + spaceWeight: 9, + spaceColor: { + type: DataLayerColorType.constant, + color: 'rgba(143,143,143,0)', + }, + angle: 45 + }, strokeColor: { type: DataLayerColorType.constant, color: '#3388ff', @@ -596,10 +670,8 @@ export const mapDataSourceValid = (dataSource: MapDataSourceSettings): boolean = if (dataSource.dsType === DatasourceType.device && !dataSource.dsDeviceId) { return false; } - if (dataSource.dsType === DatasourceType.entity && !dataSource.dsEntityAliasId) { - return false; - } - return true; + return !(dataSource.dsType === DatasourceType.entity && !dataSource.dsEntityAliasId); + }; export const mapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { @@ -1112,6 +1184,13 @@ export interface MarkerIconInfo { size: [number, number]; } +export interface ShapeFillImageInfo { + url: string; + opacity?: number; + angle?: number; + scale?: number; +} + export type MapStringFunction = (data: FormattedData, dsData: FormattedData[]) => string; @@ -1126,6 +1205,9 @@ export type ClusterMarkerColorFunction = (data: FormattedData[] export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData, dsData: FormattedData[], aspect: number) => { x: number, y: number }; +export type ShapeFillImageFunction = (data: FormattedData, images: string[], + dsData: FormattedData[]) => ShapeFillImageInfo; + export type TbPolygonRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; export type TbPolygonRawCoordinates = TbPolygonRawCoordinate[]; export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; @@ -1271,11 +1353,13 @@ const imageLoader = (imageUrl: string): Observable => new Obse image.src = imageUrl; }); -const loadImageAspect = (imageUrl: string): Observable => - imageLoader(imageUrl).pipe(map(image => image.width / image.height)); +const loadImageSize = (imageUrl: string): Observable<[number, number]> => + imageLoader(imageUrl).pipe(map(image => [image.width, image.height])); export interface ImageWithAspect { url: string; + width: number; + height: number; aspect: number; } @@ -1289,9 +1373,14 @@ export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Obs return imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).pipe( switchMap((res) => { const url = res as string; - return loadImageAspect(url).pipe( - map((aspect) => { - imageWithAspect = {url, aspect}; + return loadImageSize(url).pipe( + map((size) => { + imageWithAspect = { + url, + width: size[0], + height: size[1], + aspect: size[0]/size[1] + }; imageAspectMap[hash] = imageWithAspect; return imageWithAspect; }) diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md new file mode 100644 index 0000000000..b13d612c8e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md @@ -0,0 +1,52 @@ +#### Shape fill image function + +
+
+ +*function (data, images, dsData): {url: string}* + +A JavaScript function used to compute shape fill image. + +**Parameters:** + +
    + {% include widget/lib/map/shape_fill_image_fn_args %} +
+ +**Returns:** + +Should return shape fill image data having the following structure: + +```typescript +{ + url: string; + opacity?: number; + angle?: number; + scale?: number; +} +``` + +- *url* - fill image url; +- *opacity* - optional image opacity, number value from 0 to 1; +- *angle* - optional image rotation angle, number value from 0 to 360; +- *scale* - optional image scale, number value (1 - original size, smaller value - scale down, bigger value - scale up); + +In case no data is returned, default fill image will be used. + +
+ +##### Examples + +
    +
  • +TODO: +
  • +
+ +```javascript +TODO: +{:copy-code} +``` + +
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn_args.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn_args.md new file mode 100644 index 0000000000..07b60f72ae --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn_args.md @@ -0,0 +1,9 @@ +
  • data: FormattedData object associated with data layer (markers/polygons/circles) or data point of the route (trips data layer).
    + Represents basic entity properties (ex. entityId, entityName)
    and provides access to other entity attributes/timeseries declared in datasource of the data layer configuration. +
  • +
  • images: string[] - array of image urls configured in the Shape fill images section. +
  • +
  • dsData: FormattedData[] - All available data associated with data layers including additional datasources as array of FormattedData objects
    + resolved from configured datasources. Each object represents basic entity properties (ex. entityId, entityName)
    + and provides access to other entity attributes/timeseries declared in datasources of data layers configuration including additional datasources of the map configuration. +
  • 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 ec047f91ae..e4f4721f3c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8011,8 +8011,6 @@ "groups": "Groups", "groups-hint": "List of group names assigned to the overlay, used to toggle its visibility on the map.", "color": "Color", - "fill-color": "Fill color", - "stroke": "Stroke", "color-settings": "Color settings", "color-type-constant": "Constant", "color-type-range": "Range", @@ -8127,6 +8125,27 @@ "points": "Points", "point-tooltip": "Point tooltip" }, + "shape": { + "fill": "Fill", + "fill-type-color": "Color", + "fill-type-stripe": "Stripe", + "fill-type-image": "Image", + "color": "Color", + "stripe": "Stripe", + "image": "Image", + "stroke": "Stroke", + "fill-image": "Fill image", + "fill-image-type-image": "Image", + "fill-image-type-function": "Function", + "opacity": "Opacity", + "angle": "Rotation angle", + "scale": "Scale", + "fill-image-function": "Shape fill image function", + "fill-images": "Shape fill images", + "stripe-pattern": "Stripe pattern", + "first-stripe": "First stripe", + "second-stripe": "Second stripe" + }, "polygon": { "polygon-key": "Polygon key", "polygon-key-required": "Polygon key required", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 1ea249b478..3b982a527e 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -25,6 +25,18 @@ declare module 'leaflet' { interface MarkerOptions { tbMarkerData?: FormattedData; } + interface Map { + _patterns: {[id: number]: L.TB.Pattern}; + _defRoot: SVGDefsElement; + addPattern(pattern: L.TB.Pattern): Map; + removePattern(pattern: L.TB.Pattern): Map; + hasPattern(pattern: L.TB.Pattern): boolean; + _initDefRoot(): void; + } + + interface PathOptions { + fillPattern?: L.TB.Pattern | undefined; + } interface TileLayer { _url: string; @@ -152,6 +164,109 @@ declare module 'leaflet' { container: HTMLElement; } + interface PathOptions { + fillPattern?: Pattern | undefined; + } + + interface PatternOptions { + x?: number | undefined; + y?: number | undefined; + width?: number | undefined; + height?: number | undefined; + patternUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined; + patternContentUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined; + patternTransform?: string | null | undefined; + preserveAspectRatioAlign?: "none" | "xMinYMin" | "xMidYMin" | "xMaxYMin" | "xMinYMid" | "xMidYMid" | "xMaxYMid" | "xMinYMax" | "xMidYMax" | "xMaxYMax" | undefined; + preserveAspectRatioMeetOrSlice?: "meet" | "slice" | undefined; + viewBox?: [number, number, number, number] | undefined; + angle?: number | null | undefined; + className?: string | undefined; + } + + interface PatternElementOptions { + className?: string | undefined; + } + + interface PatternShapeOptions extends PatternElementOptions { + stroke?: boolean | undefined; + color?: string | undefined; + weight?: number | undefined; + opacity?: number | undefined; + lineCap?: "butt" | "round" | "square" | "inherit" | undefined; + lineJoin?: "butt" | "round" | "square" | "inherit" | undefined; + dashArray?: number[] | null | undefined; + dashOffset?: number | null | undefined; + fill?: boolean | undefined; + fillColor?: string | undefined; + fillOpacity?: number | undefined; + fillRule?: "nonzero" | "evenodd" | "inherit" | undefined; + fillPattern?: Pattern | null | undefined; + pointerEvents?: string | undefined; + interactive?: boolean | undefined; + } + + interface PatternRectOptions extends PatternShapeOptions { + x?: number | undefined; + y?: number | undefined; + width?: number | undefined; + height?: number | undefined; + rx?: number | null | undefined; + ry?: number | null | undefined; + } + + interface PatternPathOptions extends PatternShapeOptions { + d?: string | null | undefined; + } + + interface PatternImageOptions extends PatternElementOptions { + imageUrl: string; + width: number; + height: number; + opacity?: number; + angle?: number; + scale?: number; + } + + class Pattern extends L.Evented { + constructor(options?: PatternOptions); + onAdd(map: L.Map): void; + onRemove(map: L.Map): void; + redraw(): this; + setStyle(style: PatternOptions): this; + addTo(map: L.Map): this; + remove(): this; + removeFrom(map: L.Map): this; + addElement(element: PatternElement): PatternElement | undefined; + } + + abstract class PatternElement extends L.Class { + protected constructor(options?: PatternElementOptions); + onAdd(pattern: Pattern): void; + addTo(pattern: Pattern): this; + redraw(): this; + setStyle(style: PatternElementOptions): this; + } + + abstract class PatternShape extends PatternElement { + protected constructor(options?: PatternShapeOptions); + setStyle(style: PatternShapeOptions): this; + } + + class PatternRect extends PatternShape { + constructor(options?: PatternRectOptions); + setStyle(style: PatternRectOptions): this; + } + + class PatternPath extends PatternShape { + constructor(options?: PatternPathOptions); + setStyle(style: PatternPathOptions): this; + } + + class PatternImage extends PatternElement { + constructor(options: PatternImageOptions); + setStyle(style: PatternImageOptions): this; + } + function sidebar(options: SidebarControlOptions): SidebarControl; function sidebarPane(options: O): SidebarPaneControl; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 602839b2da..6e86f3c2b0 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1581,10 +1581,10 @@ dependencies: tslib "^2.3.0" -"@geoman-io/leaflet-geoman-free@2.17.0": - version "2.17.0" - resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.17.0.tgz#9c8fce5c7a85e5d7ece3a7e5d0b82dbf2329bd81" - integrity sha512-vAY9tKB2I/Ui8d3QUBuebWnunI2sGjsfAUTXMMcf5UpISvPz67io4hpbKXid9GNsW6P4LGv1+ZzrmkpM78GzHA== +"@geoman-io/leaflet-geoman-free@2.18.3": + version "2.18.3" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.18.3.tgz#a41489920b175931fba2a1e8e81347f9e3be5481" + integrity sha512-XzxSKRk2UJUVeGiOt1jU2hyo412Qee1Q0Xsfw4A2r8EoUIo48XKSWfusYe7E53fSPr0aYgZxPevnFdcUXimpdA== dependencies: "@turf/boolean-contains" "^6.5.0" "@turf/kinks" "^6.5.0" From 2a3562ef2905ca942404192150a54eab6caf15b4 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Mon, 7 Apr 2025 20:33:48 +0300 Subject: [PATCH 074/112] UI: Updated default value maxDebugModeDurationMinutes in tenant profile --- ui-ngx/src/app/shared/models/tenant.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 23896141db..8866b54574 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -137,7 +137,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxSms: 0, smsEnabled: true, maxCreatedAlarms: 0, - maxDebugModeDurationMinutes: 0, + maxDebugModeDurationMinutes: 15, tenantServerRestLimitsConfiguration: '', customerServerRestLimitsConfiguration: '', maxWsSessionsPerTenant: 0, From 703cec8066c9585bef5a13e823df13b160296757 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Mon, 7 Apr 2025 20:59:46 +0300 Subject: [PATCH 075/112] UI: Fixed import file dialog when long file name --- .../import-dialog.component.html | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.html b/ui-ngx/src/app/shared/import-export/import-dialog.component.html index ff775fc555..5d354abae8 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.html @@ -28,32 +28,30 @@
    -
    -
    - - {{ importFileLabel | translate }} - {{ importContentLabel | translate }} - - - - - -
    -
    +
    + + {{ importFileLabel | translate }} + {{ importContentLabel | translate }} + + + + + +
    +
    +
    @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html index 6d8ac56fc1..3b91c56acf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html @@ -32,6 +32,12 @@ {{'rule-node-config.custom-expression-field-input' | translate }} * +
    +
    rule-node-config.custom-expression-field-input-required @@ -73,7 +79,7 @@ help + matTooltip="{{ 'rule-node-config.math-templatization-tooltip' | translate }}">info rule-node-config.key-field-input-required diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html index b98854d0e5..aa61beb32a 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html @@ -67,7 +67,7 @@ - help + info rule-node-config.argument-key-field-input-required diff --git a/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md b/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md new file mode 100644 index 0000000000..e9cb7bb13e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md @@ -0,0 +1,31 @@ +## Built-in mathematical functions +The following expression field provides support for built-in mathematical functions that you can utilize for various calculations. + +| Function | Description | Example | +|---------------|---------------------------------------------------------------------|-------------------| +| `abs(x)` | Absolute value of `x`. | `abs(-7) = 7` | +| `acos(x)` | Arc cosine of `x`, result in radians. Requires `-1 ≤ x ≤ 1`. | `acos(1) = 0` | +| `asin(x)` | Arc sine of `x`, result in radians. Requires `-1 ≤ x ≤ 1`. | `asin(0) = 0` | +| `atan(x)` | Arc tangent of `x`, result in radians. | `atan(0) = 0` | +| `cbrt(x)` | Cube root of `x`. | `cbrt(8) = 2` | +| `ceil(x)` | Rounds `x` up to the nearest integer. | `ceil(3.1) = 4` | +| `cos(x)` | Cosine of `x`, where `x` is in radians. | `cos(0) = 1` | +| `cosh(x)` | Hyperbolic cosine of `x`. | `cosh(0) = 1` | +| `cot(x)` | Cotangent of `x` (1 / tan(`x`)), where `x` is in radians. | `cot(0.7854) ≈ 1` | +| `exp(x)` | Computes `e^x`. | `exp(0) = 1` | +| `expm1(x)` | Computes `e^x - 1` accurately for small `x`. | `expm1(0) = 0` | +| `floor(x)` | Rounds `x` down to the nearest integer. | `floor(3.9) = 3` | +| `ln(x)` | Natural logarithm (base *e*) of `x`. Requires `x > 0`. | `ln(1) = 0` | +| `log(x)` | Natural logarithm (base *e*) of `x`. Requires `x > 0`. | `log(1) = 0` | +| `lg(x)` | Natural logarithm (base 10) of `x`. Requires `x > 0`. | `lg(10) = 1` | +| `log10(x)` | Logarithm base 10 of `x`. Requires `x > 0`. | `log10(100) = 2` | +| `log2(x)` | Logarithm base 2 of `x`. Requires `x > 0`. | `log2(8) = 3` | +| `logab(a, b)` | Logarithm of `b` with base `a`. Requires `a > 0`, `b > 0`, `a ≠ 1`. | `logab(2, 8) = 3` | +| `log1p(x)` | Computes `ln(1 + x)` accurately for small `x`. Requires `x > -1`. | `log1p(0) = 0` | +| `pow(x, y)` | Raises `x` to the power of `y` (`x^y`). | `pow(2, 3) = 8` | +| `signum(x)` | Returns the sign of `x`: -1 if `x < 0`, 0 if `x = 0`, 1 if `x > 0`. | `signum(-5) = -1` | +| `sin(x)` | Sine of `x`, where `x` is in radians. | `sin(0) = 0` | +| `sinh(x)` | Hyperbolic sine of `x`. | `sinh(0) = 0` | +| `sqrt(x)` | Square root of `x`. Requires `x ≥ 0`. | `sqrt(4) = 2` | +| `tan(x)` | Tangent of `x`, where `x` is in radians. | `tan(0) = 0` | +| `tanh(x)` | Hyperbolic tangent of `x`. | `tanh(0) = 0` | From a38831cc017641155d7e349afe16a50dcc7ddfb6 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 8 Apr 2025 17:20:27 +0300 Subject: [PATCH 082/112] Fix repo open catch --- .../org/thingsboard/server/service/sync/vc/GitRepository.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java index 3500e48a79..df0f79171f 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java @@ -42,7 +42,6 @@ import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -158,7 +157,7 @@ public class GitRepository { return repository; } catch (RepositoryNotFoundException e) { log.warn("{} not a git repository, reinitializing", directory); - } catch (TransportException e) { + } catch (org.eclipse.jgit.errors.TransportException | org.eclipse.jgit.api.errors.TransportException e) { if (StringUtils.containsIgnoreCase(e.getMessage(), "missing commit")) { log.warn("Couldn't fetch {} due to {}, reinitializing", directory, e.getMessage()); } else { From 10666295edc935f30a9284f54dbea50a83d96d0c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 8 Apr 2025 17:31:04 +0300 Subject: [PATCH 083/112] UI: Fixed incorrect js library calculate delete enabled status --- .../admin/resource/js-library-table-config.resolver.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts index 9c5dba6dbb..8525983a36 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -97,7 +97,7 @@ export class JsLibraryTableConfigResolver { entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), ); - this.config.cellActionDescriptors = this.configureCellActions(getCurrentAuthUser(this.store)); + this.config.cellActionDescriptors = this.configureCellActions(); this.config.groupActionDescriptors = [{ name: this.translate.instant('action.delete'), @@ -137,6 +137,7 @@ export class JsLibraryTableConfigResolver { resourceSubType: '' }; const authUser = getCurrentAuthUser(this.store); + this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority); return this.config; @@ -309,7 +310,7 @@ export class JsLibraryTableConfigResolver { } } - private configureCellActions(authUser: AuthUser): Array> { + private configureCellActions(): Array> { const actions: Array> = []; actions.push( { @@ -321,7 +322,7 @@ export class JsLibraryTableConfigResolver { { name: this.translate.instant('javascript.delete'), icon: 'delete', - isEnabled: (resource) => this.isResourceEditable(resource, authUser.authority), + isEnabled: (resource) => this.config.deleteEnabled(resource), onAction: ($event, entity) => this.deleteResource($event, entity) }, ); From a38d39fda9a452216356d0857d928ca2905c9dbb Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 8 Apr 2025 18:23:19 +0300 Subject: [PATCH 084/112] TenantProfileEdgeTest - revert tenant profile update --- .../server/edge/TenantProfileEdgeTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java index e58126737d..51cfa44ff9 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TenantProfileEdgeTest.java @@ -44,9 +44,8 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { @Test public void testTenantProfiles() throws Exception { loginSysAdmin(); - - // save current values into tmp to revert after test - TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile); // updated edge tenant profile edgeTenantProfile.setName("Tenant Profile Edge Test"); @@ -64,14 +63,15 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { Assert.assertEquals("Updated tenant profile Edge Test", tenantProfileMsg.getDescription()); Assert.assertEquals("Tenant Profile Edge Test", tenantProfileMsg.getName()); + doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class); loginTenantAdmin(); } @Test public void testIsolatedTenantProfile() throws Exception { loginSysAdmin(); - - TenantProfile edgeTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile originalTenantProfile = doGet("/api/tenantProfile/" + tenantProfileId.getId(), TenantProfile.class); + TenantProfile edgeTenantProfile = new TenantProfile(originalTenantProfile); // set tenant profile isolated and add 2 queues - main and isolated edgeTenantProfile.setIsolatedTbRuleEngine(true); @@ -110,6 +110,10 @@ public class TenantProfileEdgeTest extends AbstractEdgeTest { Assert.assertNotNull(queue); Assert.assertEquals(tenantId, queue.getTenantId()); } + + loginSysAdmin(); + doPost("/api/tenantProfile", originalTenantProfile, TenantProfile.class); + loginTenantAdmin(); } private TenantProfileQueueConfiguration createQueueConfig(String queueName, String queueTopic) { From 72ddc1bb485b37fcb7826fc1453c99cab78a4846 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 3 Apr 2025 18:23:22 +0300 Subject: [PATCH 085/112] UI: Fixed twice call registerOnChanges (first call when cleanUControl from default function noop) --- .../widget/config/datasources.component.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts index dd9873e4b9..f8d5064860 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts @@ -150,7 +150,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid datasourcesMode: DatasourceType; - private propagateChange = (_val: any) => {}; + private propagateChange: (value: any) => void; + private propagateChangePending = false; constructor(private fb: UntypedFormBuilder, private utils: UtilsService, @@ -161,7 +162,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid registerOnChange(fn: any): void { this.propagateChange = fn; - if (this.validate(null)) { + if (this.propagateChangePending) { setTimeout(() => { this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); }, 0); @@ -227,7 +228,7 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid if (this.singleDatasource && !this.datasourcesFormArray.length) { this.addDatasource(false); } - if (changed) { + if (changed || this.validate(null)) { setTimeout(() => { this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value); }, 0); @@ -329,7 +330,12 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid if (this.datasourcesOptional) { datasources = datasources ? datasources.filter(d => datasourceValid(d)) : []; } - this.propagateChange(datasources); + if (this.propagateChange) { + this.propagateChange(datasources); + this.propagateChangePending = false; + } else { + this.propagateChangePending = true; + } } public onDatasourceDrop(event: CdkDragDrop) { From a83a88f8babd0d3759a8815391804d5e19737cf2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 8 Apr 2025 18:33:26 +0300 Subject: [PATCH 086/112] UI: Add to file input truncate text with tooltip --- .../src/app/shared/components/file-input.component.html | 8 ++++---- .../src/app/shared/components/file-input.component.scss | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index ff008bf630..105257dd24 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -51,9 +51,9 @@
    - -
    {{ noFileText }}
    -
    {{ fileName }}
    -
    dashboard.maximum-upload-file-size
    + +
    {{ noFileText }}
    +
    {{ fileName }}
    +
    dashboard.maximum-upload-file-size
    diff --git a/ui-ngx/src/app/shared/components/file-input.component.scss b/ui-ngx/src/app/shared/components/file-input.component.scss index 00598adc3b..1c21eb1798 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.scss +++ b/ui-ngx/src/app/shared/components/file-input.component.scss @@ -131,15 +131,11 @@ $previewSize: 100px !default; .tb-file-name { color: rgba(0, 0, 0, 0.54); font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; } .tb-file-hint { color: rgba(0, 0, 0, 0.38); font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; } } From 1f8fa241d639464bc36c79b22cab8f968800d140 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 8 Apr 2025 19:05:05 +0300 Subject: [PATCH 087/112] Adjusted doc file --- .../assets/help/en_US/math/math-methods_fn.md | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md b/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md index e9cb7bb13e..9a5ddf3530 100644 --- a/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md +++ b/ui-ngx/src/assets/help/en_US/math/math-methods_fn.md @@ -1,5 +1,30 @@ +## Built-in operators +The following operators can be used in expressions: + +| Operator | Description | Example | +|----------|-------------------------------|-----------------| +| `+` | Addition or unary plus | `2 + 3` or `+3` | +| `-` | Subtraction or unary minus | `5 - 2` or `-3` | +| `*` | Multiplication | `4 * 2` | +| `/` | Division | `10 / 2` | +| `^` | Exponentiation | `2 ^ 3` | +| `%` | Modulo | `10 % 3` | + + +## Implicit multiplication +Expressions supports implicit multiplication, allowing you to omit the multiplication operator in certain cases. For example, the expression `2cos(yx)` is interpreted as `2*cos(y*x)`. Similarly, `3x` is interpreted as `3*x`, and `xy` as `x*y`. This feature makes writing expressions more concise and natural. + +## Built-in constants +Available constants for calculations: + +| Constant | Description | Value | +|-------------|-----------------------------|-------------------| +| `π` or `pi` | The mathematical constant π | 3.141592653589793 | +| `e` | Euler's number | 2.718281828459045 | +| `φ` | The golden ratio | 1.618033988749895 | + ## Built-in mathematical functions -The following expression field provides support for built-in mathematical functions that you can utilize for various calculations. +The following built-in mathematical functions can be used to perform various calculations: | Function | Description | Example | |---------------|---------------------------------------------------------------------|-------------------| From 1b2ce0c74a579d055d4c0ad0e8a24d30f00aad28 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 8 Apr 2025 20:12:13 +0300 Subject: [PATCH 088/112] UI: Fix add/edit for invalid widget (not loaded due to errors). Minor style improvements. --- .../components/alarm/alarm-table-config.ts | 2 +- .../attribute/attribute-table.component.ts | 4 +- .../dashboard-page.component.ts | 11 ++- .../components/event/event-table-config.ts | 2 +- .../alarm/alarms-table-widget.component.ts | 6 +- .../unread-notification-widget.component.ts | 2 +- .../date-range-navigator.component.ts | 2 +- .../entity/entities-table-widget.component.ts | 2 +- .../lib/rpc/persistent-table.component.ts | 2 +- .../lib/timeseries-table-widget.component.ts | 2 +- .../widget/widget-component.service.ts | 6 +- .../widget/widget-config.component.html | 2 +- .../components/widget/widget.component.ts | 94 +++++++++---------- .../timewindow-config-dialog.component.ts | 4 +- .../components/time/timezone.component.ts | 2 +- 15 files changed, 75 insertions(+), 68 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index fb0ff6de90..b450940ca1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -243,7 +243,7 @@ export class AlarmTableConfig extends EntityTableConfig if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 538afa3a06..d4ff2dc041 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -338,7 +338,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if (this.isClientSideTelemetryTypeMap.get(this.attributeScope)) { return; } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; @@ -389,7 +389,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-filter-panel', backdropClass: 'cdk-overlay-transparent-backdrop', diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 5fad94601d..9490db52a4 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1335,8 +1335,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC addWidgetFromType(widget: WidgetInfo) { this.onAddWidgetClosed(); - this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe( - (widgetTypeInfo) => { + this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe({ + next: (widgetTypeInfo) => { const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo); if (!config.title) { config.title = 'New ' + widgetTypeInfo.widgetName; @@ -1389,8 +1389,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } }); } + }, + error: (errorData) => { + const errorMessages: string[] = errorData.errorMessages; + this.dialogService.alert(this.translate.instant('widget.widget-type-load-error'), + errorMessages.join('
    ').replace(/\n/g, '
    ')); } - ); + }); } onRevertWidgetEdit() { diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 5d770fed3b..5578f2417a 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -571,7 +571,7 @@ export class EventTableConfig extends EntityTableConfig { if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-panel-container', backdropClass: 'cdk-overlay-transparent-backdrop', 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 4aa8e52391..7384f76b62 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 @@ -550,7 +550,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-panel-container', backdropClass: 'cdk-overlay-transparent-backdrop', @@ -621,7 +621,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-filter-panel', backdropClass: 'cdk-overlay-transparent-backdrop', @@ -1189,7 +1189,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts index dc0fc8b2d8..ce13fe227e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts @@ -237,7 +237,7 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy { if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-panel-container', backdropClass: 'cdk-overlay-transparent-backdrop', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts index 551ffe4ba1..a1c987e29a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts @@ -126,7 +126,7 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements $event.stopPropagation(); } this.datePickerSelect.close(); - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; 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 c395192a60..653c388364 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 @@ -513,7 +513,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-panel-container', backdropClass: 'cdk-overlay-transparent-backdrop', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index dfda3d243c..586f425b13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -418,7 +418,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit, O if ($event) { $event.stopPropagation(); } - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig(); config.backdropClass = 'cdk-overlay-transparent-backdrop'; config.hasBackdrop = true; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 9544947060..2653b79059 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -455,7 +455,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI $event.stopPropagation(); } if (this.sources.length) { - const target = $event.target || $event.srcElement || $event.currentTarget; + const target = $event.target || $event.currentTarget; const config = new OverlayConfig({ panelClass: 'tb-panel-container', backdropClass: 'cdk-overlay-transparent-backdrop', diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index f4e69c954c..c0f37b3a37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -243,7 +243,11 @@ export class WidgetComponentService { if (widgetInfo) { return widgetInfo; } else { - return {} as WidgetInfo; + return { + typeParameters: { + hideDataTab: true + } + } as WidgetInfo; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 2b187ab0b2..efa1dffe0b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -165,7 +165,7 @@ [widgetType] = "modelValue.widgetType" [defaultIconColor]="widgetSettings.get('color').value" [actionSources]="modelValue.actionSources" - [additionalWidgetActionTypes]="modelValue.typeParameters.additionalWidgetActionTypes" + [additionalWidgetActionTypes]="modelValue.typeParameters?.additionalWidgetActionTypes" formControlName="actions">
    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 fc996fb2c6..9287b92d57 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 @@ -54,7 +54,6 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { @@ -205,7 +204,6 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType, @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap, - private widgetService: WidgetService, private resources: ResourcesService, private timeService: TimeService, private deviceService: DeviceService, @@ -346,17 +344,17 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.subscriptionContext.widgetUtils = this.widgetContext.utils; this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); - this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe( - (widgetInfo) => { + this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe({ + next: (widgetInfo) => { this.widgetInfo = widgetInfo; this.loadFromWidgetInfo(); }, - (errorData) => { + error: (errorData) => { this.widgetInfo = errorData.widgetInfo; this.errorMessages = errorData.errorMessages; this.loadFromWidgetInfo(); } - ); + }); const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; if (isNotEmptyStr(noDataDisplayMessage)) { @@ -521,15 +519,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.widgetTypeInstance.onDestroy = () => {}; } - this.initialize().subscribe( - () => { + this.initialize().subscribe({ + next: () => { this.onInit(); }, - (err) => { + error: () => { this.widgetContext.inited = true; // console.log(err); } - ); + }); } private detectChanges(detectContainerChanges = false) { @@ -681,8 +679,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, private reInitImpl() { this.onDestroy(); if (!this.typeParameters.useCustomDatasources) { - this.createDefaultSubscription().subscribe( - () => { + this.createDefaultSubscription().subscribe({ + next: () => { if (this.destroyed) { this.onDestroy(); } else { @@ -692,7 +690,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.onInit(); } }, - () => { + error: () => { if (this.destroyed) { this.onDestroy(); } else { @@ -701,7 +699,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.onInit(); } } - ); + }); } else { this.widgetContext.reset(); this.subscriptionInited = true; @@ -751,8 +749,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } )); if (!this.typeParameters.useCustomDatasources) { - this.createDefaultSubscription().subscribe( - () => { + this.createDefaultSubscription().subscribe({ + next: () => { this.subscriptionInited = true; try { this.configureDynamicWidgetComponent(); @@ -762,11 +760,11 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, initSubject.error(err); } }, - (err) => { + error: (err) => { this.subscriptionInited = true; initSubject.error(err); } - ); + }); } else { this.loadingData = false; this.subscriptionInited = true; @@ -791,7 +789,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } - private handleWidgetException(e) { + private handleWidgetException(e: any) { console.error(e); this.widgetErrorData = this.utils.processWidgetException(e); this.detectChanges(); @@ -866,8 +864,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, const createSubscriptionSubject = new ReplaySubject(); options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); - subscription.init$.subscribe( - () => { + subscription.init$.subscribe({ + next: () => { this.widgetContext.subscriptions[subscription.id] = subscription; if (subscribe) { subscription.subscribe(); @@ -875,10 +873,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, - (err) => { + error: (err) => { createSubscriptionSubject.error(err); } - ); + }); return createSubscriptionSubject.asObservable(); } @@ -900,15 +898,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } else { options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo); } - this.createSubscription(options, subscribe).subscribe( - (subscription) => { + this.createSubscription(options, subscribe).subscribe({ + next: (subscription) => { createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, - (err) => { + error: (err) => { createSubscriptionSubject.error(err); } - ); + }); return createSubscriptionSubject.asObservable(); } @@ -937,7 +935,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.dataUpdatePending = true; } } - } catch (e){} + } catch (e){/**/} }, onLatestDataUpdated: () => { try { @@ -948,15 +946,15 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.latestDataUpdatePending = true; } } - } catch (e){} + } catch (e){/**/} }, - onDataUpdateError: (subscription, e) => { + onDataUpdateError: (_subscription, e) => { this.handleWidgetException(e); }, - onLatestDataUpdateError: (subscription, e) => { + onLatestDataUpdateError: (_subscription, e) => { this.handleWidgetException(e); }, - onSubscriptionMessage: (subscription, message) => { + onSubscriptionMessage: (_subscription, message) => { if (this.displayWidgetInstance()) { if (this.widgetInstanceInited) { this.displayMessage(message.severity, message.message); @@ -965,7 +963,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } }, - onInitialPageDataChanged: (subscription, nextPageData) => { + onInitialPageDataChanged: (_subscription, _nextPageData) => { this.reInit(); }, forceReInit: () => { @@ -977,12 +975,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.detectChanges(); } }, - legendDataUpdated: (subscription, detectChanges) => { + legendDataUpdated: (_subscription, detectChanges) => { if (detectChanges) { this.detectChanges(); } }, - timeWindowUpdated: (subscription, timeWindowConfig) => { + timeWindowUpdated: (_subscription, timeWindowConfig) => { this.ngZone.run(() => { this.widget.config.timewindow = timeWindowConfig; this.detectChanges(true); @@ -1017,8 +1015,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.defaultComponentsOptions(options); - this.createSubscription(options).subscribe( - (subscription) => { + this.createSubscription(options).subscribe({ + next: (subscription) => { // backward compatibility this.widgetContext.datasources = subscription.datasources; @@ -1032,12 +1030,12 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, createSubscriptionSubject.complete(); }); }, - (err) => { + error: (err) => { this.ngZone.run(() => { createSubscriptionSubject.error(err); }); } - ); + }); } else if (this.widget.type === widgetType.rpc) { this.loadingData = false; options = { @@ -1074,7 +1072,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.detectChanges(); } }, - onRpcErrorCleared: (subscription) => { + onRpcErrorCleared: (_subscription) => { if (this.dynamicWidgetComponent) { this.dynamicWidgetComponent.rpcErrorText = null; this.dynamicWidgetComponent.rpcRejection = null; @@ -1085,20 +1083,20 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } } }; - this.createSubscription(options).subscribe( - (subscription) => { + this.createSubscription(options).subscribe({ + next: (subscription) => { this.widgetContext.defaultSubscription = subscription; this.ngZone.run(() => { createSubscriptionSubject.next(); createSubscriptionSubject.complete(); }); }, - (err) => { + error: (err) => { this.ngZone.run(() => { createSubscriptionSubject.error(err); }); } - ); + }); this.detectChanges(); } else if (this.widget.type === widgetType.static) { this.loadingData = false; @@ -1159,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } const state = objToBase64URI([ stateObject ]); const isSinglePage = this.route.snapshot.data.singlePageMode; - let url; + let url: string; if (isSinglePage) { url = `/dashboard/${targetDashboardId}?state=${state}`; } else { @@ -1168,7 +1166,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, if (descriptor.openNewBrowserTab) { window.open(url, '_blank'); } else { - this.router.navigateByUrl(url); + this.router.navigateByUrl(url).then(() => {}); } break; case WidgetActionType.openURL: @@ -1467,7 +1465,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, popoverWidth = '25vw', popoverHeight = '25vh', popoverStyle: { [klass: string]: any } = {}) { - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { @@ -1560,7 +1558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } private elementClick($event: Event) { - const elementClicked = ($event.target || $event.srcElement) as Element; + const elementClicked = ($event.target) as Element; const descriptors = this.getActionDescriptors('elementClick'); if (descriptors.length) { const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 1ad4dab88c..1e1f831c4f 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -554,7 +554,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if ($event) { $event.stopPropagation(); } - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { @@ -614,7 +614,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if ($event) { $event.stopPropagation(); } - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { diff --git a/ui-ngx/src/app/shared/components/time/timezone.component.ts b/ui-ngx/src/app/shared/components/time/timezone.component.ts index 34e2486106..19affc2aab 100644 --- a/ui-ngx/src/app/shared/components/time/timezone.component.ts +++ b/ui-ngx/src/app/shared/components/time/timezone.component.ts @@ -151,7 +151,7 @@ export class TimezoneComponent implements ControlValueAccessor, OnInit { if (this.disablePanel) { return; } - const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; + const trigger = ($event.target || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { From b128650ef9415730cc55c527114df90494142fd5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 9 Apr 2025 11:01:28 +0300 Subject: [PATCH 089/112] UI: Updated shape image fill image function example. Improved this example popover style --- ...marker-image-settings-panel.component.html | 3 ++- ...e-fill-image-settings-panel.component.html | 3 ++- .../widget/lib/map/shape_fill_image_fn.md | 25 +++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html index ff06899a8b..d889f96ee7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings-panel.component.html @@ -46,7 +46,8 @@ [functionArgs]="['data', 'images', 'dsData']" [globalVariables]="functionScopeVariables" functionTitle="{{ 'widgets.maps.data-layer.marker.marker-image-function' | translate }}" - helpId="widget/lib/map/marker_image_fn"> + helpId="widget/lib/map/marker_image_fn" + [helpPopupStyle]="{width: '900px'}"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html index 19c83d1270..660f43a75d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html @@ -58,7 +58,8 @@ [functionArgs]="['data', 'images', 'dsData']" [globalVariables]="functionScopeVariables" functionTitle="{{ 'widgets.maps.data-layer.shape.fill-image-function' | translate }}" - helpId="widget/lib/map/shape_fill_image_fn"> + helpId="widget/lib/map/shape_fill_image_fn" + [helpPopupStyle]="{width: '900px'}"> diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md index b13d612c8e..c6ea2e1ae6 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md @@ -39,12 +39,33 @@ In case no data is returned, default fill image will be used.
    • -TODO: +Calculate image URL and rotation angle depending on windSpeed and windDirection telemetry values for a weather station device type.
      +Let's assume 3 images are defined in the Shape fill images section. Each image corresponds to a particular wind speed level: low (e.g., <5 m/s), medium (e.g., 5-15 m/s), and high (e.g., >15 m/s).
    ```javascript -TODO: +const type = data.Type; +if (type === 'weather station') { + const result = { + url: images[0], + opacity: 0.8, + angle: 0 + }; + const windSpeed = data.windSpeed; + const windDirection = data.windDirection; + if (typeof windSpeed !== 'undefined' && typeof windDirection !== 'undefined') { + if (windSpeed < 5) { + result.url = images[0]; + } else if (windSpeed < 15) { + result.url = images[1]; + } else { + result.url = images[2]; + } + result.angle = windDirection; + } + return result; +} {:copy-code} ``` From 19b18a596f2279e81e36ff998560d0309bd538d1 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 9 Apr 2025 11:07:02 +0300 Subject: [PATCH 090/112] Changed back icons to helps --- .../rule-node/action/math-function-config.component.html | 2 +- .../rule-node/common/arguments-map-config.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html index 3b91c56acf..3147162f06 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html @@ -79,7 +79,7 @@ info + matTooltip="{{ 'rule-node-config.math-templatization-tooltip' | translate }}">help rule-node-config.key-field-input-required diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html index aa61beb32a..b98854d0e5 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/arguments-map-config.component.html @@ -67,7 +67,7 @@ - info + help rule-node-config.argument-key-field-input-required From d72de7e1751f6c6038ebcdf4910108e17b191e05 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 9 Apr 2025 11:37:21 +0300 Subject: [PATCH 091/112] UI: Fixed customer home dashboard not correct work active/inactive device link --- ui-ngx/src/assets/dashboard/customer_user_home_page.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/dashboard/customer_user_home_page.json b/ui-ngx/src/assets/dashboard/customer_user_home_page.json index 6e381a9ab2..17d7262f23 100644 --- a/ui-ngx/src/assets/dashboard/customer_user_home_page.json +++ b/ui-ngx/src/assets/dashboard/customer_user_home_page.json @@ -193,7 +193,7 @@ "padding": "16px", "settings": { "useMarkdownTextFunction": false, - "markdownTextPattern": "", + "markdownTextPattern": "", "applyDefaultMarkdownStyle": false, "markdownCss": ".tb-card-content {\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: row;\n}\n\n.tb-content-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n gap: 12px;\n}\n\n.tb-card-header {\n height: 36px;\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n}\n\n.tb-item-cards {\n flex: 1;\n display: flex;\n flex-direction: row;\n gap: 12px;\n}\n\na.tb-item-card {\n flex: 1;\n display: flex;\n flex-direction: column;\n padding: 8px 12px;\n border: 1px solid;\n border-radius: 10px;\n margin-bottom: 12px;\n}\n\na.tb-item-card.tb-inactive {\n background: rgba(209, 39, 48, 0.04);\n border-color: rgba(209, 39, 48, 0.06);\n}\n\na.tb-item-card.tb-active {\n background: rgba(48, 86, 128, 0.04);\n border-color: rgba(48, 86, 128, 0.12);\n}\n\na.tb-item-card.tb-total {\n background: rgba(0, 0, 0, 0.01);\n border-color: rgba(0, 0, 0, 0.05);\n}\n\n.tb-item-title-container {\n display: grid;\n}\n\n.tb-item-title {\n font-weight: 400;\n font-size: 14px;\n line-height: 20px;\n letter-spacing: 0.2px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis; \n color: rgba(0, 0, 0, 0.76);\n}\n\n.tb-item-title.tb-home-widget-link:after {\n position: absolute;\n right: 0;\n}\n\na.tb-item-card:hover .tb-item-title.tb-home-widget-link:after { \n color: rgba(0, 0, 0, 0.38);\n}\n\na.tb-item-card:hover {\n box-shadow: 0px 4px 10px rgba(23, 33, 90, 0.08);\n}\n\n.tb-count-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.tb-count {\n font-style: normal;\n font-weight: 500;\n font-size: 24px;\n line-height: 36px;\n white-space: nowrap;\n color: rgba(0, 0, 0, 0.87);\n}\n\n@media screen and (max-width: 959px) {\n .tb-item-cards {\n flex-direction: column;\n }\n a.tb-item-card {\n margin-bottom: 0;\n }\n}\n\n@media screen and (max-width: 1279px) {\n a.tb-item-card {\n flex-direction: row;\n align-items: center;\n }\n .tb-item-title.tb-home-widget-link:after {\n position: relative;\n }\n .tb-count-container {\n align-items: flex-end;\n }\n}\n\n@media screen and (min-width: 960px) and (max-width: 1819px) {\n .tb-item-title {\n font-size: 11px;\n line-height: 16px;\n }\n .tb-count {\n font-size: 16px;\n line-height: 24px;\n }\n a.tb-item-card {\n padding: 4px 8px;\n margin-bottom: 6px;\n }\n a.tb-item-card:hover {\n box-shadow: 0px 2px 5px rgba(23, 33, 90, 0.08);\n }\n}\n" }, From 79381092b2a4acfb8fef8a8244eb87b76433edcc Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Apr 2025 12:07:50 +0300 Subject: [PATCH 092/112] Do not push edge notifications in case edges disabled. Check if edges enabled before processing msg by edge consumer --- .../queue/DefaultTbClusterService.java | 14 ++++++++----- .../queue/DefaultTbEdgeConsumerService.java | 21 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 92c48a5fed..561dc7122a 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -551,11 +551,15 @@ public class DefaultTbClusterService implements TbClusterService { } private void processEdgeNotification(EdgeId edgeId, ToEdgeNotificationMsg toEdgeNotificationMsg) { - var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); - serviceIdOpt.ifPresentOrElse( - serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), - () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) - ); + if (edgesEnabled) { + var serviceIdOpt = Optional.ofNullable(edgeIdServiceIdCache.get(edgeId)); + serviceIdOpt.ifPresentOrElse( + serviceId -> pushMsgToEdgeNotification(toEdgeNotificationMsg, serviceId.get()), + () -> broadcastEdgeNotification(edgeId, toEdgeNotificationMsg) + ); + } else { + log.trace("Edges disabled. Ignoring edge notification {} for edgeId: {}", toEdgeNotificationMsg, edgeId); + } } private void pushMsgToEdgeNotification(ToEdgeNotificationMsg toEdgeNotificationMsg, String serviceId) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java index fdaa2103e2..2e4c15f8e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java @@ -51,6 +51,7 @@ import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; @@ -195,36 +196,42 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService msg, TbCallback callback) { ToEdgeNotificationMsg toEdgeNotificationMsg = msg.getValue(); try { + EdgeRpcService edgeRpcService = edgeCtx.getEdgeRpcService(); + if (edgeRpcService == null) { + log.debug("No EdgeRpcService available (edge functionality disabled), ignoring msg: {}", toEdgeNotificationMsg); + callback.onSuccess(); + return; + } if (toEdgeNotificationMsg.hasEdgeHighPriority()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeHighPriority()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasEdgeEventUpdate()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getEdgeEventUpdate()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasToEdgeSyncRequest()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getToEdgeSyncRequest()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasFromEdgeSyncResponse()) { EdgeSessionMsg edgeSessionMsg = ProtoUtils.fromProto(toEdgeNotificationMsg.getFromEdgeSyncResponse()); - edgeCtx.getEdgeRpcService().onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); + edgeRpcService.onToEdgeSessionMsg(edgeSessionMsg.getTenantId(), edgeSessionMsg); callback.onSuccess(); } else if (toEdgeNotificationMsg.hasComponentLifecycle()) { ComponentLifecycleMsg componentLifecycle = ProtoUtils.fromProto(toEdgeNotificationMsg.getComponentLifecycle()); TenantId tenantId = componentLifecycle.getTenantId(); EdgeId edgeId = new EdgeId(componentLifecycle.getEntityId().getId()); if (ComponentLifecycleEvent.DELETED.equals(componentLifecycle.getEvent())) { - edgeCtx.getEdgeRpcService().deleteEdge(tenantId, edgeId); + edgeRpcService.deleteEdge(tenantId, edgeId); } else if (ComponentLifecycleEvent.UPDATED.equals(componentLifecycle.getEvent())) { Edge edge = edgeCtx.getEdgeService().findEdgeById(tenantId, edgeId); - edgeCtx.getEdgeRpcService().updateEdge(tenantId, edge); + edgeRpcService.updateEdge(tenantId, edge); } callback.onSuccess(); } } catch (Exception e) { - log.error("Error processing edge notification message", e); + log.error("Error processing edge notification message {}", toEdgeNotificationMsg, e); callback.onFailure(e); } From 33fd8af17e6ac3aeb13ff24a3d97c12a81d7bead Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 9 Apr 2025 12:29:35 +0300 Subject: [PATCH 093/112] Add EDQS query stats; improvements --- .../src/main/resources/thingsboard.yml | 2 + .../server/common/data/JavaSerDesUtil.java | 4 +- .../edqs/repo/DefaultEdqsRepository.java | 5 +- .../server/edqs/repo/TenantRepo.java | 23 +++-- .../edqs/stats/DefaultEdqsStatsService.java | 94 +++++++++++++++++++ .../server/edqs/stats/EdqsStatsService.java | 81 ---------------- .../common/stats/DummyEdqsStatsService.java | 41 ++++++++ .../server/common/stats/EdqsStatsService.java | 33 +++++++ .../server/common/stats/StatsTimer.java | 12 ++- .../server/dao/entity/BaseEntityService.java | 48 ++++++---- 10 files changed, 224 insertions(+), 119 deletions(-) create mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java create mode 100644 common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java create mode 100644 common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 37f2bcd5a7..d8a22e478d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1768,6 +1768,8 @@ queue: stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" + # Threshold for slow queries to log, in milliseconds + slow_query_threshold: "${TB_EDQS_SLOW_QUERY_THRESHOLD_MS:3000}" vc: # Default topic name topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java index e14e2d5fa1..55a0cd90aa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/JavaSerDesUtil.java @@ -36,7 +36,7 @@ public class JavaSerDesUtil { try (ObjectInputStream ois = new ObjectInputStream(is)) { return (T) ois.readObject(); } catch (IOException | ClassNotFoundException e) { - log.error("Error during deserialization message, [{}]", e.getMessage()); + log.error("Error during deserialization", e); return null; } } @@ -50,7 +50,7 @@ public class JavaSerDesUtil { ois.writeObject(msq); return boas.toByteArray(); } catch (IOException e) { - log.error("Error during serialization message, [{}]", e.getMessage()); + log.error("Error during serialization", e); throw new RuntimeException(e); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java index 1deaca83a7..d8be25a206 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java @@ -27,10 +27,9 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; -import org.thingsboard.server.edqs.stats.EdqsStatsService; +import org.thingsboard.server.edqs.stats.DefaultEdqsStatsService; import org.thingsboard.server.queue.edqs.EdqsComponent; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Predicate; @@ -42,7 +41,7 @@ import java.util.function.Predicate; public class DefaultEdqsRepository implements EdqsRepository { private final static ConcurrentMap repos = new ConcurrentHashMap<>(); - private final Optional statsService; + private final DefaultEdqsStatsService statsService; public TenantRepo get(TenantId tenantId) { return repos.computeIfAbsent(tenantId, id -> new TenantRepo(id, statsService)); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 5fdc820ecc..a47559d6d8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.edqs.data.ApiUsageStateData; import org.thingsboard.server.edqs.data.AssetData; import org.thingsboard.server.edqs.data.CustomerData; @@ -54,7 +55,6 @@ import org.thingsboard.server.edqs.query.EdqsQuery; import org.thingsboard.server.edqs.query.SortableEntityData; import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor; import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory; -import org.thingsboard.server.edqs.stats.EdqsStatsService; import org.thingsboard.server.edqs.util.RepositoryUtils; import java.util.ArrayList; @@ -63,7 +63,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.UUID; @@ -94,9 +93,9 @@ public class TenantRepo { private final Lock entityUpdateLock = new ReentrantLock(); private final TenantId tenantId; - private final Optional edqsStatsService; + private final EdqsStatsService edqsStatsService; - public TenantRepo(TenantId tenantId, Optional edqsStatsService) { + public TenantRepo(TenantId tenantId, EdqsStatsService edqsStatsService) { this.tenantId = tenantId; this.edqsStatsService = edqsStatsService; } @@ -144,7 +143,7 @@ public class TenantRepo { EntityData to = getOrCreate(entity.getTo()); boolean added = repo.add(from, to, entity.getType()); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.RELATION); } } else if (RelationTypeGroup.DASHBOARD.equals(entity.getTypeGroup())) { if (EntityRelation.CONTAINS_TYPE.equals(entity.getType()) && entity.getFrom().getEntityType() == EntityType.CUSTOMER) { @@ -164,7 +163,7 @@ public class TenantRepo { if (relationsRepo != null) { boolean removed = relationsRepo.remove(entityRelation.getFrom().getId(), entityRelation.getTo().getId(), entityRelation.getType()); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.RELATION, EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(ObjectType.RELATION); } } } else if (RelationTypeGroup.DASHBOARD.equals(entityRelation.getTypeGroup())) { @@ -222,7 +221,7 @@ public class TenantRepo { if (removed.getFields() != null) { getEntitySet(entityType).remove(removed); } - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(entity.type()); UUID customerId = removed.getCustomerId(); if (customerId != null) { @@ -243,7 +242,7 @@ public class TenantRepo { Integer keyId = KeyDictionary.get(attributeKv.getKey()); boolean added = entityData.putAttr(keyId, attributeKv.getScope(), attributeKv.getDataPoint()); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.ATTRIBUTE_KV); } } } @@ -253,7 +252,7 @@ public class TenantRepo { if (entityData != null) { boolean removed = entityData.removeAttr(KeyDictionary.get(attributeKv.getKey()), attributeKv.getScope()); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.ATTRIBUTE_KV, EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(ObjectType.ATTRIBUTE_KV); } } } @@ -264,7 +263,7 @@ public class TenantRepo { Integer keyId = KeyDictionary.get(latestTsKv.getKey()); boolean added = entityData.putTs(keyId, latestTsKv.getDataPoint()); if (added) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.LATEST_TS_KV); } } } @@ -274,7 +273,7 @@ public class TenantRepo { if (entityData != null) { boolean removed = entityData.removeTs(KeyDictionary.get(latestTsKv.getKey())); if (removed) { - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.LATEST_TS_KV, EdqsEventType.DELETED)); + edqsStatsService.reportRemoved(ObjectType.LATEST_TS_KV); } } } @@ -292,7 +291,7 @@ public class TenantRepo { return getEntityMap(entityType).computeIfAbsent(entityId, id -> { log.debug("[{}] Adding {} {}", tenantId, entityType, id); EntityData entityData = constructEntityData(entityType, entityId); - edqsStatsService.ifPresent(statService -> statService.reportEvent(tenantId, ObjectType.fromEntityType(entityType), EdqsEventType.UPDATED)); + edqsStatsService.reportAdded(ObjectType.fromEntityType(entityType)); return entityData; }); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java new file mode 100644 index 0000000000..f490f25de3 --- /dev/null +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.edqs.stats; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.stats.EdqsStatsService; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsTimer; +import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.queue.edqs.EdqsComponent; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@EdqsComponent +@Service +@Slf4j +@ConditionalOnProperty(name = "queue.edqs.stats.enabled", havingValue = "true", matchIfMissing = true) +public class DefaultEdqsStatsService implements EdqsStatsService { + + private final StatsFactory statsFactory; + + @Value("${queue.edqs.stats.slow_query_threshold:3000}") + private int slowQueryThreshold; + + private final ConcurrentHashMap objectCounters = new ConcurrentHashMap<>(); + private final StatsTimer dataQueryTimer; + private final StatsTimer countQueryTimer; + + private DefaultEdqsStatsService(StatsFactory statsFactory) { + this.statsFactory = statsFactory; + dataQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityDataQueryTimer"); + countQueryTimer = statsFactory.createTimer(StatsType.EDQS, "entityCountQueryTimer"); + } + + @Override + public void reportAdded(ObjectType objectType) { + getObjectCounter(objectType).incrementAndGet(); + } + + @Override + public void reportRemoved(ObjectType objectType) { + getObjectCounter(objectType).decrementAndGet(); + } + + @Override + public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) { + double timingMs = timingNanos / 1000_000.0; + if (timingMs < slowQueryThreshold) { + log.info("[{}] Executed data query in {} ms: {}", tenantId, timingMs, query); + } else { + log.warn("[{}] Executed slow data query in {} ms: {}", tenantId, timingMs, query); + } + dataQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS); + } + + @Override + public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) { + double timingMs = timingNanos / 1000_000.0; + if (timingMs < slowQueryThreshold) { + log.info("[{}] Executed count query in {} ms: {}", tenantId, timingMs, query); + } else { + log.warn("[{}] Executed slow count query in {} ms: {}", tenantId, timingMs, query); + } + countQueryTimer.record(timingNanos, TimeUnit.NANOSECONDS); + } + + private AtomicInteger getObjectCounter(ObjectType objectType) { + return objectCounters.computeIfAbsent(objectType, type -> + statsFactory.createGauge("edqsObjectsCount", new AtomicInteger(), "objectType", type.name())); + } + +} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java deleted file mode 100644 index 442453fc93..0000000000 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright © 2016-2025 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.edqs.stats; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.edqs.EdqsEventType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.StatsFactory; -import org.thingsboard.server.common.stats.StatsType; -import org.thingsboard.server.queue.edqs.EdqsComponent; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -@EdqsComponent -@Service -@Slf4j -@RequiredArgsConstructor -@ConditionalOnProperty(name = "queue.edqs.stats.enabled", havingValue = "true", matchIfMissing = true) -public class EdqsStatsService { - - private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); - private final StatsFactory statsFactory; - - public void reportEvent(TenantId tenantId, ObjectType objectType, EdqsEventType eventType) { - statsMap.computeIfAbsent(tenantId, id -> new EdqsStats(tenantId, statsFactory)) - .reportEvent(objectType, eventType); - } - - @Getter - @AllArgsConstructor - static class EdqsStats { - - private final TenantId tenantId; - private final ConcurrentHashMap entityCounters = new ConcurrentHashMap<>(); - private final StatsFactory statsFactory; - - private AtomicInteger getOrCreateObjectCounter(ObjectType objectType) { - return entityCounters.computeIfAbsent(objectType, - type -> statsFactory.createGauge(StatsType.EDQS.getName() + "_object_count", new AtomicInteger(), - "tenantId", tenantId.toString(), "objectType", type.name())); - } - - @Override - public String toString() { - return entityCounters.entrySet().stream() - .map(counters -> counters.getKey().name()+ " total = [" + counters.getValue() + "]") - .collect(Collectors.joining(", ")); - } - - public void reportEvent(ObjectType objectType, EdqsEventType eventType) { - AtomicInteger objectCounter = getOrCreateObjectCounter(objectType); - if (eventType == EdqsEventType.UPDATED){ - objectCounter.incrementAndGet(); - } else if (eventType == EdqsEventType.DELETED) { - objectCounter.decrementAndGet(); - } - } - } - -} diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java new file mode 100644 index 0000000000..df78e5fc89 --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DummyEdqsStatsService.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.stats; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +@Service +@ConditionalOnMissingBean(value = EdqsStatsService.class, ignored = DummyEdqsStatsService.class) +public class DummyEdqsStatsService implements EdqsStatsService { + + @Override + public void reportAdded(ObjectType objectType) {} + + @Override + public void reportRemoved(ObjectType objectType) {} + + @Override + public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) {} + + @Override + public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) {} + +} diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java new file mode 100644 index 0000000000..106e43e913 --- /dev/null +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/EdqsStatsService.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.stats; + +import org.thingsboard.server.common.data.ObjectType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public interface EdqsStatsService { + + void reportAdded(ObjectType objectType); + + void reportRemoved(ObjectType objectType); + + void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos); + + void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos); + +} diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java index 89f33ac922..b9f3533b92 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/StatsTimer.java @@ -34,10 +34,14 @@ public class StatsTimer { this.timer = micrometerTimer; } - public void record(long timeMs) { + public void record(long timeMs) { + record(timeMs, TimeUnit.MILLISECONDS); + } + + public void record(long timing, TimeUnit timeUnit) { count++; - totalTime += timeMs; - timer.record(timeMs, TimeUnit.MILLISECONDS); + totalTime += timeUnit.toMillis(timing); + timer.record(timing, timeUnit); } public double getAvg() { @@ -47,7 +51,7 @@ public class StatsTimer { return (double) totalTime / count; } - public void reset() { + public void reset() { count = 0; totalTime = 0; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index a762aabf38..4df33779ee 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasEmail; @@ -46,6 +47,7 @@ import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.msg.edqs.EdqsApiService; +import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.ArrayList; @@ -89,6 +91,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Lazy private EdqsApiService edqsApiService; + @Autowired + private EdqsStatsService edqsStatsService; + @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); @@ -96,14 +101,19 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityCountQuery(query); + TbStopWatch stopWatch = TbStopWatch.create(); + Long result; if (edqsApiService.isEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { EdqsRequest request = EdqsRequest.builder() .entityCountQuery(query) .build(); EdqsResponse response = processEdqsRequest(tenantId, customerId, request); - return response.getEntityCountQueryResult(); + result = response.getEntityCountQueryResult(); + } else { + result = entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); } - return this.entityQueryDao.countEntitiesByQuery(tenantId, customerId, query); + edqsStatsService.reportCountQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos()); + return result; } @Override @@ -113,27 +123,31 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); validateEntityDataQuery(query); + TbStopWatch stopWatch = TbStopWatch.create(); + PageData result; if (edqsApiService.isEnabled() && validForEdqs(query)) { EdqsRequest request = EdqsRequest.builder() .entityDataQuery(query) .build(); EdqsResponse response = processEdqsRequest(tenantId, customerId, request); - return response.getEntityDataQueryResult(); - } - - if (!isValidForOptimization(query)) { - return this.entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); - } - - // 1 step - find entity data by filter and sort columns - PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); - if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { - return entityDataByQuery; + result = response.getEntityDataQueryResult(); + } else { + if (!isValidForOptimization(query)) { + result = entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + } else { + // 1 step - find entity data by filter and sort columns + PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); + if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { + result = entityDataByQuery; + } else { + // 2 step - find entity data by entity ids from the 1st step + List entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); + result = new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); + } + } } - - // 2 step - find entity data by entity ids from the 1st step - List result = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); - return new PageData<>(result, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); + edqsStatsService.reportDataQuery(tenantId, query, stopWatch.stopAndGetTotalTimeNanos()); + return result; } private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE From e967a994309dd38d864629983aadeffa6755fbfe Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 9 Apr 2025 12:36:28 +0300 Subject: [PATCH 094/112] Change log levels in DefaultEdqsStatsService --- .../server/edqs/stats/DefaultEdqsStatsService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java index f490f25de3..7769d2bfb8 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java @@ -68,7 +68,7 @@ public class DefaultEdqsStatsService implements EdqsStatsService { public void reportDataQuery(TenantId tenantId, EntityDataQuery query, long timingNanos) { double timingMs = timingNanos / 1000_000.0; if (timingMs < slowQueryThreshold) { - log.info("[{}] Executed data query in {} ms: {}", tenantId, timingMs, query); + log.debug("[{}] Executed data query in {} ms: {}", tenantId, timingMs, query); } else { log.warn("[{}] Executed slow data query in {} ms: {}", tenantId, timingMs, query); } @@ -79,7 +79,7 @@ public class DefaultEdqsStatsService implements EdqsStatsService { public void reportCountQuery(TenantId tenantId, EntityCountQuery query, long timingNanos) { double timingMs = timingNanos / 1000_000.0; if (timingMs < slowQueryThreshold) { - log.info("[{}] Executed count query in {} ms: {}", tenantId, timingMs, query); + log.debug("[{}] Executed count query in {} ms: {}", tenantId, timingMs, query); } else { log.warn("[{}] Executed slow count query in {} ms: {}", tenantId, timingMs, query); } From a91d9faad9eb1c9a00ec02b0d842f06140d2acfc Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 9 Apr 2025 12:43:35 +0300 Subject: [PATCH 095/112] Fix EDQS tests init --- .../thingsboard/server/edqs/repo/DefaultEdqsRepository.java | 4 ++-- .../org/thingsboard/server/edqs/repo/AbstractEDQTest.java | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java index d8be25a206..215c64194f 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; -import org.thingsboard.server.edqs.stats.DefaultEdqsStatsService; +import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.queue.edqs.EdqsComponent; import java.util.concurrent.ConcurrentHashMap; @@ -41,7 +41,7 @@ import java.util.function.Predicate; public class DefaultEdqsRepository implements EdqsRepository { private final static ConcurrentMap repos = new ConcurrentHashMap<>(); - private final DefaultEdqsStatsService statsService; + private final EdqsStatsService statsService; public TenantRepo get(TenantId tenantId) { return repos.computeIfAbsent(tenantId, id -> new TenantRepo(id, statsService)); diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java index 01a7495148..330d22a3c6 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java @@ -20,6 +20,7 @@ import org.junit.Before; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.TestExecutionListeners; @@ -58,6 +59,7 @@ import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.stats.DummyEdqsStatsService; import org.thingsboard.server.edqs.util.EdqsConverter; import java.util.Collections; @@ -78,6 +80,8 @@ public abstract class AbstractEDQTest { protected DefaultEdqsRepository repository; @Autowired protected EdqsConverter edqsConverter; + @MockBean + private DummyEdqsStatsService edqsStatsService; protected final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); protected final CustomerId customerId = new CustomerId(UUID.randomUUID()); From 4e392a956bfcb92fa0ccc547621e703e3d4931fa Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 9 Apr 2025 13:49:02 +0300 Subject: [PATCH 096/112] Added forbidden argument names in accordance with constants --- .../panel/calculated-field-argument-panel.component.html | 4 ++-- .../panel/calculated-field-argument-panel.component.ts | 7 ++++--- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html index 7521395cb4..92855d882f 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html @@ -55,11 +55,11 @@ class="tb-error"> warning - } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('equalCtx')) { + } @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('forbiddenName')) { warning diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 2a64c90f20..6f46e47af9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -67,7 +67,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI readonly defaultLimit = Math.floor(this.maxDataPointsPerRollingArg / 10); argumentFormGroup = this.fb.group({ - argumentName: ['', [Validators.required, this.uniqNameRequired(), this.notEqualCtxValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + argumentName: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenArgumentNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], refEntityId: this.fb.group({ entityType: [ArgumentEntityType.Current], id: [''] @@ -254,10 +254,11 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } } - private notEqualCtxValidator(): ValidatorFn { + private forbiddenArgumentNameValidator(): ValidatorFn { return (control: FormControl) => { const trimmedValue = control.value.trim().toLowerCase(); - return trimmedValue === 'ctx' ? { equalCtx: true } : null; + const forbiddenArgumentNames = ['ctx', 'e', 'pi']; + return forbiddenArgumentNames.includes(trimmedValue) ? { forbiddenName: true } : null; }; } 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 e4f4721f3c..133e333dbe 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1069,7 +1069,7 @@ "argument-name-pattern": "Argument name is invalid.", "argument-name-duplicate": "Argument with such name already exists.", "argument-name-max-length": "Argument name should be less than 256 characters.", - "argument-name-ctx": "Argument name 'ctx' is reserved and cannot be used.", + "argument-name-forbidden": "Argument name is reserved and cannot be used.", "argument-type-required": "Argument type is required.", "max-args": "Maximum number of arguments reached.", "decimals-range": "Decimals by default should be a number between 0 and 15.", From 16eb027956ffef4657a3e529e170d0e2067d17c2 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 9 Apr 2025 14:52:11 +0300 Subject: [PATCH 097/112] EDQS: configurable string compression threshold; fix stats service condition --- application/src/main/resources/thingsboard.yml | 2 ++ .../edqs/data/dp/CompressedStringDataPoint.java | 3 +-- .../server/edqs/stats/DefaultEdqsStatsService.java | 6 ++---- .../thingsboard/server/edqs/util/EdqsConverter.java | 12 ++++++++---- edqs/src/main/resources/edqs.yml | 2 ++ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d8a22e478d..ecd4ddebb7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1765,6 +1765,8 @@ queue: max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" # Maximum timeout for requests to EDQS max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" + # Strings longer than this threshold will be compressed + string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}" stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java index f63abbbeb7..cf4267e443 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java @@ -17,13 +17,12 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.SneakyThrows; -import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.common.util.TbBytePool; +import org.thingsboard.server.common.data.kv.DataType; import org.xerial.snappy.Snappy; public class CompressedStringDataPoint extends AbstractDataPoint { - public static final int MIN_STR_SIZE_TO_COMPRESS = 512; @Getter private final byte[] compressedValue; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java index 7769d2bfb8..3767d0f60b 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/DefaultEdqsStatsService.java @@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.stats; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; @@ -27,16 +27,14 @@ import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsTimer; import org.thingsboard.server.common.stats.StatsType; -import org.thingsboard.server.queue.edqs.EdqsComponent; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -@EdqsComponent @Service @Slf4j -@ConditionalOnProperty(name = "queue.edqs.stats.enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnExpression("'${queue.edqs.api.supported:true}' == 'true' && '${queue.edqs.stats.enabled:true}' == 'true'") public class DefaultEdqsStatsService implements EdqsStatsService { private final StatsFactory statsFactory; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java index bb79e0b8df..167037b889 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java @@ -27,6 +27,7 @@ import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.common.util.TbStringPool; import org.thingsboard.server.common.data.AttributeScope; @@ -63,6 +64,9 @@ import java.util.UUID; @Slf4j public class EdqsConverter { + @Value("${queue.edqs.string_compression_length_threshold:512}") + private int stringCompressionLengthThreshold; + private final Map> converters = new HashMap<>(); private final Converter defaultConverter = new JsonConverter<>(Entity.class); @@ -131,7 +135,7 @@ public class EdqsConverter { }); } - public static DataPointProto toDataPointProto(long ts, KvEntry kvEntry) { + public DataPointProto toDataPointProto(long ts, KvEntry kvEntry) { DataPointProto.Builder proto = DataPointProto.newBuilder(); proto.setTs(ts); switch (kvEntry.getDataType()) { @@ -140,7 +144,7 @@ public class EdqsConverter { case DOUBLE -> proto.setDoubleV(kvEntry.getDoubleValue().get()); case STRING -> { String strValue = kvEntry.getStrValue().get(); - if (strValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + if (strValue.length() < stringCompressionLengthThreshold) { proto.setStringV(strValue); } else { proto.setCompressedStringV(ByteString.copyFrom(compress(strValue))); @@ -148,7 +152,7 @@ public class EdqsConverter { } case JSON -> { String jsonValue = kvEntry.getJsonValue().get(); - if (jsonValue.length() < CompressedStringDataPoint.MIN_STR_SIZE_TO_COMPRESS) { + if (jsonValue.length() < stringCompressionLengthThreshold) { proto.setJsonV(jsonValue); } else { proto.setCompressedJsonV(ByteString.copyFrom(compress(jsonValue))); @@ -158,7 +162,7 @@ public class EdqsConverter { return proto.build(); } - public static DataPoint fromDataPointProto(DataPointProto proto) { + public DataPoint fromDataPointProto(DataPointProto proto) { long ts = proto.getTs(); if (proto.hasBoolV()) { return new BoolDataPoint(ts, proto.getBoolV()); diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index 353d76391b..2c0e0b8c5d 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -71,6 +71,8 @@ queue: max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" # Maximum timeout for requests to EDQS max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" + # Strings longer than this threshold will be compressed + string_compression_length_threshold: "${TB_EDQS_STRING_COMPRESSION_LENGTH_THRESHOLD:512}" stats: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" From acc278f563d28e451de82ff78b98b1b3e9b6d52e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 9 Apr 2025 16:27:03 +0300 Subject: [PATCH 098/112] UI: Fixed show popover some time not showing occasionally on first interaction --- .../src/app/shared/components/popover.component.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/popover.component.ts b/ui-ngx/src/app/shared/components/popover.component.ts index 4344b32fd7..2ccbb615d5 100644 --- a/ui-ngx/src/app/shared/components/popover.component.ts +++ b/ui-ngx/src/app/shared/components/popover.component.ts @@ -375,7 +375,6 @@ export class TbPopoverComponent implements OnDestroy, OnInit { tbComponentInjector: Injector | null = null; tbComponentStyle: { [klass: string]: any } = {}; tbOverlayClassName!: string; - tbOverlayStyle: { [klass: string]: any } = {}; tbPopoverInnerStyle: { [klass: string]: any } = {}; tbPopoverInnerContentStyle: { [klass: string]: any } = {}; tbBackdrop = false; @@ -470,6 +469,16 @@ export class TbPopoverComponent implements OnDestroy, OnInit { return this.tbModal ? 'tb-popover-overlay-backdrop' : ''; } + + set tbOverlayStyle(value: { [klass: string]: any }) { + this._tbOverlayStyle = value; + this.cdr.markForCheck(); + } + + get tbOverlayStyle(): { [klass: string]: any } { + return this._tbOverlayStyle; + } + preferredPlacement: PopoverPlacement = 'top'; strictPosition = false; origin!: CdkOverlayOrigin; @@ -485,6 +494,7 @@ export class TbPopoverComponent implements OnDestroy, OnInit { this.cdr.markForCheck(); } }, {threshold: [0.5]}); + private _tbOverlayStyle: { [klass: string]: any } = {}; constructor( public cdr: ChangeDetectorRef, From c443b5e8c689b4b1be13a3a29c5d9efcf029f933 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Apr 2025 16:59:37 +0300 Subject: [PATCH 099/112] Add max poll records for edge consumers. silent errors in case of processing edge events --- .../service/edge/EdgeMsgConstructorUtils.java | 8 +++--- .../service/edge/rpc/EdgeGrpcSession.java | 10 +++---- .../ttl/KafkaEdgeTopicsCleanUpService.java | 2 +- .../src/main/resources/thingsboard.yml | 27 ++++++++++++++----- .../server/queue/discovery/TopicService.java | 12 ++++----- .../server/queue/kafka/TbKafkaSettings.java | 2 +- .../provider/TbCoreQueueProducerProvider.java | 1 - 7 files changed, 38 insertions(+), 24 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 9bcd8fc373..d996a1dbc9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -507,7 +507,7 @@ public class EdgeMsgConstructorUtils { JsonObject data = entityData.getAsJsonObject(); builder.setPostTelemetryMsg(JsonConverter.convertToTelemetryProto(data.getAsJsonObject("data"), ts)); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to telemetry proto, entityData [{}]", tenantId, entityId, entityData, e); } break; case ATTRIBUTES_UPDATED: @@ -522,7 +522,7 @@ public class EdgeMsgConstructorUtils { builder.setPostAttributeScope(getScopeOfDefault(data)); builder.setAttributeTs(ts); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", tenantId, entityId, entityData, e); } break; case POST_ATTRIBUTES: @@ -533,7 +533,7 @@ public class EdgeMsgConstructorUtils { builder.setPostAttributeScope(getScopeOfDefault(data)); builder.setAttributeTs(ts); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to PostAttributesMsg, entityData [{}]", tenantId, entityId, entityData, e); } break; case ATTRIBUTES_DELETED: @@ -546,7 +546,7 @@ public class EdgeMsgConstructorUtils { attributeDeleteMsg.build(); builder.setAttributeDeleteMsg(attributeDeleteMsg); } catch (Exception e) { - log.warn("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e); + log.trace("[{}][{}] Can't convert to AttributeDeleteMsg proto, entityData [{}]", tenantId, entityId, entityData, e); } break; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index ecd0c6d9f3..9ce5973639 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -203,7 +203,7 @@ public abstract class EdgeGrpcSession implements Closeable { @Override public void onError(Throwable t) { - log.error("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); + log.trace("[{}][{}] Stream was terminated due to error:", tenantId, sessionId, t); closeSession(); } @@ -255,7 +255,7 @@ public abstract class EdgeGrpcSession implements Closeable { private void doSync(EdgeSyncCursor cursor) { if (cursor.hasNext()) { EdgeEventFetcher next = cursor.getNext(); - log.info("[{}][{}] starting sync process, cursor current idx = {}, class = {}", + log.debug("[{}][{}] starting sync process, cursor current idx = {}, class = {}", tenantId, edge.getId(), cursor.getCurrentIdx(), next.getClass().getSimpleName()); ListenableFuture> future = startProcessingEdgeEvents(next); Futures.addCallback(future, new FutureCallback<>() { @@ -651,7 +651,7 @@ public abstract class EdgeGrpcSession implements Closeable { default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction()); } } catch (Exception e) { - log.error("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); + log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); } if (downlinkMsg != null) { result.add(downlinkMsg); @@ -763,7 +763,7 @@ public abstract class EdgeGrpcSession implements Closeable { try { outputStream.onNext(responseMsg); } catch (Exception e) { - log.error("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); + log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); connected = false; sessionCloseListener.accept(edge, sessionId); } finally { @@ -909,7 +909,7 @@ public abstract class EdgeGrpcSession implements Closeable { } } catch (Exception e) { String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); - log.error("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); + log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); return Futures.immediateFailedFuture(e); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java index 73712542fd..c32ed92e31 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java @@ -62,7 +62,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { @Value("${sql.ttl.edge_events.edge_events_ttl:2628000}") private long ttlSeconds; - @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") + @Value("${queue.edge.event_notifications_topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; public KafkaEdgeTopicsCleanUpService(PartitionService partitionService, EdgeService edgeService, diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index ecd4ddebb7..b4d178a737 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1590,11 +1590,24 @@ queue: # tb_rule_engine.sq: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" + tb_edge: + # Properties for consumers targeting edge service update topics. + - key: max.poll.records + # Define the maximum number of records that can be polled from tb_edge topics per request. + value: "${TB_QUEUE_KAFKA_EDGE_EVENTS_MAX_POLL_RECORDS:10}" + tb_edge.notifications: + # Properties for consumers targeting high-priority edge notifications. + # These notifications include RPC calls, lifecycle events, and new queue messages, + # requiring minimal latency and swift processing. + - key: max.poll.records + # Define the maximum number of records that can be polled from tb_edge.notifications. topics. + value: "${TB_QUEUE_KAFKA_EDGE_HP_EVENTS_MAX_POLL_RECORDS:10}" tb_edge_event.notifications: - # Example of specific consumer properties value per topic for edge event + # Properties for consumers targeting downlinks meant for specific edge topics. + # Topic names are dynamically constructed using tenant and edge identifiers. - key: max.poll.records - # Example of specific consumer properties value per topic for edge event - value: "${TB_QUEUE_KAFKA_EDGE_EVENT_MAX_POLL_RECORDS:50}" + # Define the maximum number of records that can be polled from tb_edge_event.notifications.. topics. + value: "${TB_QUEUE_KAFKA_EDGE_NOTIFICATIONS_MAX_POLL_RECORDS:10}" tb_housekeeper: # Consumer properties for Housekeeper tasks topic - key: max.poll.records @@ -1841,11 +1854,13 @@ queue: # Interval in milliseconds to poll messages poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" edge: - # Default topic name + # Topic name to notify edge service on entity updates, assignment, etc. topic: "${TB_QUEUE_EDGE_TOPIC:tb_edge}" - # For high-priority notifications that require minimum latency and processing time + # Topic prefix for high-priority edge notifications (rpc, lifecycle, new messages in queue) that require minimum latency and processing time. + # Each tb-core has its own topic: . notifications_topic: "${TB_QUEUE_EDGE_NOTIFICATIONS_TOPIC:tb_edge.notifications}" - # For edge events messages + # Topic prefix for downlinks to be pushed to specific edge. + # Every edge has its own unique topic: .. event_notifications_topic: "${TB_QUEUE_EDGE_EVENT_NOTIFICATIONS_TOPIC:tb_edge_event.notifications}" # Amount of partitions used by Edge services partitions: "${TB_QUEUE_EDGE_PARTITIONS:10}" diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 42263138c1..4b6c0aa139 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -32,22 +32,22 @@ public class TopicService { @Value("${queue.prefix:}") private String prefix; - @Value("${queue.core.notifications-topic:tb_core.notifications}") + @Value("${queue.core.notifications_topic:tb_core.notifications}") private String tbCoreNotificationsTopic; - @Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}") + @Value("${queue.rule-engine.notifications_topic:tb_rule_engine.notifications}") private String tbRuleEngineNotificationsTopic; - @Value("${queue.transport.notifications-topics:tb_transport.notifications}") + @Value("${queue.transport.notifications_topics:tb_transport.notifications}") private String tbTransportNotificationsTopic; - @Value("${queue.edge.notifications-topic:tb_edge.notifications}") + @Value("${queue.edge.notifications_topic:tb_edge.notifications}") private String tbEdgeNotificationsTopic; - @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") + @Value("${queue.edge.event_notifications_topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; - @Value("${queue.calculated-fields.notifications-topic:calculated_field.notifications}") + @Value("${queue.calculated-fields.notifications_topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 181d422f16..d447d92cb3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -50,7 +50,7 @@ import java.util.Properties; @Component public class TbKafkaSettings { - private static final List DYNAMIC_TOPICS = List.of("tb_edge_event.notifications"); + private static final List DYNAMIC_TOPICS = List.of("tb_edge.notifications", "tb_edge_event.notifications"); @Value("${queue.kafka.bootstrap.servers}") private String servers; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java index 7c3e415e9f..98a3d78304 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbCoreQueueProducerProvider.java @@ -115,7 +115,6 @@ public class TbCoreQueueProducerProvider implements TbQueueProducerProvider { return toHousekeeper; } - @Override public TbQueueProducer> getTbEdgeMsgProducer() { return toEdge; From 68289044c578f05596abceda500625c06e6af545 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 9 Apr 2025 17:56:47 +0300 Subject: [PATCH 100/112] TbKafkaSettings - use startswith strategy for consumer props in case direct .get return null --- .../ttl/KafkaEdgeTopicsCleanUpService.java | 2 +- .../server/queue/discovery/TopicService.java | 12 +++++----- .../server/queue/kafka/TbKafkaSettings.java | 24 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java index c32ed92e31..73712542fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/KafkaEdgeTopicsCleanUpService.java @@ -62,7 +62,7 @@ public class KafkaEdgeTopicsCleanUpService extends AbstractCleanUpService { @Value("${sql.ttl.edge_events.edge_events_ttl:2628000}") private long ttlSeconds; - @Value("${queue.edge.event_notifications_topic:tb_edge_event.notifications}") + @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; public KafkaEdgeTopicsCleanUpService(PartitionService partitionService, EdgeService edgeService, diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 4b6c0aa139..7271e1cb10 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -32,22 +32,22 @@ public class TopicService { @Value("${queue.prefix:}") private String prefix; - @Value("${queue.core.notifications_topic:tb_core.notifications}") + @Value("${queue.core.notifications-topic:tb_core.notifications}") private String tbCoreNotificationsTopic; - @Value("${queue.rule-engine.notifications_topic:tb_rule_engine.notifications}") + @Value("${queue.rule-engine.notifications-topic:tb_rule_engine.notifications}") private String tbRuleEngineNotificationsTopic; - @Value("${queue.transport.notifications_topics:tb_transport.notifications}") + @Value("${queue.transport.notifications-topic:tb_transport.notifications}") private String tbTransportNotificationsTopic; - @Value("${queue.edge.notifications_topic:tb_edge.notifications}") + @Value("${queue.edge.notifications-topic:tb_edge.notifications}") private String tbEdgeNotificationsTopic; - @Value("${queue.edge.event_notifications_topic:tb_edge_event.notifications}") + @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; - @Value("${queue.calculated-fields.notifications_topic:calculated_field.notifications}") + @Value("${queue.calculated-fields.notifications-topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index d447d92cb3..82b5af179f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -50,8 +50,6 @@ import java.util.Properties; @Component public class TbKafkaSettings { - private static final List DYNAMIC_TOPICS = List.of("tb_edge.notifications", "tb_edge_event.notifications"); - @Value("${queue.kafka.bootstrap.servers}") private String servers; @@ -163,18 +161,20 @@ public class TbKafkaSettings { props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); - consumerPropertiesPerTopic - .getOrDefault(topic, Collections.emptyList()) - .forEach(kv -> props.put(kv.getKey(), kv.getValue())); - if (topic != null) { - DYNAMIC_TOPICS.stream() - .filter(topic::startsWith) - .findFirst() - .ifPresent(prefix -> consumerPropertiesPerTopic.getOrDefault(prefix, Collections.emptyList()) - .forEach(kv -> props.put(kv.getKey(), kv.getValue()))); + List properties = consumerPropertiesPerTopic.get(topic); + if (properties == null) { + for (Map.Entry> entry : consumerPropertiesPerTopic.entrySet()) { + if (topic.startsWith(entry.getKey())) { + properties = entry.getValue(); + break; + } + } + } + if (properties != null) { + properties.forEach(kv -> props.put(kv.getKey(), kv.getValue())); + } } - return props; } From 570a1e31c3746d951ecaf0d572f943ca9bb629c5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 9 Apr 2025 18:43:05 +0300 Subject: [PATCH 101/112] UI: Remove duplicate locales in zh_CN locale --- .../assets/locale/locale.constant-zh_CN.json | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 9819c65dba..aaf69bb984 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -7832,33 +7832,6 @@ "items-per-page-separator": "至" }, "language": { - "language": "语言", - "locales": { - "ar_AE": "العربية (الإمارات العربية المتحدة)", - "ca_ES": "català (Espanya)", - "cs_CZ": "čeština (Česko)", - "da_DK": "dansk (Danmark)", - "de_DE": "Deutsch (Deutschland)", - "el_GR": "Ελληνικά (Ελλάδα)", - "en_US": "English (United States)", - "es_ES": "español (España)", - "fa_IR": "فارسی (ایران)", - "fr_FR": "français (France)", - "it_IT": "italiano (Italia)", - "ja_JP": "日本語 (日本)", - "ka_GE": "ქართული (საქართველო)", - "ko_KR": "한국어 (대한민국)", - "lt_LT": "lietuvių (Lietuva)", - "lv_LV": "latviešu (Latvija)", - "nl_BE": "Nederlands (België)", - "pl_PL": "polski (Polska)", - "pt_BR": "português (Brasil)", - "ro_RO": "română (România)", - "sl_SI": "slovenščina (Slovenija)", - "tr_TR": "Türkçe (Türkiye)", - "uk_UA": "українська (Україна)", - "zh_CN": "中文 (中国)", - "zh_TW": "中文 (台灣)" - } + "language": "语言" } } From 7b57316b5f53b58ed938e03ff274b41cb321f403 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 1 Apr 2025 13:22:12 +0300 Subject: [PATCH 102/112] UI: Fix erase of externalId on save widget. --- .../src/app/modules/home/models/widget-component.models.ts | 5 ++++- ui-ngx/src/app/shared/models/widget.models.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 00afa2b68d..aee890a5b2 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 @@ -115,6 +115,7 @@ import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/co import { UtilsService } from '@core/services/utils.service'; import { CompiledTbFunction } from '@shared/models/js-function.models'; import { FormProperty } from '@shared/models/dynamic-form.models'; +import { ExportableEntity } from '@shared/models/base-data'; export interface IWidgetAction { name: string; @@ -573,7 +574,7 @@ export interface IDynamicWidgetComponent { [key: string]: any; } -export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { +export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor, ExportableEntity { widgetName: string; fullFqn: string; deprecated: boolean; @@ -681,6 +682,7 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({ fullFqn: fullWidgetTypeFqn(widgetTypeEntity), deprecated: widgetTypeEntity.deprecated, scada: widgetTypeEntity.scada, + externalId: widgetTypeEntity.externalId, type: widgetTypeEntity.descriptor.type, sizeX: widgetTypeEntity.descriptor.sizeX, sizeY: widgetTypeEntity.descriptor.sizeY, @@ -736,6 +738,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: name: widgetInfo.widgetName, deprecated: widgetInfo.deprecated, scada: widgetInfo.scada, + externalId: widgetInfo.externalId, descriptor }; }; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index ec55d4e8a3..c55dcdee37 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -206,7 +206,7 @@ export interface WidgetControllerDescriptor { actionSources?: {[actionSourceId: string]: WidgetActionSource}; } -export interface BaseWidgetType extends BaseData, HasTenantId, HasVersion { +export interface BaseWidgetType extends BaseData, HasTenantId, HasVersion, ExportableEntity { tenantId: TenantId; fqn: string; name: string; From ac3b389203abfeafbbb2959dcd8e8de71a13fc0c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 10 Apr 2025 10:51:11 +0300 Subject: [PATCH 103/112] UI: Add Echart in modules map --- ui-ngx/src/app/modules/common/modules-map.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 70432e8cdf..530a41f006 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -76,6 +76,8 @@ import * as TranslateCore from '@ngx-translate/core'; import * as MatDateTimePicker from '@mat-datetimepicker/core'; import _moment from 'moment'; import * as tslib from 'tslib'; +import * as Echarts from 'echarts'; +import * as EchartsCore from 'echarts/core'; import * as TbCore from '@core/public-api'; import * as TbShared from '@shared/public-api'; @@ -405,6 +407,8 @@ class ModulesMap implements IModulesMap { '@mat-datetimepicker/core': MatDateTimePicker, moment: _moment, tslib, + 'echarts': Echarts, + 'echarts/core': EchartsCore, '@core/public-api': TbCore, '@shared/public-api': TbShared, From ced71331efa6e3e840b557a0b555e1d786ac724c Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 1 Apr 2025 12:04:26 +0300 Subject: [PATCH 104/112] UI: JavaScript library (Module type) minor style enhancement: remove redundant space before required symbol, decrease help popover width (prod-5636). --- .../home/pages/admin/resource/js-resource.component.html | 3 +-- ui-ngx/src/app/shared/components/js-func.component.scss | 4 ++++ .../src/assets/help/en_US/resource/js-resource-module_fn.md | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html index 6287d27f2c..15d899df6b 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -91,8 +91,7 @@ + {{ 'javascript.module-script' | translate }} +that are exported for use in other parts of the application. ##### Examples You can declare variables: From 713050937c50151eb5c87bb65426f2071326835f Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 10 Apr 2025 10:53:09 +0300 Subject: [PATCH 105/112] UI: JavaScript resource help popup too wide width issue: use [helpPopupStyle]. --- .../home/pages/admin/resource/js-resource.component.html | 1 + ui-ngx/src/assets/help/en_US/resource/js-resource-module_fn.md | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html index 15d899df6b..62036a8ea1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -80,6 +80,7 @@ (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> -that are exported for use in other parts of the application. +These modules can contain any JavaScript code, facilitating the reuse of specific logic. This includes variables or functions that are exported for use in other parts of the application. ##### Examples You can declare variables: From 661e7eaa415ce5ad9c65bffeb1d5d3c44b0a55b0 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Thu, 10 Apr 2025 12:04:47 +0300 Subject: [PATCH 106/112] Fix cf check --- .../server/common/data/sync/ie/EntityExportData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java index 072be6acf2..a1692aef04 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java @@ -107,7 +107,7 @@ public class EntityExportData> { @JsonIgnore public boolean hasCalculatedFields() { - return calculatedFields != null; + return calculatedFields != null && !calculatedFields.isEmpty(); } } From 7d4ccad788a46e388c91306cb0d43392e8860787 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 10 Apr 2025 13:20:33 +0300 Subject: [PATCH 107/112] UI: New map widgets - introduce preserveAspectRatio option for shape image fill settings. --- .../lib/maps/data-layer/shapes-data-layer.ts | 34 ++++++++++++++----- .../widget/lib/maps/leaflet/leaflet-tb.ts | 10 +++++- ...e-fill-image-settings-panel.component.html | 5 +++ ...ape-fill-image-settings-panel.component.ts | 3 ++ ...pe-fill-stripe-settings-panel.component.ts | 2 +- .../shape-fill-stripe-settings.component.ts | 6 ++-- .../shared/models/widget/maps/map.models.ts | 4 +++ .../widget/lib/map/shape_fill_image_fn.md | 2 ++ .../assets/locale/locale.constant-en_US.json | 1 + ui-ngx/src/typings/leaflet-extend-tb.d.ts | 9 +++-- 10 files changed, 60 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts index c742f152d4..93502588b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/shapes-data-layer.ts @@ -49,9 +49,10 @@ interface ShapePatternInfo { image: string; width: number; height: number; - opacity?: number; - angle?: number; - scale?: number; + preserveAspectRatio: boolean; + opacity: number; + angle: number; + scale: number; }; fillStripe?: { weight: number; @@ -122,23 +123,35 @@ abstract class ShapePatternProcessor { fillOpacity: 1, stroke: false, fill: true, fillColor: patternInfo.fillColor}); pattern.addElement(fillRect); } else if (patternInfo.type === ShapeFillType.image) { - pattern = new L.TB.Pattern({ + const patternOptions: L.TB.PatternOptions = { width: 1, height: 1, patternUnits: 'objectBoundingBox', patternContentUnits: 'objectBoundingBox', - preserveAspectRatioAlign: 'xMidYMid', - preserveAspectRatioMeetOrSlice: 'slice', viewBox: [0,0,patternInfo.fillImage.width,patternInfo.fillImage.height] - }); - const imagePatternShape = new L.TB.PatternImage({ + }; + if (patternInfo.fillImage.preserveAspectRatio) { + patternOptions.preserveAspectRatioAlign = 'xMidYMid'; + patternOptions.preserveAspectRatioMeetOrSlice = 'slice'; + } else { + patternOptions.preserveAspectRatioAlign = 'none'; + } + pattern = new L.TB.Pattern(patternOptions); + const imagePatternOptions: L.TB.PatternImageOptions = { imageUrl: patternInfo.fillImage.image, width: patternInfo.fillImage.width, height: patternInfo.fillImage.height, opacity: patternInfo.fillImage.opacity, angle: patternInfo.fillImage.angle, scale: patternInfo.fillImage.scale - }); + }; + if (patternInfo.fillImage.preserveAspectRatio) { + imagePatternOptions.preserveAspectRatioAlign = 'xMidYMid'; + imagePatternOptions.preserveAspectRatioMeetOrSlice = 'slice'; + } else { + imagePatternOptions.preserveAspectRatioAlign = 'none'; + } + const imagePatternShape = new L.TB.PatternImage(imagePatternOptions); pattern.addElement(imagePatternShape); } else if (patternInfo.type === ShapeFillType.stripe) { const stripeInfo = patternInfo.fillStripe; @@ -221,6 +234,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor { const imageUrl = image?.url || '/assets/widget-preview-empty.svg'; + const preserveAspectRatio = isDefinedAndNotNull(image?.preserveAspectRatio) ? image.preserveAspectRatio : true; const opacity = isDefinedAndNotNull(image?.opacity) ? image.opacity : 1; const imagePipe = this.dataLayer.getCtx().$injector.get(ImagePipe); return loadImageWithAspect(imagePipe, imageUrl).pipe( @@ -241,6 +256,7 @@ class ShapeImagePatternProcessor extends ShapePatternProcessor implements L this._dom.setAttribute('y', '0'); this._dom.setAttribute('width', `${options.width}`); this._dom.setAttribute('height', `${options.height}`); - this._dom.setAttribute('preserveAspectRatio', 'xMidYMid slice'); + if (options.preserveAspectRatioAlign) { + let preserveAspectRatioValue = options.preserveAspectRatioAlign; + if (preserveAspectRatioValue !== 'none' && options.preserveAspectRatioMeetOrSlice) { + preserveAspectRatioValue += (' ' + options.preserveAspectRatioMeetOrSlice); + } + dom.setAttribute('preserveAspectRatio', preserveAspectRatioValue); + } else { + dom.removeAttribute('preserveAspectRatio'); + } const transforms: string[] = []; if (options.angle) { transforms.push(`rotate(${options.angle})`); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html index 660f43a75d..d2782d67cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.html @@ -30,6 +30,11 @@
    +
    + + {{ 'widgets.maps.data-layer.shape.preserve-aspect-ratio' | translate }} + +
    widgets.maps.data-layer.shape.opacity
    diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts index 0d14c5f81c..fef64d2e44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts @@ -53,6 +53,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit { { type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []], image: [this.shapeFillImageSettings?.image, [Validators.required]], + preserveAspectRatio: [this.shapeFillImageSettings?.preserveAspectRatio, []], opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]], angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]], scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]], @@ -83,6 +84,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit { const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value; if (type === ShapeFillImageType.image) { this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false}); @@ -90,6 +92,7 @@ export class ShapeFillImageSettingsPanelComponent implements OnInit { this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false}); } else { this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('preserveAspectRatio').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false}); this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts index c9df56338b..86d1ae67fd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts @@ -97,7 +97,7 @@ export class ShapeFillStripeSettingsPanelComponent implements OnInit { private updatePreview() { const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; - const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings); + const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings, 136, 118); this.stripePreviewStyle = { background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`) }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts index 63b2b4b212..678d57b1e3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts @@ -126,15 +126,15 @@ export class ShapeFillStripeSettingsComponent implements ControlValueAccessor { } } -export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings): string => { +export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings, previewWidth = 48, previewHeight = 48): string => { const weight = isDefinedAndNotNull(settings?.weight) ? settings.weight : 3; const spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9; const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45; const height = weight + spaceWeight; const color = settings?.color?.color || '#8f8f8f'; const spaceColor = settings?.spaceColor?.color || 'rgba(143,143,143,0)'; - const svgStr = ` - + const svgStr = ` + diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index e09d5e5756..0468fe1fab 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -501,6 +501,7 @@ export enum ShapeFillImageType { export interface ShapeFillImageSettings { type: ShapeFillImageType; image?: string; + preserveAspectRatio?: boolean; opacity?: number; // (0-1) angle?: number; // (0-360) scale?: number; // (0-...) @@ -550,6 +551,7 @@ export const defaultBasePolygonsDataLayerSettings = (mapType: MapType): Partial< fillImage: { type: ShapeFillImageType.image, image: '/assets/widget-preview-empty.svg', + preserveAspectRatio: true, opacity: 1, angle: 0, scale: 1 @@ -600,6 +602,7 @@ export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial Date: Thu, 10 Apr 2025 13:37:30 +0300 Subject: [PATCH 108/112] Updated gateway image version to stable (#13157) --- application/src/main/resources/thingsboard.yml | 2 +- .../controller/DeviceConnectivityControllerTest.java | 8 ++++++-- .../server/dao/device/DeviceConnectivityServiceImpl.java | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b4d178a737..716797c2d2 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1423,7 +1423,7 @@ device: pem_cert_file: "${DEVICE_CONNECTIVITY_COAPS_CA_ROOT_CERT:cafile.pem}" gateway: # The docker tag for thingsboard/tb-gateway image used in docker-compose file for gateway launch - image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:latest}" + image_version: "${DEVICE_CONNECTIVITY_GATEWAY_IMAGE_VERSION:3.7-stable}" # Edges parameters edges: diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java index cb475d7f16..4ccf40415d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -23,6 +23,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; @@ -94,6 +95,9 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { private DeviceProfileId mqttDeviceProfileId; private DeviceProfileId coapDeviceProfileId; + @Value("${device.connectivity.gateway.image_version:3.7-stable}") + private String gatewayImageVersion; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -298,7 +302,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "services:\n" + " # ThingsBoard IoT Gateway Service Configuration\n" + " tb-gateway:\n" + - " image: thingsboard/tb-gateway:latest\n" + + " image: thingsboard/tb-gateway:" + gatewayImageVersion + "\n" + " container_name: tb-gateway\n" + " restart: always\n" + "\n" + @@ -847,7 +851,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest { "-t \"application/json\" -e \"{temperature:25}\" coap://test.domain:5683/api/v1/%s/telemetry", credentials.getCredentialsId())); assertThat(linuxCoapCommands.get(COAPS).get(0).asText()).isEqualTo("curl -f -S -o " + CA_ROOT_CERT_PEM + " http://localhost:80/api/device-connectivity/coaps/certificate/download"); assertThat(linuxCoapCommands.get(COAPS).get(1).asText()).isEqualTo(String.format("coap-client-openssl -v 6 -m POST " + - "-R "+ CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); + "-R " + CA_ROOT_CERT_PEM + " -t \"application/json\" -e \"{temperature:25}\" coaps://test.domain:5684/api/v1/%s/telemetry", credentials.getCredentialsId())); JsonNode dockerCoapCommands = commands.get(COAP).get(DOCKER); assertThat(dockerCoapCommands.get(COAP).asText()).isEqualTo(String.format("docker run --rm -it " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java index eb7ce56b9f..6f22e95c87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -85,7 +85,7 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService private String mqttsPemCertFile; @Value("${device.connectivity.coaps.pem_cert_file:}") private String coapsPemCertFile; - @Value("${device.connectivity.gateway.image_version:latest}") + @Value("${device.connectivity.gateway.image_version:3.7-stable}") private String gatewayImageVersion; @Override From 6af342090e577016145a3023c04f0dd9782a6e1f Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Thu, 10 Apr 2025 15:30:20 +0300 Subject: [PATCH 109/112] Timewindow: added timezone selection in widget settings --- .../widget/config/timewindow-config-panel.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html index ff0acd5fca..138dde324e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html @@ -33,6 +33,7 @@ noMargin isEdit="true" alwaysDisplayTypePrefix + timezone="true" [historyOnly]="onlyHistoryTimewindow" quickIntervalOnly="{{ widgetType === widgetTypes.latest }}" aggregation="{{ widgetType === widgetTypes.timeseries }}" From 2abebbd352f6354d3920e207d84c21a40563ad2f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 10 Apr 2025 15:35:25 +0300 Subject: [PATCH 110/112] UI: Improved CSS styles for copy-code button in markdown --- .../device-check-connectivity-dialog.component.scss | 1 - .../src/app/shared/components/markdown.component.scss | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss index 123bcfb708..eca728893c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss @@ -124,7 +124,6 @@ background: #F3F6FA; border-color: #305680; padding-right: 38px; - overflow: scroll; padding-bottom: 4px; min-height: 42px; scrollbar-width: thin; diff --git a/ui-ngx/src/app/shared/components/markdown.component.scss b/ui-ngx/src/app/shared/components/markdown.component.scss index 4989a77021..65ad64b189 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.scss +++ b/ui-ngx/src/app/shared/components/markdown.component.scss @@ -270,7 +270,7 @@ outline: none; position: absolute; width: 206px; - height: 42px; + height: 32px; top: 0; right: 32px; background: 0 0; @@ -281,11 +281,11 @@ user-select: none; &.multiline { - right: 44px; + right: 38px; } p { - padding: 8px; + padding: 8px 8px 0; top: 1px; transition: .2s; color: #2a7dec; @@ -301,10 +301,10 @@ background-color: #fff; position: absolute; width: 38px; - height: 38px; + height: 28px; top: 3px; right: 3px; - padding: 10px; + padding: 10px 10px 0; img { position: initial; From 8567dd0bd24d2535f80211a53a01e8848698888d Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 10 Apr 2025 17:41:19 +0300 Subject: [PATCH 111/112] Fix partition change events handling by consumer services --- ...faultTbCalculatedFieldConsumerService.java | 2 -- .../queue/DefaultTbCoreConsumerService.java | 2 -- .../queue/DefaultTbEdgeConsumerService.java | 5 +---- .../DefaultTbRuleEngineConsumerService.java | 2 -- .../processing/AbstractConsumerService.java | 6 ++++-- ...AbstractPartitionBasedConsumerService.java | 19 ++++++++++++++++--- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 125a06299d..e3bc3be90d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.queue; import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -70,7 +69,6 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent -@Slf4j public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBasedConsumerService implements TbCalculatedFieldConsumerService { @Value("${queue.calculated_fields.poll_interval:25}") diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index a3003ba6ff..e115e4f7b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -118,7 +117,6 @@ import java.util.stream.Collectors; @Service @TbCoreComponent -@Slf4j public class DefaultTbCoreConsumerService extends AbstractConsumerService implements TbCoreConsumerService { @Value("${queue.core.poll-interval}") diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java index 2e4c15f8e8..d3dc2932f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java @@ -19,8 +19,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; @@ -45,12 +43,12 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdgeNotificationMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.provider.TbCoreQueueFactory; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeContextComponent; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.queue.processing.AbstractConsumerService; import org.thingsboard.server.service.queue.processing.IdMsgPair; @@ -66,7 +64,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -@Slf4j @Service @TbCoreComponent public class DefaultTbEdgeConsumerService extends AbstractConsumerService implements TbEdgeConsumerService { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 98f857750a..f51678310e 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.queue; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; @@ -65,7 +64,6 @@ import java.util.stream.Collectors; @Service @TbRuleEngineComponent -@Slf4j public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedConsumerService implements TbRuleEngineConsumerService { private final TbRuleEngineConsumerContext ctx; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index d12bd896ff..26b689df9d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -17,7 +17,8 @@ package org.thingsboard.server.service.queue.processing; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -62,10 +63,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -@Slf4j @RequiredArgsConstructor public abstract class AbstractConsumerService extends TbApplicationEventListener { + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final ActorSystemContext actorContext; protected final TbTenantProfileCache tenantProfileCache; protected final TbDeviceProfileCache deviceProfileCache; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java index 97aa81d41c..f42908bcb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java @@ -28,6 +28,8 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -35,7 +37,7 @@ public abstract class AbstractPartitionBasedConsumerService pendingEvents = new ArrayList<>(); public AbstractPartitionBasedConsumerService(ActorSystemContext actorContext, TbTenantProfileCache tenantProfileCache, @@ -61,8 +63,16 @@ public abstract class AbstractPartitionBasedConsumerService Date: Thu, 10 Apr 2025 18:00:22 +0300 Subject: [PATCH 112/112] UI: Add change detection when updating dashboard state to prevent rare cases when compiled template in markdown widget is not updated. --- .../home/components/dashboard-page/dashboard-page.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 9490db52a4..60d05d4c86 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1118,6 +1118,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.dashboardCtx.aliasController.dashboardStateChanged(); this.isRightLayoutOpened = openRightLayout ? true : false; this.updateLayouts(layoutsData); + this.cd.markForCheck(); } setTimeout(() => { this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened);