UI: Refactoring after review

This commit is contained in:
Vladyslav_Prykhodko 2025-05-16 14:01:44 +03:00
parent be4407c3ca
commit 8b14e72f8c
12 changed files with 137 additions and 131 deletions

View File

@ -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) => {

View File

@ -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<number>;
}

View File

@ -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,

View File

@ -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<AppState>) {
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<AllMeasures> {
return this.converter?.unitsGroupByMeasure(measure, unitSystem);
getUnitsGroupedByMeasure(measure?: AllMeasures, unitSystem?: UnitSystem, tagFilter?: string): UnitInfoGroupByMeasure<AllMeasures> {
return this.converter?.unitsGroupByMeasure(measure, unitSystem, tagFilter);
}
getUnitInfo(symbol: AllMeasuresUnits | string): UnitInfo {

View File

@ -48,7 +48,7 @@
</mat-form-field>
<div class="tb-units-field">
<tb-unit-input
[supportsUnitConversion]="supportsUnitConversion"
supportsUnitConversion
formControlName="units">
</tb-unit-input>
</div>

View File

@ -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) {

View File

@ -100,7 +100,7 @@
<div class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-unit-input
supportsUnitConversion=""
supportsUnitConversion
formControlName="units">
</tb-unit-input>
</div>

View File

@ -84,7 +84,7 @@
<div class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-unit-input
supportsUnitConversion=""
supportsUnitConversion
formControlName="units">
</tb-unit-input>
</div>

View File

@ -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({

View File

@ -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<Array<[AllMeasures, Array<UnitInfo>]>> {
this.searchText = searchText;
return this.getGroupedUnits().pipe(
map(unit => this.searchUnit(unit, searchText))
map(unit => searchUnit(unit, searchText))
);
}
private getGroupedUnits(): Observable<Array<[AllMeasures, Array<UnitInfo>]>> {
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<UnitInfo>]>, searchText?: string): Array<[AllMeasures, Array<UnitInfo>]> {
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<UnitInfo>]);
}
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;
}
}

View File

@ -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<AllMeasures> {
unitsGroupByMeasure(measureName?: AllMeasures, unitSystem?: UnitSystem, tagFilter?: string): UnitInfoGroupByMeasure<AllMeasures> {
const results: UnitInfoGroupByMeasure<AllMeasures> = {};
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<AllMeasures, TbMeasure<AllMeasuresUnits
}
export function getUnitConverter(translate: TranslateService): Converter {
const unitCache = buildUnitCache(allMeasures, translate);
const unitCache = buildUnitCache(deepClone(allMeasures), translate);
return new Converter(allMeasures, unitCache);
}
@ -669,3 +675,84 @@ export const isTbUnitMapping = (unit: any): boolean => {
if (typeof unit !== 'object' || unit === null) return false;
return isNotEmptyStr(unit.from);
};
export const searchUnit =
(units: Array<[AllMeasures, Array<UnitInfo>]>, searchText?: string): Array<[AllMeasures, Array<UnitInfo>]> => {
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<UnitInfo>]);
}
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;
}

View File

@ -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);