Browse Source

UI: Add persistence settings in save ts rule node

pull/12413/head
Vladyslav_Prykhodko 2 years ago
parent
commit
2d1ead5f54
  1. 10
      ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts
  2. 39
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html
  3. 114
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts
  4. 31
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html
  5. 83
      ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts
  6. 40
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html
  7. 104
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts
  8. 78
      ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.models.ts
  9. 3
      ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html
  10. 19
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
ui-ngx/src/app/modules/home/components/rule-node/action/action-rule-node-config.module.ts

@ -42,6 +42,12 @@ import { DeleteAttributesConfigComponent } from './delete-attributes-config.comp
import { MathFunctionConfigComponent } from './math-function-config.component';
import { DeviceStateConfigComponent } from './device-state-config.component';
import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-config.component';
import {
AdvancedPersistenceSettingComponent
} from '@home/components/rule-node/action/advanced-persistence-setting.component';
import {
AdvancedPersistenceSettingRowComponent
} from '@home/components/rule-node/action/advanced-persistence-setting-row.component';
@NgModule({
declarations: [
@ -67,7 +73,9 @@ import { SendRestApiCallReplyConfigComponent } from './send-rest-api-call-reply-
PushToEdgeConfigComponent,
PushToCloudConfigComponent,
MathFunctionConfigComponent,
DeviceStateConfigComponent
DeviceStateConfigComponent,
AdvancedPersistenceSettingComponent,
AdvancedPersistenceSettingRowComponent,
],
imports: [
CommonModule,

39
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.html

@ -0,0 +1,39 @@
<!--
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]="persistenceSettingRowForm" class="tb-form-panel stroked no-gap no-padding-bottom">
<div class="tb-form-panel-title mb-4">{{ title }}</div>
<mat-form-field>
<mat-label translate>rule-node-config.save-time-series.strategy</mat-label>
<mat-select formControlName="type">
@for (strategy of persistenceStrategies; track strategy) {
<mat-option [value]="strategy">{{ PersistenceTypeTranslationMap.get(strategy) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
@if(persistenceSettingRowForm.get('type').value === PersistenceType.DEDUPLICATE) {
<tb-time-unit-input
required
labelText="{{ 'rule-node-config.save-time-series.deduplication-interval' | translate }}"
requiredText="{{ 'rule-node-config.save-time-series.deduplication-interval-required' | translate }}"
minErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
maxErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
[maxTime]="maxDeduplicateTime"
formControlName="deduplicationIntervalSecs">
</tb-time-unit-input>
}
</section>

114
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting-row.component.ts

@ -0,0 +1,114 @@
///
/// 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, forwardRef, Input } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import {
AdvancedPersistenceConfig,
defaultAdvancedPersistenceConfig,
maxDeduplicateTimeSecs,
PersistenceType,
PersistenceTypeTranslationMap
} from '@home/components/rule-node/action/timeseries-config.models';
import { isDefinedAndNotNull } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-advanced-persistence-setting-row',
templateUrl: './advanced-persistence-setting-row.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AdvancedPersistenceSettingRowComponent),
multi: true
},{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AdvancedPersistenceSettingRowComponent),
multi: true
}]
})
export class AdvancedPersistenceSettingRowComponent implements ControlValueAccessor, Validator {
@Input()
title: string;
persistenceSettingRowForm = this.fb.group({
type: [defaultAdvancedPersistenceConfig.type],
deduplicationIntervalSecs: [{value: 60, disabled: true}]
});
PersistenceType = PersistenceType;
persistenceStrategies = [PersistenceType.ON_EVERY_MESSAGE, PersistenceType.DEDUPLICATE, PersistenceType.SKIP];
PersistenceTypeTranslationMap = PersistenceTypeTranslationMap;
maxDeduplicateTime = maxDeduplicateTimeSecs;
private propagateChange: (value: any) => void = () => {};
constructor(private fb: FormBuilder) {
this.persistenceSettingRowForm.get('type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => this.updatedValidation());
this.persistenceSettingRowForm.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => this.propagateChange(value));
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any) {
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.persistenceSettingRowForm.disable({emitEvent: false});
} else {
this.persistenceSettingRowForm.enable({emitEvent: false});
this.updatedValidation();
}
}
validate(): ValidationErrors | null {
return this.persistenceSettingRowForm.valid ? null : {
persistenceSettingRow: false
};
}
writeValue(value: AdvancedPersistenceConfig) {
if (isDefinedAndNotNull(value)) {
this.persistenceSettingRowForm.patchValue(value, {emitEvent: false});
} else {
this.persistenceSettingRowForm.patchValue(defaultAdvancedPersistenceConfig);
}
}
private updatedValidation() {
if (this.persistenceSettingRowForm.get('type').value === PersistenceType.DEDUPLICATE) {
this.persistenceSettingRowForm.get('deduplicationIntervalSecs').enable({emitEvent: false});
} else {
this.persistenceSettingRowForm.get('deduplicationIntervalSecs').disable({emitEvent: false})
}
}
}

