Added calculated field import/export

This commit is contained in:
mpetrov 2025-02-05 17:57:18 +02:00
parent 634ff46ef7
commit 669bfcbb22
6 changed files with 104 additions and 6 deletions

View File

@ -37,6 +37,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators'; import { catchError, filter, switchMap } from 'rxjs/operators';
import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models'; import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models';
import { CalculatedFieldDialogComponent } from './components/public-api'; import { CalculatedFieldDialogComponent } from './components/public-api';
import { ImportExportService } from '@shared/import-export/import-export.service';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> { export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
@ -55,7 +56,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private renderer: Renderer2, private renderer: Renderer2,
public entityName: string public entityName: string,
private importExportService: ImportExportService
) { ) {
super(); super();
this.tableTitle = this.translate.instant('entity.type-calculated-fields'); 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.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); 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}; this.defaultSortOrder = {property: 'name', direction: Direction.DESC};
@ -82,6 +98,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.columns.push(expressionColumn); this.columns.push(expressionColumn);
this.cellActionDescriptors.push( this.cellActionDescriptors.push(
{
name: this.translate.instant('action.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
},
{ {
name: '', name: '',
nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings), nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings),
@ -166,6 +188,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
.afterClosed(); .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 { private getDebugConfigLabel(debugSettings: EntityDebugSettings): string {
const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil); const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil);

View File

@ -34,6 +34,7 @@ import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { ImportExportService } from '@shared/import-export/import-export.service';
@Component({ @Component({
selector: 'tb-calculated-fields-table', selector: 'tb-calculated-fields-table',
@ -59,6 +60,7 @@ export class CalculatedFieldsTableComponent {
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private renderer: Renderer2, private renderer: Renderer2,
private importExportService: ImportExportService,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
effect(() => { effect(() => {
@ -73,7 +75,8 @@ export class CalculatedFieldsTableComponent {
this.popoverService, this.popoverService,
this.destroyRef, this.destroyRef,
this.renderer, this.renderer,
this.entityName() this.entityName(),
this.importExportService
); );
this.cd.markForCheck(); this.cd.markForCheck();
} }

View File

@ -71,7 +71,7 @@ export class ImportDialogComponent extends DialogComponent<ImportDialogComponent
this.importFormGroup = this.fb.group({ this.importFormGroup = this.fb.group({
importType: ['file'], importType: ['file'],
fileContent: [null, [Validators.required]], fileContent: [null, [Validators.required]],
jsonContent: [null, [Validators.required]] jsonContent: [{ value: null, disabled: true }, [Validators.required]]
}); });
this.importFormGroup.get('importType').valueChanges.pipe( this.importFormGroup.get('importType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)

View File

@ -88,6 +88,8 @@ import {
ExportResourceDialogDialogResult ExportResourceDialogDialogResult
} from '@shared/import-export/export-resource-dialog.component'; } from '@shared/import-export/export-resource-dialog.component';
import { FormProperty, propertyValid } from '@shared/models/dynamic-form.models'; 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, export type editMissingAliasesFunction = (widgets: Array<Widget>, isSingleWidget: boolean,
customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>; customTitle: string, missingEntityAliases: EntityAliases) => Observable<EntityAliases>;
@ -116,6 +118,7 @@ export class ImportExportService {
private imageService: ImageService, private imageService: ImageService,
private utils: UtilsService, private utils: UtilsService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private calculatedFieldsService: CalculatedFieldsService,
private dialog: MatDialog) { 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) { public exportDashboard(dashboardId: string) {
this.getIncludeResourcesPreference('includeResourcesInExportDashboard').subscribe(includeResources => { this.getIncludeResourcesPreference('includeResourcesInExportDashboard').subscribe(includeResources => {
this.openExportDialog('dashboard.export', 'dashboard.export-prompt', includeResources).subscribe(result => { 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 { private validateImportedImage(image: ImageExportData): boolean {
return !(!isNotEmptyStr(image.data) return !(!isNotEmptyStr(image.data)
|| !isNotEmptyStr(image.title) || !isNotEmptyStr(image.title)
@ -1209,6 +1252,11 @@ export class ImportExportService {
return profile; return profile;
} }
private prepareCalculatedFieldExport(calculatedField: CalculatedField): CalculatedField {
delete calculatedField.entityId;
return this.prepareExport(calculatedField);
}
private prepareExport(data: any): any { private prepareExport(data: any): any {
const exportedData = deepClone(data); const exportedData = deepClone(data);
if (isDefined(exportedData.id)) { if (isDefined(exportedData.id)) {

View File

@ -15,16 +15,15 @@
/// ///
import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models'; 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 { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType } from '@shared/models/alias.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; debugSettings?: EntityDebugSettings;
externalId?: string;
configuration: CalculatedFieldConfiguration; configuration: CalculatedFieldConfiguration;
type: CalculatedFieldType; type: CalculatedFieldType;
entityId: EntityId; entityId: EntityId;
@ -46,6 +45,13 @@ export interface CalculatedFieldConfiguration {
type: CalculatedFieldType; type: CalculatedFieldType;
expression: string; expression: string;
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
output: CalculatedFieldOutput;
}
export interface CalculatedFieldOutput {
type: OutputType;
name: string;
scope?: AttributeScope;
} }
export enum ArgumentEntityType { export enum ArgumentEntityType {

View File

@ -1043,6 +1043,12 @@
"asset-name": "Asset name", "asset-name": "Asset name",
"timeseries": "Time series", "timeseries": "Time series",
"output": "Output", "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", "output-type": "Output type",
"delete-title": "Are you sure you want to delete the calculated field '{{title}}'?", "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.", "delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.",