From 16e04efc9ecdac4338ef9027a2323df155011d4a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 2 Dec 2024 16:35:52 +0200 Subject: [PATCH] UI: Add support of js modules: Table widgets, Markdown widgets, QR code widget, Data key generation function. --- .../app/core/api/entity-data-subscription.ts | 30 +-- ui-ngx/src/app/core/utils.ts | 45 +++- .../config/data-key-config.component.html | 1 + .../alarm/alarms-table-widget.component.html | 10 +- .../alarm/alarms-table-widget.component.ts | 193 +++++++++------ .../entities-table-widget.component.html | 8 +- .../entity/entities-table-widget.component.ts | 221 +++++++++++------- .../widget/lib/markdown-widget.component.ts | 26 ++- .../widget/lib/qrcode-widget.component.ts | 31 ++- .../alarms-table-key-settings.component.html | 2 + ...larms-table-widget-settings.component.html | 1 + .../markdown-widget-settings.component.html | 1 + .../qrcode-widget-settings.component.html | 1 + ...meseries-table-key-settings.component.html | 2 + ...s-table-latest-key-settings.component.html | 2 + ...eries-table-widget-settings.component.html | 1 + ...custom-action-pretty-editor.component.html | 1 + ...ction-pretty-resources-tabs.component.html | 1 + ...entities-table-key-settings.component.html | 2 + ...ities-table-widget-settings.component.html | 1 + .../widget/lib/table-widget.models.ts | 187 +++++++++------ .../timeseries-table-widget.component.html | 12 +- .../lib/timeseries-table-widget.component.ts | 219 ++++++++++------- .../app/shared/models/js-function.models.ts | 2 +- ui-ngx/src/app/shared/models/widget.models.ts | 2 +- 25 files changed, 633 insertions(+), 369 deletions(-) diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 8df8b7c09b..46cd2af7ab 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -64,7 +64,7 @@ import { PageData } from '@shared/models/page/page-data'; import { DataAggregator, onAggregatedData } from '@core/api/data-aggregator'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; -import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, switchMap } from 'rxjs'; +import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, tap } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { @@ -89,8 +89,8 @@ export interface SubscriptionDataKey { timeForComparison?: ComparisonDuration; comparisonCustomIntervalValue?: number; comparisonResultType?: ComparisonResultType; - funcBody: string; - func?: DataKeyFunction; + funcBody: TbFunction; + func?: CompiledTbFunction; postFuncBody: TbFunction; postFunc?: CompiledTbFunction; index?: number; @@ -210,7 +210,7 @@ export class EntityDataSubscription { dataKey.index = i; if (this.datasourceType === DatasourceType.function) { if (!dataKey.func) { - dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction; + dataKey.func = await firstValueFrom(compileTbFunction(this.http, dataKey.funcBody, 'time', 'prevValue')); } } else { if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) { @@ -276,9 +276,9 @@ export class EntityDataSubscription { } public subscribe(): Observable { - return from(this.initializeSubscription()).pipe( - switchMap(() => { - this.entityDataResolveSubject = new ReplaySubject(1); + this.entityDataResolveSubject = new ReplaySubject(1); + from(this.initializeSubscription()).pipe( + tap(() => { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { this.started = true; this.dataResolved = true; @@ -621,13 +621,13 @@ export class EntityDataSubscription { ); this.subscriber.subscribe(); } - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - return of(null); - } else { - return this.entityDataResolveSubject.asObservable(); - } }) - ); + ).subscribe(); + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return of(null); + } else { + return this.entityDataResolveSubject.asObservable(); + } } public start() { @@ -1211,7 +1211,7 @@ export class EntityDataSubscription { prevSeries = [0, 0]; } for (let time = startTime; time <= endTime && (this.timeseriesTimer || this.history); time += this.frequency) { - const value = dataKey.func(time, prevSeries[1]); + const value = dataKey.func.execute(time, prevSeries[1]); const series: [number, any] = [time, value]; data.push(series); prevSeries = series; @@ -1232,7 +1232,7 @@ export class EntityDataSubscription { prevSeries = [0, 0]; } const time = Date.now() + this.latestTsOffset; - const value = dataKey.func(time, prevSeries[1]); + const value = dataKey.func.execute(time, prevSeries[1]); const series: [number, any] = [time, value]; this.datasourceData[0][datasourceKey].data = [series]; this.listener.dataUpdated(this.datasourceData[0][datasourceKey], diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 43030a19f6..34735732d0 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -15,16 +15,22 @@ /// import _ from 'lodash'; -import { from, Observable, Subject } from 'rxjs'; -import { finalize, share } from 'rxjs/operators'; +import { from, Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { catchError, finalize, share } from 'rxjs/operators'; import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models'; -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; +import { + CompiledTbFunction, + compileTbFunction, GenericFunction, + isNotEmptyTbFunction, + TbFunction +} from '@shared/models/js-function.models'; const varsRegex = /\${([^}]*)}/g; @@ -673,6 +679,39 @@ export function parseFunction(source: any, params: string[] = ['def']): (...args return res; } +export function parseTbFunction(http: HttpClient, source: TbFunction, params: string[] = ['def']): Observable> { + if (isNotEmptyTbFunction(source)) { + return compileTbFunction(http, source, ...params).pipe( + catchError(() => { + return of(null); + }), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); + } else { + return of(null); + } +} + +export function safeExecuteTbFunction(func: CompiledTbFunction, params = []) { + let res = null; + if (func) { + try { + res = func.execute(...params); + } + catch (err) { + console.log('error in external function:', err); + res = null; + } + } + return res; +} + + export function safeExecute(func: (...args: any[]) => any, params = []) { let res = null; if (func && typeof (func) === 'function') { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html index ade0765402..29c20f957c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -72,6 +72,7 @@ [functionArgs]="['time', 'prevValue']" [globalVariables]="functionScopeVariables" [validationArgs]="[[1, 1],[1, '1']]" + withModules resultType="any" helpId="widget/config/datakey_generation_fn" formControlName="funcBody"> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html index 7db8a28556..1df8050167 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html @@ -70,7 +70,7 @@ [indeterminate]="alarmsDatasource.selection.hasValue() && !(alarmsDatasource.isAllSelected() | async)"> - + @@ -82,10 +82,10 @@ {{ column.title }} - + - +
@@ -171,7 +171,7 @@ [class.invisible]="alarmsDatasource.dataLoading" [class.tb-pointer]="hasRowAction" *matRowDef="let alarm; columns: displayedColumns; let row = index" - [style]="rowStyle(alarm, row)" + [style]="rowStyle(alarm, row) | async" (click)="onRowClick($event, alarm)"> } = {}; private columnWidth: {[key: string]: string} = {}; private columnDefaultVisibility: {[key: string]: boolean} = {}; private columnSelectionAvailability: {[key: string]: boolean} = {}; private columnsWithCellClick: Array = []; - private rowStylesInfo: RowStyleInfo; + private rowStylesInfo: Observable; private widgetTimewindowChanged$: Subscription; @@ -389,7 +389,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } this.alarmFilterAction.show = enableFilter; - this.rowStylesInfo = getRowStyleInfo(this.settings, 'alarm, ctx'); + this.rowStylesInfo = getRowStyleInfo(this.ctx, this.settings, 'alarm, ctx'); const pageSize = this.settings.defaultPageSize; if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { @@ -438,10 +438,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, keySettings.columnWidth = '120px'; } } - this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); - this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); - this.contentsInfo[dataKey.def].units = dataKey.units; - this.contentsInfo[dataKey.def].decimals = dataKey.decimals; + this.stylesInfo[dataKey.def] = getCellStyleInfo(this.ctx, keySettings, 'value, alarm, ctx'); + const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, alarm, ctx'); + const contentInfo: CellContentInfo = { + contentFunction: contentFunctionInfo, + units: dataKey.units, + decimals: dataKey.decimals + }; + this.contentsInfo[dataKey.def] = contentInfo; this.columnWidth[dataKey.def] = getColumnWidth(keySettings); this.columnDefaultVisibility[dataKey.def] = getColumnDefaultVisibility(keySettings, this.ctx); this.columnSelectionAvailability[dataKey.def] = getColumnSelectionAvailability(keySettings); @@ -719,100 +723,135 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return widthStyle(columnWidth); } - public rowStyle(alarm: AlarmDataInfo, row: number): any { + public rowStyle(alarm: AlarmDataInfo, row: number): Observable { + let style$: Observable; let res = this.rowStyleCache[row]; if (!res) { - res = {}; - if (alarm && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { - try { - res = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); - if (!isObject(res)) { - throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); - } - if (Array.isArray(res)) { - throw new TypeError(`Array instead of style object`); + style$ = this.rowStylesInfo.pipe( + map(styleInfo => { + if (styleInfo.useRowStyleFunction && styleInfo.rowStyleFunction) { + const style = styleInfo.rowStyleFunction.execute(alarm, this.ctx); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } + return style; + } else { + return {}; } - } catch (e) { - res = {}; + }), + catchError(e => { console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your row style function.`); - } - } - this.rowStyleCache[row] = res; + return of({}); + }) + ); + style$ = style$.pipe( + tap((style) => { + this.rowStyleCache[row] = style; + }) + ); + } else { + style$ = of(res); } - return res; + return style$; } - public cellStyle(alarm: AlarmDataInfo, key: EntityColumn, row: number): any { + public cellStyle(alarm: AlarmDataInfo, key: EntityColumn, row: number): Observable { + let style$: Observable; const col = this.columns.indexOf(key); const index = row * this.columns.length + col; let res = this.cellStyleCache[index]; if (!res) { - res = {}; if (alarm && key) { - const styleInfo = this.stylesInfo[key.def]; - const value = getAlarmValue(alarm, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - res = styleInfo.cellStyleFunction(value, alarm, this.ctx); - if (!isObject(res)) { - throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); - } - if (Array.isArray(res)) { - throw new TypeError(`Array instead of style object`); + style$ = this.stylesInfo[key.def].pipe( + map(styleInfo => { + const value = getAlarmValue(alarm, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + const style = styleInfo.cellStyleFunction.execute(value, alarm, this.ctx); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } + return style; + } else { + return this.defaultStyle(key, value); } - } catch (e) { - res = {}; + }), + catchError(e => { console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your cell style function.`); - } - } else { - res = this.defaultStyle(key, value); - } + return of({}); + }) + ); + } else { + style$ = of({}); } - this.cellStyleCache[index] = res; - } - if (!res.width) { - const columnWidth = this.columnWidth[key.def]; - res = Object.assign(res, widthStyle(columnWidth)); + style$ = style$.pipe( + map((style) => { + if (!style.width) { + const columnWidth = this.columnWidth[key.def]; + style = Object.assign(style, widthStyle(columnWidth)); + } + return style; + }), + tap((style) => { + this.cellStyleCache[index] = style; + }) + ); + } else { + style$ = of(res); } - return res; + return style$; } - public cellContent(alarm: AlarmDataInfo, key: EntityColumn, row: number): SafeHtml { + public cellContent(alarm: AlarmDataInfo, key: EntityColumn, row: number): Observable { + let content$: Observable; const col = this.columns.indexOf(key); const index = row * this.columns.length + col; let res = this.cellContentCache[index]; if (isUndefined(res)) { - res = ''; - if (alarm && key) { - const contentInfo = this.contentsInfo[key.def]; - const value = getAlarmValue(alarm, key); - let content = ''; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - content = contentInfo.cellContentFunction(value, alarm, this.ctx); - } catch (e) { - content = '' + value; - } - } else { - content = this.defaultContent(key, contentInfo, value); - } - - if (isDefined(content)) { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - res = this.domSanitizer.bypassSecurityTrustHtml(content); - break; - default: - res = content; + const contentInfo = this.contentsInfo[key.def]; + content$ = contentInfo.contentFunction.pipe( + map((contentFunction) => { + let content: any = ''; + if (alarm && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getAlarmValue(alarm, key); + if (contentFunction.useCellContentFunction && contentFunction.cellContentFunction) { + try { + content = contentFunction.cellContentFunction.execute(value, alarm, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); + } + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + content = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + } + } } - } - } - this.cellContentCache[index] = res; + return content; + }) + ); + content$ = content$.pipe( + tap((content) => { + this.cellContentCache[index] = content; + }) + ); + } else { + content$ = of(res); } - return res; + return content$; } public onCellClick($event: Event, alarm: AlarmDataInfo, key: EntityColumn, columnIndex: number) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html index 1a72f6ecdb..73b4f574e3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html @@ -43,8 +43,8 @@ {{ column.title }} @@ -59,7 +59,7 @@
- +
@@ -99,7 +99,7 @@ [class.invisible]="entityDatasource.dataLoading" [class.tb-pointer]="hasRowAction" *matRowDef="let entity; columns: displayedColumns; let row = index" - [style]="rowStyle(entity, row)" + [style]="rowStyle(entity, row) | async" (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"> } = {}; private columnWidth: {[key: string]: string} = {}; private columnDefaultVisibility: {[key: string]: boolean} = {}; private columnSelectionAvailability: {[key: string]: boolean} = {}; private columnsWithCellClick: Array = []; - private rowStylesInfo: RowStyleInfo; + private rowStylesInfo: Observable; private searchAction: WidgetAction = { name: 'action.search', @@ -307,7 +308,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true; this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; - this.rowStylesInfo = getRowStyleInfo(this.settings, 'entity, ctx'); + this.rowStylesInfo = getRowStyleInfo(this.ctx, this.settings, 'entity, ctx'); const pageSize = this.settings.defaultPageSize; if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { @@ -361,11 +362,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } as EntityColumn ); this.contentsInfo.entityName = { - useCellContentFunction: false + contentFunction: of({ + useCellContentFunction: false + }) }; - this.stylesInfo.entityName = { + this.stylesInfo.entityName = of({ useCellStyleFunction: false - }; + }); this.columnWidth.entityName = '0px'; this.columnDefaultVisibility.entityName = true; this.columnSelectionAvailability.entityName = true; @@ -385,11 +388,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } as EntityColumn ); this.contentsInfo.entityLabel = { - useCellContentFunction: false + contentFunction: of({ + useCellContentFunction: false + }) }; - this.stylesInfo.entityLabel = { + this.stylesInfo.entityLabel = of({ useCellStyleFunction: false - }; + }); this.columnWidth.entityLabel = '0px'; this.columnDefaultVisibility.entityLabel = true; this.columnSelectionAvailability.entityLabel = true; @@ -409,13 +414,18 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } as EntityColumn ); this.contentsInfo.entityType = { - useCellContentFunction: true, - cellContentFunction: (entityType: EntityType) => - entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : '' + contentFunction: of({ + useCellContentFunction: true, + cellContentFunction: new CompiledTbFunction( + (entityType: EntityType) => + entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : '', + [] + ) + }) }; - this.stylesInfo.entityType = { + this.stylesInfo.entityType = of({ useCellStyleFunction: false - }; + }); this.columnWidth.entityType = '0px'; this.columnDefaultVisibility.entityType = true; this.columnSelectionAvailability.entityType = true; @@ -447,8 +457,14 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } } - this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, entity, ctx'); - this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, entity, ctx'); + this.stylesInfo[dataKey.def] = getCellStyleInfo(this.ctx, keySettings, 'value, entity, ctx'); + const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, entity, ctx'); + const contentInfo: CellContentInfo = { + contentFunction: contentFunctionInfo, + units: dataKey.units, + decimals: dataKey.decimals + }; + this.contentsInfo[dataKey.def] = contentInfo; this.contentsInfo[dataKey.def].units = dataKey.units; this.contentsInfo[dataKey.def].decimals = dataKey.decimals; this.columnWidth[dataKey.def] = getColumnWidth(keySettings); @@ -595,98 +611,135 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni return widthStyle(columnWidth); } - public rowStyle(entity: EntityData, row: number): any { + public rowStyle(entity: EntityData, row: number): Observable { + let style$: Observable; let res = this.rowStyleCache[row]; if (!res) { - res = {}; - if (entity && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { - try { - res = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); - if (!isObject(res)) { - throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); - } - if (Array.isArray(res)) { - throw new TypeError(`Array instead of style object`); + style$ = this.rowStylesInfo.pipe( + map(styleInfo => { + if (styleInfo.useRowStyleFunction && styleInfo.rowStyleFunction) { + const style = styleInfo.rowStyleFunction.execute(entity, this.ctx); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } + return style; + } else { + return {}; } - } catch (e) { - res = {}; + }), + catchError(e => { console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your row style function.`); - } - } - this.rowStyleCache[row] = res; + return of({}); + }) + ); + style$ = style$.pipe( + tap((style) => { + this.rowStyleCache[row] = style; + }) + ); + } else { + style$ = of(res); } - return res; + return style$; } - public cellStyle(entity: EntityData, key: EntityColumn, row: number): any { + public cellStyle(entity: EntityData, key: EntityColumn, row: number): Observable { + let style$: Observable; const col = this.columns.indexOf(key); const index = row * this.columns.length + col; let res = this.cellStyleCache[index]; if (!res) { - res = {}; if (entity && key) { - const styleInfo = this.stylesInfo[key.def]; - const value = getEntityValue(entity, key); - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - res = styleInfo.cellStyleFunction(value, entity, this.ctx); - if (!isObject(res)) { - throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); - } - if (Array.isArray(res)) { - throw new TypeError(`Array instead of style object`); + style$ = this.stylesInfo[key.def].pipe( + map((styleInfo) => { + const value = getEntityValue(entity, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + const style = styleInfo.cellStyleFunction.execute(value, entity, this.ctx); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } + return style; + } else { + return {}; } - } catch (e) { - res = {}; + }), + catchError(e => { console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + `returns '${e}'. Please check your cell style function.`); - } - } - this.cellStyleCache[index] = res; + return of({}); + }) + ); + } else { + style$ = of({}); } + style$ = style$.pipe( + map((style) => { + if (!style.width) { + const columnWidth = this.columnWidth[key.def]; + style = Object.assign(style, widthStyle(columnWidth)); + } + return style; + }), + tap((style) => { + this.cellStyleCache[index] = style; + }) + ); + } else { + style$ = of(res); } - if (!res.width) { - const columnWidth = this.columnWidth[key.def]; - res = Object.assign(res, widthStyle(columnWidth)); - } - return res; + return style$; } - public cellContent(entity: EntityData, key: EntityColumn, row: number): SafeHtml { + public cellContent(entity: EntityData, key: EntityColumn, row: number): Observable { + let content$: Observable; const col = this.columns.indexOf(key); const index = row * this.columns.length + col; let res = this.cellContentCache[index]; if (isUndefined(res)) { - res = ''; - if (entity && key) { - const contentInfo = this.contentsInfo[key.def]; - const value = getEntityValue(entity, key); - let content: string; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - content = contentInfo.cellContentFunction(value, entity, this.ctx); - } catch (e) { - content = '' + value; - } - } else { - content = this.defaultContent(key, contentInfo, value); - } - - if (isDefined(content)) { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - res = this.domSanitizer.bypassSecurityTrustHtml(content); - break; - default: - res = content; + const contentInfo = this.contentsInfo[key.def]; + content$ = contentInfo.contentFunction.pipe( + map((contentFunction) => { + let content: any = ''; + if (entity && key) { + const contentInfo = this.contentsInfo[key.def]; + const value = getEntityValue(entity, key); + if (contentFunction.useCellContentFunction && contentFunction.cellContentFunction) { + try { + content = contentFunction.cellContentFunction.execute(value, entity, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + content = this.defaultContent(key, contentInfo, value); + } + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + content = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + } + } } - } - } - this.cellContentCache[index] = res; + return content; + }) + ); + content$ = content$.pipe( + tap((content) => { + this.cellContentCache[index] = content; + }) + ); + } else { + content$ = of(res); } - return res; + return content$; } private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index 2a61d008d4..08417f63f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -28,18 +28,21 @@ import { hashCode, isDefinedAndNotNull, isNotEmptyStr, - parseFunction, - safeExecute + parseTbFunction, + safeExecuteTbFunction } from '@core/utils'; import cssjs from '@core/css/css'; import { UtilsService } from '@core/services/utils.service'; import { HOME_COMPONENTS_MODULE_TOKEN, WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { EntityDataPageLink } from '@shared/models/query/query.models'; +import { CompiledTbFunction, TbFunction } from '@shared/models/js-function.models'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; interface MarkdownWidgetSettings { markdownTextPattern: string; useMarkdownTextFunction: boolean; - markdownTextFunction: string; + markdownTextFunction: TbFunction; applyDefaultMarkdownStyle: boolean; markdownCss: string; } @@ -53,7 +56,7 @@ type MarkdownTextFunction = (data: FormattedData[], ctx: WidgetContext) => strin export class MarkdownWidgetComponent extends PageComponent implements OnInit { settings: MarkdownWidgetSettings; - markdownTextFunction: MarkdownTextFunction; + markdownTextFunction: Observable>; markdownClass: string; @@ -78,7 +81,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { this.ctx.$scope.markdownWidget = this; this.settings = this.ctx.settings; this.markdownTextFunction = this.settings.useMarkdownTextFunction ? - parseFunction(this.settings.markdownTextFunction, ['data', 'ctx']) : null; + parseTbFunction(this.ctx.http, this.settings.markdownTextFunction, ['data', 'ctx']) : of(null); let cssString = this.settings.markdownCss; if (isNotEmptyStr(cssString)) { const cssParser = new cssjs(); @@ -126,8 +129,19 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { initialData = []; } const data = formattedDataFormDatasourceData(initialData); + let markdownText = this.settings.useMarkdownTextFunction ? - safeExecute(this.markdownTextFunction, [data, this.ctx]) : this.settings.markdownTextPattern; + this.markdownTextFunction.pipe(map(markdownTextFunction => safeExecuteTbFunction(markdownTextFunction, [data, this.ctx]))) : this.settings.markdownTextPattern; + if (typeof markdownText === 'string') { + this.updateMarkdownText(markdownText, data); + } else { + markdownText.subscribe((text) => { + this.updateMarkdownText(text, data); + }); + } + } + + private updateMarkdownText(markdownText: string, data: FormattedData[]) { const allData: FormattedData = flatDataWithoutOverride(data); markdownText = createLabelFromPattern(markdownText, allData); if (this.markdownText !== markdownText) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts index bec9ea32aa..a2e9cd3579 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts @@ -27,14 +27,18 @@ import { formattedDataFormDatasourceData, isNumber, isObject, - parseFunction, - safeExecute, unwrapModule + parseTbFunction, + safeExecuteTbFunction, + unwrapModule } from '@core/utils'; +import { CompiledTbFunction, TbFunction } from '@shared/models/js-function.models'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; interface QrCodeWidgetSettings { qrCodeTextPattern: string; useQrCodeTextFunction: boolean; - qrCodeTextFunction: string; + qrCodeTextFunction: TbFunction; } type QrCodeTextFunction = (data: FormattedData[]) => string; @@ -47,7 +51,7 @@ type QrCodeTextFunction = (data: FormattedData[]) => string; export class QrCodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit { settings: QrCodeWidgetSettings; - qrCodeTextFunction: QrCodeTextFunction; + qrCodeTextFunction: Observable>; @Input() ctx: WidgetContext; @@ -68,7 +72,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte ngOnInit(): void { this.ctx.$scope.qrCodeWidget = this; this.settings = this.ctx.settings; - this.qrCodeTextFunction = this.settings.useQrCodeTextFunction ? parseFunction(this.settings.qrCodeTextFunction, ['data']) : null; + this.qrCodeTextFunction = this.settings.useQrCodeTextFunction ? parseTbFunction(this.ctx.http, this.settings.qrCodeTextFunction, ['data']) : null; } ngAfterViewInit(): void { @@ -81,7 +85,6 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte public onDataUpdated() { let initialData: DatasourceData[]; - let qrCodeText: string; if (this.ctx.data?.length) { initialData = this.ctx.data; } else if (this.ctx.datasources?.length) { @@ -100,13 +103,19 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte } const data = formattedDataFormDatasourceData(initialData); const pattern = this.settings.useQrCodeTextFunction ? - safeExecute(this.qrCodeTextFunction, [data]) : this.settings.qrCodeTextPattern; - const allData: FormattedData = flatDataWithoutOverride(data); - qrCodeText = createLabelFromPattern(pattern, allData); - this.updateQrCodeText(qrCodeText); + this.qrCodeTextFunction.pipe(map(qrCodeTextFunction => safeExecuteTbFunction(qrCodeTextFunction, [data]))) : this.settings.qrCodeTextPattern; + if (typeof pattern === 'string') { + this.updateQrCodeText(pattern, data); + } else { + pattern.subscribe((text) => { + this.updateQrCodeText(text, data); + }); + } } - private updateQrCodeText(newQrCodeText: string): void { + private updateQrCodeText(pattern: string, data: FormattedData[]): void { + const allData: FormattedData = flatDataWithoutOverride(data); + const newQrCodeText = createLabelFromPattern(pattern, allData); if (this.qrCodeText !== newQrCodeText) { this.qrCodeText = newQrCodeText; if (!(isObject(newQrCodeText) || isNumber(newQrCodeText))) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html index 5b5dc4fa49..c739f51440 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html @@ -75,6 +75,7 @@ string; -export interface CellContentInfo { +export interface CellContentFunctionInfo { useCellContentFunction: boolean; - cellContentFunction?: CellContentFunction; + cellContentFunction?: CompiledTbFunction; +} + +export interface CellContentInfo { + contentFunction: Observable; units?: string; decimals?: number; } @@ -102,14 +111,14 @@ export type CellStyleFunction = (...args: any[]) => any; export interface CellStyleInfo { useCellStyleFunction: boolean; - cellStyleFunction?: CellStyleFunction; + cellStyleFunction?: CompiledTbFunction; } export type RowStyleFunction = (...args: any[]) => any; export interface RowStyleInfo { useRowStyleFunction: boolean; - rowStyleFunction?: RowStyleFunction; + rowStyleFunction?: CompiledTbFunction; } @@ -232,67 +241,113 @@ export function getAlarmValue(alarm: AlarmDataInfo, key: EntityColumn) { } } -export function getRowStyleInfo(settings: TableWidgetSettings, ...args: string[]): RowStyleInfo { - let rowStyleFunction: RowStyleFunction = null; - let useRowStyleFunction = false; - - if (settings.useRowStyleFunction === true) { - if (isDefined(settings.rowStyleFunction) && settings.rowStyleFunction.length > 0) { - try { - rowStyleFunction = new Function(...args, settings.rowStyleFunction) as RowStyleFunction; - useRowStyleFunction = true; - } catch (e) { - rowStyleFunction = null; - useRowStyleFunction = false; - } - } +export function getRowStyleInfo(widgetContext: WidgetContext, settings: TableWidgetSettings, ...args: string[]): Observable { + let rowStyleInfo$: Observable; + if (settings.useRowStyleFunction === true && isNotEmptyTbFunction(settings.rowStyleFunction)) { + rowStyleInfo$ = compileTbFunction(widgetContext.http, settings.rowStyleFunction, ...args).pipe( + catchError(() => { return of(null) }), + map((rowStyleFunction) => { + if (!rowStyleFunction) { + return { + useRowStyleFunction: false, + rowStyleFunction: null + } + } else { + return { + useRowStyleFunction: true, + rowStyleFunction + } + } + }) + ); + } else { + rowStyleInfo$ = of({ + useRowStyleFunction: false, + rowStyleFunction: null + }); } - return { - useRowStyleFunction, - rowStyleFunction - }; -} - -export function getCellStyleInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellStyleInfo { - let cellStyleFunction: CellStyleFunction = null; - let useCellStyleFunction = false; - - if (keySettings.useCellStyleFunction === true) { - if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { - try { - cellStyleFunction = new Function(...args, keySettings.cellStyleFunction) as CellStyleFunction; - useCellStyleFunction = true; - } catch (e) { - cellStyleFunction = null; - useCellStyleFunction = false; + return rowStyleInfo$.pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); +} + +export function getCellStyleInfo(widgetContext: WidgetContext, keySettings: TableWidgetDataKeySettings, ...args: string[]): Observable { + let cellStyleInfo$: Observable; + if (keySettings.useCellStyleFunction === true && isNotEmptyTbFunction(keySettings.cellStyleFunction)) { + cellStyleInfo$ = compileTbFunction(widgetContext.http, keySettings.cellStyleFunction, ...args).pipe( + catchError(() => { return of(null) }), + map((cellStyleFunction) => { + if (!cellStyleFunction) { + return { + useCellStyleFunction: false, + cellStyleFunction: null + } + } else { + return { + useCellStyleFunction: true, + cellStyleFunction + } + } + }) + ); + } else { + cellStyleInfo$ = of( + { + useCellStyleFunction: false, + cellStyleFunction: null } - } + ) } - return { - useCellStyleFunction, - cellStyleFunction - }; -} - -export function getCellContentInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellContentInfo { - let cellContentFunction: CellContentFunction = null; - let useCellContentFunction = false; - - if (keySettings.useCellContentFunction === true) { - if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { - try { - cellContentFunction = new Function(...args, keySettings.cellContentFunction) as CellContentFunction; - useCellContentFunction = true; - } catch (e) { - cellContentFunction = null; - useCellContentFunction = false; + return cellStyleInfo$.pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); +} + +export function getCellContentFunctionInfo(widgetContext: WidgetContext, keySettings: TableWidgetDataKeySettings, ...args: string[]): Observable { + let cellContentFunctionInfo$: Observable; + if (keySettings.useCellContentFunction === true && isNotEmptyTbFunction(keySettings.cellContentFunction)) { + cellContentFunctionInfo$ = compileTbFunction(widgetContext.http, keySettings.cellContentFunction, ...args).pipe( + catchError(() => { return of(null) }), + map((cellContentFunction) => { + if (!cellContentFunction) { + return { + useCellContentFunction: false, + cellContentFunction: null + } + } else { + return { + useCellContentFunction: true, + cellContentFunction + } + } + }) + ); + } else { + cellContentFunctionInfo$ = of( + { + useCellContentFunction: false, + cellContentFunction: null } - } + ) } - return { - cellContentFunction, - useCellContentFunction - }; + return cellContentFunctionInfo$.pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false + }) + ); } export function getColumnWidth(keySettings: TableWidgetDataKeySettings): string { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 9c45801f29..2867980dc2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -49,15 +49,15 @@ Timestamp + [innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex) | async" + [style]="cellStyle(source, null, 0, row, row[0], rowIndex) | async"> {{ h.dataKey.label }} + [innerHTML]="cellContent(source, h, h.index, row, row[h.index], rowIndex) | async" + [style]="cellStyle(source, h, h.index, row, row[h.index], rowIndex) | async"> @@ -70,7 +70,7 @@
- +
@@ -110,7 +110,7 @@ ; contentInfo: CellContentInfo; order?: number; } @@ -200,7 +201,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private displayedColumns: Array = []; - private rowStylesInfo: RowStyleInfo; + private rowStylesInfo: Observable; private subscriptions: Subscription[] = []; private widgetTimewindowChanged$: Subscription; @@ -348,7 +349,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.dateFormatFilter = isDefined(this.settings.dateFormat?.format) ? this.settings.dateFormat?.format : 'yyyy-MM-dd HH:mm:ss'; } - this.rowStylesInfo = getRowStyleInfo(this.settings, 'rowData, ctx'); + this.rowStylesInfo = getRowStyleInfo(this.ctx, this.settings, 'rowData, ctx'); const pageSize = this.settings.defaultPageSize; if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { @@ -519,12 +520,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI dataKeys.forEach((dataKey, index) => { const sortable = !dataKey.usePostProcessing; const keySettings: TableWidgetDataKeySettings = dataKey.settings; - const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); - const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + const styleInfo = getCellStyleInfo(this.ctx, keySettings, 'value, rowData, ctx'); + const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, rowData, ctx'); const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); - contentInfo.units = dataKey.units; - contentInfo.decimals = dataKey.decimals; + const contentInfo: CellContentInfo = { + contentFunction: contentFunctionInfo, + units: dataKey.units, + decimals: dataKey.decimals + }; header.push({ index: index + 1, dataKey, @@ -542,12 +546,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI const index = dataKeys.length + latestIndex; const sortable = !dataKey.usePostProcessing; const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; - const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); - const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); + const styleInfo = getCellStyleInfo(this.ctx, keySettings, 'value, rowData, ctx'); + const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, rowData, ctx'); const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); - contentInfo.units = dataKey.units; - contentInfo.decimals = dataKey.decimals; + const contentInfo: CellContentInfo = { + contentFunction: contentFunctionInfo, + units: dataKey.units, + decimals: dataKey.decimals + }; header.push({ index: index + 1, dataKey, @@ -658,111 +665,143 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI return source.datasource.entityId; } - public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): any { + public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): Observable { + let style$: Observable; let res = this.rowStyleCache[index]; if (!res) { - res = {}; - if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.label] = row[headerInfo.index]; - }); - res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx); - if (!isObject(res)) { - throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); - } - if (Array.isArray(res)) { - throw new TypeError(`Array instead of style object`); + style$ = this.rowStylesInfo.pipe( + map(styleInfo => { + if (styleInfo.useRowStyleFunction && styleInfo.rowStyleFunction) { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.label] = row[headerInfo.index]; + }); + const style = styleInfo.rowStyleFunction.execute(rowData, this.ctx); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } + return style; + } else { + return {}; } - } catch (e) { - res = {}; + }), + catchError(e => { console.warn(`Row style function in widget ` + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`); - } - } - this.rowStyleCache[index] = res; + return of({}); + }) + ); + style$ = style$.pipe( + tap((style) => { + this.rowStyleCache[index] = style; + }) + ); + } else { + style$ = of(res); } - return res; + return style$; } public cellStyle(source: TimeseriesTableSource, header: TimeseriesHeader, - index: number, row: TimeseriesRow, value: any, rowIndex: number): any { + index: number, row: TimeseriesRow, value: any, rowIndex: number): Observable { + let style$: Observable; const cacheIndex = rowIndex * (source.header.length + 1) + index; let res = this.cellStyleCache[cacheIndex]; if (!res) { - res = {}; if (index > 0) { - const styleInfo = header.styleInfo; - if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.label] = row[headerInfo.index]; - }); - res = styleInfo.cellStyleFunction(value, rowData, this.ctx); - if (!isObject(res)) { - throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); - } - if (Array.isArray(res)) { - throw new TypeError(`Array instead of style object`); + style$ = header.styleInfo.pipe( + map(styleInfo => { + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.label] = row[headerInfo.index]; + }); + const style = styleInfo.cellStyleFunction.execute(value, rowData, this.ctx); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } + return style; + } else { + return {}; } - } catch (e) { - res = {}; + }), + catchError(e => { console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); - } - } + return of({}); + }) + ); + } else { + style$ = of({}); } - this.cellStyleCache[cacheIndex] = res; + style$ = style$.pipe( + tap((style) => { + this.cellStyleCache[cacheIndex] = style; + }) + ); + } else { + style$ = of(res); } - return res; + return style$; } public cellContent(source: TimeseriesTableSource, header: TimeseriesHeader, - index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml { + index: number, row: TimeseriesRow, value: any, rowIndex: number): Observable { + let content$: Observable; const cacheIndex = rowIndex * (source.header.length + 1) + index ; let res = this.cellContentCache[cacheIndex]; if (isUndefined(res)) { - res = ''; if (index === 0) { - res = row.formattedTs; + content$ = of(row.formattedTs); } else { - let content; - const contentInfo = header.contentInfo; - if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { - try { - const rowData = source.rowDataTemplate; - rowData.Timestamp = row[0]; - source.header.forEach((headerInfo) => { - rowData[headerInfo.dataKey.label] = row[headerInfo.index]; - }); - content = contentInfo.cellContentFunction(value, rowData, this.ctx); - } catch (e) { - content = '' + value; - } - } else { - const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; - const units = contentInfo.units || this.ctx.widgetConfig.units; - content = this.ctx.utils.formatValue(value, decimals, units, true); - } - - if (isDefined(content)) { - content = this.utils.customTranslation(content, content); - switch (typeof content) { - case 'string': - res = this.domSanitizer.bypassSecurityTrustHtml(content); - break; - default: - res = content; - } - } + content$ = header.contentInfo.contentFunction.pipe( + map((contentFunction) => { + let content: any; + if (contentFunction.useCellContentFunction && contentFunction.cellContentFunction) { + try { + const rowData = source.rowDataTemplate; + rowData.Timestamp = row[0]; + source.header.forEach((headerInfo) => { + rowData[headerInfo.dataKey.label] = row[headerInfo.index]; + }); + content = contentFunction.cellContentFunction.execute(value, rowData, this.ctx); + } catch (e) { + content = '' + value; + } + } else { + const decimals = (header.contentInfo.decimals || header.contentInfo.decimals === 0) ? header.contentInfo.decimals : this.ctx.widgetConfig.decimals; + const units = header.contentInfo.units || this.ctx.widgetConfig.units; + content = this.ctx.utils.formatValue(value, decimals, units, true); + } + if (isDefined(content)) { + content = this.utils.customTranslation(content, content); + switch (typeof content) { + case 'string': + content = this.domSanitizer.bypassSecurityTrustHtml(content); + break; + } + } + return content; + }) + ); } - this.cellContentCache[cacheIndex] = res; + content$ = content$.pipe( + tap((content) => { + this.cellContentCache[cacheIndex] = content; + }) + ); + } else { + content$ = of(res); } - return res; + return content$; } public onRowClick($event: Event, row: TimeseriesRow) { diff --git a/ui-ngx/src/app/shared/models/js-function.models.ts b/ui-ngx/src/app/shared/models/js-function.models.ts index 474796b74b..5bfa50de6f 100644 --- a/ui-ngx/src/app/shared/models/js-function.models.ts +++ b/ui-ngx/src/app/shared/models/js-function.models.ts @@ -188,7 +188,7 @@ const loadModuleCompletion = (http: HttpClient, moduleLink: string): Observable< ); } -type GenericFunction = (...args: any[]) => any; +export type GenericFunction = (...args: any[]) => any; export class CompiledTbFunction { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 3c91c4dd1f..79330cdffd 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -334,7 +334,7 @@ export interface KeyInfo { comparisonResultType?: ComparisonResultType; label?: string; color?: string; - funcBody?: string; + funcBody?: TbFunction; postFuncBody?: TbFunction; units?: string; decimals?: number;