Calculated adjustments
This commit is contained in:
		
							parent
							
								
									7d57cde6fa
								
							
						
					
					
						commit
						8c7c54b554
					
				@ -258,8 +258,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private importCalculatedField(): void {
 | 
			
		||||
    this.importExportService.importCalculatedField(this.entityId)
 | 
			
		||||
      .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
 | 
			
		||||
    this.importExportService.openCalculatedFieldImportDialog()
 | 
			
		||||
      .pipe(
 | 
			
		||||
        filter(Boolean),
 | 
			
		||||
        switchMap(calculatedField => this.getCalculatedFieldDialog(calculatedField, 'action.add')),
 | 
			
		||||
        filter(Boolean),
 | 
			
		||||
        switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
 | 
			
		||||
        filter(Boolean),
 | 
			
		||||
        takeUntilDestroyed(this.destroyRef)
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(() => this.updateData());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,7 @@
 | 
			
		||||
                    matTooltipPosition="above">
 | 
			
		||||
              <mat-icon
 | 
			
		||||
                [matBadgeHidden]="!(argument.refEntityKey.type === ArgumentType.Rolling
 | 
			
		||||
                          && calculatedFieldType === CalculatedFieldType.SIMPLE)"
 | 
			
		||||
                          && calculatedFieldType === CalculatedFieldType.SIMPLE) && !entityNameErrorSet.has(argument.refEntityId?.id)"
 | 
			
		||||
                matBadgeColor="warn"
 | 
			
		||||
                matBadgeSize="small"
 | 
			
		||||
                matBadge="*"
 | 
			
		||||
 | 
			
		||||
@ -58,6 +58,8 @@ import { MatSort } from '@angular/material/sort';
 | 
			
		||||
import { getCurrentAuthState } from '@core/auth/auth.selectors';
 | 
			
		||||
import { Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { catchError } from 'rxjs/operators';
 | 
			
		||||
import { NEVER } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-calculated-field-arguments-table',
 | 
			
		||||
@ -88,6 +90,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
  errorText = '';
 | 
			
		||||
  argumentsFormArray = this.fb.array<AbstractControl>([]);
 | 
			
		||||
  entityNameMap = new Map<string, string>();
 | 
			
		||||
  entityNameErrorSet = new Set<string>();
 | 
			
		||||
  sortOrder = { direction: 'asc', property: '' };
 | 
			
		||||
  dataSource = new CalculatedFieldArgumentDatasource();
 | 
			
		||||
 | 
			
		||||
@ -168,6 +171,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
        buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
 | 
			
		||||
        tenantId: this.tenantId,
 | 
			
		||||
        entityName: this.entityName,
 | 
			
		||||
        entityHasError: this.entityNameErrorSet.has(argument.refEntityId?.id),
 | 
			
		||||
        usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
 | 
			
		||||
      };
 | 
			
		||||
      this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
 | 
			
		||||
