UI: Add unit conversion in dashboard filter
This commit is contained in:
parent
19405416be
commit
59f23ba25d
@ -26,24 +26,36 @@
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<div mat-dialog-content>
|
||||
<fieldset [disabled]="isLoading$ | async" class="flex flex-col">
|
||||
<mat-checkbox formControlName="editable" style="margin-bottom: 16px;">
|
||||
<div class="tb-form-panel no-padding no-border">
|
||||
<div class="tb-form-row">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="editable">
|
||||
<div tb-hint-tooltip-icon="{{ 'filter.editable-hint' | translate }}">
|
||||
{{ 'filter.editable' | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="flex flex-row items-center justify-start gap-2">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>filter.display-label</mat-label>
|
||||
<input matInput formControlName="label">
|
||||
</mat-form-field>
|
||||
<mat-checkbox formControlName="autogeneratedLabel">
|
||||
{{ 'filter.autogenerated-label' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label translate>filter.order-priority</mat-label>
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div class="tb-form-row space-between">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="autogeneratedLabel">
|
||||
<div tb-hint-tooltip-icon="{{ 'filter.custom-label-hint' | translate }}">
|
||||
{{ 'filter.custom-label' | translate }}
|
||||
</div>
|
||||
</mat-slide-toggle>
|
||||
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="medium-width flex-xs">
|
||||
<input matInput placeholder="{{ 'filter.display-label' | translate}}" formControlName="label">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="tb-form-row space-between">
|
||||
<div class="min-w-44" translate>filter.order-priority</div>
|
||||
<mat-form-field subscriptSizing="dynamic" appearance="outline" class="medium-width flex-xs">
|
||||
<input matInput type="number" formControlName="order">
|
||||
</mat-form-field>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="tb-form-row space-between" *ngIf="showUniInput">
|
||||
<div class="min-w-44" translate>filter.unit</div>
|
||||
<tb-unit-input supportsUnitConversion formControlName="unit" class="medium-width flex-xs">
|
||||
</tb-unit-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="flex items-center justify-end">
|
||||
<button mat-button color="primary"
|
||||
|
||||
@ -14,18 +14,21 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
|
||||
import { Component, Inject, SkipSelf } from '@angular/core';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
|
||||
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogComponent } from '@app/shared/components/dialog.component';
|
||||
import {
|
||||
BooleanOperation, createDefaultFilterPredicateUserInfo,
|
||||
EntityKeyValueType, generateUserFilterValueLabel,
|
||||
KeyFilterPredicateUserInfo, NumericOperation,
|
||||
BooleanOperation,
|
||||
createDefaultFilterPredicateUserInfo,
|
||||
EntityKeyValueType,
|
||||
generateUserFilterValueLabel,
|
||||
KeyFilterPredicateUserInfo,
|
||||
NumericOperation,
|
||||
StringOperation
|
||||
} from '@shared/models/query/query.models';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -47,11 +50,12 @@ export interface FilterUserInfoDialogData {
|
||||
})
|
||||
export class FilterUserInfoDialogComponent extends
|
||||
DialogComponent<FilterUserInfoDialogComponent, KeyFilterPredicateUserInfo>
|
||||
implements OnInit, ErrorStateMatcher {
|
||||
implements ErrorStateMatcher {
|
||||
|
||||
filterUserInfoFormGroup: UntypedFormGroup;
|
||||
|
||||
submitted = false;
|
||||
showUniInput = false;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
protected router: Router,
|
||||
@ -68,10 +72,14 @@ export class FilterUserInfoDialogComponent extends
|
||||
{
|
||||
editable: [userInfo.editable],
|
||||
label: [userInfo.label],
|
||||
autogeneratedLabel: [userInfo.autogeneratedLabel],
|
||||
autogeneratedLabel: [!userInfo.autogeneratedLabel],
|
||||
order: [userInfo.order]
|
||||
}
|
||||
);
|
||||
if (this.data.valueType === EntityKeyValueType.NUMERIC) {
|
||||
this.showUniInput = true;
|
||||
this.filterUserInfoFormGroup.addControl('unit', this.fb.control(userInfo.unit), {emitEvent: false});
|
||||
}
|
||||
this.onAutogeneratedLabelChange();
|
||||
if (!this.data.readonly) {
|
||||
this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.pipe(
|
||||
@ -85,7 +93,7 @@ export class FilterUserInfoDialogComponent extends
|
||||
}
|
||||
|
||||
private onAutogeneratedLabelChange() {
|
||||
const autogeneratedLabel: boolean = this.filterUserInfoFormGroup.get('autogeneratedLabel').value;
|
||||
const autogeneratedLabel: boolean = !this.filterUserInfoFormGroup.get('autogeneratedLabel').value;
|
||||
if (autogeneratedLabel) {
|
||||
const generatedLabel = generateUserFilterValueLabel(this.data.key, this.data.valueType, this.data.operation, this.translate);
|
||||
this.filterUserInfoFormGroup.get('label').patchValue(generatedLabel, {emitEvent: false});
|
||||
@ -95,9 +103,6 @@ export class FilterUserInfoDialogComponent extends
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
|
||||
const customErrorState = !!(control && control.invalid && this.submitted);
|
||||
@ -112,6 +117,7 @@ export class FilterUserInfoDialogComponent extends
|
||||
this.submitted = true;
|
||||
if (this.filterUserInfoFormGroup.valid) {
|
||||
const keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo = this.filterUserInfoFormGroup.getRawValue();
|
||||
keyFilterPredicateUserInfo.autogeneratedLabel = !keyFilterPredicateUserInfo.autogeneratedLabel;;
|
||||
this.dialogRef.close(keyFilterPredicateUserInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<form [formGroup]="userFilterFormGroup" (ngSubmit)="save()" style="width: 400px;">
|
||||
<form [formGroup]="userFilterFormGroup" (ngSubmit)="save()" style="width: 400px;" class="user-filter-dialog">
|
||||
<mat-toolbar color="primary">
|
||||
<h2>{{ filter.filter }}</h2>
|
||||
<span class="flex-1"></span>
|
||||
@ -42,6 +42,10 @@
|
||||
<mat-form-field class="mat-block">
|
||||
<mat-label>{{ userInputControl.get('label').value }}</mat-label>
|
||||
<input required type="number" matInput [formControl]="userInputControl.get('value')">
|
||||
<div matSuffix class="mr-2"
|
||||
[class.!hidden]="!userInputControl.get('unitSymbol').value">
|
||||
{{ userInputControl.get('unitSymbol').value }}
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
:host ::ng-deep {
|
||||
.user-filter-dialog {
|
||||
.mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix.mat-form-field-hide-placeholder {
|
||||
.mat-mdc-form-field-icon-suffix {
|
||||
place-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-form-field.mat-mdc-form-field-has-icon-suffix {
|
||||
.mat-mdc-form-field-icon-suffix {
|
||||
place-self: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,31 +14,26 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { Component, DestroyRef, Inject, OnInit, SkipSelf } from '@angular/core';
|
||||
import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import {
|
||||
AbstractControl, UntypedFormArray,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
UntypedFormGroup,
|
||||
FormGroupDirective,
|
||||
NgForm,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { FormArray, FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { DialogComponent } from '@app/shared/components/dialog.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
EntityKeyValueType,
|
||||
Filter, FilterPredicateValue,
|
||||
Filter,
|
||||
FilterPredicateValue,
|
||||
filterToUserFilterInfoList,
|
||||
UserFilterInputInfo
|
||||
} from '@shared/models/query/query.models';
|
||||
import { isDefinedAndNotNull } from '@core/utils';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { UnitService } from '@core/services/unit.service';
|
||||
import { getSourceTbUnitSymbol, TbUnitConverter } from '@shared/models/unit.models';
|
||||
|
||||
export interface UserFilterDialogData {
|
||||
filter: Filter;
|
||||
@ -48,14 +43,14 @@ export interface UserFilterDialogData {
|
||||
selector: 'tb-user-filter-dialog',
|
||||
templateUrl: './user-filter-dialog.component.html',
|
||||
providers: [{provide: ErrorStateMatcher, useExisting: UserFilterDialogComponent}],
|
||||
styleUrls: []
|
||||
styleUrls: ['./user-filter-dialog.component.scss'],
|
||||
})
|
||||
export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogComponent, Filter>
|
||||
implements OnInit, ErrorStateMatcher {
|
||||
implements ErrorStateMatcher {
|
||||
|
||||
filter: Filter;
|
||||
|
||||
userFilterFormGroup: UntypedFormGroup;
|
||||
userFilterFormGroup: FormGroup;
|
||||
|
||||
valueTypeEnum = EntityKeyValueType;
|
||||
|
||||
@ -66,14 +61,15 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
|
||||
@Inject(MAT_DIALOG_DATA) public data: UserFilterDialogData,
|
||||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
|
||||
public dialogRef: MatDialogRef<UserFilterDialogComponent, Filter>,
|
||||
private fb: UntypedFormBuilder,
|
||||
public translate: TranslateService,
|
||||
private destroyRef: DestroyRef) {
|
||||
private fb: FormBuilder,
|
||||
private translate: TranslateService,
|
||||
private destroyRef: DestroyRef,
|
||||
private unitService: UnitService) {
|
||||
super(store, router, dialogRef);
|
||||
this.filter = data.filter;
|
||||
const userInputs = filterToUserFilterInfoList(this.filter, translate);
|
||||
const userInputs = filterToUserFilterInfoList(this.filter, this.translate);
|
||||
|
||||
const userInputControls: Array<AbstractControl> = [];
|
||||
const userInputControls: Array<FormGroup> = [];
|
||||
for (const userInput of userInputs) {
|
||||
userInputControls.push(this.createUserInputFormControl(userInput));
|
||||
}
|
||||
@ -83,12 +79,21 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
|
||||
});
|
||||
}
|
||||
|
||||
private createUserInputFormControl(userInput: UserFilterInputInfo): AbstractControl {
|
||||
private createUserInputFormControl(userInput: UserFilterInputInfo): FormGroup {
|
||||
const predicateValue: FilterPredicateValue<string | number | boolean> = (userInput.info.keyFilterPredicate as any).value;
|
||||
const value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue;
|
||||
let value = isDefinedAndNotNull(predicateValue.userValue) ? predicateValue.userValue : predicateValue.defaultValue;
|
||||
let unitSymbol = '';
|
||||
let valueConvertor: TbUnitConverter;
|
||||
if (userInput.valueType === EntityKeyValueType.NUMERIC) {
|
||||
unitSymbol = this.unitService.getTargetUnitSymbol(userInput.unit);
|
||||
const sourceUnit = getSourceTbUnitSymbol(userInput.unit);
|
||||
value = this.unitService.convertUnitValue(value as number, userInput.unit);
|
||||
valueConvertor = this.unitService.geUnitConverter(unitSymbol, sourceUnit);
|
||||
}
|
||||
const userInputControl = this.fb.group({
|
||||
label: [userInput.label],
|
||||
valueType: [userInput.valueType],
|
||||
unitSymbol: [unitSymbol],
|
||||
value: [value,
|
||||
userInput.valueType === EntityKeyValueType.NUMERIC ||
|
||||
userInput.valueType === EntityKeyValueType.DATE_TIME ? [Validators.required] : []]
|
||||
@ -96,19 +101,20 @@ export class UserFilterDialogComponent extends DialogComponent<UserFilterDialogC
|
||||
userInputControl.get('value').valueChanges.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(userValue => {
|
||||
(userInput.info.keyFilterPredicate as any).value.userValue = userValue;
|
||||
let value = userValue;
|
||||
if (valueConvertor) {
|
||||
value = valueConvertor(value as number);
|
||||
}
|
||||
(userInput.info.keyFilterPredicate as any).value.userValue = value;
|
||||
});
|
||||
return userInputControl;
|
||||
}
|
||||
|
||||
userInputsFormArray(): UntypedFormArray {
|
||||
return this.userFilterFormGroup.get('userInputs') as UntypedFormArray;
|
||||
userInputsFormArray(): FormArray {
|
||||
return this.userFilterFormGroup.get('userInputs') as FormArray;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
|
||||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
|
||||
const customErrorState = !!(control && control.invalid && this.submitted);
|
||||
return originalErrorState || customErrorState;
|
||||
|
||||
@ -37,6 +37,7 @@ import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { UserId } from '../id/user-id';
|
||||
import { Direction } from '@shared/models/page/sort-order';
|
||||
import { TbUnit } from '@shared/models/unit.models';
|
||||
|
||||
export enum EntityKeyType {
|
||||
ATTRIBUTE = 'ATTRIBUTE',
|
||||
@ -179,7 +180,8 @@ export function createDefaultFilterPredicateUserInfo(): KeyFilterPredicateUserIn
|
||||
editable: true,
|
||||
label: '',
|
||||
autogeneratedLabel: true,
|
||||
order: 0
|
||||
order: 0,
|
||||
unit: ''
|
||||
};
|
||||
}
|
||||
|
||||
@ -373,6 +375,7 @@ export interface KeyFilterPredicateUserInfo {
|
||||
label: string;
|
||||
autogeneratedLabel: boolean;
|
||||
order?: number;
|
||||
unit?: TbUnit;
|
||||
}
|
||||
|
||||
export interface KeyFilterPredicateInfo {
|
||||
@ -621,6 +624,7 @@ export interface UserFilterInputInfo {
|
||||
label: string;
|
||||
valueType: EntityKeyValueType;
|
||||
info: KeyFilterPredicateInfo;
|
||||
unit: TbUnit;
|
||||
}
|
||||
|
||||
export function filterToUserFilterInfoList(filter: Filter, translate: TranslateService): Array<UserFilterInputInfo> {
|
||||
@ -654,6 +658,7 @@ export function predicateInfoToUserFilterInfoList(key: EntityKey,
|
||||
const userInput: UserFilterInputInfo = {
|
||||
info: predicateInfo,
|
||||
label: predicateInfo.userInfo.label,
|
||||
unit: predicateInfo.userInfo.unit,
|
||||
valueType
|
||||
};
|
||||
if (predicateInfo.userInfo.autogeneratedLabel) {
|
||||
@ -882,7 +887,7 @@ export function entityDataToEntityInfo(entityData: EntityData): EntityInfo {
|
||||
if (additionalInfoJson && additionalInfoJson.description) {
|
||||
entityInfo.entityDescription = additionalInfoJson.description;
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {/**/}
|
||||
}
|
||||
}
|
||||
if (fields.queueName && fields.serviceId) {
|
||||
|
||||
@ -2951,6 +2951,7 @@
|
||||
"missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.",
|
||||
"filter": "Filter",
|
||||
"editable": "Editable",
|
||||
"editable-hint": "Allow user to change the filter value in dashboards.",
|
||||
"no-filters-found": "No filters found.",
|
||||
"no-filter-text": "No filter specified",
|
||||
"add-filter-prompt": "Please add filter",
|
||||
@ -2991,7 +2992,9 @@
|
||||
"user-parameters": "User parameters",
|
||||
"display-label": "Label to display",
|
||||
"autogenerated-label": "Auto generate label",
|
||||
"order-priority": "Field order priority",
|
||||
"custom-label": "Custom label",
|
||||
"custom-label-hint": "Enable to set your own label for the filter. When disabled, a label will be generated automatically.",
|
||||
"order-priority": "Display order",
|
||||
"key-filter": "Key filter",
|
||||
"key-filters": "Key filters",
|
||||
"key-name": "Key name",
|
||||
@ -3035,7 +3038,8 @@
|
||||
"switch-to-dynamic-value": "Switch to dynamic value",
|
||||
"switch-to-default-value": "Switch to default value",
|
||||
"inherit-owner": "Inherit from owner",
|
||||
"source-attribute-not-set": "If source attribute isn't set"
|
||||
"source-attribute-not-set": "If source attribute isn't set",
|
||||
"unit": "Unit"
|
||||
},
|
||||
"fullscreen": {
|
||||
"expand": "Expand to fullscreen",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user