Added calculated field import/export
This commit is contained in:
		
							parent
							
								
									634ff46ef7
								
							
						
					
					
						commit
						669bfcbb22
					
				@ -37,6 +37,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
 | 
			
		||||
import { catchError, filter, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models';
 | 
			
		||||
import { CalculatedFieldDialogComponent } from './components/public-api';
 | 
			
		||||
import { ImportExportService } from '@shared/import-export/import-export.service';
 | 
			
		||||
 | 
			
		||||
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
 | 
			
		||||
 | 
			
		||||
@ -55,7 +56,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
 | 
			
		||||
              private popoverService: TbPopoverService,
 | 
			
		||||
              private destroyRef: DestroyRef,
 | 
			
		||||
              private renderer: Renderer2,
 | 
			
		||||
              public entityName: string
 | 
			
		||||
              public entityName: string,
 | 
			
		||||
              private importExportService: ImportExportService
 | 
			
		||||
  ) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.tableTitle = this.translate.instant('entity.type-calculated-fields');
 | 
			
		||||
@ -71,6 +73,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
 | 
			
		||||
    this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
 | 
			
		||||
    this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
 | 
			
		||||
    this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
 | 
			
		||||
    this.addActionDescriptors = [
 | 
			
		||||
      {
 | 
			
		||||
        name: this.translate.instant('calculated-fields.create'),
 | 
			
		||||
        icon: 'insert_drive_file',
 | 
			
		||||
        isEnabled: () => true,
 | 
			
		||||
        onAction: ($event) => this.getTable().addEntity($event)
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: this.translate.instant('calculated-fields.import'),
 | 
			
		||||
        icon: 'file_upload',
 | 
			
		||||
        isEnabled: () => true,
 | 
			
		||||
        onAction: () => this.importCalculatedField()
 | 
			
		||||
      }
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    this.defaultSortOrder = {property: 'name', direction: Direction.DESC};
 | 
			
		||||
 | 
			
		||||
@ -82,6 +98,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
 | 
			
		||||
    this.columns.push(expressionColumn);
 | 
			
		||||
 | 
			
		||||
    this.cellActionDescriptors.push(
 | 
			
		||||
      {
 | 
			
		||||
        name: this.translate.instant('action.export'),
 | 
			
		||||
        icon: 'file_download',
 | 
			
		||||
        isEnabled: () => true,
 | 
			
		||||
        onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: '',
 | 
			
		||||
        nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings),
 | 
			
		||||
@ -166,6 +188,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
 | 
			
		||||
      .afterClosed();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
 | 
			
		||||
    if ($event) {
 | 
			
		||||
      $event.stopPropagation();
 | 
			
		||||
    }
 | 
			
		||||
    this.importExportService.exportCalculatedField(calculatedField.id.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private importCalculatedField(): void {
 | 
			
		||||
    this.importExportService.importCalculatedField(this.entityId)
 | 
			
		||||
      .pipe(filter(Boolean), takeUntilDestroyed(this.destroyRef))
 | 
			
		||||
      .subscribe(() => this.updateData());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getDebugConfigLabel(debugSettings: EntityDebugSettings): string {
 | 
			
		||||
    const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/
 | 
			
		||||
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
 | 
			
		||||
import { TbPopoverService } from '@shared/components/popover.service';
 | 
			
		||||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
 | 
			
		||||
import { ImportExportService } from '@shared/import-export/import-export.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-calculated-fields-table',
 | 
			
		||||
@ -59,6 +60,7 @@ export class CalculatedFieldsTableComponent {
 | 
			
		||||
              private popoverService: TbPopoverService,
 | 
			
		||||
              private cd: ChangeDetectorRef,
 | 
			
		||||
              private renderer: Renderer2,
 | 
			
		||||
              private importExportService: ImportExportService,
 | 
			
		||||
              private destroyRef: DestroyRef) {
 | 
			
		||||
 | 
			
		||||
    effect(() => {
 | 
			
		||||
@ -73,7 +75,8 @@ export class CalculatedFieldsTableComponent {
 | 
			
		||||
          this.popoverService,
 | 
			
		||||
          this.destroyRef,
 | 
			
		||||
          this.renderer,
 | 
			
		||||
          this.entityName()
 | 
			
		||||
          this.entityName(),
 | 
			
		||||
          this.importExportService
 | 
			
		||||
        );
 | 
			
		||||
        this.cd.markForCheck();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -71,7 +71,7 @@ export class ImportDialogComponent extends DialogComponent<ImportDialogComponent
 | 
			
		||||
    this.importFormGroup = this.fb.group({
 | 
			
		||||
      importType: ['file'],
 | 
			
		||||
      fileContent: [null, [Validators.required]],
 | 
			
		||||
      jsonContent: [null, [Validators.required]]
 | 
			
		||||
      jsonContent: [{ value: null, disabled: true }, [Validators.required]]
 | 
			
		||||
    });
 | 
			
		||||
    this.importFormGroup.get('importType').valueChanges.pipe(
 | 
			
		||||
      takeUntilDestroyed(this.destroyRef)
 | 
			
		||||
 | 
			
		||||
@ -88,6 +88,8 @@ import {
 | 
			
		||||
  ExportResourceDialogDialogResult
 | 
			
		||||
} from '@shared/import-export/export-resource-dialog.component';
 | 
			
		||||
import { FormProperty, propertyValid } from '@shared/models/dynamic-form.models';
 | 
			
		||||
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
 | 
			
		||||
import { CalculatedField } from '@shared/models/calculated-field.models';
 | 
			
		||||
 | 
			
		||||
export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
 | 
			
		||||
                                          customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
 | 
			
		||||
@ -116,6 +118,7 @@ export class ImportExportService {
 | 
			
		||||
              private imageService: ImageService,
 | 
			
		||||
              private utils: UtilsService,
 | 
			
		||||
              private itembuffer: ItemBufferService,
 | 
			
		||||
              private calculatedFieldsService: CalculatedFieldsService,
 | 
			
		||||
              private dialog: MatDialog) {
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
@ -171,6 +174,35 @@ export class ImportExportService {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public exportCalculatedField(calculatedFieldId: string): void {
 | 
			
		||||
    this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({
 | 
			
		||||
      next: (calculatedField) => {
 | 
			
		||||
        let name = calculatedField.name;
 | 
			
		||||
        name = name.toLowerCase().replace(/\W/g, '_');
 | 
			
		||||
        this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name);
 | 
			
		||||
      },
 | 
			
		||||
      error: (e) => {
 | 
			
		||||
        this.handleExportError(e, 'calculated-fields.export-failed-error');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public importCalculatedField(entityId: EntityId): 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)),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public exportDashboard(dashboardId: string) {
 | 
			
		||||
    this.getIncludeResourcesPreference('includeResourcesInExportDashboard').subscribe(includeResources => {
 | 
			
		||||
      this.openExportDialog('dashboard.export', 'dashboard.export-prompt', includeResources).subscribe(result => {
 | 
			
		||||
@ -957,6 +989,17 @@ 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)
 | 
			
		||||
      && isNotEmptyStr(configuration.output.name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private validateImportedImage(image: ImageExportData): boolean {
 | 
			
		||||
    return !(!isNotEmptyStr(image.data)
 | 
			
		||||
      || !isNotEmptyStr(image.title)
 | 
			
		||||
@ -1209,6 +1252,11 @@ export class ImportExportService {
 | 
			
		||||
    return profile;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private prepareCalculatedFieldExport(calculatedField: CalculatedField): CalculatedField {
 | 
			
		||||
    delete calculatedField.entityId;
 | 
			
		||||
    return this.prepareExport(calculatedField);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private prepareExport(data: any): any {
 | 
			
		||||
    const exportedData = deepClone(data);
 | 
			
		||||
    if (isDefined(exportedData.id)) {
 | 
			
		||||
 | 
			
		||||
@ -15,16 +15,15 @@
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models';
 | 
			
		||||
import { BaseData } from '@shared/models/base-data';
 | 
			
		||||
import { BaseData, ExportableEntity } from '@shared/models/base-data';
 | 
			
		||||
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
 | 
			
		||||
import { EntityId } from '@shared/models/id/entity-id';
 | 
			
		||||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
 | 
			
		||||
import { EntityType } from '@shared/models/entity-type.models';
 | 
			
		||||
import { AliasFilterType } from '@shared/models/alias.models';
 | 
			
		||||
 | 
			
		||||
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId {
 | 
			
		||||
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
 | 
			
		||||
  debugSettings?: EntityDebugSettings;
 | 
			
		||||
  externalId?: string;
 | 
			
		||||
  configuration: CalculatedFieldConfiguration;
 | 
			
		||||
  type: CalculatedFieldType;
 | 
			
		||||
  entityId: EntityId;
 | 
			
		||||
@ -46,6 +45,13 @@ export interface CalculatedFieldConfiguration {
 | 
			
		||||
  type: CalculatedFieldType;
 | 
			
		||||
  expression: string;
 | 
			
		||||
  arguments: Record<string, CalculatedFieldArgument>;
 | 
			
		||||
  output: CalculatedFieldOutput;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CalculatedFieldOutput {
 | 
			
		||||
  type: OutputType;
 | 
			
		||||
  name: string;
 | 
			
		||||
  scope?: AttributeScope;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum ArgumentEntityType {
 | 
			
		||||
 | 
			
		||||
@ -1043,6 +1043,12 @@
 | 
			
		||||
        "asset-name": "Asset name",
 | 
			
		||||
        "timeseries": "Time series",
 | 
			
		||||
        "output": "Output",
 | 
			
		||||
        "create": "Create new calculated field",
 | 
			
		||||
        "file": "Calculated field file",
 | 
			
		||||
        "invalid-file-error": "Invalid file format. Please make sure the file is a valid JSON file.",
 | 
			
		||||
        "import": "Import calculated field",
 | 
			
		||||
        "export": "Export calculated field",
 | 
			
		||||
        "export-failed-error": "Unable to export calculated field: {{error}}",
 | 
			
		||||
        "output-type": "Output type",
 | 
			
		||||
        "delete-title": "Are you sure you want to delete the calculated field '{{title}}'?",
 | 
			
		||||
        "delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user