diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index dc3d539acb..17a473c8b8 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -33,7 +33,6 @@ import { svgIcons, svgIconsUrl } from '@shared/models/icon.models'; import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; import { SETTINGS_KEY } from '@core/settings/settings.effects'; import { initCustomJQueryEvents } from '@shared/models/jquery-event.models'; -import { UnitService } from '@core/services/unit.service'; @Component({ selector: 'tb-root', @@ -47,8 +46,7 @@ export class AppComponent { private translate: TranslateService, private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer, - private authService: AuthService, - private unitService: UnitService) { + private authService: AuthService) { console.log(`ThingsBoard Version: ${env.tbVersion}`); @@ -96,14 +94,12 @@ export class AppComponent { this.store.select(selectUserReady).pipe( filter((data) => data.isUserLoaded), tap((data) => { - const userDetails = getCurrentAuthState(this.store).userDetails; - let userLang = userDetails?.additionalInfo?.lang ?? null; + let userLang = getCurrentAuthState(this.store).userDetails?.additionalInfo?.lang ?? null; if (!userLang && !data.isAuthenticated) { const settings = this.storageService.getItem(SETTINGS_KEY); userLang = settings?.userLang ?? null; } this.notifyUserLang(userLang); - this.unitService.setUnitSystem(userDetails?.additionalInfo?.unitSystem); }), skip(1), ).subscribe((data) => { 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 3338cffa25..8c4120739e 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -62,10 +62,11 @@ import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { PopoverPlacement } from '@shared/components/popover.models'; import { PersistentRpc } from '@shared/models/rpc.models'; -import { EventEmitter, Injector } from '@angular/core'; +import { EventEmitter } from '@angular/core'; 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'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -234,8 +235,8 @@ export class WidgetSubscriptionContext { utils: UtilsService; dashboardUtils: DashboardUtilsService; raf: RafService; + unitService: UnitService; widgetUtils: IWidgetUtils; - $injector: Injector; getServerTimeDiff: () => Observable; } diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index d3a851016d..e86685bfe1 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -1419,7 +1419,7 @@ export class WidgetSubscription implements IWidgetSubscription { if (this.displayLegend) { const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : this.decimals; const units = isNotEmptyTbUnits(dataKey.units) ? dataKey.units : this.units; - const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, {decimals, units}) + const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.unitService, {decimals, units}) const legendKey: LegendKey = { dataKey, dataIndex: dataKeyIndex, diff --git a/ui-ngx/src/app/core/services/unit.service.ts b/ui-ngx/src/app/core/services/unit.service.ts index 5d60e80390..c68115f1a3 100644 --- a/ui-ngx/src/app/core/services/unit.service.ts +++ b/ui-ngx/src/app/core/services/unit.service.ts @@ -32,6 +32,10 @@ import { import { isNotEmptyStr, isObject } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { selectAuth, selectIsAuthenticated } from '@core/auth/auth.selectors'; +import { filter, switchMap, take } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -41,12 +45,19 @@ export class UnitService { private currentUnitSystem: UnitSystem = UnitSystem.METRIC; private converter: Converter; - constructor(private translate: TranslateService) { + constructor(private translate: TranslateService, + private store: Store) { this.translate.onLangChange.pipe( takeUntilDestroyed() ).subscribe(() => { this.converter = getUnitConverter(this.translate); }); + this.store.select(selectIsAuthenticated).pipe( + filter((data) => data), + switchMap(() => this.store.select(selectAuth).pipe(take(1))) + ).subscribe((data) => { + this.setUnitSystem(data.userDetails?.additionalInfo?.unitSystem) + }) } getUnitSystem(): UnitSystem { @@ -65,8 +76,8 @@ export class UnitService { return this.converter?.listUnits(measure, unitSystem); } - getUnitsGroupedByMeasure(measure?: AllMeasures, unitSystem?: UnitSystem): UnitInfoGroupByMeasure { - return this.converter?.unitsGroupByMeasure(measure, unitSystem); + getUnitsGroupedByMeasure(measure?: AllMeasures, unitSystem?: UnitSystem, tagFilter?: string): UnitInfoGroupByMeasure { + return this.converter?.unitsGroupByMeasure(measure, unitSystem, tagFilter); } getUnitInfo(symbol: AllMeasuresUnits | string): UnitInfo { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html index f1c8787fff..9c32e5f74e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html @@ -48,7 +48,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts index 87b2fbbc48..2fee7a3a27 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts @@ -118,10 +118,6 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective; } - get supportsUnitConversion(): boolean { - return this.widgetConfigComponent.modelValue?.typeParameters?.supportsUnitConversion ?? false; - } - private propagateChange = (_val: any) => {}; constructor(private fb: UntypedFormBuilder, @@ -220,7 +216,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn hideDataKeyName: true, hideDataKeyLabel: true, hideDataKeyColor: true, - supportsUnitConversion: this.supportsUnitConversion + supportsUnitConversion: true } }).afterClosed().subscribe((updatedDataKey) => { if (updatedDataKey) { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html index 4b4dcd148d..307645822d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html @@ -100,7 +100,7 @@
widget-config.units-short
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html index f0d36264e9..608284996e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html @@ -84,7 +84,7 @@
widget-config.units-short
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 d5a0b78ce5..fed2615bbd 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 @@ -105,6 +105,7 @@ import { ExceptionData } from '@shared/models/error.models'; import { WidgetComponentService } from './widget-component.service'; import { Timewindow } from '@shared/models/time/time.models'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { UnitService } from '@core/services/unit.service'; import { DashboardService } from '@core/http/dashboard.service'; import { WidgetSubscription } from '@core/api/widget-subscription'; import { EntityService } from '@core/http/entity.service'; @@ -216,6 +217,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, private dashboardUtils: DashboardUtilsService, private mobileService: MobileService, private raf: RafService, + private unitService: UnitService, private ngZone: NgZone, private cd: ChangeDetectorRef, private http: HttpClient) { @@ -341,8 +343,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.subscriptionContext.utils = this.utils; this.subscriptionContext.dashboardUtils = this.dashboardUtils; this.subscriptionContext.raf = this.raf; + this.subscriptionContext.unitService = this.unitService; this.subscriptionContext.widgetUtils = this.widgetContext.utils; - this.subscriptionContext.$injector = this.injector; this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); this.widgetComponentService.getWidgetInfo(this.widget.typeFullFqn).subscribe({ diff --git a/ui-ngx/src/app/shared/components/unit-input.component.ts b/ui-ngx/src/app/shared/components/unit-input.component.ts index ff807e0e6f..ad63bee626 100644 --- a/ui-ngx/src/app/shared/components/unit-input.component.ts +++ b/ui-ngx/src/app/shared/components/unit-input.component.ts @@ -34,7 +34,9 @@ import { Observable, of, shareReplay } from 'rxjs'; import { AllMeasures, getSourceTbUnitSymbol, + getTbUnitFromSearch, isTbUnitMapping, + searchUnit, TbUnit, UnitInfo, UnitSystem @@ -43,7 +45,7 @@ import { map, mergeMap } from 'rxjs/operators'; import { UnitService } from '@core/services/unit.service'; import { TbPopoverService } from '@shared/components/popover.service'; import { UnitSettingsPanelComponent } from '@shared/components/unit-settings-panel.component'; -import { isDefinedAndNotNull, isEqual, isNotEmptyStr } from '@core/utils'; +import { isDefinedAndNotNull, isEqual } from '@core/utils'; @Component({ selector: 'tb-unit-input', @@ -200,7 +202,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang hostView: this.viewContainerRef, preferredPlacement: ['left', 'bottom', 'top'], context: { - unit: this.extractTbUnit(this.unitsFormControl.value), + unit: getTbUnitFromSearch(this.unitsFormControl.value), required: this.required, disabled: this.disabled, tagFilter: this.tagFilter, @@ -217,7 +219,7 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang } private updateModel(value: UnitInfo | TbUnit ) { - let res = this.extractTbUnit(value); + let res = getTbUnitFromSearch(value); if (this.onlySystemUnits && !isTbUnitMapping(res)) { const unitInfo = this.unitService.getUnitInfo(res as string); if (unitInfo) { @@ -238,106 +240,17 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit, OnChang private fetchUnits(searchText?: string): Observable]>> { this.searchText = searchText; return this.getGroupedUnits().pipe( - map(unit => this.searchUnit(unit, searchText)) + map(unit => searchUnit(unit, searchText)) ); } private getGroupedUnits(): Observable]>> { if (this.fetchUnits$ === null) { - this.fetchUnits$ = of(this.unitService.getUnitsGroupedByMeasure(this.measure, this.unitSystem)).pipe( - map(data => { - let objectData = Object.entries(data) as Array<[AllMeasures, UnitInfo[]]>; - - if (this.tagFilter) { - objectData = objectData - .map((measure) => [measure[0], measure[1].filter(u => u.tags.includes(this.tagFilter))] as [AllMeasures, UnitInfo[]]) - .filter((measure) => measure[1].length > 0); - } - return objectData; - }), + this.fetchUnits$ = of(this.unitService.getUnitsGroupedByMeasure(this.measure, this.unitSystem, this.tagFilter)).pipe( + map(data => Object.entries(data) as Array<[AllMeasures, UnitInfo[]]>), shareReplay(1) ); } return this.fetchUnits$; } - - private searchUnit(units: Array<[AllMeasures, Array]>, searchText?: string): Array<[AllMeasures, Array]> { - if (isNotEmptyStr(searchText)) { - const filterValue = searchText.trim().toUpperCase(); - - const scoredGroups = units - .map(([measure, unitInfos]) => { - const scoredUnits = unitInfos - .map(unit => ({ - unit, - score: this.calculateRelevanceScore(unit, filterValue) - })) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score) - .map(({ unit }) => unit); - - let groupScore = scoredUnits.length > 0 - ? Math.max(...scoredUnits.map(unit => this.calculateRelevanceScore(unit, filterValue))) - : 0; - - if (measure.toUpperCase() === filterValue) { - groupScore += 200; - } - - return { measure, units: scoredUnits, groupScore }; - }) - .filter(group => group.units.length > 0) - .sort((a, b) => { - if (b.groupScore !== a.groupScore) { - return b.groupScore - a.groupScore; - } - return b.units.length - a.units.length; - }); - - return scoredGroups.map(group => [group.measure, group.units] as [AllMeasures, Array]); - } - return units; - } - - private calculateRelevanceScore(unit: UnitInfo, filterValue: string): number { - const name = unit.name.toUpperCase(); - const abbr = unit.abbr.toUpperCase(); - const tags = unit.tags.map(tag => tag.toUpperCase()); - - let score = 0; - - if (name === filterValue || abbr === filterValue) { - score += 100; - } else if (tags.includes(filterValue)) { - score += 80; - } else if (name.startsWith(filterValue) || abbr.startsWith(filterValue)) { - score += 60; - } else if (tags.some(tag => tag.startsWith(filterValue))) { - score += 50; - } else if (tags.some(tag => tag.includes(filterValue))) { - score += 30; - } - - if (score > 0) { - score += Math.max(0, 10 - (name.length + abbr.length) / 2); - } - - return score; - } - - private extractTbUnit(value: TbUnit | UnitInfo | null): TbUnit { - if (value === null) { - return null; - } - if (value === undefined) { - return undefined; - } - if (typeof value === 'string') { - return value; - } - if ('abbr' in value) { - return value.abbr; - } - return value; - } } diff --git a/ui-ngx/src/app/shared/models/unit.models.ts b/ui-ngx/src/app/shared/models/unit.models.ts index 94a831afa0..7d91e59441 100644 --- a/ui-ngx/src/app/shared/models/unit.models.ts +++ b/ui-ngx/src/app/shared/models/unit.models.ts @@ -104,7 +104,7 @@ import voltage, { VoltageUnits } from '@shared/models/units/voltage'; import volume, { VolumeUnits } from '@shared/models/units/volume'; import volumeFlow, { VolumeFlowUnits } from '@shared/models/units/volume-flow'; import { TranslateService } from '@ngx-translate/core'; -import { isNotEmptyStr } from '@core/utils'; +import { deepClone, isNotEmptyStr } from '@core/utils'; export type AllMeasuresUnits = | AbsorbedDoseRateUnits @@ -548,7 +548,7 @@ export class Converter { return results; } - unitsGroupByMeasure(measureName?: AllMeasures, unitSystem?: UnitSystem): UnitInfoGroupByMeasure { + unitsGroupByMeasure(measureName?: AllMeasures, unitSystem?: UnitSystem, tagFilter?: string): UnitInfoGroupByMeasure { const results: UnitInfoGroupByMeasure = {}; const measures = measureName @@ -573,9 +573,15 @@ export class Converter { } for (const abbr of Object.keys(units) as AllMeasuresUnits[]) { - results[name].push(this.describe(abbr)); + const unitInfo = this.describe(abbr); + if (!tagFilter || unitInfo.tags.includes(tagFilter)) { + results[name].push(unitInfo); + } } } + if (!results[name].length) { + delete results[name]; + } } return results; } @@ -641,7 +647,7 @@ function buildUnitCache(measures: Record { if (typeof unit !== 'object' || unit === null) return false; return isNotEmptyStr(unit.from); }; + + +export const searchUnit = + (units: Array<[AllMeasures, Array]>, searchText?: string): Array<[AllMeasures, Array]> => { + if (isNotEmptyStr(searchText)) { + const filterValue = searchText.trim().toUpperCase(); + + const scoredGroups = units + .map(([measure, unitInfos]) => { + const scoredUnits = unitInfos + .map(unit => ({ + unit, + score: calculateRelevanceScore(unit, filterValue) + })) + .filter(({score}) => score > 0) + .sort((a, b) => b.score - a.score) + .map(({unit}) => unit); + + let groupScore = scoredUnits.length > 0 + ? Math.max(...scoredUnits.map(unit => calculateRelevanceScore(unit, filterValue))) + : 0; + + if (measure.toUpperCase() === filterValue) { + groupScore += 200; + } + + return {measure, units: scoredUnits, groupScore}; + }) + .filter(group => group.units.length > 0) + .sort((a, b) => { + if (b.groupScore !== a.groupScore) { + return b.groupScore - a.groupScore; + } + return b.units.length - a.units.length; + }); + + return scoredGroups.map(group => [group.measure, group.units] as [AllMeasures, Array]); + } + return units; + } + +function calculateRelevanceScore (unit: UnitInfo, filterValue: string): number{ + const name = unit.name.toUpperCase(); + const abbr = unit.abbr.toUpperCase(); + const tags = unit.tags.map(tag => tag.toUpperCase()); + let score = 0; + + if (name === filterValue || abbr === filterValue) { + score += 100; + } else if (tags.includes(filterValue)) { + score += 80; + } else if (name.startsWith(filterValue) || abbr.startsWith(filterValue)) { + score += 60; + } else if (tags.some(tag => tag.startsWith(filterValue))) { + score += 50; + } else if (tags.some(tag => tag.includes(filterValue))) { + score += 30; + } + + if (score > 0) { + score += Math.max(0, 10 - (name.length + abbr.length) / 2); + } + + return score; +} + +export const getTbUnitFromSearch = (value: TbUnit | UnitInfo | null): TbUnit => { + if (value === null) { + return null; + } + if (value === undefined) { + return undefined; + } + if (typeof value === 'string') { + return value; + } + if ('abbr' in value) { + return value.abbr; + } + return value; +} diff --git a/ui-ngx/src/app/shared/models/widget-settings.models.ts b/ui-ngx/src/app/shared/models/widget-settings.models.ts index 971c888991..193a18cf89 100644 --- a/ui-ngx/src/app/shared/models/widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/widget-settings.models.ts @@ -875,15 +875,16 @@ export abstract class ValueFormatProcessor { protected hideZeroDecimals: boolean; protected unitSymbol: string; - static fromSettings($injector: Injector, settings: ValueFormatSettings): ValueFormatProcessor { + static fromSettings($injector: Injector, settings: ValueFormatSettings): ValueFormatProcessor; + static fromSettings(unitService: UnitService, settings: ValueFormatSettings): ValueFormatProcessor; + static fromSettings(unitServiceOrInjector: Injector | UnitService, settings: ValueFormatSettings): ValueFormatProcessor { if (settings.units !== null && typeof settings.units === 'object') { - return new UnitConverterValueFormatProcessor($injector, settings) + return new UnitConverterValueFormatProcessor(unitServiceOrInjector, settings) } - return new SimpleValueFormatProcessor($injector, settings); + return new SimpleValueFormatProcessor(settings); } - protected constructor(protected $injector: Injector, - protected settings: ValueFormatSettings) { + protected constructor(protected settings: ValueFormatSettings) { } abstract format(value: any): string; @@ -908,9 +909,8 @@ export class SimpleValueFormatProcessor extends ValueFormatProcessor { private readonly isDefinedUnit: boolean; - constructor(protected $injector: Injector, - protected settings: ValueFormatSettings) { - super($injector, settings); + constructor(protected settings: ValueFormatSettings) { + super(settings); this.unitSymbol = !settings.ignoreUnitSymbol && isNotEmptyStr(settings.units) ? (settings.units as string) : null; this.isDefinedDecimals = isDefinedAndNotNull(settings.decimals); this.hideZeroDecimals = !settings.showZeroDecimals; @@ -928,10 +928,10 @@ export class UnitConverterValueFormatProcessor extends ValueFormatProcessor { private readonly unitConverter: TbUnitConverter; - constructor(protected $injector: Injector, + constructor(protected unitServiceOrInjector: Injector | UnitService, protected settings: ValueFormatSettings) { - super($injector, settings); - const unitService = this.$injector.get(UnitService); + super(settings); + const unitService = this.unitServiceOrInjector instanceof UnitService ? this.unitServiceOrInjector : this.unitServiceOrInjector.get(UnitService); const unit = settings.units; this.unitSymbol = settings.ignoreUnitSymbol ? null : unitService.getTargetUnitSymbol(unit); this.unitConverter = unitService.geUnitConverter(unit);