31
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html

@ -0,0 +1,31 @@
<!--
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]="persistenceForm" class="tb-form-panel no-border no-padding">
<tb-advanced-persistence-setting-row
formControlName="timeseries"
title="{{ 'rule-node-config.save-time-series.time-series' | translate }}"
></tb-advanced-persistence-setting-row>
<tb-advanced-persistence-setting-row
formControlName="latest"
title="{{ 'rule-node-config.save-time-series.latest' | translate }}"
></tb-advanced-persistence-setting-row>
<tb-advanced-persistence-setting-row
formControlName="webSockets"
title="{{ 'rule-node-config.save-time-series.web-sockets' | translate }}"
></tb-advanced-persistence-setting-row>
</section>

83
ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts

@ -0,0 +1,83 @@
///
/// 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 {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import { Component, forwardRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AdvancedPersistenceStrategy } from '@home/components/rule-node/action/timeseries-config.models';
@Component({
selector: 'tb-advanced-persistence-settings',
templateUrl: './advanced-persistence-setting.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AdvancedPersistenceSettingComponent),
multi: true
},{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AdvancedPersistenceSettingComponent),
multi: true
}]
})
export class AdvancedPersistenceSettingComponent implements ControlValueAccessor, Validator {
persistenceForm = this.fb.group({
timeseries: [null],
latest: [null],
webSockets: [null]
});
private propagateChange: (value: any) => void = () => {};
constructor(private fb: FormBuilder) {
this.persistenceForm.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => this.propagateChange(value));
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any) {
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.persistenceForm.disable({emitEvent: false});
} else {
this.persistenceForm.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.persistenceForm.valid ? null : {
persistenceForm: false
};
}
writeValue(value: AdvancedPersistenceStrategy) {
this.persistenceForm.patchValue(value, {emitEvent: false});
}
}

40
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html

