29 changed files with 1136 additions and 79 deletions
@ -0,0 +1,71 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div [formGroup]="limitForm" class="tb-form-table-row overflow-hidden items-start"> |
||||
|
<div class="flex-shrink capitalize" [class]="isPanelView ? 'basis-10' : 'basis-1/5'"> |
||||
|
{{ labelKey | translate}} |
||||
|
</div> |
||||
|
<div class="flex-grow flex-shrink flex flex-col gap-2 gt-sm:flex-row gt-sm:gap-3" [class]="isPanelView ? 'basis-1/2' : 'basis-40%'"> |
||||
|
<mat-form-field class="tb-inline-field flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="type"> |
||||
|
<mat-option *ngFor="let type of ValueSourceTypes" [value]="type"> |
||||
|
{{ ValueSourceTypeTranslation.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-entity-alias-input |
||||
|
required |
||||
|
class="flex-1" |
||||
|
*ngIf="limitForm.get('type').value === ValueSourceType.entity" |
||||
|
[aliasController]="aliasController" |
||||
|
formControlName="entityAlias"> |
||||
|
</tb-entity-alias-input> |
||||
|
</div> |
||||
|
<div class="flex-grow flex-shrink" [class]="isPanelView ? 'basis-1/2' : 'basis-40%'"> |
||||
|
<mat-form-field *ngIf="limitForm.get('type').value === ValueSourceType.constant" |
||||
|
appearance="outline" class="tb-inline-field !w-full number" subscriptSizing="dynamic" |
||||
|
[class.tb-suffix-absolute]="!limitForm.get('value').value"> |
||||
|
<input matInput formControlName="value" type="number" placeholder="{{ 'widgets.chart.chart-axis.scale-auto' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-data-key-input |
||||
|
required |
||||
|
*ngIf="limitForm.get('type').value === ValueSourceType.latestKey" |
||||
|
requiredText="widgets.chart.chart-axis.key-required" |
||||
|
[datasourceType]="datasource?.type" |
||||
|
[entityAliasId]="datasource?.entityAliasId" |
||||
|
[deviceId]="datasource?.deviceId" |
||||
|
[aliasController]="aliasController" |
||||
|
[dataKeyType]="datasource?.type === DatasourceType.function ? DataKeyType.function : null" |
||||
|
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
||||
|
[callbacks]="callbacks" |
||||
|
[editable]="false" |
||||
|
formControlName="value"> |
||||
|
</tb-data-key-input> |
||||
|
<tb-data-key-input |
||||
|
required |
||||
|
*ngIf="limitForm.get('type').value === ValueSourceType.entity" |
||||
|
requiredText="widgets.chart.chart-axis.entity-key-required" |
||||
|
[datasourceType]="DatasourceType.entity" |
||||
|
[aliasController]="aliasController" |
||||
|
[entityAlias]="limitForm.get('entityAlias').value" |
||||
|
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]" |
||||
|
[callbacks]="callbacks" |
||||
|
[editable]="false" |
||||
|
formControlName="value"> |
||||
|
</tb-data-key-input> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,15 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
@ -0,0 +1,204 @@ |
|||||
|
///
|
||||
|
/// 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, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormGroup, ValidationErrors, Validator, |
||||
|
Validators, |
||||
|
} from '@angular/forms'; |
||||
|
import { ValueSourceType, ValueSourceTypes, ValueSourceTypeTranslation } from '@shared/models/widget-settings.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { AxisLimitConfig } from '@home/components/widget/lib/chart/time-series-chart.models'; |
||||
|
import { Datasource, DatasourceType, } from '@shared/models/widget.models'; |
||||
|
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { IAliasController } from '@core/api/widget-api.models'; |
||||
|
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; |
||||
|
import { isEqual } from '@core/utils'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-axis-scale-row', |
||||
|
templateUrl: './axis-scale-row.component.html', |
||||
|
styleUrl: './axis-scale-row.component.scss', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AxisScaleRowComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AxisScaleRowComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
] |
||||
|
}) |
||||
|
export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@Input() |
||||
|
isPanelView = false; |
||||
|
|
||||
|
@Input() |
||||
|
aliasController: IAliasController; |
||||
|
|
||||
|
@Input() |
||||
|
callbacks: DataKeysCallbacks; |
||||
|
|
||||
|
@Input() |
||||
|
datasource: Datasource; |
||||
|
|
||||
|
@Input() |
||||
|
labelKey: string; |
||||
|
|
||||
|
ValueSourceType = ValueSourceType; |
||||
|
|
||||
|
DataKeyType = DataKeyType; |
||||
|
|
||||
|
DatasourceType = DatasourceType; |
||||
|
|
||||
|
ValueSourceTypeTranslation = ValueSourceTypeTranslation; |
||||
|
|
||||
|
ValueSourceTypes = ValueSourceTypes; |
||||
|
|
||||
|
limitForm: UntypedFormGroup; |
||||
|
|
||||
|
private propagateChanges: (value: any) => void = () => {}; |
||||
|
|
||||
|
private modelValue: AxisLimitConfig | null = null; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.limitForm = this.fb.group({ |
||||
|
type: [ValueSourceType.constant], |
||||
|
value: [null], |
||||
|
entityAlias: [null] |
||||
|
}); |
||||
|
this.subscribeToTypeChanges(); |
||||
|
|
||||
|
this.limitForm.valueChanges |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe(value => { |
||||
|
this.updateValidators(); |
||||
|
this.updateView(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
writeValue(value: AxisLimitConfig) { |
||||
|
this.modelValue = value; |
||||
|
|
||||
|
if (!this.limitForm) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (value) { |
||||
|
this.limitForm.patchValue({ |
||||
|
type: value.type ?? ValueSourceType.constant, |
||||
|
value: value.value ?? null, |
||||
|
entityAlias: value.entityAlias ?? null |
||||
|
}, |
||||
|
{ emitEvent: false } |
||||
|
); |
||||
|
} else { |
||||
|
this.limitForm.patchValue({ |
||||
|
type: ValueSourceType.constant, |
||||
|
value: null, |
||||
|
entityAlias: null |
||||
|
}, |
||||
|
{ emitEvent: false } |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any) { |
||||
|
this.propagateChanges = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any) { |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.limitForm.valid ? null : { |
||||
|
axisLimitForm: { |
||||
|
valid: false, |
||||
|
errors: this.getFormErrors() |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private getFormErrors(): any { |
||||
|
const errors: any = {}; |
||||
|
Object.keys(this.limitForm.controls).forEach(key => { |
||||
|
const control = this.limitForm.get(key); |
||||
|
if (control && control.errors) { |
||||
|
errors[key] = control.errors; |
||||
|
} |
||||
|
}); |
||||
|
return errors; |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
const axisTypeControl = this.limitForm.get('type'); |
||||
|
const axisValueControl = this.limitForm.get('value'); |
||||
|
const axisEntityAliasControl = this.limitForm.get('entityAlias'); |
||||
|
|
||||
|
if (axisTypeControl && axisValueControl && axisEntityAliasControl) { |
||||
|
const type = axisTypeControl.value; |
||||
|
if (type === ValueSourceType.latestKey || type === ValueSourceType.entity) { |
||||
|
axisValueControl.setValidators([Validators.required]); |
||||
|
} else { |
||||
|
axisValueControl.clearValidators(); |
||||
|
} |
||||
|
|
||||
|
if (type === ValueSourceType.entity) { |
||||
|
axisEntityAliasControl.setValidators([Validators.required]); |
||||
|
} else { |
||||
|
axisEntityAliasControl.clearValidators(); |
||||
|
} |
||||
|
|
||||
|
axisValueControl.updateValueAndValidity({ emitEvent: false }); |
||||
|
axisEntityAliasControl.updateValueAndValidity({ emitEvent: false }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private subscribeToTypeChanges() { |
||||
|
this.limitForm.controls.type.valueChanges |
||||
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
||||
|
.subscribe(() => { |
||||
|
const axisValueControl = this.limitForm.get('value'); |
||||
|
const axisEntityAliasControl = this.limitForm.get('entityAlias'); |
||||
|
if (axisValueControl) { |
||||
|
axisValueControl.setValue(null, { emitEvent: true }); |
||||
|
} |
||||
|
if (axisEntityAliasControl) { |
||||
|
axisEntityAliasControl.setValue(null, { emitEvent: true }); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateView(value: AxisLimitConfig) { |
||||
|
if (!isEqual(this.modelValue, value)) { |
||||
|
this.modelValue = value; |
||||
|
this.propagateChanges(value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue