From 5afd7cbfaec236aa271343e8cffabd314f9dd7d1 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Apr 2026 18:51:59 +0300 Subject: [PATCH 1/4] Expose http-utils functions via WidgetContext.httpUtils Added IWidgetHttpUtils interface and httpUtils property to WidgetContext so widget action code can use defaultHttpOptionsFromConfig and related functions without modifying ThingsBoard source code. --- ui-ngx/src/app/core/api/widget-api.models.ts | 9 +++++++++ ui-ngx/src/app/core/http/http-utils.ts | 11 ++++++++++- .../home/models/widget-component.models.ts | 19 ++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index f92db80cc4..12f5bc60d8 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -67,6 +67,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { MatDialogRef } from '@angular/material/dialog'; import { TbUnit } from '@shared/models/unit.models'; import { UnitService } from '@core/services/unit.service'; +import { HttpOptionsResult, HttpUploadOptionsResult, QueryParams, RequestConfig } from '@core/http/http-utils'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -94,6 +95,14 @@ export interface IWidgetUtils { getEntityDetailsPageURL: (id: string, entityType: EntityType) => string; } +export interface IWidgetHttpUtils { + defaultHttpOptions: (ignoreLoading?: boolean, ignoreErrors?: boolean, resendRequest?: boolean, queryParams?: QueryParams) => HttpOptionsResult; + defaultHttpOptionsFromConfig: (config?: RequestConfig) => HttpOptionsResult; + defaultHttpOptionsFromParams: (queryParams?: QueryParams, config?: RequestConfig) => HttpOptionsResult; + defaultHttpUploadOptions: (ignoreLoading?: boolean, ignoreErrors?: boolean, resendRequest?: boolean, queryParams?: QueryParams) => HttpUploadOptionsResult; + createDefaultHttpOptions: (queryParamsOrConfig?: QueryParams | RequestConfig, config?: RequestConfig) => HttpOptionsResult; +} + export interface PlaceMapItemActionData { action: WidgetAction | WidgetActionDescriptor; additionalParams?: any; diff --git a/ui-ngx/src/app/core/http/http-utils.ts b/ui-ngx/src/app/core/http/http-utils.ts index 8b90821c09..98f65f924a 100644 --- a/ui-ngx/src/app/core/http/http-utils.ts +++ b/ui-ngx/src/app/core/http/http-utils.ts @@ -15,7 +15,7 @@ /// import { InterceptorHttpParams } from '../interceptors/interceptor-http-params'; -import { HttpHeaders } from '@angular/common/http'; +import { HttpHeaders, HttpParams } from '@angular/common/http'; import { InterceptorConfig } from '../interceptors/interceptor-config'; export type QueryParams = { [param:string]: any }; @@ -27,6 +27,15 @@ export interface RequestConfig { queryParams?: QueryParams; } +export interface HttpOptionsResult { + headers: HttpHeaders; + params: HttpParams; +} + +export interface HttpUploadOptionsResult { + params: HttpParams; +} + export function hasRequestConfig(config?: any): boolean { if (!config) { return false; 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 4cf6eb68ec..a9d57a0d41 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 @@ -38,6 +38,7 @@ import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import { IAliasController, IStateController, + IWidgetHttpUtils, IWidgetSubscription, IWidgetUtils, RpcApi, @@ -58,6 +59,13 @@ import { ViewContainerRef } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { + createDefaultHttpOptions, + defaultHttpOptions, + defaultHttpOptionsFromConfig, + defaultHttpOptionsFromParams, + defaultHttpUploadOptions +} from '@core/http/http-utils'; import { RafService } from '@core/services/raf.service'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { TenantId } from '@shared/models/id/tenant-id'; @@ -68,7 +76,8 @@ import { formatValue, getEntityDetailsPageURL, hasDatasourceLabelsVariables, - isDefined, isDefinedAndNotNull + isDefined, + isDefinedAndNotNull } from '@core/utils'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -294,6 +303,14 @@ export class WidgetContext { getEntityDetailsPageURL }; + httpUtils: IWidgetHttpUtils = { + defaultHttpOptions, + defaultHttpOptionsFromConfig, + defaultHttpOptionsFromParams, + defaultHttpUploadOptions, + createDefaultHttpOptions + }; + $widgetElement: JQuery; $container: JQuery; $containerParent: JQuery; From a5244922ecf624d450b314bee96d70003cee82cf Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Apr 2026 17:39:24 +0300 Subject: [PATCH 2/4] Fix missing 'type' property in alarm rule condition on save When a NO_DATA filter is present, the condition type control gets disabled. Angular's FormGroup.value excludes disabled controls, causing the 'type' field to be omitted from the saved result. This leads to a JSON parse error on the backend. Explicitly include the type control value in the save output while keeping unit/value/count excluded when disabled. --- .../alarm-rules/cf-alarm-rule-condition-dialog.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index 49f8e92458..f2f27c3b8b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -332,7 +332,10 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent Date: Wed, 8 Apr 2026 18:24:35 +0300 Subject: [PATCH 3/4] Add roundDown option to ShortNumberPipe Add optional roundDown parameter that uses Math.floor instead of Math.round when abbreviating numbers. This prevents values like 3993979 from rounding up to 4M, showing 3.9M instead. Also improve typing: replace any with ShortNumberArgs interface and ensure transform always returns string. --- .../src/app/shared/pipe/short-number.pipe.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts index 0fda04844f..ea57709688 100644 --- a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts @@ -16,20 +16,26 @@ import { Pipe, PipeTransform } from '@angular/core'; +export interface ShortNumberArgs { + long?: boolean; + roundDown?: boolean; +} + @Pipe({ - name: 'shortNumber', - standalone: false + name: 'shortNumber', + standalone: false }) export class ShortNumberPipe implements PipeTransform { - transform(number: number, args?: any): any { - if (isNaN(number)) return 0; - if (number === null) return 0; - if (number === 0) return 0; + transform(number: number, args?: ShortNumberArgs): string { + if (isNaN(number)) return '0'; + if (number === null) return '0'; + if (number === 0) return '0'; let abs = Math.abs(number); const rounder = Math.pow(10, 1); const isNegative = number < 0; - const isLong = args && args.long; + const isLong = args?.long; + const roundDown = args?.roundDown; let key = ''; const powers = [ @@ -42,7 +48,7 @@ export class ShortNumberPipe implements PipeTransform { for (let i = 0; i < powers.length; i++) { let reduced = abs / powers[i].value; - reduced = Math.round(reduced * rounder) / rounder; + reduced = (roundDown ? Math.floor : Math.round)(reduced * rounder) / rounder; if (reduced >= 1) { abs = reduced; key = isLong ? powers[i].longKey : powers[i].key; From 284c7d9d3062870181140f1acc1f4be72092e1d2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Apr 2026 19:04:34 +0300 Subject: [PATCH 4/4] Use Number.isFinite for input validation Replaces separate isNaN/null checks with a single Number.isFinite guard that also handles Infinity/-Infinity. --- ui-ngx/src/app/shared/pipe/short-number.pipe.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts index ea57709688..fedf958067 100644 --- a/ui-ngx/src/app/shared/pipe/short-number.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/short-number.pipe.ts @@ -28,9 +28,9 @@ export interface ShortNumberArgs { export class ShortNumberPipe implements PipeTransform { transform(number: number, args?: ShortNumberArgs): string { - if (isNaN(number)) return '0'; - if (number === null) return '0'; - if (number === 0) return '0'; + if (!Number.isFinite(number) || number === 0) { + return '0'; + } let abs = Math.abs(number); const rounder = Math.pow(10, 1); const isNegative = number < 0;