@ -16,6 +16,46 @@
-->
<section [formGroup]="timeseriesConfigForm" class="tb-form-panel no-border no-padding">
<div class="tb-form-panel stroked no-padding-bottom no-gap" formGroupName="persistenceSettings">
<div class="mb-4 flex flex-row items-center justify-between">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'rule-node-config.save-time-series.persistence-settings-hint' | translate}}" translate>
rule-node-config.save-time-series.persistence-settings
</div>
<tb-toggle-select appearance="fill" selectMediaBreakpoint="xs"
formControlName="isAdvanced">
<tb-toggle-option [value]=false>{{ 'rule-node-config.basic-mode' | translate}}</tb-toggle-option>
<tb-toggle-option [value]=true>{{ 'rule-node-config.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
@if(!timeseriesConfigForm.get('persistenceSettings.isAdvanced').value) {
<mat-form-field>
<mat-label translate>rule-node-config.save-time-series.strategy</mat-label>
<mat-select formControlName="type">
@for (strategy of persistenceStrategies; track strategy) {
<mat-option [value]="strategy">{{ PersistenceTypeTranslationMap.get(strategy) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
@if(timeseriesConfigForm.get('persistenceSettings.type').value === PersistenceType.DEDUPLICATE) {
<tb-time-unit-input
required
labelText="{{ 'rule-node-config.save-time-series.deduplication-interval' | translate }}"
requiredText="{{ 'rule-node-config.save-time-series.deduplication-interval-required' | translate }}"
minErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
maxErrorText="{{ 'rule-node-config.save-time-series.deduplication-interval-min-max-range' | translate }}"
[maxTime]="maxDeduplicateTime"
formControlName="deduplicationIntervalSecs">
</tb-time-unit-input>
}
}
@else {
<tb-advanced-persistence-settings
class="mb-4"
formControlName="advanced"
></tb-advanced-persistence-settings>
}
</div>
<section class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>

104
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.ts

@ -15,8 +15,18 @@
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { RuleNodeConfigurationComponent } from '@shared/models/rule-node.models';
import {
defaultAdvancedPersistenceStrategy,
maxDeduplicateTimeSecs,
PersistenceSettings,
PersistenceSettingsForm,
PersistenceType,
PersistenceTypeTranslationMap,
TimeseriesNodeConfiguration,
TimeseriesNodeConfigurationForm
} from '@home/components/rule-node/action/timeseries-config.models';
@Component({
selector: 'tb-action-node-timeseries-config',
@ -25,20 +35,98 @@ import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/m
})
export class TimeseriesConfigComponent extends RuleNodeConfigurationComponent {
timeseriesConfigForm: UntypedFormGroup;
timeseriesConfigForm: FormGroup;
constructor(private fb: UntypedFormBuilder) {
PersistenceType = PersistenceType;
persistenceStrategies = [PersistenceType.ON_EVERY_MESSAGE, PersistenceType.DEDUPLICATE, PersistenceType.WEBSOCKETS_ONLY];
PersistenceTypeTranslationMap = PersistenceTypeTranslationMap;
maxDeduplicateTime = maxDeduplicateTimeSecs
constructor(private fb: FormBuilder) {
super();
}
protected configForm(): UntypedFormGroup {
protected configForm(): FormGroup {
return this.timeseriesConfigForm;
}
protected onConfigurationSet(configuration: RuleNodeConfiguration) {
protected validatorTriggers(): string[] {
return ['persistenceSettings.isAdvanced', 'persistenceSettings.type'];
}
protected prepareInputConfig(config: TimeseriesNodeConfiguration): TimeseriesNodeConfigurationForm {
let persistenceSettings: PersistenceSettingsForm;
if (config?.persistenceSettings) {
const isAdvanced = config?.persistenceSettings?.type === PersistenceType.ADVANCED;
persistenceSettings = {
...config.persistenceSettings,
isAdvanced: isAdvanced,
type: isAdvanced ? PersistenceType.ON_EVERY_MESSAGE : config.persistenceSettings.type,
advanced: isAdvanced ? config.persistenceSettings : defaultAdvancedPersistenceStrategy
}
} else {
persistenceSettings = {
type: PersistenceType.ON_EVERY_MESSAGE,
isAdvanced: false,
deduplicationIntervalSecs: 10,
advanced: defaultAdvancedPersistenceStrategy
};
}
return {
...config,
persistenceSettings: persistenceSettings
}
}
protected prepareOutputConfig(config: TimeseriesNodeConfigurationForm): TimeseriesNodeConfiguration {
let persistenceSettings: PersistenceSettings;
if (config.persistenceSettings.isAdvanced) {
persistenceSettings = {
...config.persistenceSettings.advanced,
type: PersistenceType.ADVANCED
};
} else {
persistenceSettings = {
type: config.persistenceSettings.type,
deduplicationIntervalSecs: config.persistenceSettings?.deduplicationIntervalSecs
};
}
return {
...config,
persistenceSettings
};
}
protected onConfigurationSet(config: TimeseriesNodeConfigurationForm) {
this.timeseriesConfigForm = this.fb.group({
defaultTTL: [configuration ? configuration.defaultTTL : null, [Validators.required, Validators.min(0)]],
useServerTs: [configuration ? configuration.useServerTs : false, []]
persistenceSettings: this.fb.group({
isAdvanced: [config?.persistenceSettings?.isAdvanced ?? false],
type: [config?.persistenceSettings?.type ?? PersistenceType.ON_EVERY_MESSAGE],
deduplicationIntervalSecs: [
{value: config?.persistenceSettings?.deduplicationIntervalSecs ?? 10, disabled: true},
[Validators.required, Validators.max(maxDeduplicateTimeSecs)]
],
advanced: [{value: null, disabled: true}]
}),
defaultTTL: [config?.defaultTTL ?? null, [Validators.required, Validators.min(0)]],
useServerTs: [config?.useServerTs ?? false]
});
}
protected updateValidators(emitEvent: boolean, _trigger?: string) {
const persistenceForm = this.timeseriesConfigForm.get('persistenceSettings') as FormGroup;
const isAdvanced: boolean = persistenceForm.get('isAdvanced').value;
const type: PersistenceType = persistenceForm.get('type').value;
if (!isAdvanced && type === PersistenceType.DEDUPLICATE) {
persistenceForm.get('deduplicationIntervalSecs').enable({emitEvent});
} else {
persistenceForm.get('deduplicationIntervalSecs').disable({emitEvent});
}
if (isAdvanced) {
persistenceForm.get('advanced').enable({emitEvent});
} else {
persistenceForm.get('advanced').disable({emitEvent});
}
}
}

78
ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.models.ts

@ -0,0 +1,78 @@
///
/// 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 { DAY, SECOND } from '@shared/models/time/time.models';
export const maxDeduplicateTimeSecs = DAY / SECOND + 1;
export interface TimeseriesNodeConfiguration {
persistenceSettings: PersistenceSettings;
defaultTTL: number;
useServerTs: boolean;
}
export interface TimeseriesNodeConfigurationForm extends Omit<TimeseriesNodeConfiguration, 'persistenceSettings'> {
persistenceSettings: PersistenceSettingsForm
}
export type PersistenceSettings = BasicPersistenceSettings & Partial<DeduplicatePersistenceStrategy> & Partial<AdvancedPersistenceStrategy>;
export type PersistenceSettingsForm = Omit<PersistenceSettings, keyof AdvancedPersistenceStrategy> & {
isAdvanced: boolean;
advanced?: Partial<AdvancedPersistenceStrategy>;
type: PersistenceType;
};
export enum PersistenceType {
ON_EVERY_MESSAGE = 'ON_EVERY_MESSAGE',
DEDUPLICATE = 'DEDUPLICATE',
WEBSOCKETS_ONLY = 'WEBSOCKETS_ONLY',
ADVANCED = 'ADVANCED',
SKIP = 'SKIP'
}
export const PersistenceTypeTranslationMap = new Map<PersistenceType, string>([
[PersistenceType.ON_EVERY_MESSAGE, 'rule-node-config.save-time-series.strategy-type.every-message'],
[PersistenceType.DEDUPLICATE, 'rule-node-config.save-time-series.strategy-type.deduplicate'],
[PersistenceType.WEBSOCKETS_ONLY, 'rule-node-config.save-time-series.strategy-type.web-sockets-only'],
[PersistenceType.SKIP, 'rule-node-config.save-time-series.strategy-type.skip'],
])
export interface BasicPersistenceSettings {
type: PersistenceType;
}
export interface DeduplicatePersistenceStrategy extends BasicPersistenceSettings{
deduplicationIntervalSecs: number;
}
export interface AdvancedPersistenceStrategy extends BasicPersistenceSettings{
timeseries: AdvancedPersistenceConfig;
latest: AdvancedPersistenceConfig;
webSockets: AdvancedPersistenceConfig;
}
export type AdvancedPersistenceConfig = WithOptional<DeduplicatePersistenceStrategy, 'deduplicationIntervalSecs'>;
export const defaultAdvancedPersistenceConfig: AdvancedPersistenceConfig = {
type: PersistenceType.ON_EVERY_MESSAGE
}
export const defaultAdvancedPersistenceStrategy: Omit<AdvancedPersistenceStrategy, 'type'> = {
timeseries: defaultAdvancedPersistenceConfig,
latest: defaultAdvancedPersistenceConfig,
webSockets: defaultAdvancedPersistenceConfig,
}

3
ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.html

@ -16,12 +16,13 @@
-->
<section [formGroup]="timeInputForm" class="flex gap-4">
<mat-form-field class="max-w-66% flex-full">
<mat-form-field class="max-w-66% flex-full" subscriptSizing="dynamic">
<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-hint></mat-hint>
<mat-error *ngIf="timeInputForm.get('time').hasError('required') && requiredText">
{{ requiredText }}
</mat-error>

19
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5068,6 +5068,25 @@
"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.",
"basic-mode": "Basic",
"advanced-mode": "Advanced",
"save-time-series": {
"persistence-settings": "Persistence settings",
"persistence-settings-hint": "Persistence settings hint",
"strategy": "Strategy",
"deduplication-interval": "Deduplication interval",
"deduplication-interval-required": "Deduplication interval is required",
"deduplication-interval-min-max-range": "Deduplication interval should be at least 1 second and at most 1 day",
"strategy-type": {
"every-message": "On every message",
"skip": "Skip",
"deduplicate": "Deduplicate",
"web-sockets-only": "WebSockets only"
},
"time-series": "Time series",
"latest": "Latest",
"web-sockets": "WebSockets"
},
"key-val": {
"key": "Key",
"value": "Value",

Loading…
Cancel
Save