From f42e0e49b91f9587609e77f80ce0613286627627 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 28 Feb 2025 15:16:13 +0200 Subject: [PATCH] UI: Processing settings for save attribute node --- ...dvanced-persistence-setting.component.html | 12 ++- .../advanced-persistence-setting.component.ts | 52 +++++++++-- .../action/attributes-config.component.html | 41 +++++++++ .../action/attributes-config.component.ts | 91 ++++++++++++++++++- .../action/attributes-config.model.ts | 59 ++++++++++++ .../action/timeseries-config.component.html | 1 + .../assets/locale/locale.constant-en_US.json | 16 ++++ 7 files changed, 257 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html index 094f6dbc2a..f951a9f826 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.html @@ -18,18 +18,22 @@
- - + - diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts index 01314ec4f3..4609a7d010 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/advanced-persistence-setting.component.ts @@ -19,12 +19,15 @@ import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormGroup, ValidationErrors, Validator } from '@angular/forms'; -import { Component, forwardRef } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { AdvancedProcessingStrategy } from '@home/components/rule-node/action/timeseries-config.models'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { AttributeAdvancedProcessingStrategy } from '@home/components/rule-node/action/attributes-config.model'; @Component({ selector: 'tb-advanced-persistence-settings', @@ -39,19 +42,48 @@ import { AdvancedProcessingStrategy } from '@home/components/rule-node/action/ti multi: true }] }) -export class AdvancedPersistenceSettingComponent implements ControlValueAccessor, Validator { +export class AdvancedPersistenceSettingComponent implements OnInit, ControlValueAccessor, Validator { - persistenceForm = this.fb.group({ - timeseries: [null], - latest: [null], - webSockets: [null] - }); + @Input() + @coerceBoolean() + timeseries = false; + + @Input() + @coerceBoolean() + attribute = false; + + @Input() + @coerceBoolean() + latest = false; + + @Input() + @coerceBoolean() + webSockets = false; + + persistenceForm: UntypedFormGroup; private propagateChange: (value: any) => void = () => {}; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.persistenceForm = this.fb.group({}); + if (this.timeseries) { + this.persistenceForm.addControl('timeseries', this.fb.control(null, [])); + } + if (this.attribute) { + this.persistenceForm.addControl('attribute', this.fb.control(null, [])); + } + if (this.attribute) { + this.persistenceForm.addControl('latest', this.fb.control(null, [])); + } + if (this.attribute) { + this.persistenceForm.addControl('webSockets', this.fb.control(null, [])); + } this.persistenceForm.valueChanges.pipe( - takeUntilDestroyed() + takeUntilDestroyed(this.destroyRef) ).subscribe(value => this.propagateChange(value)); } @@ -76,7 +108,7 @@ export class AdvancedPersistenceSettingComponent implements ControlValueAccessor }; } - writeValue(value: AdvancedProcessingStrategy) { + writeValue(value: AdvancedProcessingStrategy | AttributeAdvancedProcessingStrategy) { this.persistenceForm.patchValue(value, {emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html index 03ea4fccdd..69feec1732 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.html @@ -16,6 +16,47 @@ -->
+
+
+
+ rule-node-config.save-attribute.processing-settings +
+ + {{ 'rule-node-config.basic-mode' | translate}} + {{ 'rule-node-config.advanced-mode' | translate }} + +
+ @if(!attributesConfigForm.get('processingSettings.isAdvanced').value) { + + rule-node-config.save-attribute.strategy + + @for (strategy of persistenceStrategies; track strategy) { + {{ PersistenceTypeTranslationMap.get(strategy) | translate }} + } + + + + @if(attributesConfigForm.get('processingSettings.type').value === PersistenceType.DEDUPLICATE) { + + + } + } @else { + + } +
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts index 5a404c8de4..7257565608 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.component.ts @@ -15,10 +15,22 @@ /// import { Component } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { FormGroup, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { RuleNodeConfiguration, RuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; import { AttributeScope, telemetryTypeTranslations } from '@app/shared/models/telemetry/telemetry.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + maxDeduplicateTimeSecs, + ProcessingSettings, + ProcessingSettingsForm, + ProcessingType, + ProcessingTypeTranslationMap +} from '@home/components/rule-node/action/timeseries-config.models'; +import { + AttributeNodeConfiguration, + AttributeNodeConfigurationForm, + defaultAttributeAdvancedPersistenceStrategy +} from '@home/components/rule-node/action/attributes-config.model'; @Component({ selector: 'tb-action-node-attributes-config', @@ -31,6 +43,12 @@ export class AttributesConfigComponent extends RuleNodeConfigurationComponent { attributeScopes = Object.keys(AttributeScope); telemetryTypeTranslationsMap = telemetryTypeTranslations; + PersistenceType = ProcessingType; + persistenceStrategies = [ProcessingType.ON_EVERY_MESSAGE, ProcessingType.DEDUPLICATE, ProcessingType.WEBSOCKETS_ONLY]; + PersistenceTypeTranslationMap = ProcessingTypeTranslationMap; + + maxDeduplicateTime = maxDeduplicateTimeSecs; + attributesConfigForm: UntypedFormGroup; constructor(private fb: UntypedFormBuilder) { @@ -41,8 +59,64 @@ export class AttributesConfigComponent extends RuleNodeConfigurationComponent { return this.attributesConfigForm; } + protected validatorTriggers(): string[] { + return ['processingSettings.isAdvanced', 'processingSettings.type']; + } + + protected prepareInputConfig(config: AttributeNodeConfiguration): AttributeNodeConfigurationForm { + let processingSettings: ProcessingSettingsForm; + if (config?.processingSettings) { + const isAdvanced = config?.processingSettings?.type === ProcessingType.ADVANCED; + processingSettings = { + type: isAdvanced ? ProcessingType.ON_EVERY_MESSAGE : config.processingSettings.type, + isAdvanced: isAdvanced, + deduplicationIntervalSecs: config.processingSettings?.deduplicationIntervalSecs ?? 60, + advanced: isAdvanced ? config.processingSettings : defaultAttributeAdvancedPersistenceStrategy + } + } else { + processingSettings = { + type: ProcessingType.ON_EVERY_MESSAGE, + isAdvanced: false, + deduplicationIntervalSecs: 60, + advanced: defaultAttributeAdvancedPersistenceStrategy + }; + } + return { + ...config, + processingSettings: processingSettings + } + } + + protected prepareOutputConfig(config: AttributeNodeConfigurationForm): AttributeNodeConfiguration { + let processingSettings: ProcessingSettings; + if (config.processingSettings.isAdvanced) { + processingSettings = { + ...config.processingSettings.advanced, + type: ProcessingType.ADVANCED + }; + } else { + processingSettings = { + type: config.processingSettings.type, + deduplicationIntervalSecs: config.processingSettings?.deduplicationIntervalSecs + }; + } + return { + ...config, + processingSettings + }; + } + protected onConfigurationSet(configuration: RuleNodeConfiguration) { this.attributesConfigForm = this.fb.group({ + processingSettings: this.fb.group({ + isAdvanced: [configuration?.processingSettings?.isAdvanced ?? false], + type: [configuration?.processingSettings?.type ?? ProcessingType.ON_EVERY_MESSAGE], + deduplicationIntervalSecs: [ + {value: configuration?.processingSettings?.deduplicationIntervalSecs ?? 60, disabled: true}, + [Validators.required, Validators.max(maxDeduplicateTimeSecs)] + ], + advanced: [{value: null, disabled: true}] + }), scope: [configuration ? configuration.scope : null, [Validators.required]], notifyDevice: [configuration ? configuration.notifyDevice : true, []], sendAttributesUpdatedNotification: [configuration ? configuration.sendAttributesUpdatedNotification : false, []], @@ -62,4 +136,19 @@ export class AttributesConfigComponent extends RuleNodeConfigurationComponent { }); } + protected updateValidators(emitEvent: boolean, _trigger?: string) { + const processingForm = this.attributesConfigForm.get('processingSettings') as FormGroup; + const isAdvanced: boolean = processingForm.get('isAdvanced').value; + const type: ProcessingType = processingForm.get('type').value; + if (!isAdvanced && type === ProcessingType.DEDUPLICATE) { + processingForm.get('deduplicationIntervalSecs').enable({emitEvent}); + } else { + processingForm.get('deduplicationIntervalSecs').disable({emitEvent}); + } + if (isAdvanced) { + processingForm.get('advanced').enable({emitEvent}); + } else { + processingForm.get('advanced').disable({emitEvent}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts new file mode 100644 index 0000000000..fb53a1ceaa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/attributes-config.model.ts @@ -0,0 +1,59 @@ +/// +/// 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 { DAY, SECOND } from '@shared/models/time/time.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { BasicProcessingSettings, ProcessingType } from '@home/components/rule-node/action/timeseries-config.models'; + +export interface AttributeNodeConfiguration { + processingSettings: AttributeProcessingSettings; + scope: AttributeScope; + notifyDevice: boolean; + sendAttributesUpdatedNotification: boolean; + updateAttributesOnlyOnValueChange: boolean; +} + +export interface AttributeNodeConfigurationForm extends Omit { + processingSettings: AttributeProcessingSettingsForm +} + +export type AttributeProcessingSettings = BasicProcessingSettings & Partial & Partial; + +export type AttributeProcessingSettingsForm = Omit & { + isAdvanced: boolean; + advanced?: Partial; + type: ProcessingType; +}; + +export interface AttributeDeduplicateProcessingStrategy extends BasicProcessingSettings{ + deduplicationIntervalSecs: number; +} + +export interface AttributeAdvancedProcessingStrategy extends BasicProcessingSettings{ + attribute: AttributeAdvancedProcessingConfig; + webSockets: AttributeAdvancedProcessingConfig; +} + +export type AttributeAdvancedProcessingConfig = WithOptional; + +export const defaultAdvancedProcessingConfig: AttributeAdvancedProcessingConfig = { + type: ProcessingType.ON_EVERY_MESSAGE +} + +export const defaultAttributeAdvancedPersistenceStrategy: Omit = { + attribute: defaultAdvancedProcessingConfig, + webSockets: defaultAdvancedProcessingConfig, +} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html index 6e3e9fb6d1..250e66541b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html @@ -53,6 +53,7 @@ }
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 18801bda2c..291cae525f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5155,6 +5155,22 @@ "latest": "Latest values", "web-sockets": "WebSockets" }, + "save-attribute": { + "processing-settings": "Processing settings", + "processing-settings-hint": "Define how incoming messages are processed. In Basic mode, select a preconfigured processing strategy or enable only WebSocket updates. Advanced mode allows you to select individual processing strategies for each action.", + "advanced-settings-hint": "Be cautious when configuring processing strategies. Certain combinations can lead to unexpected behavior.", + "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" + }, + "attribute": "Attribute" + }, "key-val": { "key": "Key", "value": "Value",