dashboardjavacloudcoapiotiot-analyticsiot-platformiot-solutionskafkalwm2mmicroservicesmiddlewaremqttnettyplatformsnmpthingsboardvisualizationwebsocketswidgets
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
9.5 KiB
323 lines
9.5 KiB
///
|
|
/// Copyright © 2016-2026 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, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
|
import {
|
|
AbstractControl,
|
|
ControlValueAccessor,
|
|
FormBuilder,
|
|
NG_VALIDATORS,
|
|
NG_VALUE_ACCESSOR,
|
|
ValidationErrors,
|
|
Validator,
|
|
ValidatorFn,
|
|
Validators
|
|
} from '@angular/forms';
|
|
import { TimeUnit, timeUnitTranslations } from '@home/components/rule-node/rule-node-config.models';
|
|
import { isDefinedAndNotNull, isNumeric } from '@core/utils';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
|
|
import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models';
|
|
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
|
|
|
|
interface TimeUnitInputModel {
|
|
time: number;
|
|
timeUnit: TimeUnit
|
|
}
|
|
|
|
@Component({
|
|
selector: 'tb-time-unit-input',
|
|
templateUrl: './time-unit-input.component.html',
|
|
providers: [{
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => TimeUnitInputComponent),
|
|
multi: true
|
|
},{
|
|
provide: NG_VALIDATORS,
|
|
useExisting: forwardRef(() => TimeUnitInputComponent),
|
|
multi: true
|
|
}]
|
|
})
|
|
export class TimeUnitInputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
|
|
|
|
@Input()
|
|
labelText: string;
|
|
|
|
@Input()
|
|
hintText: string;
|
|
|
|
@Input()
|
|
@coerceBoolean()
|
|
required: boolean;
|
|
|
|
@Input()
|
|
requiredText: string;
|
|
|
|
@Input()
|
|
@coerceNumber()
|
|
minTime = 0;
|
|
|
|
@Input()
|
|
minErrorText: string;
|
|
|
|
@Input()
|
|
@coerceNumber()
|
|
maxTime: number;
|
|
|
|
@Input()
|
|
maxErrorText: string;
|
|
|
|
@Input()
|
|
@coerceNumber()
|
|
stepMultipleOf: number;
|
|
|
|
@Input()
|
|
stepMultipleOfErrorText: string;
|
|
|
|
@Input()
|
|
subscriptSizing: SubscriptSizing = 'fixed';
|
|
|
|
@Input()
|
|
appearance: MatFormFieldAppearance = 'fill';
|
|
|
|
@Input()
|
|
@coerceBoolean()
|
|
inlineField: boolean;
|
|
|
|
@Input()
|
|
@coerceBoolean()
|
|
sameWidthInputs: boolean = false;
|
|
|
|
@Input()
|
|
containerClass: string | string[] | Record<string, boolean | undefined | null> = "flex gap-4";
|
|
|
|
timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
|
|
|
|
timeUnitTranslations = timeUnitTranslations;
|
|
|
|
timeInputForm = this.fb.group({
|
|
time: [0],
|
|
timeUnit: [TimeUnit.SECONDS]
|
|
});
|
|
|
|
private timeIntervalsInSec = new Map<TimeUnit, number>([
|
|
[TimeUnit.DAYS, DAY/SECOND],
|
|
[TimeUnit.HOURS, HOUR/SECOND],
|
|
[TimeUnit.MINUTES, MINUTE/SECOND],
|
|
[TimeUnit.SECONDS, SECOND/SECOND],
|
|
]);
|
|
|
|
minValueValidator = 0;
|
|
|
|
private modelValue: number;
|
|
|
|
private propagateChange: (value: any) => void = () => {};
|
|
|
|
constructor(private fb: FormBuilder,
|
|
private destroyRef: DestroyRef) {
|
|
}
|
|
|
|
ngOnInit() {
|
|
if (isDefinedAndNotNull(this.maxTime)) {
|
|
this.updatedAllowTimeUnitInterval(this.maxTime);
|
|
}
|
|
|
|
this.refreshTimeValidators();
|
|
|
|
this.timeInputForm.get('timeUnit').valueChanges.pipe(
|
|
takeUntilDestroyed(this.destroyRef)
|
|
).subscribe(() => {
|
|
this.refreshTimeValidators();
|
|
this.timeInputForm.get('time').updateValueAndValidity({onlySelf: true});
|
|
this.timeInputForm.get('time').markAsTouched({onlySelf: true});
|
|
});
|
|
|
|
this.timeInputForm.valueChanges.pipe(
|
|
takeUntilDestroyed(this.destroyRef)
|
|
).subscribe(value => {
|
|
this.updatedModel(value);
|
|
});
|
|
}
|
|
|
|
get minValue(): number {
|
|
return Math.ceil(this.minTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value));
|
|
}
|
|
|
|
get hasError(): string {
|
|
if (this.timeInputForm.get('time').hasError('required') && this.requiredText) {
|
|
return this.requiredText;
|
|
} else if (this.timeInputForm.get('time').hasError('min') && this.minErrorText) {
|
|
return this.minErrorText;
|
|
} else if (this.timeInputForm.get('time').hasError('max') && this.maxErrorText) {
|
|
return this.maxErrorText;
|
|
} else if (this.timeInputForm.get('time').hasError('stepMultipleOf') && this.stepMultipleOfErrorText) {
|
|
return this.stepMultipleOfErrorText;
|
|
}
|
|
}
|
|
|
|
ngOnChanges(changes: SimpleChanges): void {
|
|
for (const propName of Object.keys(changes)) {
|
|
const change = changes[propName];
|
|
if (!change.firstChange && change.currentValue !== change.previousValue) {
|
|
if (propName === 'maxTime') {
|
|
if (isDefinedAndNotNull(this.maxTime)) {
|
|
this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
|
|
this.updatedAllowTimeUnitInterval(this.maxTime);
|
|
}
|
|
}
|
|
if (['minTime', 'maxTime', 'required', 'stepMultipleOf'].includes(propName)) {
|
|
this.refreshTimeValidators();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
registerOnChange(fn: any) {
|
|
this.propagateChange = fn;
|
|
}
|
|
|
|
registerOnTouched(_fn: any) {
|
|
}
|
|
|
|
setDisabledState(isDisabled: boolean) {
|
|
if (isDisabled) {
|
|
this.timeInputForm.disable({emitEvent: false});
|
|
} else {
|
|
this.timeInputForm.enable({emitEvent: false});
|
|
if(!this.timeInputForm.valid) {
|
|
setTimeout(() => this.updatedModel(this.timeInputForm.value, true))
|
|
}
|
|
}
|
|
}
|
|
|
|
writeValue(sec: number) {
|
|
if (sec !== this.modelValue) {
|
|
if (isDefinedAndNotNull(sec) && isNumeric(sec) && Number(sec) !== 0) {
|
|
this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: true});
|
|
this.modelValue = sec;
|
|
} else {
|
|
this.timeInputForm.patchValue({
|
|
time: 0,
|
|
timeUnit: TimeUnit.SECONDS
|
|
}, {emitEvent: false});
|
|
this.modelValue = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
validate(): ValidationErrors | null {
|
|
return this.timeInputForm.disabled || this.timeInputForm.valid ? null : {
|
|
timeInput: false
|
|
};
|
|
}
|
|
|
|
private updatedModel(value: Partial<TimeUnitInputModel>, forceUpdated = false) {
|
|
const time = isDefinedAndNotNull(value.time) ? value.time * this.timeIntervalsInSec.get(value.timeUnit) : null;
|
|
if (this.modelValue !== time || forceUpdated) {
|
|
this.modelValue = time;
|
|
this.propagateChange(time);
|
|
}
|
|
}
|
|
|
|
private parseTime(value: number): TimeUnitInputModel {
|
|
for (const [timeUnit, timeValue] of this.timeIntervalsInSec) {
|
|
const calc = value / timeValue;
|
|
if (Number.isInteger(calc)) {
|
|
return {
|
|
time: calc,
|
|
timeUnit: timeUnit
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private createStepMultipleOfValidator(): ValidatorFn {
|
|
return (control: AbstractControl): ValidationErrors | null => {
|
|
const time = control.value;
|
|
if (!isDefinedAndNotNull(time) || !isNumeric(time)) {
|
|
return null;
|
|
}
|
|
const numericTime = Number(time);
|
|
if (numericTime === 0) {
|
|
return null;
|
|
}
|
|
|
|
const timeUnit = control.parent?.get('timeUnit')?.value as TimeUnit;
|
|
if (!timeUnit) {
|
|
return null;
|
|
}
|
|
|
|
const unitInSec = this.timeIntervalsInSec.get(timeUnit);
|
|
const totalTimeInSec = numericTime * unitInSec;
|
|
const multipleOfVal = this.stepMultipleOf;
|
|
|
|
let isValid: boolean;
|
|
if (totalTimeInSec < multipleOfVal) {
|
|
isValid = (multipleOfVal % totalTimeInSec === 0);
|
|
} else {
|
|
isValid = (totalTimeInSec % multipleOfVal === 0);
|
|
}
|
|
return isValid ? null : { stepMultipleOf: true };
|
|
};
|
|
}
|
|
|
|
private updatedAllowTimeUnitInterval(maxTime: number) {
|
|
const maxTimeMs = maxTime * SECOND;
|
|
this.timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
|
|
if (maxTimeMs < MINUTE) {
|
|
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.MINUTES && item !== TimeUnit.HOURS && item !== TimeUnit.DAYS);
|
|
} else if (maxTimeMs < HOUR) {
|
|
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.HOURS && item !== TimeUnit.DAYS);
|
|
} else if (maxTimeMs < DAY) {
|
|
this.timeUnits = this.timeUnits.filter(item => item !== TimeUnit.DAYS);
|
|
}
|
|
}
|
|
|
|
private refreshTimeValidators() {
|
|
const timeControl = this.timeInputForm.get('time');
|
|
if (!timeControl) return;
|
|
|
|
const currentUnit = this.timeInputForm.get('timeUnit')?.value as TimeUnit;
|
|
if (!currentUnit) return;
|
|
|
|
const secondsInUnit = this.timeIntervalsInSec.get(currentUnit) ?? 1;
|
|
const validators: ValidatorFn[] = [Validators.pattern(/^\d*$/)];
|
|
|
|
if (this.required) {
|
|
validators.push(Validators.required);
|
|
}
|
|
|
|
if (isDefinedAndNotNull(this.minTime)) {
|
|
this.minValueValidator = Math.ceil(this.minTime / secondsInUnit);
|
|
validators.push(Validators.min(this.minValueValidator));
|
|
} else {
|
|
this.minValueValidator = 0;
|
|
validators.push(Validators.min(this.minValueValidator));
|
|
}
|
|
|
|
if (isDefinedAndNotNull(this.maxTime)) {
|
|
const maxInCurrentUnit = Math.floor(this.maxTime / secondsInUnit);
|
|
validators.push(Validators.max(maxInCurrentUnit));
|
|
}
|
|
|
|
if (isDefinedAndNotNull(this.stepMultipleOf) && this.stepMultipleOf > 0) {
|
|
validators.push(this.createStepMultipleOfValidator());
|
|
}
|
|
|
|
timeControl.setValidators(validators);
|
|
timeControl.updateValueAndValidity({ onlySelf: true, emitEvent: false });
|
|
}
|
|
}
|
|
|