2025-01-24 15:34:18 +02:00
|
|
|
///
|
|
|
|
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
|
|
|
|
///
|
|
|
|
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
/// you may not use this file except in compliance with the License.
|
|
|
|
|
/// You may obtain a copy of the License at
|
|
|
|
|
///
|
|
|
|
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
///
|
|
|
|
|
/// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
/// See the License for the specific language governing permissions and
|
|
|
|
|
/// limitations under the License.
|
|
|
|
|
///
|
|
|
|
|
|
|
|
|
|
import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models';
|
|
|
|
|
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
|
|
|
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
|
|
|
import { Direction } from '@shared/models/page/sort-order';
|
|
|
|
|
import { MatDialog } from '@angular/material/dialog';
|
2025-02-03 17:54:11 +02:00
|
|
|
import { PageLink } from '@shared/models/page/page-link';
|
2025-01-24 15:34:18 +02:00
|
|
|
import { Observable, of } from 'rxjs';
|
|
|
|
|
import { PageData } from '@shared/models/page/page-data';
|
|
|
|
|
import { EntityId } from '@shared/models/id/entity-id';
|
|
|
|
|
import { MINUTE } from '@shared/models/time/time.models';
|
|
|
|
|
import { Store } from '@ngrx/store';
|
|
|
|
|
import { AppState } from '@core/core.state';
|
2025-01-31 11:25:22 +02:00
|
|
|
import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors';
|
2025-02-03 17:54:11 +02:00
|
|
|
import { DestroyRef, Renderer2 } from '@angular/core';
|
2025-01-24 15:34:18 +02:00
|
|
|
import { EntityDebugSettings } from '@shared/models/entity.models';
|
|
|
|
|
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
|
|
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
|
|
|
import { TbPopoverService } from '@shared/components/popover.service';
|
|
|
|
|
import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component';
|
|
|
|
|
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
|
2025-02-11 15:43:13 +02:00
|
|
|
import { catchError, filter, switchMap, tap } from 'rxjs/operators';
|
2025-02-06 15:47:52 +02:00
|
|
|
import {
|
|
|
|
|
CalculatedField,
|
|
|
|
|
CalculatedFieldDebugDialogData,
|
2025-02-10 16:11:46 +02:00
|
|
|
CalculatedFieldDialogData,
|
2025-02-11 15:43:13 +02:00
|
|
|
CalculatedFieldTestScriptInputParams,
|
2025-02-06 15:47:52 +02:00
|
|
|
} from '@shared/models/calculated-field.models';
|
2025-02-10 15:35:14 +02:00
|
|
|
import {
|
|
|
|
|
CalculatedFieldDebugDialogComponent,
|
|
|
|
|
CalculatedFieldDialogComponent,
|
|
|
|
|
CalculatedFieldScriptTestDialogComponent
|
|
|
|
|
} from './components/public-api';
|
2025-02-05 17:57:18 +02:00
|
|
|
import { ImportExportService } from '@shared/import-export/import-export.service';
|
2025-01-24 15:34:18 +02:00
|
|
|
|
2025-02-03 17:54:11 +02:00
|
|
|
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
|
2025-01-24 15:34:18 +02:00
|
|
|
|
2025-01-31 12:30:36 +02:00
|
|
|
// TODO: [Calculated Fields] remove hardcode when BE variable implemented
|
2025-01-24 15:34:18 +02:00
|
|
|
readonly calculatedFieldsDebugPerTenantLimitsConfiguration =
|
|
|
|
|
getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1';
|
|
|
|
|
readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
|
2025-01-31 11:25:22 +02:00
|
|
|
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
|
2025-02-06 15:47:52 +02:00
|
|
|
additionalDebugActionConfig = {
|
|
|
|
|
title: this.translate.instant('calculated-fields.see-debug-events'),
|
2025-02-11 15:43:13 +02:00
|
|
|
action: (calculatedField: CalculatedField) => this.openDebugDialog.call(this, calculatedField),
|
2025-02-06 15:47:52 +02:00
|
|
|
};
|
2025-01-24 15:34:18 +02:00
|
|
|
|
|
|
|
|
constructor(private calculatedFieldsService: CalculatedFieldsService,
|
|
|
|
|
private translate: TranslateService,
|
|
|
|
|
private dialog: MatDialog,
|
|
|
|
|
public entityId: EntityId = null,
|
|
|
|
|
private store: Store<AppState>,
|
|
|
|
|
private durationLeft: DurationLeftPipe,
|
|
|
|
|
private popoverService: TbPopoverService,
|
|
|
|
|
private destroyRef: DestroyRef,
|
2025-02-04 17:03:20 +02:00
|
|
|
private renderer: Renderer2,
|
2025-02-05 17:57:18 +02:00
|
|
|
public entityName: string,
|
|
|
|
|
private importExportService: ImportExportService
|
2025-01-24 15:34:18 +02:00
|
|
|
) {
|
|
|
|
|
super();
|
2025-01-24 17:20:55 +02:00
|
|
|
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
|
2025-01-24 15:34:18 +02:00
|
|
|
this.detailsPanelEnabled = false;
|
2025-01-31 11:25:22 +02:00
|
|
|
this.pageMode = false;
|
2025-01-24 17:20:55 +02:00
|
|
|
this.entityType = EntityType.CALCULATED_FIELD;
|
|
|
|
|
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
|
2025-01-24 15:34:18 +02:00
|
|
|
|
2025-02-03 17:54:11 +02:00
|
|
|
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
|
2025-01-31 11:25:22 +02:00
|
|
|
this.addEntity = this.addCalculatedField.bind(this);
|
2025-01-31 12:30:36 +02:00
|
|
|
this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name});
|
2025-01-31 11:25:22 +02:00
|
|
|
this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text');
|
|
|
|
|
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);
|
2025-02-05 17:57:18 +02:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
];
|
2025-01-24 15:34:18 +02:00
|
|
|
|
|
|
|
|
this.defaultSortOrder = {property: 'name', direction: Direction.DESC};
|
|
|
|
|
|
2025-01-31 15:33:30 +02:00
|
|
|
const expressionColumn = new EntityTableColumn<CalculatedField>('expression', 'calculated-fields.expression', '33%', entity => entity.configuration?.expression);
|
|
|
|
|
expressionColumn.sortable = false;
|
|
|
|
|
|
|
|
|
|
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%'));
|
|
|
|
|
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '50px'));
|
|
|
|
|
this.columns.push(expressionColumn);
|
2025-01-24 15:34:18 +02:00
|
|
|
|
|
|
|
|
this.cellActionDescriptors.push(
|
2025-02-05 17:57:18 +02:00
|
|
|
{
|
|
|
|
|
name: this.translate.instant('action.export'),
|
|
|
|
|
icon: 'file_download',
|
|
|
|
|
isEnabled: () => true,
|
|
|
|
|
onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
|
|
|
|
|
},
|
2025-01-24 15:34:18 +02:00
|
|
|
{
|
|
|
|
|
name: '',
|
2025-01-24 17:20:55 +02:00
|
|
|
nameFunction: entity => this.getDebugConfigLabel(entity?.debugSettings),
|
2025-01-24 15:34:18 +02:00
|
|
|
icon: 'mdi:bug',
|
|
|
|
|
isEnabled: () => true,
|
|
|
|
|
iconFunction: ({ debugSettings }) => this.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
|
|
|
|
|
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: this.translate.instant('action.edit'),
|
|
|
|
|
icon: 'edit',
|
|
|
|
|
isEnabled: () => true,
|
2025-01-31 19:08:56 +02:00
|
|
|
onAction: (_, entity) => this.editCalculatedField(entity),
|
2025-01-24 15:34:18 +02:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-03 17:54:11 +02:00
|
|
|
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
|
2025-01-31 12:30:36 +02:00
|
|
|
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
|
2025-01-24 15:34:18 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-11 15:43:13 +02:00
|
|
|
onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
|
|
|
|
|
const { debugSettings = {}, id } = calculatedField;
|
2025-02-06 15:47:52 +02:00
|
|
|
const additionalActionConfig = {
|
|
|
|
|
...this.additionalDebugActionConfig,
|
2025-02-11 15:43:13 +02:00
|
|
|
action: () => this.openDebugDialog(calculatedField)
|
2025-02-06 15:47:52 +02:00
|
|
|
};
|
2025-02-03 17:54:11 +02:00
|
|
|
const { viewContainerRef } = this.getTable();
|
2025-01-24 15:34:18 +02:00
|
|
|
if ($event) {
|
|
|
|
|
$event.stopPropagation();
|
|
|
|
|
}
|
|
|
|
|
const trigger = $event.target as Element;
|
|
|
|
|
if (this.popoverService.hasPopover(trigger)) {
|
|
|
|
|
this.popoverService.hidePopover(trigger);
|
|
|
|
|
} else {
|
2025-02-03 17:54:11 +02:00
|
|
|
const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
|
2025-01-24 15:34:18 +02:00
|
|
|
viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null,
|
|
|
|
|
{
|
|
|
|
|
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
|
|
|
|
|
maxDebugModeDuration: this.maxDebugModeDuration,
|
2025-01-31 11:25:22 +02:00
|
|
|
entityLabel: this.translate.instant('debug-settings.calculated-field'),
|
2025-02-06 15:47:52 +02:00
|
|
|
additionalActionConfig,
|
2025-01-24 15:34:18 +02:00
|
|
|
...debugSettings
|
|
|
|
|
},
|
|
|
|
|
{},
|
|
|
|
|
{}, {}, true);
|
|
|
|
|
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((settings: EntityDebugSettings) => {
|
|
|
|
|
this.onDebugConfigChanged(id.id, settings);
|
|
|
|
|
debugStrategyPopover.hide();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-03 17:54:11 +02:00
|
|
|
private addCalculatedField(): Observable<CalculatedField> {
|
|
|
|
|
return this.getCalculatedFieldDialog()
|
2025-01-31 11:25:22 +02:00
|
|
|
.pipe(
|
|
|
|
|
filter(Boolean),
|
2025-01-31 12:30:36 +02:00
|
|
|
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField })),
|
2025-01-31 11:25:22 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-11 15:43:13 +02:00
|
|
|
private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
|
|
|
|
|
this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty)
|
2025-01-31 11:25:22 +02:00
|
|
|
.pipe(
|
|
|
|
|
filter(Boolean),
|
2025-01-31 12:30:36 +02:00
|
|
|
switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField })),
|
2025-01-31 11:25:22 +02:00
|
|
|
)
|
|
|
|
|
.subscribe((res) => {
|
|
|
|
|
if (res) {
|
|
|
|
|
this.updateData();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-11 15:43:13 +02:00
|
|
|
private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
|
2025-02-03 17:54:11 +02:00
|
|
|
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
|
2025-01-31 11:25:22 +02:00
|
|
|
disableClose: true,
|
|
|
|
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
|
|
|
|
data: {
|
|
|
|
|
value,
|
|
|
|
|
buttonTitle,
|
|
|
|
|
entityId: this.entityId,
|
|
|
|
|
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
|
|
|
|
|
tenantId: this.tenantId,
|
2025-02-04 17:03:20 +02:00
|
|
|
entityName: this.entityName,
|
2025-02-06 15:47:52 +02:00
|
|
|
additionalDebugActionConfig: this.additionalDebugActionConfig,
|
2025-02-11 15:48:45 +02:00
|
|
|
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
|
2025-02-11 15:43:13 +02:00
|
|
|
isDirty
|
2025-02-11 16:04:23 +02:00
|
|
|
},
|
|
|
|
|
...(isDirty ? { enterAnimationDuration: 0 } : {})
|
2025-01-31 11:25:22 +02:00
|
|
|
})
|
2025-01-31 19:08:56 +02:00
|
|
|
.afterClosed();
|
2025-01-31 11:25:22 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-11 15:43:13 +02:00
|
|
|
private openDebugDialog(calculatedField: CalculatedField): void {
|
2025-02-06 15:47:52 +02:00
|
|
|
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
|
|
|
|
|
disableClose: true,
|
|
|
|
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
2025-02-06 17:01:33 +02:00
|
|
|
data: {
|
|
|
|
|
tenantId: this.tenantId,
|
2025-02-11 15:43:13 +02:00
|
|
|
value: calculatedField,
|
|
|
|
|
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
|
2025-02-06 17:01:33 +02:00
|
|
|
}
|
2025-02-06 15:47:52 +02:00
|
|
|
})
|
|
|
|
|
.afterClosed()
|
|
|
|
|
.subscribe();
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-05 17:57:18 +02:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 15:34:18 +02:00
|
|
|
private getDebugConfigLabel(debugSettings: EntityDebugSettings): string {
|
|
|
|
|
const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil);
|
|
|
|
|
|
|
|
|
|
if (!isDebugActive) {
|
|
|
|
|
return debugSettings?.failuresEnabled ? this.translate.instant('debug-settings.failures') : this.translate.instant('common.disabled');
|
|
|
|
|
} else {
|
2025-01-31 19:08:56 +02:00
|
|
|
return this.durationLeft.transform(debugSettings?.allEnabledUntil);
|
2025-01-24 15:34:18 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isDebugActive(allEnabledUntil: number): boolean {
|
|
|
|
|
return allEnabledUntil > new Date().getTime();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
|
2025-01-31 11:25:22 +02:00
|
|
|
this.calculatedFieldsService.getCalculatedFieldById(id).pipe(
|
2025-01-24 15:34:18 +02:00
|
|
|
switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })),
|
|
|
|
|
catchError(() => of(null)),
|
|
|
|
|
takeUntilDestroyed(this.destroyRef),
|
|
|
|
|
).subscribe(() => this.updateData());
|
|
|
|
|
}
|
2025-02-10 15:35:14 +02:00
|
|
|
|
2025-02-11 15:43:13 +02:00
|
|
|
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>): Observable<string> {
|
|
|
|
|
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptInputParams, string>(CalculatedFieldScriptTestDialogComponent,
|
2025-02-10 15:35:14 +02:00
|
|
|
{
|
|
|
|
|
disableClose: true,
|
|
|
|
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
|
|
|
|
|
data: {
|
2025-02-11 16:04:23 +02:00
|
|
|
arguments: argumentsObj ?? Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => { acc[key] = ''; return acc; }, {}),
|
2025-02-11 15:43:13 +02:00
|
|
|
expression: calculatedField.configuration.expression,
|
2025-02-10 15:35:14 +02:00
|
|
|
}
|
2025-02-11 15:43:13 +02:00
|
|
|
}).afterClosed()
|
|
|
|
|
.pipe(
|
|
|
|
|
filter(Boolean),
|
|
|
|
|
tap(expression =>
|
|
|
|
|
this.editCalculatedField({...calculatedField, configuration: {...calculatedField.configuration, expression } }, true)
|
|
|
|
|
),
|
|
|
|
|
);
|
2025-02-10 15:35:14 +02:00
|
|
|
}
|
2025-01-24 15:34:18 +02:00
|
|
|
}
|