Browse Source

UI: Add support of js modules: Table widgets, Markdown widgets, QR code widget, Data key generation function.

pull/12171/head
Igor Kulikov 2 years ago
parent
commit
16e04efc9e
  1. 30
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  2. 45
      ui-ngx/src/app/core/utils.ts
  3. 1
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  4. 10
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html
  5. 193
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  6. 8
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html
  7. 221
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  8. 26
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts
  9. 31
      ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts
  10. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html
  11. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html
  12. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html
  13. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html
  15. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html
  16. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  17. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html
  18. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html
  19. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.html
  20. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html
  21. 187
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  22. 12
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  23. 219
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  24. 2
      ui-ngx/src/app/shared/models/js-function.models.ts
  25. 2
      ui-ngx/src/app/shared/models/widget.models.ts

30
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 { DataAggregator, onAggregatedData } from '@core/api/data-aggregator';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType } from '@shared/models/entity-type.models'; 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 { EntityId } from '@shared/models/id/entity-id';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { import {
@ -89,8 +89,8 @@ export interface SubscriptionDataKey {
timeForComparison?: ComparisonDuration; timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number; comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType; comparisonResultType?: ComparisonResultType;
funcBody: string; funcBody: TbFunction;
func?: DataKeyFunction; func?: CompiledTbFunction<DataKeyFunction>;
postFuncBody: TbFunction; postFuncBody: TbFunction;
postFunc?: CompiledTbFunction<DataKeyPostFunction>; postFunc?: CompiledTbFunction<DataKeyPostFunction>;
index?: number; index?: number;
@ -210,7 +210,7 @@ export class EntityDataSubscription {
dataKey.index = i; dataKey.index = i;
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
if (!dataKey.func) { 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 { } else {
if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) { if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) {
@ -276,9 +276,9 @@ export class EntityDataSubscription {
} }
public subscribe(): Observable<EntityDataLoadResult> { public subscribe(): Observable<EntityDataLoadResult> {
return from(this.initializeSubscription()).pipe( this.entityDataResolveSubject = new ReplaySubject(1);
switchMap(() => { from(this.initializeSubscription()).pipe(
this.entityDataResolveSubject = new ReplaySubject(1); tap(() => {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.started = true; this.started = true;
this.dataResolved = true; this.dataResolved = true;
@ -621,13 +621,13 @@ export class EntityDataSubscription {
); );
this.subscriber.subscribe(); 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() { public start() {
@ -1211,7 +1211,7 @@ export class EntityDataSubscription {
prevSeries = [0, 0]; prevSeries = [0, 0];
} }
for (let time = startTime; time <= endTime && (this.timeseriesTimer || this.history); time += this.frequency) { 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]; const series: [number, any] = [time, value];
data.push(series); data.push(series);
prevSeries = series; prevSeries = series;
@ -1232,7 +1232,7 @@ export class EntityDataSubscription {
prevSeries = [0, 0]; prevSeries = [0, 0];
} }
const time = Date.now() + this.latestTsOffset; 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]; const series: [number, any] = [time, value];
this.datasourceData[0][datasourceKey].data = [series]; this.datasourceData[0][datasourceKey].data = [series];
this.listener.dataUpdated(this.datasourceData[0][datasourceKey], this.listener.dataUpdated(this.datasourceData[0][datasourceKey],

45
ui-ngx/src/app/core/utils.ts

@ -15,16 +15,22 @@
/// ///
import _ from 'lodash'; import _ from 'lodash';
import { from, Observable, Subject } from 'rxjs'; import { from, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { finalize, share } from 'rxjs/operators'; import { catchError, finalize, share } from 'rxjs/operators';
import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models'; import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models'; 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 { TranslateService } from '@ngx-translate/core';
import { serverErrorCodesTranslations } from '@shared/models/constants'; import { serverErrorCodesTranslations } from '@shared/models/constants';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import {
CompiledTbFunction,
compileTbFunction, GenericFunction,
isNotEmptyTbFunction,
TbFunction
} from '@shared/models/js-function.models';
const varsRegex = /\${([^}]*)}/g; const varsRegex = /\${([^}]*)}/g;
@ -673,6 +679,39 @@ export function parseFunction(source: any, params: string[] = ['def']): (...args
return res; return res;
} }
export function parseTbFunction<T extends GenericFunction>(http: HttpClient, source: TbFunction, params: string[] = ['def']): Observable<CompiledTbFunction<T>> {
if (isNotEmptyTbFunction(source)) {
return compileTbFunction<T>(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<T extends GenericFunction>(func: CompiledTbFunction<T>, 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 = []) { export function safeExecute(func: (...args: any[]) => any, params = []) {
let res = null; let res = null;
if (func && typeof (func) === 'function') { if (func && typeof (func) === 'function') {

1
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -72,6 +72,7 @@
[functionArgs]="['time', 'prevValue']" [functionArgs]="['time', 'prevValue']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1],[1, '1']]" [validationArgs]="[[1, 1],[1, '1']]"
withModules
resultType="any" resultType="any"
helpId="widget/config/datakey_generation_fn" helpId="widget/config/datakey_generation_fn"
formControlName="funcBody"> formControlName="funcBody">

10
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)"> [indeterminate]="alarmsDatasource.selection.hasValue() && !(alarmsDatasource.isAllSelected() | async)">
</mat-checkbox> </mat-checkbox>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let alarm; let row = index" [style]="rowStyle(alarm, row)"> <mat-cell *matCellDef="let alarm; let row = index" [style]="rowStyle(alarm, row) | async">
<mat-checkbox (click)="$event.stopPropagation();" <mat-checkbox (click)="$event.stopPropagation();"
(change)="$event ? alarmsDatasource.toggleSelection(alarm) : null" (change)="$event ? alarmsDatasource.toggleSelection(alarm) : null"
[checked]="alarmsDatasource.isSelected(alarm)"> [checked]="alarmsDatasource.isSelected(alarm)">
@ -82,10 +82,10 @@
{{ column.title }} {{ column.title }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let alarm; let row = index" <mat-cell *matCellDef="let alarm; let row = index"
[style]="cellStyle(alarm, column, row)" [style]="cellStyle(alarm, column, row) | async"
(click)="onCellClick($event, alarm, column, $index)" (click)="onCellClick($event, alarm, column, $index)"
[class.tb-pointer]="columnHasCellClick($index)"> [class.tb-pointer]="columnHasCellClick($index)">
<span [innerHTML]="cellContent(alarm, column, row)"></span> <span [innerHTML]="cellContent(alarm, column, row) | async"></span>
<ng-container *ngIf="column.entityKey.key === 'assignee'"> <ng-container *ngIf="column.entityKey.key === 'assignee'">
<span class="assignee-cell flex flex-row items-center justify-start"> <span class="assignee-cell flex flex-row items-center justify-start">
<span *ngIf="alarm.assigneeId && checkAssigneeHasName(alarm.assignee); else unassigned" <span *ngIf="alarm.assigneeId && checkAssigneeHasName(alarm.assignee); else unassigned"
@ -129,7 +129,7 @@
</div> </div>
</ng-container> </ng-container>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let alarm; let row = index" [style]="rowStyle(alarm, row)"> <mat-cell *matCellDef="let alarm; let row = index" [style]="rowStyle(alarm, row) | async">
<ng-container *ngIf="alarmsDatasource.countCellButtonAction"> <ng-container *ngIf="alarmsDatasource.countCellButtonAction">
<div [class.lt-lg:!hidden]="showCellActionsMenu && alarmsDatasource.countCellButtonAction !== 1" class="flex flex-row items-stretch justify-end" <div [class.lt-lg:!hidden]="showCellActionsMenu && alarmsDatasource.countCellButtonAction !== 1" class="flex flex-row items-stretch justify-end"
[style.min-width]="(alarmsDatasource.countCellButtonAction * 48) + 'px'"> [style.min-width]="(alarmsDatasource.countCellButtonAction * 48) + 'px'">
@ -171,7 +171,7 @@
[class.invisible]="alarmsDatasource.dataLoading" [class.invisible]="alarmsDatasource.dataLoading"
[class.tb-pointer]="hasRowAction" [class.tb-pointer]="hasRowAction"
*matRowDef="let alarm; columns: displayedColumns; let row = index" *matRowDef="let alarm; columns: displayedColumns; let row = index"
[style]="rowStyle(alarm, row)" [style]="rowStyle(alarm, row) | async"
(click)="onRowClick($event, alarm)"></mat-row> (click)="onRowClick($event, alarm)"></mat-row>
</table> </table>
<span [class.!hidden]="(alarmsDatasource.isEmpty() | async) === false || alarmsDatasource.dataLoading" <span [class.!hidden]="(alarmsDatasource.isEmpty() | async) === false || alarmsDatasource.dataLoading"

193
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -53,7 +53,7 @@ import { Direction } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { debounceTime, distinctUntilChanged, map, take, takeUntil, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, map, take, takeUntil, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort'; import { MatSort, SortDirection } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -69,7 +69,7 @@ import {
findEntityKeyByColumnDef, findEntityKeyByColumnDef,
fromEntityColumnDef, fromEntityColumnDef,
getAlarmValue, getAlarmValue,
getCellContentInfo, getCellContentFunctionInfo,
getCellStyleInfo, getCellStyleInfo,
getColumnDefaultVisibility, getColumnDefaultVisibility,
getColumnSelectionAvailability, getColumnSelectionAvailability,
@ -217,13 +217,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
private defaultSortOrder = '-' + alarmFields.createdTime.value; private defaultSortOrder = '-' + alarmFields.createdTime.value;
private contentsInfo: {[key: string]: CellContentInfo} = {}; private contentsInfo: {[key: string]: CellContentInfo} = {};
private stylesInfo: {[key: string]: CellStyleInfo} = {}; private stylesInfo: {[key: string]: Observable<CellStyleInfo>} = {};
private columnWidth: {[key: string]: string} = {}; private columnWidth: {[key: string]: string} = {};
private columnDefaultVisibility: {[key: string]: boolean} = {}; private columnDefaultVisibility: {[key: string]: boolean} = {};
private columnSelectionAvailability: {[key: string]: boolean} = {}; private columnSelectionAvailability: {[key: string]: boolean} = {};
private columnsWithCellClick: Array<number> = []; private columnsWithCellClick: Array<number> = [];
private rowStylesInfo: RowStyleInfo; private rowStylesInfo: Observable<RowStyleInfo>;
private widgetTimewindowChanged$: Subscription; private widgetTimewindowChanged$: Subscription;
@ -389,7 +389,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
this.alarmFilterAction.show = enableFilter; 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; const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
@ -438,10 +438,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
keySettings.columnWidth = '120px'; keySettings.columnWidth = '120px';
} }
} }
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); this.stylesInfo[dataKey.def] = getCellStyleInfo(this.ctx, keySettings, 'value, alarm, ctx');
this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, alarm, ctx');
this.contentsInfo[dataKey.def].units = dataKey.units; const contentInfo: CellContentInfo = {
this.contentsInfo[dataKey.def].decimals = dataKey.decimals; contentFunction: contentFunctionInfo,
units: dataKey.units,
decimals: dataKey.decimals
};
this.contentsInfo[dataKey.def] = contentInfo;
this.columnWidth[dataKey.def] = getColumnWidth(keySettings); this.columnWidth[dataKey.def] = getColumnWidth(keySettings);
this.columnDefaultVisibility[dataKey.def] = getColumnDefaultVisibility(keySettings, this.ctx); this.columnDefaultVisibility[dataKey.def] = getColumnDefaultVisibility(keySettings, this.ctx);
this.columnSelectionAvailability[dataKey.def] = getColumnSelectionAvailability(keySettings); this.columnSelectionAvailability[dataKey.def] = getColumnSelectionAvailability(keySettings);
@ -719,100 +723,135 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
return widthStyle(columnWidth); return widthStyle(columnWidth);
} }
public rowStyle(alarm: AlarmDataInfo, row: number): any { public rowStyle(alarm: AlarmDataInfo, row: number): Observable<any> {
let style$: Observable<any>;
let res = this.rowStyleCache[row]; let res = this.rowStyleCache[row];
if (!res) { if (!res) {
res = {}; style$ = this.rowStylesInfo.pipe(
if (alarm && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { map(styleInfo => {
try { if (styleInfo.useRowStyleFunction && styleInfo.rowStyleFunction) {
res = this.rowStylesInfo.rowStyleFunction(alarm, this.ctx); const style = styleInfo.rowStyleFunction.execute(alarm, this.ctx);
if (!isObject(res)) { if (!isObject(style)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
} }
if (Array.isArray(res)) { if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`); 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}' ` + console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your row style function.`); `returns '${e}'. Please check your row style function.`);
} return of({});
} })
this.rowStyleCache[row] = res; );
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<any> {
let style$: Observable<any>;
const col = this.columns.indexOf(key); const col = this.columns.indexOf(key);
const index = row * this.columns.length + col; const index = row * this.columns.length + col;
let res = this.cellStyleCache[index]; let res = this.cellStyleCache[index];
if (!res) { if (!res) {
res = {};
if (alarm && key) { if (alarm && key) {
const styleInfo = this.stylesInfo[key.def]; style$ = this.stylesInfo[key.def].pipe(
const value = getAlarmValue(alarm, key); map(styleInfo => {
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { const value = getAlarmValue(alarm, key);
try { if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
res = styleInfo.cellStyleFunction(value, alarm, this.ctx); const style = styleInfo.cellStyleFunction.execute(value, alarm, this.ctx);
if (!isObject(res)) { if (!isObject(style)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
} }
if (Array.isArray(res)) { if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`); 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}' ` + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your cell style function.`); `returns '${e}'. Please check your cell style function.`);
} return of({});
} else { })
res = this.defaultStyle(key, value); );
} } else {
style$ = of({});
} }
this.cellStyleCache[index] = res; style$ = style$.pipe(
} map((style) => {
if (!res.width) { if (!style.width) {
const columnWidth = this.columnWidth[key.def]; const columnWidth = this.columnWidth[key.def];
res = Object.assign(res, widthStyle(columnWidth)); 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<SafeHtml> {
let content$: Observable<SafeHtml>;
const col = this.columns.indexOf(key); const col = this.columns.indexOf(key);
const index = row * this.columns.length + col; const index = row * this.columns.length + col;
let res = this.cellContentCache[index]; let res = this.cellContentCache[index];
if (isUndefined(res)) { if (isUndefined(res)) {
res = ''; const contentInfo = this.contentsInfo[key.def];
if (alarm && key) { content$ = contentInfo.contentFunction.pipe(
const contentInfo = this.contentsInfo[key.def]; map((contentFunction) => {
const value = getAlarmValue(alarm, key); let content: any = '';
let content = ''; if (alarm && key) {
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { const contentInfo = this.contentsInfo[key.def];
try { const value = getAlarmValue(alarm, key);
content = contentInfo.cellContentFunction(value, alarm, this.ctx); if (contentFunction.useCellContentFunction && contentFunction.cellContentFunction) {
} catch (e) { try {
content = '' + value; content = contentFunction.cellContentFunction.execute(value, alarm, this.ctx);
} } catch (e) {
} else { content = '' + value;
content = this.defaultContent(key, contentInfo, value); }
} } else {
content = this.defaultContent(key, contentInfo, value);
if (isDefined(content)) { }
content = this.utils.customTranslation(content, content); if (isDefined(content)) {
switch (typeof content) { content = this.utils.customTranslation(content, content);
case 'string': switch (typeof content) {
res = this.domSanitizer.bypassSecurityTrustHtml(content); case 'string':
break; content = this.domSanitizer.bypassSecurityTrustHtml(content);
default: break;
res = content; }
}
} }
} return content;
} })
this.cellContentCache[index] = res; );
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) { public onCellClick($event: Event, alarm: AlarmDataInfo, key: EntityColumn, columnIndex: number) {

8
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html

@ -43,8 +43,8 @@
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; let $index = index; trackBy: trackByColumnDef;"> <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; let $index = index; trackBy: trackByColumnDef;">
<mat-header-cell [style]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="!column.sortable"> {{ column.title }} </mat-header-cell> <mat-header-cell [style]="headerStyle(column)" *matHeaderCellDef mat-sort-header [disabled]="!column.sortable"> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index" <mat-cell *matCellDef="let entity; let row = index"
[innerHTML]="cellContent(entity, column, row)" [innerHTML]="cellContent(entity, column, row) | async"
[style]="cellStyle(entity, column, row)" [style]="cellStyle(entity, column, row) | async"
(click)="onCellClick($event, entity, column, $index)" (click)="onCellClick($event, entity, column, $index)"
[class.tb-pointer]="columnHasCellClick($index)"> [class.tb-pointer]="columnHasCellClick($index)">
</mat-cell> </mat-cell>
@ -59,7 +59,7 @@
</div> </div>
</ng-container> </ng-container>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index" [style]="rowStyle(entity, row)"> <mat-cell *matCellDef="let entity; let row = index" [style]="rowStyle(entity, row) | async">
<ng-container *ngIf="entityDatasource.countCellButtonAction"> <ng-container *ngIf="entityDatasource.countCellButtonAction">
<div [class.lt-lg:!hidden]="showCellActionsMenu && entityDatasource.countCellButtonAction !== 1" class="flex flex-row items-stretch justify-end" <div [class.lt-lg:!hidden]="showCellActionsMenu && entityDatasource.countCellButtonAction !== 1" class="flex flex-row items-stretch justify-end"
[style.min-width]="(entityDatasource.countCellButtonAction * 48) + 'px'"> [style.min-width]="(entityDatasource.countCellButtonAction * 48) + 'px'">
@ -99,7 +99,7 @@
[class.invisible]="entityDatasource.dataLoading" [class.invisible]="entityDatasource.dataLoading"
[class.tb-pointer]="hasRowAction" [class.tb-pointer]="hasRowAction"
*matRowDef="let entity; columns: displayedColumns; let row = index" *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)"></mat-row> (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
</table> </table>
<span [class.!hidden]="(entityDatasource.isEmpty() | async) === false || entityDatasource.dataLoading" <span [class.!hidden]="(entityDatasource.isEmpty() | async) === false || entityDatasource.dataLoading"

221
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -50,7 +50,7 @@ import { BehaviorSubject, fromEvent, merge, Observable, of, Subject } from 'rxjs
import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { debounceTime, distinctUntilChanged, map, takeUntil, tap } from 'rxjs/operators'; import { catchError, debounceTime, distinctUntilChanged, map, takeUntil, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort'; import { MatSort, SortDirection } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -66,7 +66,7 @@ import {
findColumnByEntityKey, findColumnByEntityKey,
findEntityKeyByColumnDef, findEntityKeyByColumnDef,
fromEntityColumnDef, fromEntityColumnDef,
getCellContentInfo, getCellContentFunctionInfo,
getCellStyleInfo, getCellStyleInfo,
getColumnDefaultVisibility, getColumnDefaultVisibility,
getColumnSelectionAvailability, getColumnSelectionAvailability,
@ -106,6 +106,7 @@ import { hidePageSizePixelValue } from '@shared/models/constants';
import { AggregationType } from '@shared/models/time/time.models'; import { AggregationType } from '@shared/models/time/time.models';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models'; import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { CompiledTbFunction } from '@shared/models/js-function.models';
interface EntitiesTableWidgetSettings extends TableWidgetSettings { interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string; entitiesTitle: string;
@ -164,13 +165,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private defaultSortOrder = 'entityName'; private defaultSortOrder = 'entityName';
private contentsInfo: {[key: string]: CellContentInfo} = {}; private contentsInfo: {[key: string]: CellContentInfo} = {};
private stylesInfo: {[key: string]: CellStyleInfo} = {}; private stylesInfo: {[key: string]: Observable<CellStyleInfo>} = {};
private columnWidth: {[key: string]: string} = {}; private columnWidth: {[key: string]: string} = {};
private columnDefaultVisibility: {[key: string]: boolean} = {}; private columnDefaultVisibility: {[key: string]: boolean} = {};
private columnSelectionAvailability: {[key: string]: boolean} = {}; private columnSelectionAvailability: {[key: string]: boolean} = {};
private columnsWithCellClick: Array<number> = []; private columnsWithCellClick: Array<number> = [];
private rowStylesInfo: RowStyleInfo; private rowStylesInfo: Observable<RowStyleInfo>;
private searchAction: WidgetAction = { private searchAction: WidgetAction = {
name: 'action.search', 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.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : 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; const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
@ -361,11 +362,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} as EntityColumn } as EntityColumn
); );
this.contentsInfo.entityName = { this.contentsInfo.entityName = {
useCellContentFunction: false contentFunction: of({
useCellContentFunction: false
})
}; };
this.stylesInfo.entityName = { this.stylesInfo.entityName = of({
useCellStyleFunction: false useCellStyleFunction: false
}; });
this.columnWidth.entityName = '0px'; this.columnWidth.entityName = '0px';
this.columnDefaultVisibility.entityName = true; this.columnDefaultVisibility.entityName = true;
this.columnSelectionAvailability.entityName = true; this.columnSelectionAvailability.entityName = true;
@ -385,11 +388,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} as EntityColumn } as EntityColumn
); );
this.contentsInfo.entityLabel = { this.contentsInfo.entityLabel = {
useCellContentFunction: false contentFunction: of({
useCellContentFunction: false
})
}; };
this.stylesInfo.entityLabel = { this.stylesInfo.entityLabel = of({
useCellStyleFunction: false useCellStyleFunction: false
}; });
this.columnWidth.entityLabel = '0px'; this.columnWidth.entityLabel = '0px';
this.columnDefaultVisibility.entityLabel = true; this.columnDefaultVisibility.entityLabel = true;
this.columnSelectionAvailability.entityLabel = true; this.columnSelectionAvailability.entityLabel = true;
@ -409,13 +414,18 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} as EntityColumn } as EntityColumn
); );
this.contentsInfo.entityType = { this.contentsInfo.entityType = {
useCellContentFunction: true, contentFunction: of({
cellContentFunction: (entityType: EntityType) => useCellContentFunction: true,
entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : '' cellContentFunction: new CompiledTbFunction(
(entityType: EntityType) =>
entityType ? this.translate.instant(entityTypeTranslations.get(entityType).type) : '',
[]
)
})
}; };
this.stylesInfo.entityType = { this.stylesInfo.entityType = of({
useCellStyleFunction: false useCellStyleFunction: false
}; });
this.columnWidth.entityType = '0px'; this.columnWidth.entityType = '0px';
this.columnDefaultVisibility.entityType = true; this.columnDefaultVisibility.entityType = true;
this.columnSelectionAvailability.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.stylesInfo[dataKey.def] = getCellStyleInfo(this.ctx, keySettings, 'value, entity, ctx');
this.contentsInfo[dataKey.def] = getCellContentInfo(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].units = dataKey.units;
this.contentsInfo[dataKey.def].decimals = dataKey.decimals; this.contentsInfo[dataKey.def].decimals = dataKey.decimals;
this.columnWidth[dataKey.def] = getColumnWidth(keySettings); this.columnWidth[dataKey.def] = getColumnWidth(keySettings);
@ -595,98 +611,135 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
return widthStyle(columnWidth); return widthStyle(columnWidth);
} }
public rowStyle(entity: EntityData, row: number): any { public rowStyle(entity: EntityData, row: number): Observable<any> {
let style$: Observable<any>;
let res = this.rowStyleCache[row]; let res = this.rowStyleCache[row];
if (!res) { if (!res) {
res = {}; style$ = this.rowStylesInfo.pipe(
if (entity && this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { map(styleInfo => {
try { if (styleInfo.useRowStyleFunction && styleInfo.rowStyleFunction) {
res = this.rowStylesInfo.rowStyleFunction(entity, this.ctx); const style = styleInfo.rowStyleFunction.execute(entity, this.ctx);
if (!isObject(res)) { if (!isObject(style)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
} }
if (Array.isArray(res)) { if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`); 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}' ` + console.warn(`Row style function in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your row style function.`); `returns '${e}'. Please check your row style function.`);
} return of({});
} })
this.rowStyleCache[row] = res; );
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<any> {
let style$: Observable<any>;
const col = this.columns.indexOf(key); const col = this.columns.indexOf(key);
const index = row * this.columns.length + col; const index = row * this.columns.length + col;
let res = this.cellStyleCache[index]; let res = this.cellStyleCache[index];
if (!res) { if (!res) {
res = {};
if (entity && key) { if (entity && key) {
const styleInfo = this.stylesInfo[key.def]; style$ = this.stylesInfo[key.def].pipe(
const value = getEntityValue(entity, key); map((styleInfo) => {
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { const value = getEntityValue(entity, key);
try { if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
res = styleInfo.cellStyleFunction(value, entity, this.ctx); const style = styleInfo.cellStyleFunction.execute(value, entity, this.ctx);
if (!isObject(res)) { if (!isObject(style)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
} }
if (Array.isArray(res)) { if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`); 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}' ` + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` +
`returns '${e}'. Please check your cell style function.`); `returns '${e}'. Please check your cell style function.`);
} return of({});
} })
this.cellStyleCache[index] = res; );
} 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) { return style$;
const columnWidth = this.columnWidth[key.def];
res = Object.assign(res, widthStyle(columnWidth));
}
return res;
} }
public cellContent(entity: EntityData, key: EntityColumn, row: number): SafeHtml { public cellContent(entity: EntityData, key: EntityColumn, row: number): Observable<SafeHtml> {
let content$: Observable<SafeHtml>;
const col = this.columns.indexOf(key); const col = this.columns.indexOf(key);
const index = row * this.columns.length + col; const index = row * this.columns.length + col;
let res = this.cellContentCache[index]; let res = this.cellContentCache[index];
if (isUndefined(res)) { if (isUndefined(res)) {
res = ''; const contentInfo = this.contentsInfo[key.def];
if (entity && key) { content$ = contentInfo.contentFunction.pipe(
const contentInfo = this.contentsInfo[key.def]; map((contentFunction) => {
const value = getEntityValue(entity, key); let content: any = '';
let content: string; if (entity && key) {
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { const contentInfo = this.contentsInfo[key.def];
try { const value = getEntityValue(entity, key);
content = contentInfo.cellContentFunction(value, entity, this.ctx); if (contentFunction.useCellContentFunction && contentFunction.cellContentFunction) {
} catch (e) { try {
content = '' + value; content = contentFunction.cellContentFunction.execute(value, entity, this.ctx);
} } catch (e) {
} else { content = '' + value;
content = this.defaultContent(key, contentInfo, value); }
} } else {
content = this.defaultContent(key, contentInfo, value);
if (isDefined(content)) { }
content = this.utils.customTranslation(content, content); if (isDefined(content)) {
switch (typeof content) { content = this.utils.customTranslation(content, content);
case 'string': switch (typeof content) {
res = this.domSanitizer.bypassSecurityTrustHtml(content); case 'string':
break; content = this.domSanitizer.bypassSecurityTrustHtml(content);
default: break;
res = content; }
}
} }
} return content;
} })
this.cellContentCache[index] = res; );
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 { private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any {

26
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts

@ -28,18 +28,21 @@ import {
hashCode, hashCode,
isDefinedAndNotNull, isDefinedAndNotNull,
isNotEmptyStr, isNotEmptyStr,
parseFunction, parseTbFunction,
safeExecute safeExecuteTbFunction
} from '@core/utils'; } from '@core/utils';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { HOME_COMPONENTS_MODULE_TOKEN, WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { HOME_COMPONENTS_MODULE_TOKEN, WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { EntityDataPageLink } from '@shared/models/query/query.models'; 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 { interface MarkdownWidgetSettings {
markdownTextPattern: string; markdownTextPattern: string;
useMarkdownTextFunction: boolean; useMarkdownTextFunction: boolean;
markdownTextFunction: string; markdownTextFunction: TbFunction;
applyDefaultMarkdownStyle: boolean; applyDefaultMarkdownStyle: boolean;
markdownCss: string; markdownCss: string;
} }
@ -53,7 +56,7 @@ type MarkdownTextFunction = (data: FormattedData[], ctx: WidgetContext) => strin
export class MarkdownWidgetComponent extends PageComponent implements OnInit { export class MarkdownWidgetComponent extends PageComponent implements OnInit {
settings: MarkdownWidgetSettings; settings: MarkdownWidgetSettings;
markdownTextFunction: MarkdownTextFunction; markdownTextFunction: Observable<CompiledTbFunction<MarkdownTextFunction>>;
markdownClass: string; markdownClass: string;
@ -78,7 +81,7 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
this.ctx.$scope.markdownWidget = this; this.ctx.$scope.markdownWidget = this;
this.settings = this.ctx.settings; this.settings = this.ctx.settings;
this.markdownTextFunction = this.settings.useMarkdownTextFunction ? 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; let cssString = this.settings.markdownCss;
if (isNotEmptyStr(cssString)) { if (isNotEmptyStr(cssString)) {
const cssParser = new cssjs(); const cssParser = new cssjs();
@ -126,8 +129,19 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
initialData = []; initialData = [];
} }
const data = formattedDataFormDatasourceData(initialData); const data = formattedDataFormDatasourceData(initialData);
let markdownText = this.settings.useMarkdownTextFunction ? 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); const allData: FormattedData = flatDataWithoutOverride(data);
markdownText = createLabelFromPattern(markdownText, allData); markdownText = createLabelFromPattern(markdownText, allData);
if (this.markdownText !== markdownText) { if (this.markdownText !== markdownText) {

31
ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts

@ -27,14 +27,18 @@ import {
formattedDataFormDatasourceData, formattedDataFormDatasourceData,
isNumber, isNumber,
isObject, isObject,
parseFunction, parseTbFunction,
safeExecute, unwrapModule safeExecuteTbFunction,
unwrapModule
} from '@core/utils'; } from '@core/utils';
import { CompiledTbFunction, TbFunction } from '@shared/models/js-function.models';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
interface QrCodeWidgetSettings { interface QrCodeWidgetSettings {
qrCodeTextPattern: string; qrCodeTextPattern: string;
useQrCodeTextFunction: boolean; useQrCodeTextFunction: boolean;
qrCodeTextFunction: string; qrCodeTextFunction: TbFunction;
} }
type QrCodeTextFunction = (data: FormattedData[]) => string; type QrCodeTextFunction = (data: FormattedData[]) => string;
@ -47,7 +51,7 @@ type QrCodeTextFunction = (data: FormattedData[]) => string;
export class QrCodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit { export class QrCodeWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
settings: QrCodeWidgetSettings; settings: QrCodeWidgetSettings;
qrCodeTextFunction: QrCodeTextFunction; qrCodeTextFunction: Observable<CompiledTbFunction<QrCodeTextFunction>>;
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -68,7 +72,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
ngOnInit(): void { ngOnInit(): void {
this.ctx.$scope.qrCodeWidget = this; this.ctx.$scope.qrCodeWidget = this;
this.settings = this.ctx.settings; 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 { ngAfterViewInit(): void {
@ -81,7 +85,6 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
public onDataUpdated() { public onDataUpdated() {
let initialData: DatasourceData[]; let initialData: DatasourceData[];
let qrCodeText: string;
if (this.ctx.data?.length) { if (this.ctx.data?.length) {
initialData = this.ctx.data; initialData = this.ctx.data;
} else if (this.ctx.datasources?.length) { } else if (this.ctx.datasources?.length) {
@ -100,13 +103,19 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
} }
const data = formattedDataFormDatasourceData(initialData); const data = formattedDataFormDatasourceData(initialData);
const pattern = this.settings.useQrCodeTextFunction ? const pattern = this.settings.useQrCodeTextFunction ?
safeExecute(this.qrCodeTextFunction, [data]) : this.settings.qrCodeTextPattern; this.qrCodeTextFunction.pipe(map(qrCodeTextFunction => safeExecuteTbFunction(qrCodeTextFunction, [data]))) : this.settings.qrCodeTextPattern;
const allData: FormattedData = flatDataWithoutOverride(data); if (typeof pattern === 'string') {
qrCodeText = createLabelFromPattern(pattern, allData); this.updateQrCodeText(pattern, data);
this.updateQrCodeText(qrCodeText); } 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) { if (this.qrCodeText !== newQrCodeText) {
this.qrCodeText = newQrCodeText; this.qrCodeText = newQrCodeText;
if (!(isObject(newQrCodeText) || isNumber(newQrCodeText))) { if (!(isObject(newQrCodeText) || isNumber(newQrCodeText))) {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html

@ -75,6 +75,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellStyleFunction" formControlName="cellStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'alarm', 'ctx']" [functionArgs]="['value', 'alarm', 'ctx']"
functionTitle="{{ 'widgets.table.cell-style-function' | translate }}" functionTitle="{{ 'widgets.table.cell-style-function' | translate }}"
@ -98,6 +99,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellContentFunction" formControlName="cellContentFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'alarm', 'ctx']" [functionArgs]="['value', 'alarm', 'ctx']"
functionTitle="{{ 'widgets.table.cell-content-function' | translate }}" functionTitle="{{ 'widgets.table.cell-content-function' | translate }}"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html

@ -117,6 +117,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="rowStyleFunction" formControlName="rowStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['alarm', 'ctx']" [functionArgs]="['alarm', 'ctx']"
functionTitle="{{ 'widgets.table.row-style-function' | translate }}" functionTitle="{{ 'widgets.table.row-style-function' | translate }}"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html

@ -21,6 +21,7 @@
</mat-slide-toggle> </mat-slide-toggle>
<tb-js-func [class.!hidden]="!markdownWidgetSettingsForm.get('useMarkdownTextFunction').value" <tb-js-func [class.!hidden]="!markdownWidgetSettingsForm.get('useMarkdownTextFunction').value"
formControlName="markdownTextFunction" formControlName="markdownTextFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['data', 'ctx']" [functionArgs]="['data', 'ctx']"
functionTitle="{{ 'widgets.markdown.markdown-text-function' | translate }}" functionTitle="{{ 'widgets.markdown.markdown-text-function' | translate }}"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/qrcode-widget-settings.component.html

@ -30,6 +30,7 @@
<tb-js-func required <tb-js-func required
[class.!hidden]="!qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value" [class.!hidden]="!qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value"
formControlName="qrCodeTextFunction" formControlName="qrCodeTextFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['data']" [functionArgs]="['data']"
functionTitle="{{ 'widgets.qr-code.qr-code-text-function' | translate }}" functionTitle="{{ 'widgets.qr-code.qr-code-text-function' | translate }}"

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html

@ -63,6 +63,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellStyleFunction" formControlName="cellStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'rowData', 'ctx']" [functionArgs]="['value', 'rowData', 'ctx']"
functionTitle="{{ 'widgets.table.cell-style-function' | translate }}" functionTitle="{{ 'widgets.table.cell-style-function' | translate }}"
@ -86,6 +87,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellContentFunction" formControlName="cellContentFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'rowData', 'ctx']" [functionArgs]="['value', 'rowData', 'ctx']"
functionTitle="{{ 'widgets.table.cell-content-function' | translate }}" functionTitle="{{ 'widgets.table.cell-content-function' | translate }}"

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html

@ -72,6 +72,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellStyleFunction" formControlName="cellStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'rowData', 'ctx']" [functionArgs]="['value', 'rowData', 'ctx']"
functionTitle="{{ 'widgets.table.cell-style-function' | translate }}" functionTitle="{{ 'widgets.table.cell-style-function' | translate }}"
@ -95,6 +96,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellContentFunction" formControlName="cellContentFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'rowData', 'ctx']" [functionArgs]="['value', 'rowData', 'ctx']"
functionTitle="{{ 'widgets.table.cell-content-function' | translate }}" functionTitle="{{ 'widgets.table.cell-content-function' | translate }}"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -93,6 +93,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="rowStyleFunction" formControlName="rowStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['rowData', 'ctx']" [functionArgs]="['rowData', 'ctx']"
functionTitle="{{ 'widgets.table.row-style-function' | translate }}" functionTitle="{{ 'widgets.table.row-style-function' | translate }}"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html

@ -51,6 +51,7 @@
<tb-js-func <tb-js-func
[(ngModel)]="action.customFunction" [(ngModel)]="action.customFunction"
(ngModelChange)="onActionUpdated()" (ngModelChange)="onActionUpdated()"
withModules
[fillHeight]="true" [fillHeight]="true"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
[disableUndefinedCheck]="true" [disableUndefinedCheck]="true"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html

@ -95,6 +95,7 @@
<tb-js-func <tb-js-func
[(ngModel)]="action.customFunction" [(ngModel)]="action.customFunction"
(ngModelChange)="notifyActionUpdated()" (ngModelChange)="notifyActionUpdated()"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']"
[disableUndefinedCheck]="true" [disableUndefinedCheck]="true"
[validationArgs]="[]" [validationArgs]="[]"

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-key-settings.component.html

@ -75,6 +75,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellStyleFunction" formControlName="cellStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'entity', 'ctx']" [functionArgs]="['value', 'entity', 'ctx']"
functionTitle="{{ 'widgets.table.cell-style-function' | translate }}" functionTitle="{{ 'widgets.table.cell-style-function' | translate }}"
@ -98,6 +99,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="cellContentFunction" formControlName="cellContentFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value', 'entity', 'ctx']" [functionArgs]="['value', 'entity', 'ctx']"
functionTitle="{{ 'widgets.table.cell-content-function' | translate }}" functionTitle="{{ 'widgets.table.cell-content-function' | translate }}"

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/entity/entities-table-widget-settings.component.html

@ -114,6 +114,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func required <tb-js-func required
formControlName="rowStyleFunction" formControlName="rowStyleFunction"
withModules
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['entity', 'ctx']" [functionArgs]="['entity', 'ctx']"
functionTitle="{{ 'widgets.table.row-style-function' | translate }}" functionTitle="{{ 'widgets.table.row-style-function' | translate }}"

187
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -26,9 +26,14 @@ import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; import {
import { forkJoin, Observable, of } from 'rxjs'; CompiledTbFunction,
import { catchError, map } from 'rxjs/operators'; compileTbFunction,
isNotEmptyTbFunction,
TbFunction
} from '@shared/models/js-function.models';
import { forkJoin, Observable, of, ReplaySubject } from 'rxjs';
import { catchError, map, share } from 'rxjs/operators';
type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile';
@ -43,7 +48,7 @@ export interface TableWidgetSettings {
displayPagination: boolean; displayPagination: boolean;
defaultPageSize: number; defaultPageSize: number;
useRowStyleFunction: boolean; useRowStyleFunction: boolean;
rowStyleFunction?: string; rowStyleFunction?: TbFunction;
reserveSpaceForHiddenAction?: boolean; reserveSpaceForHiddenAction?: boolean;
} }
@ -51,9 +56,9 @@ export interface TableWidgetDataKeySettings {
customTitle?: string; customTitle?: string;
columnWidth?: string; columnWidth?: string;
useCellStyleFunction: boolean; useCellStyleFunction: boolean;
cellStyleFunction?: string; cellStyleFunction?: TbFunction;
useCellContentFunction: boolean; useCellContentFunction: boolean;
cellContentFunction?: string; cellContentFunction?: TbFunction;
defaultColumnVisibility?: ColumnVisibilityOptions; defaultColumnVisibility?: ColumnVisibilityOptions;
columnSelectionToDisplay?: ColumnSelectionOptions; columnSelectionToDisplay?: ColumnSelectionOptions;
} }
@ -91,9 +96,13 @@ export interface DisplayColumn {
export type CellContentFunction = (...args: any[]) => string; export type CellContentFunction = (...args: any[]) => string;
export interface CellContentInfo { export interface CellContentFunctionInfo {
useCellContentFunction: boolean; useCellContentFunction: boolean;
cellContentFunction?: CellContentFunction; cellContentFunction?: CompiledTbFunction<CellContentFunction>;
}
export interface CellContentInfo {
contentFunction: Observable<CellContentFunctionInfo>;
units?: string; units?: string;
decimals?: number; decimals?: number;
} }
@ -102,14 +111,14 @@ export type CellStyleFunction = (...args: any[]) => any;
export interface CellStyleInfo { export interface CellStyleInfo {
useCellStyleFunction: boolean; useCellStyleFunction: boolean;
cellStyleFunction?: CellStyleFunction; cellStyleFunction?: CompiledTbFunction<CellStyleFunction>;
} }
export type RowStyleFunction = (...args: any[]) => any; export type RowStyleFunction = (...args: any[]) => any;
export interface RowStyleInfo { export interface RowStyleInfo {
useRowStyleFunction: boolean; useRowStyleFunction: boolean;
rowStyleFunction?: RowStyleFunction; rowStyleFunction?: CompiledTbFunction<RowStyleFunction>;
} }
@ -232,67 +241,113 @@ export function getAlarmValue(alarm: AlarmDataInfo, key: EntityColumn) {
} }
} }
export function getRowStyleInfo(settings: TableWidgetSettings, ...args: string[]): RowStyleInfo { export function getRowStyleInfo(widgetContext: WidgetContext, settings: TableWidgetSettings, ...args: string[]): Observable<RowStyleInfo> {
let rowStyleFunction: RowStyleFunction = null; let rowStyleInfo$: Observable<RowStyleInfo>;
let useRowStyleFunction = false; if (settings.useRowStyleFunction === true && isNotEmptyTbFunction(settings.rowStyleFunction)) {
rowStyleInfo$ = compileTbFunction<RowStyleFunction>(widgetContext.http, settings.rowStyleFunction, ...args).pipe(
if (settings.useRowStyleFunction === true) { catchError(() => { return of(null) }),
if (isDefined(settings.rowStyleFunction) && settings.rowStyleFunction.length > 0) { map((rowStyleFunction) => {
try { if (!rowStyleFunction) {
rowStyleFunction = new Function(...args, settings.rowStyleFunction) as RowStyleFunction; return {
useRowStyleFunction = true; useRowStyleFunction: false,
} catch (e) { rowStyleFunction: null
rowStyleFunction = null; }
useRowStyleFunction = false; } else {
} return {
} useRowStyleFunction: true,
rowStyleFunction
}
}
})
);
} else {
rowStyleInfo$ = of({
useRowStyleFunction: false,
rowStyleFunction: null
});
} }
return { return rowStyleInfo$.pipe(
useRowStyleFunction, share({
rowStyleFunction connector: () => new ReplaySubject(1),
}; resetOnError: false,
} resetOnComplete: false,
resetOnRefCountZero: false
export function getCellStyleInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellStyleInfo { })
let cellStyleFunction: CellStyleFunction = null; );
let useCellStyleFunction = false; }
if (keySettings.useCellStyleFunction === true) { export function getCellStyleInfo(widgetContext: WidgetContext, keySettings: TableWidgetDataKeySettings, ...args: string[]): Observable<CellStyleInfo> {
if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { let cellStyleInfo$: Observable<CellStyleInfo>;
try { if (keySettings.useCellStyleFunction === true && isNotEmptyTbFunction(keySettings.cellStyleFunction)) {
cellStyleFunction = new Function(...args, keySettings.cellStyleFunction) as CellStyleFunction; cellStyleInfo$ = compileTbFunction<CellStyleFunction>(widgetContext.http, keySettings.cellStyleFunction, ...args).pipe(
useCellStyleFunction = true; catchError(() => { return of(null) }),
} catch (e) { map((cellStyleFunction) => {
cellStyleFunction = null; if (!cellStyleFunction) {
useCellStyleFunction = false; return {
useCellStyleFunction: false,
cellStyleFunction: null
}
} else {
return {
useCellStyleFunction: true,
cellStyleFunction
}
}
})
);
} else {
cellStyleInfo$ = of(
{
useCellStyleFunction: false,
cellStyleFunction: null
} }
} )
} }
return { return cellStyleInfo$.pipe(
useCellStyleFunction, share({
cellStyleFunction connector: () => new ReplaySubject(1),
}; resetOnError: false,
} resetOnComplete: false,
resetOnRefCountZero: false
export function getCellContentInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellContentInfo { })
let cellContentFunction: CellContentFunction = null; );
let useCellContentFunction = false; }
if (keySettings.useCellContentFunction === true) { export function getCellContentFunctionInfo(widgetContext: WidgetContext, keySettings: TableWidgetDataKeySettings, ...args: string[]): Observable<CellContentFunctionInfo> {
if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { let cellContentFunctionInfo$: Observable<CellContentFunctionInfo>;
try { if (keySettings.useCellContentFunction === true && isNotEmptyTbFunction(keySettings.cellContentFunction)) {
cellContentFunction = new Function(...args, keySettings.cellContentFunction) as CellContentFunction; cellContentFunctionInfo$ = compileTbFunction<CellContentFunction>(widgetContext.http, keySettings.cellContentFunction, ...args).pipe(
useCellContentFunction = true; catchError(() => { return of(null) }),
} catch (e) { map((cellContentFunction) => {
cellContentFunction = null; if (!cellContentFunction) {
useCellContentFunction = false; return {
useCellContentFunction: false,
cellContentFunction: null
}
} else {
return {
useCellContentFunction: true,
cellContentFunction
}
}
})
);
} else {
cellContentFunctionInfo$ = of(
{
useCellContentFunction: false,
cellContentFunction: null
} }
} )
} }
return { return cellContentFunctionInfo$.pipe(
cellContentFunction, share({
useCellContentFunction connector: () => new ReplaySubject(1),
}; resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false
})
);
} }
export function getColumnWidth(keySettings: TableWidgetDataKeySettings): string { export function getColumnWidth(keySettings: TableWidgetDataKeySettings): string {

12
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -49,15 +49,15 @@
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'"> <ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index" <mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex)" [innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex) | async"
[style]="cellStyle(source, null, 0, row, row[0], rowIndex)"> [style]="cellStyle(source, null, 0, row, row[0], rowIndex) | async">
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;"> <ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;">
<mat-header-cell *matHeaderCellDef mat-sort-header [disabled]="!h.sortable"> {{ h.dataKey.label }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header [disabled]="!h.sortable"> {{ h.dataKey.label }} </mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index" <mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, h, h.index, row, row[h.index], rowIndex)" [innerHTML]="cellContent(source, h, h.index, row, row[h.index], rowIndex) | async"
[style]="cellStyle(source, h, h.index, row, row[h.index], rowIndex)"> [style]="cellStyle(source, h, h.index, row, row[h.index], rowIndex) | async">
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction"> <ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">
@ -70,7 +70,7 @@
</div> </div>
</ng-container> </ng-container>
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index" [style]="rowStyle(source, row, rowIndex)"> <mat-cell *matCellDef="let row; let rowIndex = index" [style]="rowStyle(source, row, rowIndex) | async">
<ng-container *ngIf="source.timeseriesDatasource.countCellButtonAction"> <ng-container *ngIf="source.timeseriesDatasource.countCellButtonAction">
<div [class.lt-lg:!hidden]="showCellActionsMenu && source.timeseriesDatasource.countCellButtonAction !== 1" class="flex flex-row items-stretch justify-end" <div [class.lt-lg:!hidden]="showCellActionsMenu && source.timeseriesDatasource.countCellButtonAction !== 1" class="flex flex-row items-stretch justify-end"
[style.min-width]="(source.timeseriesDatasource.countCellButtonAction * 40) + 'px'"> [style.min-width]="(source.timeseriesDatasource.countCellButtonAction * 40) + 'px'">
@ -110,7 +110,7 @@
<mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row> <mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row [class.tb-pointer]="hasRowAction" <mat-row [class.tb-pointer]="hasRowAction"
*matRowDef="let row; columns: source.displayedColumns; let rowIndex = index" *matRowDef="let row; columns: source.displayedColumns; let rowIndex = index"
[style]="rowStyle(source, row, rowIndex)" [style]="rowStyle(source, row, rowIndex) | async"
(click)="onRowClick($event, row)"></mat-row> (click)="onRowClick($event, row)"></mat-row>
</table> </table>
<span [class.!hidden]="(source.timeseriesDatasource.isEmpty() | async) === false" <span [class.!hidden]="(source.timeseriesDatasource.isEmpty() | async) === false"

219
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -20,7 +20,8 @@ import {
Component, Component,
ElementRef, ElementRef,
Injector, Injector,
Input, NgZone, Input,
NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
@ -78,7 +79,7 @@ import {
checkHasActions, checkHasActions,
constructTableCssString, constructTableCssString,
DisplayColumn, DisplayColumn,
getCellContentInfo, getCellContentFunctionInfo,
getCellStyleInfo, getCellStyleInfo,
getColumnDefaultVisibility, getColumnDefaultVisibility,
getColumnSelectionAvailability, getColumnSelectionAvailability,
@ -131,7 +132,7 @@ interface TimeseriesHeader {
show: boolean; show: boolean;
columnDefaultVisibility?: boolean; columnDefaultVisibility?: boolean;
columnSelectionAvailability?: boolean; columnSelectionAvailability?: boolean;
styleInfo: CellStyleInfo; styleInfo: Observable<CellStyleInfo>;
contentInfo: CellContentInfo; contentInfo: CellContentInfo;
order?: number; order?: number;
} }
@ -200,7 +201,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private displayedColumns: Array<DisplayColumn[]> = []; private displayedColumns: Array<DisplayColumn[]> = [];
private rowStylesInfo: RowStyleInfo; private rowStylesInfo: Observable<RowStyleInfo>;
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
private widgetTimewindowChanged$: 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.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; const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
@ -519,12 +520,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
dataKeys.forEach((dataKey, index) => { dataKeys.forEach((dataKey, index) => {
const sortable = !dataKey.usePostProcessing; const sortable = !dataKey.usePostProcessing;
const keySettings: TableWidgetDataKeySettings = dataKey.settings; const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const styleInfo = getCellStyleInfo(this.ctx, keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, rowData, ctx');
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
contentInfo.units = dataKey.units; const contentInfo: CellContentInfo = {
contentInfo.decimals = dataKey.decimals; contentFunction: contentFunctionInfo,
units: dataKey.units,
decimals: dataKey.decimals
};
header.push({ header.push({
index: index + 1, index: index + 1,
dataKey, dataKey,
@ -542,12 +546,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const index = dataKeys.length + latestIndex; const index = dataKeys.length + latestIndex;
const sortable = !dataKey.usePostProcessing; const sortable = !dataKey.usePostProcessing;
const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const styleInfo = getCellStyleInfo(this.ctx, keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); const contentFunctionInfo = getCellContentFunctionInfo(this.ctx, keySettings, 'value, rowData, ctx');
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx); const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings); const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
contentInfo.units = dataKey.units; const contentInfo: CellContentInfo = {
contentInfo.decimals = dataKey.decimals; contentFunction: contentFunctionInfo,
units: dataKey.units,
decimals: dataKey.decimals
};
header.push({ header.push({
index: index + 1, index: index + 1,
dataKey, dataKey,
@ -658,111 +665,143 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
return source.datasource.entityId; return source.datasource.entityId;
} }
public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): any { public rowStyle(source: TimeseriesTableSource, row: TimeseriesRow, index: number): Observable<any> {
let style$: Observable<any>;
let res = this.rowStyleCache[index]; let res = this.rowStyleCache[index];
if (!res) { if (!res) {
res = {}; style$ = this.rowStylesInfo.pipe(
if (this.rowStylesInfo.useRowStyleFunction && this.rowStylesInfo.rowStyleFunction) { map(styleInfo => {
try { if (styleInfo.useRowStyleFunction && styleInfo.rowStyleFunction) {
const rowData = source.rowDataTemplate; const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0]; rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => { source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.label] = row[headerInfo.index]; rowData[headerInfo.dataKey.label] = row[headerInfo.index];
}); });
res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx); const style = styleInfo.rowStyleFunction.execute(rowData, this.ctx);
if (!isObject(res)) { if (!isObject(style)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
} }
if (Array.isArray(res)) { if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`); throw new TypeError(`Array instead of style object`);
}
return style;
} else {
return {};
} }
} catch (e) { }),
res = {}; catchError(e => {
console.warn(`Row style function in widget ` + console.warn(`Row style function in widget ` +
`'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`); `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your row style function.`);
} return of({});
} })
this.rowStyleCache[index] = res; );
style$ = style$.pipe(
tap((style) => {
this.rowStyleCache[index] = style;
})
);
} else {
style$ = of(res);
} }
return res; return style$;
} }
public cellStyle(source: TimeseriesTableSource, header: TimeseriesHeader, public cellStyle(source: TimeseriesTableSource, header: TimeseriesHeader,
index: number, row: TimeseriesRow, value: any, rowIndex: number): any { index: number, row: TimeseriesRow, value: any, rowIndex: number): Observable<any> {
let style$: Observable<any>;
const cacheIndex = rowIndex * (source.header.length + 1) + index; const cacheIndex = rowIndex * (source.header.length + 1) + index;
let res = this.cellStyleCache[cacheIndex]; let res = this.cellStyleCache[cacheIndex];
if (!res) { if (!res) {
res = {};
if (index > 0) { if (index > 0) {
const styleInfo = header.styleInfo; style$ = header.styleInfo.pipe(
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { map(styleInfo => {
try { if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
const rowData = source.rowDataTemplate; const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0]; rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => { source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.label] = row[headerInfo.index]; rowData[headerInfo.dataKey.label] = row[headerInfo.index];
}); });
res = styleInfo.cellStyleFunction(value, rowData, this.ctx); const style = styleInfo.cellStyleFunction.execute(value, rowData, this.ctx);
if (!isObject(res)) { if (!isObject(style)) {
throw new TypeError(`${res === null ? 'null' : typeof res} instead of style object`); throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`);
} }
if (Array.isArray(res)) { if (Array.isArray(style)) {
throw new TypeError(`Array instead of style object`); 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 ` + 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.`); `'${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, public cellContent(source: TimeseriesTableSource, header: TimeseriesHeader,
index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml { index: number, row: TimeseriesRow, value: any, rowIndex: number): Observable<SafeHtml> {
let content$: Observable<SafeHtml>;
const cacheIndex = rowIndex * (source.header.length + 1) + index ; const cacheIndex = rowIndex * (source.header.length + 1) + index ;
let res = this.cellContentCache[cacheIndex]; let res = this.cellContentCache[cacheIndex];
if (isUndefined(res)) { if (isUndefined(res)) {
res = '';
if (index === 0) { if (index === 0) {
res = row.formattedTs; content$ = of(row.formattedTs);
} else { } else {
let content; content$ = header.contentInfo.contentFunction.pipe(
const contentInfo = header.contentInfo; map((contentFunction) => {
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { let content: any;
try { if (contentFunction.useCellContentFunction && contentFunction.cellContentFunction) {
const rowData = source.rowDataTemplate; try {
rowData.Timestamp = row[0]; const rowData = source.rowDataTemplate;
source.header.forEach((headerInfo) => { rowData.Timestamp = row[0];
rowData[headerInfo.dataKey.label] = row[headerInfo.index]; source.header.forEach((headerInfo) => {
}); rowData[headerInfo.dataKey.label] = row[headerInfo.index];
content = contentInfo.cellContentFunction(value, rowData, this.ctx); });
} catch (e) { content = contentFunction.cellContentFunction.execute(value, rowData, this.ctx);
content = '' + value; } catch (e) {
} content = '' + value;
} else { }
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals; } else {
const units = contentInfo.units || this.ctx.widgetConfig.units; const decimals = (header.contentInfo.decimals || header.contentInfo.decimals === 0) ? header.contentInfo.decimals : this.ctx.widgetConfig.decimals;
content = this.ctx.utils.formatValue(value, decimals, units, true); const units = header.contentInfo.units || this.ctx.widgetConfig.units;
} content = this.ctx.utils.formatValue(value, decimals, units, true);
}
if (isDefined(content)) { if (isDefined(content)) {
content = this.utils.customTranslation(content, content); content = this.utils.customTranslation(content, content);
switch (typeof content) { switch (typeof content) {
case 'string': case 'string':
res = this.domSanitizer.bypassSecurityTrustHtml(content); content = this.domSanitizer.bypassSecurityTrustHtml(content);
break; break;
default: }
res = content; }
} 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) { public onRowClick($event: Event, row: TimeseriesRow) {

2
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<T extends GenericFunction> { export class CompiledTbFunction<T extends GenericFunction> {

2
ui-ngx/src/app/shared/models/widget.models.ts

@ -334,7 +334,7 @@ export interface KeyInfo {
comparisonResultType?: ComparisonResultType; comparisonResultType?: ComparisonResultType;
label?: string; label?: string;
color?: string; color?: string;
funcBody?: string; funcBody?: TbFunction;
postFuncBody?: TbFunction; postFuncBody?: TbFunction;
units?: string; units?: string;
decimals?: number; decimals?: number;

Loading…
Cancel
Save