diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts index 38d704d51b..b4a46d9592 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts @@ -22,7 +22,17 @@ import { telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; -import { BehaviorSubject, forkJoin, Observable, Observer, of, Subscription, throwError } from 'rxjs'; +import { + BehaviorSubject, + forkJoin, + Observable, + Observer, + of, + ReplaySubject, + Subscription, + switchMap, + throwError +} from 'rxjs'; import { catchError, delay, map, share, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectorRef, Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { @@ -45,6 +55,8 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m import { EntityId } from '@shared/models/id/entity-id'; import { isDefinedAndNotNull } from '@core/utils'; import { parseError } from '@shared/models/error.models'; +import { CompiledTbFunction, compileTbFunction } from '@shared/models/js-function.models'; +import { HttpClient } from '@angular/common/http'; @Directive() // eslint-disable-next-line @angular-eslint/directive-class-suffix @@ -163,46 +175,57 @@ type DataToValueFunction = (data: any) => V; export class DataToValueConverter { - private readonly dataToValueFunction: DataToValueFunction; + private readonly dataToValueFunction$: Observable>>; private readonly compareToValue: any; - constructor(private settings: DataToValueSettings, + constructor(private http: HttpClient, + private settings: DataToValueSettings, private valueType: ValueType) { this.compareToValue = settings.compareToValue; switch (settings.type) { case DataToValueType.FUNCTION: - try { - this.dataToValueFunction = new Function('data', settings.dataToValueFunction) as DataToValueFunction; - } catch (e) { - this.dataToValueFunction = (data) => data; - } + this.dataToValueFunction$ = compileTbFunction(this.http, settings.dataToValueFunction, 'data').pipe( + catchError(() => { + return of(new CompiledTbFunction((data: any) => data, [])); + }), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); break; case DataToValueType.NONE: break; } } - dataToValue(data: any): V { - let result: V; + dataToValue(data: any): Observable { + let result: Observable; switch (this.settings.type) { case DataToValueType.FUNCTION: - result = data; - try { - let input = data; - if (!!data) { - try { - input = JSON.parse(data); - } catch (_e) {} - } - result = this.dataToValueFunction(input); - } catch (_e) {} + result = this.dataToValueFunction$.pipe( + map((dataToValueFunction) => { + let input = data; + if (!!data) { + try { + input = JSON.parse(data); + } catch (_e) {} + } + return dataToValueFunction.execute(input); + }), + catchError(() => of(data)) + ); break; case DataToValueType.NONE: - result = data; + result = of(data); break; } if (this.valueType === ValueType.BOOLEAN) { - result = (result === this.compareToValue) as any; + result = result.pipe( + map(val => (val === this.compareToValue) as V) + ); } return result; } @@ -260,17 +283,17 @@ export abstract class ValueGetter extends ValueAction { protected simulated: boolean) { super(ctx, settings); if (this.settings.action !== GetValueAction.DO_NOTHING && this.settings.action !== GetValueAction.GET_ALARM_STATUS) { - this.dataConverter = new DataToValueConverter(settings.dataToValue, valueType); + this.dataConverter = new DataToValueConverter(ctx.http, settings.dataToValue, valueType); } } getValue(): Observable { const valueObservable: Observable = this.doGetValue().pipe( - map((data) => { + switchMap((data) => { if (this.dataConverter) { return this.dataConverter.dataToValue(data); } else { - return data; + return of(data); } }), catchError(err => { @@ -308,9 +331,10 @@ type ValueToDataFunction = (value: V) => any; export class ValueToDataConverter { private readonly constantValue: any; - private readonly valueToDataFunction: ValueToDataFunction; + private readonly valueToDataFunction$: Observable>>; - constructor(protected settings: ValueToDataSettings) { + constructor(private http: HttpClient, + private settings: ValueToDataSettings) { switch (settings.type) { case ValueToDataType.VALUE: break; @@ -318,31 +342,38 @@ export class ValueToDataConverter { this.constantValue = this.settings.constantValue; break; case ValueToDataType.FUNCTION: - try { - this.valueToDataFunction = new Function('value', settings.valueToDataFunction) as ValueToDataFunction; - } catch (e) { - this.valueToDataFunction = (data) => data; - } + this.valueToDataFunction$ = compileTbFunction(this.http, settings.valueToDataFunction, 'value').pipe( + catchError(() => { + return of(new CompiledTbFunction((value: any) => value, [])); + }), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); break; case ValueToDataType.NONE: break; } } - valueToData(value: V): any { + valueToData(value: V): Observable { switch (this.settings.type) { case ValueToDataType.VALUE: - return value; + return of(value); case ValueToDataType.CONSTANT: - return this.constantValue; + return of(this.constantValue); case ValueToDataType.FUNCTION: - let result = value; - try { - result = this.valueToDataFunction(value); - } catch (e) {} - return result; + return this.valueToDataFunction$.pipe( + map((valueToDataFunction) => { + return valueToDataFunction.execute(value); + }), + catchError(() => of(value)) + ); case ValueToDataType.NONE: - return null; + return of(null); } } } @@ -368,14 +399,15 @@ export abstract class ValueSetter extends ValueAction { protected settings: SetValueSettings, protected simulated: boolean) { super(ctx, settings); - this.valueToDataConverter = new ValueToDataConverter(settings.valueToData); + this.valueToDataConverter = new ValueToDataConverter(ctx.http, settings.valueToData); } setValue(value: V): Observable { if (this.simulated) { return of(null).pipe(delay(500)); } else { - return this.doSetValue(this.valueToDataConverter.valueToData(value)).pipe( + return this.valueToDataConverter.valueToData(value).pipe( + switchMap(data => this.doSetValue(data)), catchError(err => { throw this.handleError(err); }) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html index 87a81db0eb..cb6cd5cbe0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html @@ -138,6 +138,7 @@ { if (!behavior.id || !behavior.name || !behavior.type) { @@ -77,7 +78,7 @@ export const behaviorValid = (behavior: ScadaSymbolBehavior): boolean => { return false; } if (behavior.defaultSetValueSettings.valueToData?.type === ValueToDataType.FUNCTION - && isUndefinedOrNull(behavior.defaultSetValueSettings.valueToData?.valueToDataFunction)) { + && !isNotEmptyTbFunction(behavior.defaultSetValueSettings.valueToData?.valueToDataFunction)) { return false; } break; diff --git a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts index fc03ef2eb9..b1c47a1fd3 100644 --- a/ui-ngx/src/app/shared/models/action-widget-settings.models.ts +++ b/ui-ngx/src/app/shared/models/action-widget-settings.models.ts @@ -17,6 +17,7 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { widgetType } from '@shared/models/widget.models'; import { AlarmSeverity } from '@shared/models/alarm.models'; +import { TbFunction } from '@shared/models/js-function.models'; export enum GetValueAction { DO_NOTHING = 'DO_NOTHING', @@ -79,7 +80,7 @@ export enum DataToValueType { export interface DataToValueSettings { type: DataToValueType; - dataToValueFunction: string; + dataToValueFunction: TbFunction; compareToValue?: any; } @@ -131,7 +132,7 @@ export enum ValueToDataType { export interface ValueToDataSettings { type: ValueToDataType; constantValue: any; - valueToDataFunction: string; + valueToDataFunction: TbFunction; } export interface SetValueSettings extends ValueActionSettings {