Browse Source

UI: Add CF output strategy

pull/14225/head
Vladyslav_Prykhodko 9 months ago
parent
commit
7a13183d5b
  1. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  2. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  3. 83
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  4. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss
  5. 58
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  6. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  7. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  8. 7
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  9. 64
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  10. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
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<CalculatedFi
private applyDialogData(): void {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
if (configuration.type !== CalculatedFieldType.ALARM) {
if (isDefined(configuration?.output) && !configuration?.output?.strategy) {
configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN};
}
}
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
}

6
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<Record<string, CalculatedFieldGeofencing>>({}, notEmptyObjectValidator()),
scheduledUpdateEnabled: [true],
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF],
output: this.fb.control<CalculatedFieldOutput>({type: OutputType.Timeseries})
output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput)
});
currentEntityFilter: EntityFilter;

83
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -78,6 +78,89 @@
}
}
</div>
<div class="tb-form-panel stroked" formGroupName="strategy">
<div class="flex flex-row items-center justify-between gap-2">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
}
</tb-toggle-select>
</div>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-normal" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.processing-options' | translate }}">
{{ 'calculated-fields.output-strategy.processing-options' | translate }}
</div>
<mat-chip-listbox multiple>
@if (outputForm.get('type').value === OutputType.Timeseries) {
<mat-chip-option
[selected]="outputForm.get('strategy.saveTimeSeries').value"
[disabled]="outputForm.get('strategy.saveTimeSeries').disabled"
(click)="toggleChip('saveTimeSeries')">
{{ 'calculated-fields.output-strategy.save-time-series' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.saveLatest').value"
[disabled]="outputForm.get('strategy.saveLatest').disabled"
(click)="toggleChip('saveLatest')">
{{ 'calculated-fields.output-strategy.save-latest-values' | translate }}
</mat-chip-option>
} @else {
<mat-chip-option
[selected]="outputForm.get('strategy.saveAttribute').value"
[disabled]="outputForm.get('strategy.saveAttribute').disabled"
(click)="toggleChip('saveAttribute')">
{{ 'calculated-fields.output-strategy.save-database' | translate }}
</mat-chip-option>
}
<mat-chip-option
[selected]="outputForm.get('strategy.sendWsUpdate').value"
[disabled]="outputForm.get('strategy.sendWsUpdate').disabled"
(click)="toggleChip('sendWsUpdate')">
{{ 'calculated-fields.output-strategy.send-web-sockets' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.processCfs').value"
[disabled]="outputForm.get('strategy.processCfs').disabled"
(click)="toggleChip('processCfs')">
{{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<tb-time-unit-input
required
subscriptSizing="dynamic"
appearance="outline"
labelText="{{ 'calculated-fields.output-strategy.ttl' | translate }}"
requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}"
minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}"
formControlName="ttl">
<mat-icon class="help-icon mr-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
color="primary"
matSuffix
matTooltip="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
help_outline
</mat-icon>
</tb-time-unit-input>
}
}
</div>
</div>
<ng-template #decimalsByDefaultField>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" [formGroup]="outputForm">

7
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
}
}
}

58
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});
}
}
}
}

5
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<CalculatedFieldOutput>({
type: OutputType.Timeseries,
}),
output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
});
readonly ScriptLanguage = ScriptLanguage;

5
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<CalculatedFieldOutput>({
type: OutputType.Timeseries,
}),
output: this.fb.control<CalculatedFieldOutput>(defaultCalculatedFieldOutput),
useLatestTs: [false]
});

7
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<CalculatedFieldSimpleOutput>({
name: '',
type: OutputType.Timeseries,
decimalsByDefault: null
}),
output: this.fb.control<CalculatedFieldSimpleOutput>(defaultSimpleCalculatedFieldOutput),
useLatestTs: [false]
});

64
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, string>(
[
[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<ValueType = unknown> = CalculatedF
export type CalculatedFieldEventArguments<ValueType = unknown> = Record<string, CalculatedFieldArgumentEventValue<ValueType>>;
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; }',

22
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.",

Loading…
Cancel
Save