UI: Add time unit selector and improved save ts rule node
This commit is contained in:
parent
4dd973f75b
commit
b898ca4d15
@ -16,35 +16,33 @@
|
||||
|
||||
-->
|
||||
<section [formGroup]="timeseriesConfigForm" class="tb-form-panel no-border no-padding">
|
||||
<mat-form-field class="mat-block flex-1">
|
||||
<mat-label translate>rule-node-config.default-ttl</mat-label>
|
||||
<input type="number" min="0" step="1" matInput formControlName="defaultTTL" required>
|
||||
<mat-icon class="help-icon margin-8 cursor-pointer"
|
||||
<section class="tb-form-panel stroked">
|
||||
<mat-expansion-panel class="tb-settings">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title translate>rule-node-config.advanced-settings</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<tb-time-unit-input
|
||||
required
|
||||
labelText="{{ 'rule-node-config.default-ttl' | translate }}"
|
||||
requiredText="{{ 'rule-node-config.default-ttl-required' | translate }}"
|
||||
minErrorText="{{ 'rule-node-config.min-default-ttl-message' | translate }}"
|
||||
formControlName="defaultTTL">
|
||||
<mat-icon class="mr-2 cursor-pointer"
|
||||
aria-hidden="false"
|
||||
aria-label="help-icon"
|
||||
matSuffix
|
||||
matTooltip="{{ 'rule-node-config.default-ttl-hint' | translate }}">
|
||||
help
|
||||
</mat-icon>
|
||||
<mat-error *ngIf="timeseriesConfigForm.get('defaultTTL').hasError('required')">
|
||||
{{ 'rule-node-config.default-ttl-required' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="timeseriesConfigForm.get('defaultTTL').hasError('min')">
|
||||
{{ 'rule-node-config.min-default-ttl-message' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<div class="tb-form-panel stroked">
|
||||
</tb-time-unit-input>
|
||||
<div tb-hint-tooltip-icon="{{ 'rule-node-config.use-server-ts-hint' | translate}}"
|
||||
class="tb-form-row no-border no-padding">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="useServerTs">
|
||||
{{ 'rule-node-config.use-server-ts' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div tb-hint-tooltip-icon="{{ 'rule-node-config.skip-latest-persistence-hint' | translate}}"
|
||||
class="tb-form-row no-border no-padding">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="skipLatestPersistence">
|
||||
{{ 'rule-node-config.skip-latest-persistence' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@ -38,7 +38,6 @@ export class TimeseriesConfigComponent extends RuleNodeConfigurationComponent {
|
||||
protected onConfigurationSet(configuration: RuleNodeConfiguration) {
|
||||
this.timeseriesConfigForm = this.fb.group({
|
||||
defaultTTL: [configuration ? configuration.defaultTTL : null, [Validators.required, Validators.min(0)]],
|
||||
skipLatestPersistence: [configuration ? configuration.skipLatestPersistence : false, []],
|
||||
useServerTs: [configuration ? configuration.useServerTs : false, []]
|
||||
});
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ import { RelationsQueryConfigOldComponent } from './relations-query-config-old.c
|
||||
import { SelectAttributesComponent } from './select-attributes.component';
|
||||
import { AlarmStatusSelectComponent } from './alarm-status-select.component';
|
||||
import { ExampleHintComponent } from './example-hint.component';
|
||||
import { TimeUnitInputComponent } from './time-unit-input.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -50,7 +51,8 @@ import { ExampleHintComponent } from './example-hint.component';
|
||||
RelationsQueryConfigOldComponent,
|
||||
SelectAttributesComponent,
|
||||
AlarmStatusSelectComponent,
|
||||
ExampleHintComponent
|
||||
ExampleHintComponent,
|
||||
TimeUnitInputComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -72,7 +74,8 @@ import { ExampleHintComponent } from './example-hint.component';
|
||||
RelationsQueryConfigOldComponent,
|
||||
SelectAttributesComponent,
|
||||
AlarmStatusSelectComponent,
|
||||
ExampleHintComponent
|
||||
ExampleHintComponent,
|
||||
TimeUnitInputComponent
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
<!--
|
||||
|
||||
Copyright © 2016-2024 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.
|
||||
|
||||
-->
|
||||
<section [formGroup]="timeInputForm" class="flex gap-4">
|
||||
<mat-form-field class="max-w-66% flex-full">
|
||||
<mat-label *ngIf="labelText">{{ labelText }}</mat-label>
|
||||
<input type="number" min="0" step="1" matInput formControlName="time">
|
||||
<div matSuffix>
|
||||
<ng-content select="[matSuffix]"></ng-content>
|
||||
</div>
|
||||
<mat-error *ngIf="timeInputForm.get('time').hasError('required') && requiredText">
|
||||
{{ requiredText }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="timeInputForm.get('time').hasError('min') && minErrorText">
|
||||
{{ minErrorText }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="timeInputForm.get('time').hasError('max') && maxErrorText">
|
||||
{{ maxErrorText }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="max-w-33% flex-full">
|
||||
<mat-label translate>rule-node-config.units</mat-label>
|
||||
<mat-select formControlName="timeUnit">
|
||||
@for (timeUnit of timeUnits; track timeUnit) {
|
||||
<mat-option [value]="timeUnit">{{ timeUnitTranslations.get(timeUnit) | translate }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
@ -0,0 +1,184 @@
|
||||
///
|
||||
/// Copyright © 2016-2024 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 {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ValidationErrors,
|
||||
Validator, Validators
|
||||
} from '@angular/forms';
|
||||
import { TimeUnit, timeUnitTranslations } from '../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';
|
||||
|
||||
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 {
|
||||
|
||||
@Input()
|
||||
labelText: string;
|
||||
|
||||
@Input()
|
||||
@coerceBoolean()
|
||||
required: boolean;
|
||||
|
||||
@Input()
|
||||
requiredText: string;
|
||||
|
||||
@Input()
|
||||
minErrorText: string;
|
||||
|
||||
@Input()
|
||||
@coerceNumber()
|
||||
maxTime: number;
|
||||
|
||||
@Input()
|
||||
maxErrorText: string;
|
||||
|
||||
timeUnits = Object.values(TimeUnit).filter(item => item !== TimeUnit.MILLISECONDS) as TimeUnit[];
|
||||
|
||||
timeUnitTranslations = timeUnitTranslations;
|
||||
|
||||
timeInputForm = this.fb.group({
|
||||
time: [0, Validators.min(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],
|
||||
]);
|
||||
|
||||
private modelValue: number;
|
||||
|
||||
private propagateChange: (value: any) => void = () => {};
|
||||
|
||||
constructor(private fb: FormBuilder,
|
||||
private destroyRef: DestroyRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if(this.required || this.maxTime) {
|
||||
const timeControl = this.timeInputForm.get('time');
|
||||
const validators = [];
|
||||
if (this.required) {
|
||||
validators.push(Validators.required);
|
||||
}
|
||||
if (this.maxTime) {
|
||||
validators.push((control: AbstractControl) =>
|
||||
Validators.max(Math.floor(this.maxTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control)
|
||||
);
|
||||
}
|
||||
|
||||
timeControl.setValidators(validators);
|
||||
timeControl.updateValueAndValidity({ emitEvent: false });
|
||||
}
|
||||
|
||||
this.timeInputForm.get('timeUnit').valueChanges.pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(() => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(sec: number) {
|
||||
if (sec !== this.modelValue) {
|
||||
if (isDefinedAndNotNull(sec) && isNumeric(sec) && Number(sec) !== 0) {
|
||||
this.timeInputForm.patchValue(this.parseTime(sec), {emitEvent: false});
|
||||
this.modelValue = sec;
|
||||
} else {
|
||||
this.timeInputForm.patchValue({
|
||||
time: 0,
|
||||
timeUnit: TimeUnit.SECONDS
|
||||
}, {emitEvent: false});
|
||||
this.modelValue = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.timeInputForm.valid ? null : {
|
||||
timeInput: false
|
||||
};
|
||||
}
|
||||
|
||||
private updatedModel(value: Partial<TimeUnitInputModel>) {
|
||||
const time = value.time * this.timeIntervalsInSec.get(value.timeUnit);
|
||||
if (this.modelValue !== time) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -4541,7 +4541,7 @@
|
||||
"originator-entity": "Entity by name pattern",
|
||||
"clone-message": "Clone message",
|
||||
"transform": "Transform",
|
||||
"default-ttl": "Default TTL in seconds",
|
||||
"default-ttl": "Default TTL",
|
||||
"default-ttl-required": "Default TTL is required.",
|
||||
"default-ttl-hint": "Rule node will fetch Time-to-Live (TTL) value from the message metadata. If no value is present, it defaults to the TTL specified in the configuration. If the value is set to 0, the TTL from the tenant profile configuration will be applied.",
|
||||
"default-ttl-zero-hint": "TTL will not be applied if its value is set to 0.",
|
||||
@ -4906,9 +4906,7 @@
|
||||
"general-pattern-hint": "Use ${metadataKey} for value from metadata, $[messageKey] for value from message body.",
|
||||
"alarm-severity-pattern-hint": "Use <code><span style=\"color: #000;\">${</span>metadataKey<span style=\"color: #000;\">}</span></code> for value from metadata, <code><span style=\"color: #000;\">$[</span>messageKey<span style=\"color: #000;\">]</span></code> for value from message body. Alarm severity should be system (CRITICAL, MAJOR etc.)",
|
||||
"output-node-name-hint": "The <b>rule node name</b> corresponds to the <b>relation type</b> of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.",
|
||||
"skip-latest-persistence": "Skip latest persistence",
|
||||
"skip-latest-persistence-hint": "Rule node will not update values for incoming keys for the latest time series data. Useful for highly loaded use-cases to decrease the pressure on the DB.",
|
||||
"use-server-ts": "Use server ts",
|
||||
"use-server-ts": "Use server timestamp",
|
||||
"use-server-ts-hint": "Rule node will use the timestamp of message processing instead of the timestamp from the message. Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).",
|
||||
"kv-map-pattern-hint": "All input fields support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.",
|
||||
"kv-map-single-pattern-hint": "Input field support templatization. Use $[messageKey] to extract value from the message and ${metadataKey} to extract value from the metadata.",
|
||||
@ -5067,6 +5065,7 @@
|
||||
"request-timeout-required": "Request timeout is required",
|
||||
"request-timeout-min": "Min request timeout is 0",
|
||||
"request-timeout-hint": "The amount of time to wait in seconds for the request to complete before giving up and timing out. A value of 0 means infinity, and is not recommended.",
|
||||
"units": "Units",
|
||||
"tell-failure-aws-lambda": "Tell Failure if AWS Lambda function execution raises exception",
|
||||
"tell-failure-aws-lambda-hint": "Rule node forces failure of message processing if AWS Lambda function execution raises exception.",
|
||||
"key-val": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user