@ -198,6 +202,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
    if (this.calculatedFieldType === CalculatedFieldType.SIMPLE
 | 
			
		||||
      && this.argumentsFormArray.controls.some(control => control.value.refEntityKey.type === ArgumentType.Rolling)) {
 | 
			
		||||
      this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
 | 
			
		||||
    } else if (this.entityNameErrorSet.size) {
 | 
			
		||||
      this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
 | 
			
		||||
    } else if (!this.argumentsFormArray.controls.length) {
 | 
			
		||||
      this.errorText = 'calculated-fields.hint.arguments-empty';
 | 
			
		||||
    } else {
 | 
			
		||||
@ -234,11 +240,18 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateEntityNameMap(value: CalculatedFieldArgumentValue[]): void {
 | 
			
		||||
    this.entityNameErrorSet.clear();
 | 
			
		||||
    value.forEach(({ refEntityId = {}}) => {
 | 
			
		||||
      if (refEntityId.id && !this.entityNameMap.has(refEntityId.id)) {
 | 
			
		||||
        const { id, entityType } = refEntityId as EntityId;
 | 
			
		||||
        this.entityService.getEntity(entityType as EntityType, id, { ignoreLoading: true, ignoreErrors: true })
 | 
			
		||||
          .pipe(takeUntilDestroyed(this.destroyRef))
 | 
			
		||||
          .pipe(
 | 
			
		||||
            catchError(() => {
 | 
			
		||||
              this.entityNameErrorSet.add(id);
 | 
			
		||||
              return NEVER;
 | 
			
		||||
            }),
 | 
			
		||||
            takeUntilDestroyed(this.destroyRef)
 | 
			
		||||
          )
 | 
			
		||||
          .subscribe(entity => this.entityNameMap.set(id, entity.name));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { DialogComponent } from '@shared/components/dialog.component';
 | 
			
		||||
import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
 | 
			
		||||
import { CalculatedFieldEventBody, DebugEventType, Event, EventType } from '@shared/models/event.models';
 | 
			
		||||
import { EventTableComponent } from '@home/components/event/event-table.component';
 | 
			
		||||
import { CalculatedFieldDebugDialogData, CalculatedFieldType } from '@shared/models/calculated-field.models';
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    this.eventsTable.entitiesTable.updateData();
 | 
			
		||||
    this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = () => this.data.value.type === CalculatedFieldType.SCRIPT;
 | 
			
		||||
    this.eventsTable.entitiesTable.cellActionDescriptors[0].isEnabled = (event => this.data.value.type === CalculatedFieldType.SCRIPT && !!(event as Event).body.arguments)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  cancel(): void {
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,7 @@
 | 
			
		||||
            <div class="fixed-title-width tb-required">{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}</div>
 | 
			
		||||
            <tb-entity-autocomplete
 | 
			
		||||
              class="flex flex-1"
 | 
			
		||||
              #entityAutocomplete
 | 
			
		||||
              formControlName="id"
 | 
			
		||||
              inlineField
 | 
			
		||||
              [placeholder]="'action.set' | translate"
 | 
			
		||||
@ -165,7 +166,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      } @else {
 | 
			
		||||
        <div class="tb-form-row">
 | 
			
		||||
          <div class="fixed-title-width">{{ 'calculated-fields.time-window' | translate }}</div>
 | 
			
		||||
          <div class="fixed-title-width tb-required">{{ 'calculated-fields.time-window' | translate }}</div>
 | 
			
		||||
          <tb-timeinterval
 | 
			
		||||
            subscriptSizing="dynamic"
 | 
			
		||||
            appearance="outline"
 | 
			
		||||
@ -175,7 +176,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        @if (maxDataPointsPerRollingArg) {
 | 
			
		||||
          <div class="tb-form-row limit-field-row">
 | 
			
		||||
            <div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
 | 
			
		||||
            <div class="fixed-title-width tb-required">{{ 'calculated-fields.limit' | translate }}</div>
 | 
			
		||||
            <div class="limit-slider-container flex w-full flex-1 flex-row items-center justify-start">
 | 
			
		||||
              <mat-slider class="flex-1" min="1" max="{{maxDataPointsPerRollingArg}}">
 | 
			
		||||
                <input matSliderThumb formControlName="limit" [value]="argumentFormGroup.get('limit').value"/>
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core';
 | 
			
		||||
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core';
 | 
			
		||||
import { TbPopoverComponent } from '@shared/components/popover.component';
 | 
			
		||||
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
 | 
			
		||||
import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants';
 | 
			
		||||
@ -41,13 +41,14 @@ import { MINUTE } from '@shared/models/time/time.models';
 | 
			
		||||
import { getCurrentAuthState } from '@core/auth/auth.selectors';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { Store } from '@ngrx/store';
 | 
			
		||||
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-calculated-field-argument-panel',
 | 
			
		||||
  templateUrl: './calculated-field-argument-panel.component.html',
 | 
			
		||||
  styleUrls: ['./calculated-field-argument-panel.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit {
 | 
			
		||||
 | 
			
		||||
  @Input() buttonTitle: string;
 | 
			
		||||
  @Input() index: number;
 | 
			
		||||
@ -55,9 +56,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
  @Input() entityId: EntityId;
 | 
			
		||||
  @Input() tenantId: string;
 | 
			
		||||
  @Input() entityName: string;
 | 
			
		||||
  @Input() entityHasError: boolean;
 | 
			
		||||
  @Input() calculatedFieldType: CalculatedFieldType;
 | 
			
		||||
  @Input() usedArgumentNames: string[];
 | 
			
		||||
 | 
			
		||||
  @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
 | 
			
		||||
 | 
			
		||||
  argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
 | 
			
		||||
 | 
			
		||||
  readonly maxDataPointsPerRollingArg = getCurrentAuthState(this.store).maxDataPointsPerRollingArg;
 | 
			
		||||
@ -75,8 +79,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
      scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]],
 | 
			
		||||
    }),
 | 
			
		||||
    defaultValue: ['', [Validators.pattern(oneSpaceInsideRegex)]],
 | 
			
		||||
    limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }],
 | 
			
		||||
    timeWindow: [MINUTE * 15],
 | 
			
		||||
    limit: [{ value: this.defaultLimit, disabled: !this.maxDataPointsPerRollingArg }, [Validators.required, Validators.min(1), Validators.max(this.maxDataPointsPerRollingArg)]],
 | 
			
		||||
    timeWindow: [MINUTE * 15, [Validators.required]],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  argumentTypes: ArgumentType[];
 | 
			
		||||
@ -136,6 +140,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
 | 
			
		||||
      .filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    if (this.entityHasError) {
 | 
			
		||||
      this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  saveArgument(): void {
 | 
			
		||||
    const { refEntityId, ...restConfig } = this.argumentFormGroup.value;
 | 
			
		||||
    const value = (refEntityId.entityType === ArgumentEntityType.Current ? restConfig : { refEntityId, ...restConfig }) as CalculatedFieldArgumentValue;
 | 
			
		||||
 | 
			
		||||
@ -89,7 +89,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
 | 
			
		||||
        {entityName: this.entityName}), [Validators.required, Validators.pattern(/(?:.|\s)*\S(&:.|\s)*/)]],
 | 
			
		||||
      saveRelations: [false, []],
 | 
			
		||||
      saveAttributes: [true, []],
 | 
			
		||||
      saveCredentials: [true, []]
 | 
			
		||||
      saveCredentials: [true, []],
 | 
			
		||||
      saveCalculatedFields: [true, []]
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -187,18 +187,8 @@ export class ImportExportService {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public importCalculatedField(entityId: EntityId): Observable<CalculatedField> {
 | 
			
		||||
  public openCalculatedFieldImportDialog(): Observable<CalculatedField> {
 | 
			
		||||
    return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe(
 | 
			
		||||
      mergeMap((calculatedField: CalculatedField) => {
 | 
			
		||||
        if (!this.validateImportedCalculatedField({ entityId, ...calculatedField })) {
 | 
			
		||||
          this.store.dispatch(new ActionNotificationShow(
 | 
			
		||||
            {message: this.translate.instant('calculated-fields.invalid-file-error'),
 | 
			
		||||
              type: 'error'}));
 | 
			
		||||
          throw new Error('Invalid calculated field file');
 | 
			
		||||
        } else {
 | 
			
		||||
          return this.calculatedFieldsService.saveCalculatedField(this.prepareImport({ entityId, ...calculatedField }));
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      catchError(() => of(null)),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@ -989,16 +979,6 @@ export class ImportExportService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private validateImportedCalculatedField(calculatedField: CalculatedField): boolean {
 | 
			
		||||
    const { name, configuration, entityId } = calculatedField;
 | 
			
		||||
    return isNotEmptyStr(name)
 | 
			
		||||
      && isDefined(configuration)
 | 
			
		||||
      && isDefined(entityId?.id)
 | 
			
		||||
      && !!Object.keys(configuration.arguments).length
 | 
			
		||||
      && isDefined(configuration.expression)
 | 
			
		||||
      && isDefined(configuration.output)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private validateImportedImage(image: ImageExportData): boolean {
 | 
			
		||||
    return !(!isNotEmptyStr(image.data)
 | 
			
		||||
      || !isNotEmptyStr(image.title)
 | 
			
		||||
 | 
			
		||||
@ -127,7 +127,7 @@ export const ArgumentTypeTranslations = new Map<ArgumentType, string>(
 | 
			
		||||
export interface CalculatedFieldArgument {
 | 
			
		||||
  refEntityKey: RefEntityKey;
 | 
			
		||||
  defaultValue?: string;
 | 
			
		||||
  refEntityId?: RefEntityKey;
 | 
			
		||||
  refEntityId?: RefEntityId;
 | 
			
		||||
  limit?: number;
 | 
			
		||||
  timeWindow?: number;
 | 
			
		||||
}
 | 
			
		||||
@ -138,7 +138,7 @@ export interface RefEntityKey {
 | 
			
		||||
  scope?: AttributeScope;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RefEntityKey {
 | 
			
		||||
export interface RefEntityId {
 | 
			
		||||
  entityType: ArgumentEntityType;
 | 
			
		||||
  id: string;
 | 
			
		||||
}
 | 
			
		||||
@ -563,12 +563,12 @@ export const getCalculatedFieldArgumentsHighlights = (
 | 
			
		||||
    regex: `\\b${key}\\b`,
 | 
			
		||||
    next: argumentsObj[key].refEntityKey.type === ArgumentType.Rolling
 | 
			
		||||
      ? 'calculatedFieldRollingArgumentValue'
 | 
			
		||||
      : 'start'
 | 
			
		||||
      : 'no_regex'
 | 
			
		||||
  }));
 | 
			
		||||
  const calculatedFieldCtxArgumentsHighlightRules = {
 | 
			
		||||
    calculatedFieldCtxArgs: [
 | 
			
		||||
      dotOperatorHighlightRule,
 | 
			
		||||
      ...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'start' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule),
 | 
			
		||||
      ...calculatedFieldArgumentsKeys.map(argumentRule => argumentRule.next === 'no_regex' ? {...argumentRule, next: 'calculatedFieldSingleArgumentValue' } : argumentRule),
 | 
			
		||||
      endGroupHighlightRule
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -1074,7 +1074,8 @@
 | 
			
		||||
            "argument-type-required": "Argument type is required.",
 | 
			
		||||
            "max-args": "Maximum number of arguments reached.",
 | 
			
		||||
            "decimals-range": "Decimals by default should be a number between 0 and 15.",
 | 
			
		||||
            "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius."
 | 
			
		||||
            "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.",
 | 
			
		||||
            "arguments-entity-not-found": "Argument target entity not found."
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "confirm-on-exit": {
 | 
			
		||||
@ -5570,12 +5571,12 @@
 | 
			
		||||
        "max-calculated-fields": "Calculated fields per entity maximum number",
 | 
			
		||||
        "max-calculated-fields-range": "Calculated fields per entity maximum number can't be negative",
 | 
			
		||||
        "max-calculated-fields-required": "Calculated fields per entity maximum number is required",
 | 
			
		||||
        "max-data-points-per-rolling-arg": "Maximum data points number in rolling arguments",
 | 
			
		||||
        "max-data-points-per-rolling-arg-range": "Maximum data points number in rolling arguments can't be negative",
 | 
			
		||||
        "max-data-points-per-rolling-arg-required": "Maximum data points number in rolling arguments is required",
 | 
			
		||||
        "max-arguments-per-cf": "Arguments per calculated field maximum number",
 | 
			
		||||
        "max-arguments-per-cf-range": "Arguments per calculated field maximum number can't be negative",
 | 
			
		||||
        "max-arguments-per-cf-required": "Arguments per calculated field maximum number is required",
 | 
			
		||||
        "max-data-points-per-rolling-arg": "Max data points number in rolling arguments",
 | 
			
		||||
        "max-data-points-per-rolling-arg-range": "Max data points number in rolling arguments can't be negative",
 | 
			
		||||
        "max-data-points-per-rolling-arg-required": "Max data points number in rolling arguments is required",
 | 
			
		||||
        "max-arguments-per-cf": "Arguments per calculated field max number",
 | 
			
		||||
        "max-arguments-per-cf-range": "Arguments per calculated field max number can't be negative",
 | 
			
		||||
        "max-arguments-per-cf-required": "Arguments per calculated field max number is required",
 | 
			
		||||
        "max-state-size": "State maximum size in KB",
 | 
			
		||||
        "max-state-size-range": "State maximum size in KB can't be negative",
 | 
			
		||||
        "max-state-size-required": "State maximum size in KB is required",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user