diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 17c550dcdf..7174ec9dd9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -26,7 +26,8 @@ import { CalculatedFieldConfiguration, CalculatedFieldTestScriptFn, CalculatedFieldType, - CalculatedFieldTypeTranslations + CalculatedFieldTypeTranslations, + OutputStrategyType } from '@shared/models/calculated-field.models'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models'; @@ -36,7 +37,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { deepTrim } from '@core/utils'; +import { deepTrim, isDefined } from '@core/utils'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -121,6 +122,11 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts index 846b063042..3406922b9e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts @@ -30,9 +30,9 @@ import { CalculatedFieldGeofencingConfiguration, CalculatedFieldOutput, CalculatedFieldType, + defaultCalculatedFieldOutput, getCalculatedFieldCurrentEntityFilter, - notEmptyObjectValidator, - OutputType + notEmptyObjectValidator } from '@shared/models/calculated-field.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; @@ -83,7 +83,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V zoneGroups: this.fb.control>({}, notEmptyObjectValidator()), scheduledUpdateEnabled: [true], scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], - output: this.fb.control({type: OutputType.Timeseries}) + output: this.fb.control(defaultCalculatedFieldOutput) }); currentEntityFilter: EntityFilter; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 4d8515ac7e..144a8384ab 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -78,6 +78,89 @@ } } +
+
+
+ {{ 'calculated-fields.output-strategy.strategy' | translate }} +
+ + @for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) { + {{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }} + } + +
+ @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { +
+
+ {{ 'calculated-fields.output-strategy.processing-options' | translate }} +
+ + @if (outputForm.get('type').value === OutputType.Timeseries) { + + {{ 'calculated-fields.output-strategy.save-time-series' | translate }} + + + {{ 'calculated-fields.output-strategy.save-latest-values' | translate }} + + } @else { + + {{ 'calculated-fields.output-strategy.save-database' | translate }} + + } + + {{ 'calculated-fields.output-strategy.send-web-sockets' | translate }} + + + {{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }} + + +
+ @if (outputForm.get('type').value === OutputType.Attribute) { +
+ +
+
calculated-fields.output-strategy.update-attributes-only-on-value-change
+
+
+
+ } @else { + + + help_outline + + + } + } +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss new file mode 100644 index 0000000000..e0a24dc86e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss @@ -0,0 +1,7 @@ +:host ::ng-deep { + .mat-mdc-chip-disabled { + .mdc-evolution-chip__action { + cursor: default + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 2a9215b7cf..a3d575af90 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -28,6 +28,8 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { CalculatedFieldOutput, CalculatedFieldSimpleOutput, + OutputStrategyType, + OutputStrategyTypeTranslations, OutputType, OutputTypeTranslations } from '@shared/models/calculated-field.models'; @@ -52,6 +54,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; multi: true } ], + styleUrls: ['./calculated-field-output.component.scss'], }) export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @@ -72,6 +75,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val readonly OutputTypeTranslations = OutputTypeTranslations; readonly EntityType = EntityType; + readonly OutputStrategyType = OutputStrategyType; + readonly OutputStrategyTypes = Object.values(OutputStrategyType) as OutputStrategyType[]; + readonly OutputStrategyTypeTranslations = OutputStrategyTypeTranslations; + private fb = inject(FormBuilder); private destroyRef = inject(DestroyRef); @@ -80,6 +87,16 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], type: [OutputType.Timeseries], decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], + strategy: this.fb.group({ + type: [OutputStrategyType.IMMEDIATE], + saveTimeSeries: [true], + saveLatest: [true], + saveAttribute: [true], + sendWsUpdate: [true], + processCfs: [true], + updateAttributesOnlyOnValueChange: [true], + ttl: [0] + }) }); private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { }; @@ -87,14 +104,23 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val ngOnInit() { this.outputForm.get('type').valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(type => this.toggleScopeByOutputType(type)); + .subscribe(type => { + this.toggleScopeByOutputType(type); + this.updatedStrategy(); + }); + + this.outputForm.get('strategy.type').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.updatedStrategy(); + }); this.updatedFormWithMode(); this.outputForm.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { - this.updatedModel(value) + this.updatedModel(value); }) } @@ -134,6 +160,14 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.outputForm.enable({emitEvent: false}); this.updatedFormWithMode(); this.toggleScopeByOutputType(this.outputForm.get('type').value); + this.updatedStrategy(); + } + } + + toggleChip(controlName: string) { + const control = this.outputForm.get('strategy').get(controlName); + if (control && control.enabled) { + control.setValue(!control.value); } } @@ -164,4 +198,24 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); } } + + private updatedStrategy(): void { + const strategyType = this.outputForm.get('strategy.type').value; + this.outputForm.get('strategy').disable({emitEvent: false}); + this.outputForm.get('strategy.type').enable({emitEvent: false}); + + if (strategyType === OutputStrategyType.IMMEDIATE) { + const outputType = this.outputForm.get('type').value; + this.outputForm.get('strategy.sendWsUpdate').enable({emitEvent: false}); + this.outputForm.get('strategy.processCfs').enable({emitEvent: false}); + if (outputType === OutputType.Attribute) { + this.outputForm.get('strategy.saveAttribute').enable({emitEvent: false}); + this.outputForm.get('strategy.updateAttributesOnlyOnValueChange').enable({emitEvent: false}); + } else { + this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false}); + this.outputForm.get('strategy.saveLatest').enable({emitEvent: false}); + this.outputForm.get('strategy.ttl').enable({emitEvent: false}); + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index 4fb8bb561e..0a69c59568 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -31,6 +31,7 @@ import { CalculatedFieldOutput, CalculatedFieldPropagationConfiguration, CalculatedFieldType, + defaultCalculatedFieldOutput, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, notEmptyObjectValidator, @@ -81,9 +82,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, relationType: ['Contains', Validators.required], }), expression: [calculatedFieldDefaultScript], - output: this.fb.control({ - type: OutputType.Timeseries, - }), + output: this.fb.control(defaultCalculatedFieldOutput), }); readonly ScriptLanguage = ScriptLanguage; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts index c53f400916..731330ec76 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -30,6 +30,7 @@ import { CalculatedFieldOutput, CalculatedFieldRelatedAggregationConfiguration, CalculatedFieldType, + defaultCalculatedFieldOutput, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, notEmptyObjectValidator, @@ -87,9 +88,7 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu arguments: this.fb.control({}, notEmptyObjectValidator()), metrics: this.fb.control({}, notEmptyObjectValidator()), deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], - output: this.fb.control({ - type: OutputType.Timeseries, - }), + output: this.fb.control(defaultCalculatedFieldOutput), useLatestTs: [false] }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index 065556081c..0c70361e5e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -31,6 +31,7 @@ import { CalculatedFieldSimpleConfiguration, CalculatedFieldSimpleOutput, CalculatedFieldType, + defaultSimpleCalculatedFieldOutput, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, OutputType @@ -84,11 +85,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid arguments: this.fb.control({}), expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], expressionSCRIPT: [calculatedFieldDefaultScript], - output: this.fb.control({ - name: '', - type: OutputType.Timeseries, - decimalsByDefault: null - }), + output: this.fb.control(defaultSimpleCalculatedFieldOutput), useLatestTs: [false] }); diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 1b43700ebe..e08066066c 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -185,10 +185,12 @@ export type CalculatedFieldOutput = export interface CalculatedFieldOutputAttribute { type: OutputType.Attribute, scope: AttributeScope; + strategy: AttributeOutputStrategy; } export interface CalculatedFieldOutputTimeSeries { type: OutputType.Timeseries; + strategy: TimeSeriesOutputStrategy; } export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { @@ -196,6 +198,51 @@ export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { decimalsByDefault?: number; } +export type AttributeOutputStrategy = + | AttributeImmediateOutputStrategy + | AttributeRuleChainOutputStrategy; + +export interface AttributeImmediateOutputStrategy { + type: OutputStrategyType.IMMEDIATE; + updateAttributesOnlyOnValueChange: boolean; + saveAttribute: boolean; + sendWsUpdate: boolean; + processCfs: boolean; +} + +export interface AttributeRuleChainOutputStrategy { + type: OutputStrategyType.RULE_CHAIN; +} + +export type TimeSeriesOutputStrategy = + | TimeSeriesRuleChainOutputStrategy + | TimeSeriesImmediateOutputStrategy; + +export interface TimeSeriesRuleChainOutputStrategy { + type: OutputStrategyType.IMMEDIATE; + ttl: number; + saveTimeSeries: boolean; + saveLatest: boolean; + sendWsUpdate: boolean; + processCfs: boolean; +} + +export interface TimeSeriesImmediateOutputStrategy { + type: OutputStrategyType.RULE_CHAIN; +} + +export enum OutputStrategyType { + IMMEDIATE = 'IMMEDIATE', + RULE_CHAIN = 'RULE_CHAIN' +} + +export const OutputStrategyTypeTranslations = new Map( + [ + [OutputStrategyType.IMMEDIATE, 'calculated-fields.output-strategy.process-right-away'], + [OutputStrategyType.RULE_CHAIN, 'calculated-fields.output-strategy.process-rule-chains'], + ] +) + export enum ArgumentEntityType { Current = 'CURRENT', Device = 'DEVICE', @@ -458,6 +505,23 @@ export type CalculatedFieldArgumentEventValue = CalculatedF export type CalculatedFieldEventArguments = Record>; +export const defaultCalculatedFieldOutput: CalculatedFieldOutputTimeSeries = { + type: OutputType.Timeseries, + strategy: { + type: OutputStrategyType.IMMEDIATE, + ttl: 0, + saveTimeSeries: true, + saveLatest: true, + sendWsUpdate: true, + processCfs: true + } +} + +export const defaultSimpleCalculatedFieldOutput: CalculatedFieldSimpleOutput = { + name: '', + ...defaultCalculatedFieldOutput +} + export const CalculatedFieldCtxLatestTelemetryArgumentAutocomplete = { meta: 'object', type: '{ ts: number; value: any; }', 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 0c501f5c5e..b827ead50b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1193,6 +1193,28 @@ "filter": "Filter", "filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." }, + "output-strategy": { + "strategy": "Strategy", + "process-right-away": "Process right away", + "process-rule-chains": "Process via Rule Chains", + "processing-options": "Processing options", + "save-time-series": "Save to time series", + "save-database": "Save to database", + "save-latest-values": "Save to latest values", + "send-web-sockets": "Send to WebSockets", + "save-calculated-fields": "Send to Calculated fields", + "update-attributes-only-on-value-change": "Save attributes only if the value changes", + "ttl": "TTL", + "ttl-required": "TTL is required.", + "ttl-min": "Only 0 minimum TTL is allowed.", + "hint": { + "strategy": "Strategy", + "processing-options": "Processing options", + "update-attributes-only-on-value-change": "Updates the attributes on every incoming message disregarding if their value has changed. Increases API usage and reduces performance.", + "update-attributes-only-on-value-change-enabled": "Updates the attributes only if their value has changed. If the value is not changed, no update to the attribute timestamp nor attribute change notification will be sent.", + "ttl": "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." + } + }, "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.",