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 a1c88b3330..8df8b7c09b 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -19,14 +19,16 @@ import { DataEntry, DataSet, DataSetHolder, - DatasourceType, IndexedData, + DatasourceType, + IndexedData, widgetType } from '@shared/models/widget.models'; import { AggregationType, ComparisonDuration, createTimewindowForComparison, - getCurrentTime, IntervalMath, + getCurrentTime, + IntervalMath, SubscriptionTimewindow } from '@shared/models/time/time.models'; import { @@ -51,7 +53,8 @@ import { IndexedSubscriptionData, IntervalType, NOT_SUPPORTED, - SubscriptionData, SubscriptionDataEntry, + SubscriptionData, + SubscriptionDataEntry, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; @@ -61,9 +64,16 @@ 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 { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, switchMap } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { + CompiledTbFunction, + compileTbFunction, + isNotEmptyTbFunction, + TbFunction +} from '@shared/models/js-function.models'; +import { HttpClient } from '@angular/common/http'; import Timeout = NodeJS.Timeout; declare type DataKeyFunction = (time: number, prevValue: any) => any; @@ -81,8 +91,8 @@ export interface SubscriptionDataKey { comparisonResultType?: ComparisonResultType; funcBody: string; func?: DataKeyFunction; - postFuncBody: string; - postFunc?: DataKeyPostFunction; + postFuncBody: TbFunction; + postFunc?: CompiledTbFunction; index?: number; listIndex?: number; key?: string; @@ -109,8 +119,8 @@ export class EntityDataSubscription { constructor(private listener: EntityDataListener, private telemetryService: TelemetryWebsocketService, - private utils: UtilsService) { - this.initializeSubscription(); + private utils: UtilsService, + private http: HttpClient) { } private entityDataSubscriptionOptions = this.listener.subscriptionOptions; @@ -193,7 +203,7 @@ export class EntityDataSubscription { return [[timestamp, value]]; } - private initializeSubscription() { + private async initializeSubscription() { for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); this.dataKeysList.push(dataKey); @@ -203,9 +213,10 @@ export class EntityDataSubscription { dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction; } } else { - if (dataKey.postFuncBody && !dataKey.postFunc) { - dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue', - dataKey.postFuncBody) as DataKeyPostFunction; + if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) { + try { + dataKey.postFunc = await firstValueFrom(compileTbFunction(this.http, dataKey.postFuncBody, 'time', 'value', 'prevValue', 'timePrev', 'prevOrigValue')); + } catch (e) {} } } let key: string; @@ -265,354 +276,358 @@ export class EntityDataSubscription { } public subscribe(): Observable { - this.entityDataResolveSubject = new ReplaySubject(1); - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.started = true; - this.dataResolved = true; - this.prepareSubscriptionTimewindow(); - } - if (this.datasourceType === DatasourceType.entity) { - const entityFields: Array = - this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map( - dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) - ); - if (!entityFields.find(key => key.key === 'name')) { - entityFields.push({ - type: EntityKeyType.ENTITY_FIELD, - key: 'name' - }); - } - if (!entityFields.find(key => key.key === 'label')) { - entityFields.push({ - type: EntityKeyType.ENTITY_FIELD, - key: 'label' - }); - } - if (!entityFields.find(key => key.key === 'additionalInfo')) { - entityFields.push({ - type: EntityKeyType.ENTITY_FIELD, - key: 'additionalInfo' - }); - } + return from(this.initializeSubscription()).pipe( + switchMap(() => { + this.entityDataResolveSubject = new ReplaySubject(1); + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.started = true; + this.dataResolved = true; + this.prepareSubscriptionTimewindow(); + } + if (this.datasourceType === DatasourceType.entity) { + const entityFields: Array = + this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map( + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) + ); + if (!entityFields.find(key => key.key === 'name')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'name' + }); + } + if (!entityFields.find(key => key.key === 'label')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'label' + }); + } + if (!entityFields.find(key => key.key === 'additionalInfo')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'additionalInfo' + }); + } - this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map( - dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) - ); + this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map( + dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) + ); - this.tsFields = this.dataKeysList. - filter(dataKey => dataKey.type === DataKeyType.timeseries && + this.tsFields = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map( - dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) - ); + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - const latestTsFields = this.dataKeysList. - filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest && - (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map( - dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) - ); - this.latestValues = this.attrFields.concat(latestTsFields); - } else { - this.latestValues = this.attrFields.concat(this.tsFields); - } + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + const latestTsFields = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest && + (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map( + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); + this.latestValues = this.attrFields.concat(latestTsFields); + } else { + this.latestValues = this.attrFields.concat(this.tsFields); + } - this.aggTsValues = this.dataKeysList. + this.aggTsValues = this.dataKeysList. filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map( dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType }) ); - this.aggTsComparisonValues = this.dataKeysList. - filter(dataKey => dataKey.type === DataKeyType.timeseries && - dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map( - dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType, - previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE }) - ); + this.aggTsComparisonValues = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && + dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map( + dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType, + previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE }) + ); - this.subscriber = new TelemetrySubscriber(this.telemetryService); - this.dataCommand = new EntityDataCmd(); + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.dataCommand = new EntityDataCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; - if (this.entityDataSubscriptionOptions.additionalKeyFilters) { - if (keyFilters) { - keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); - } else { - keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; - } - } + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } - this.dataCommand.query = { - entityFilter: this.entityDataSubscriptionOptions.entityFilter, - pageLink: this.entityDataSubscriptionOptions.pageLink, - keyFilters, - entityFields, - latestValues: this.latestValues - }; + this.dataCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + pageLink: this.entityDataSubscriptionOptions.pageLink, + keyFilters, + entityFields, + latestValues: this.latestValues + }; - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.prepareSubscriptionCommands(this.dataCommand); - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - this.subscriber.setTsOffset(this.subsTw.tsOffset); - } else { - this.subscriber.setTsOffset(this.latestTsOffset); - } - } + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.prepareSubscriptionCommands(this.dataCommand); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + this.subscriber.setTsOffset(this.latestTsOffset); + } + } - this.subscriber.subscriptionCommands.push(this.dataCommand); + this.subscriber.subscriptionCommands.push(this.dataCommand); - this.subscriber.entityData$.subscribe( - (entityDataUpdate) => { - if (entityDataUpdate.data) { - this.onPageData(entityDataUpdate.data); - if (this.prematureUpdates) { - for (const update of this.prematureUpdates) { - this.onDataUpdate(update); + this.subscriber.entityData$.subscribe( + (entityDataUpdate) => { + if (entityDataUpdate.data) { + this.onPageData(entityDataUpdate.data); + if (this.prematureUpdates) { + for (const update of this.prematureUpdates) { + this.onDataUpdate(update); + } + this.prematureUpdates = null; + } + } else if (entityDataUpdate.update) { + if (!this.pageData) { + if (!this.prematureUpdates) { + this.prematureUpdates = []; + } + this.prematureUpdates.push(entityDataUpdate.update); + } else { + this.onDataUpdate(entityDataUpdate.update); + } } - this.prematureUpdates = null; } - } else if (entityDataUpdate.update) { - if (!this.pageData) { - if (!this.prematureUpdates) { - this.prematureUpdates = []; + ); + + this.subscriber.reconnect$.subscribe(() => { + if (this.started) { + const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; + if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length || + this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) { + const newSubsTw = this.listener.updateRealtimeSubscription(); + this.subsTw = newSubsTw; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { + targetCommand.tsCmd.startTs = this.subsTw.startTs; + targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + if (typeof this.subsTw.aggregation.interval === 'number') { + targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; + targetCommand.tsCmd.intervalType = IntervalType.MILLISECONDS; + } else { + targetCommand.tsCmd.intervalType = this.subsTw.aggregation.interval; + } + targetCommand.tsCmd.timeZoneId = this.subsTw.timezone; + targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; + targetCommand.tsCmd.agg = this.subsTw.aggregation.type; + targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; + this.dataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw); + }); + } + if (this.aggTsValues.length > 0 && !this.isFloatingTimewindow) { + targetCommand.aggTsCmd.startTs = this.subsTw.startTs; + targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + this.tsLatestDataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw); + }); + } } - this.prematureUpdates.push(entityDataUpdate.update); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + this.subscriber.setTsOffset(this.latestTsOffset); + } + targetCommand.query = this.dataCommand.query; + this.subscriber.subscriptionCommands = [targetCommand]; } else { - this.onDataUpdate(entityDataUpdate.update); + this.subscriber.subscriptionCommands = [this.dataCommand]; } + }); + this.subscriber.subscribe(); + } else if (this.datasourceType === DatasourceType.function) { + let tsOffset = 0; + if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) { + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; } - } - ); - this.subscriber.reconnect$.subscribe(() => { - if (this.started) { - const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; - if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length || - this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) { - const newSubsTw = this.listener.updateRealtimeSubscription(); - this.subsTw = newSubsTw; - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { - targetCommand.tsCmd.startTs = this.subsTw.startTs; - targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; - if (typeof this.subsTw.aggregation.interval === 'number') { - targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; - targetCommand.tsCmd.intervalType = IntervalType.MILLISECONDS; + const entityData: EntityData = { + entityId: { + id: NULL_UUID, + entityType: EntityType.DEVICE + }, + timeseries: {}, + latest: {} + }; + const name = DatasourceType.function; + entityData.latest[EntityKeyType.ENTITY_FIELD] = { + name: {ts: Date.now() + tsOffset, value: name} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.subscriber.setTsOffset(this.latestTsOffset); + this.countCommand = new EntityCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } + this.countCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + this.subscriber.subscriptionCommands.push(this.countCommand); + + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; + + const countKey = this.dataKeysList[0]; + + let dataReceived = false; + + this.subscriber.entityCount$.subscribe( + (entityCountUpdate) => { + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now() + this.latestTsOffset, + value: DatasourceType.entityCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + dataReceived = true; } else { - targetCommand.tsCmd.intervalType = this.subsTw.aggregation.interval; + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; + this.onDataUpdate(update); } - targetCommand.tsCmd.timeZoneId = this.subsTw.timezone; - targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; - targetCommand.tsCmd.agg = this.subsTw.aggregation.type; - targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; - this.dataAggregators.forEach((dataAggregator) => { - dataAggregator.reset(newSubsTw); - }); } - if (this.aggTsValues.length > 0 && !this.isFloatingTimewindow) { - targetCommand.aggTsCmd.startTs = this.subsTw.startTs; - targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow; - this.tsLatestDataAggregators.forEach((dataAggregator) => { - dataAggregator.reset(newSubsTw); - }); + ); + this.subscriber.subscribe(); + } else if (this.datasourceType === DatasourceType.alarmCount) { + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.subscriber.setTsOffset(this.latestTsOffset); + this.alarmCountCommand = new AlarmCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; } } - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - this.subscriber.setTsOffset(this.subsTw.tsOffset); - } else { - this.subscriber.setTsOffset(this.latestTsOffset); + this.alarmCountCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + if (this.entityDataSubscriptionOptions.alarmFilter) { + this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; } - targetCommand.query = this.dataCommand.query; - this.subscriber.subscriptionCommands = [targetCommand]; - } else { - this.subscriber.subscriptionCommands = [this.dataCommand]; - } - }); - this.subscriber.subscribe(); - } else if (this.datasourceType === DatasourceType.function) { - let tsOffset = 0; - if (this.entityDataSubscriptionOptions.type === widgetType.latest) { - tsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) { - tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; - } - - const entityData: EntityData = { - entityId: { - id: NULL_UUID, - entityType: EntityType.DEVICE - }, - timeseries: {}, - latest: {} - }; - const name = DatasourceType.function; - entityData.latest[EntityKeyType.ENTITY_FIELD] = { - name: {ts: Date.now() + tsOffset, value: name} - }; - const pageData: PageData = { - data: [entityData], - hasNext: false, - totalElements: 1, - totalPages: 1 - }; - this.onPageData(pageData); - } else if (this.datasourceType === DatasourceType.entityCount) { - this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - this.subscriber = new TelemetrySubscriber(this.telemetryService); - this.subscriber.setTsOffset(this.latestTsOffset); - this.countCommand = new EntityCountCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; - if (this.entityDataSubscriptionOptions.additionalKeyFilters) { - if (keyFilters) { - keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); - } else { - keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; - } - } - this.countCommand.query = { - entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters - }; - this.subscriber.subscriptionCommands.push(this.countCommand); + this.subscriber.subscriptionCommands.push(this.alarmCountCommand); - const entityId: EntityId = { - id: NULL_UUID, - entityType: null - }; - - const countKey = this.dataKeysList[0]; - - let dataReceived = false; + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; - this.subscriber.entityCount$.subscribe( - (entityCountUpdate) => { - if (!dataReceived) { - const entityData: EntityData = { - entityId, - latest: { - [EntityKeyType.ENTITY_FIELD]: { - name: { - ts: Date.now() + this.latestTsOffset, - value: DatasourceType.entityCount - } - }, - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: entityCountUpdate.count + '' - } - } - }, - timeseries: {} - }; - const pageData: PageData = { - data: [entityData], - hasNext: false, - totalElements: 1, - totalPages: 1 - }; - this.onPageData(pageData); - dataReceived = true; - } else { - const update: EntityData[] = [{ - entityId, - latest: { - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: entityCountUpdate.count + '' - } - } - }, - timeseries: {} - }]; - this.onDataUpdate(update); - } + const countKey = this.dataKeysList[0]; + + let dataReceived = false; + + this.subscriber.alarmCount$.subscribe( + (alarmCountUpdate) => { + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now() + this.latestTsOffset, + value: DatasourceType.alarmCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: alarmCountUpdate.count + '' + } + } + }, + timeseries: {} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + dataReceived = true; + } else { + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: alarmCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; + this.onDataUpdate(update); + } + } + ); + this.subscriber.subscribe(); } - ); - this.subscriber.subscribe(); - } else if (this.datasourceType === DatasourceType.alarmCount) { - this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - this.subscriber = new TelemetrySubscriber(this.telemetryService); - this.subscriber.setTsOffset(this.latestTsOffset); - this.alarmCountCommand = new AlarmCountCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; - if (this.entityDataSubscriptionOptions.additionalKeyFilters) { - if (keyFilters) { - keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return of(null); } else { - keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + return this.entityDataResolveSubject.asObservable(); } - } - this.alarmCountCommand.query = { - entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters - }; - if (this.entityDataSubscriptionOptions.alarmFilter) { - this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; - } - this.subscriber.subscriptionCommands.push(this.alarmCountCommand); - - const entityId: EntityId = { - id: NULL_UUID, - entityType: null - }; - - const countKey = this.dataKeysList[0]; - - let dataReceived = false; - - this.subscriber.alarmCount$.subscribe( - (alarmCountUpdate) => { - if (!dataReceived) { - const entityData: EntityData = { - entityId, - latest: { - [EntityKeyType.ENTITY_FIELD]: { - name: { - ts: Date.now() + this.latestTsOffset, - value: DatasourceType.alarmCount - } - }, - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: alarmCountUpdate.count + '' - } - } - }, - timeseries: {} - }; - const pageData: PageData = { - data: [entityData], - hasNext: false, - totalElements: 1, - totalPages: 1 - }; - this.onPageData(pageData); - dataReceived = true; - } else { - const update: EntityData[] = [{ - entityId, - latest: { - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: alarmCountUpdate.count + '' - } - } - }, - timeseries: {} - }]; - this.onDataUpdate(update); - } - } - ); - this.subscriber.subscribe(); - } - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - return of(null); - } else { - return this.entityDataResolveSubject.asObservable(); - } + }) + ); } public start() { @@ -1104,7 +1119,7 @@ export class EntityDataSubscription { this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]); let value = EntityDataSubscription.convertValue(series[1]); if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + value = dataKey.postFunc.execute(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); } prevOrigSeries = [series[0], series[1], series[2]]; series = [series[0], value, series[2]]; @@ -1119,7 +1134,7 @@ export class EntityDataSubscription { this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]); let value = EntityDataSubscription.convertValue(series[1]); if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + value = dataKey.postFunc.execute(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); } series = [time, value, series[2]]; data.push([series[0], series[1], series[2]]); diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index 5ebad5e686..d631b0bbe0 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -28,6 +28,7 @@ import { SubscriptionDataKey } from '@core/api/entity-data-subscription'; import { Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; export interface EntityDataListener { subscriptionType: widgetType; @@ -62,7 +63,8 @@ export interface EntityDataLoadResult { export class EntityDataService { constructor(private telemetryService: TelemetryWebsocketService, - private utils: UtilsService) {} + private utils: UtilsService, + private http: HttpClient) {} private static isUnresolvedDatasource(datasource: Datasource, pageLink: EntityDataPageLink): boolean { if (datasource.type === DatasourceType.entity) { @@ -103,7 +105,7 @@ export class EntityDataService { if (EntityDataService.isUnresolvedDatasource(datasource, datasource.pageLink)) { return of(null); } - listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); + listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils, this.http); return listener.subscription.subscribe(); } @@ -137,7 +139,7 @@ export class EntityDataService { listener.configDatasourceIndex, listener.subscriptionOptions.pageLink); return of(null); } - listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); + listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils, this.http); if (listener.useTimewindow) { listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index d9e8788cba..79fde27014 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -14,9 +14,6 @@ /// limitations under the License. /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import { Inject, Injectable, NgZone, Renderer2 } from '@angular/core'; import { WINDOW } from '@core/services/window.service'; import { ExceptionData, parseException } from '@app/shared/models/error.models'; @@ -53,6 +50,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { DatePipe, DOCUMENT } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; import cssjs from '@core/css/css'; +import { isNotEmptyTbFunction } from '@shared/models/js-function.models'; const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); @@ -291,7 +289,7 @@ export class UtilsService { } else if (index > -1) { dataKey.color = this.getMaterialColor(index); } - if (keyInfo.postFuncBody && keyInfo.postFuncBody.length) { + if (isNotEmptyTbFunction(keyInfo.postFuncBody)) { dataKey.usePostProcessing = true; dataKey.postFuncBody = keyInfo.postFuncBody; } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index d06493594e..621aebeae9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -107,6 +107,7 @@ { + if (this.dataKeyFormGroup.valid) { + const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; + this.dialogRef.close(dataKey); + } + }); } } 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 1930016656..ade0765402 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 @@ -174,6 +174,7 @@ [functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']" [globalVariables]="functionScopeVariables" [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" + withModules resultType="any" helpId="widget/config/datakey_postprocess_fn" formControlName="postFuncBody"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts index 4db2aaa8bf..32e8727225 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -57,6 +57,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; @Component({ selector: 'tb-data-key-config', @@ -290,10 +291,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con }); this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => { - const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; - if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { + const postFuncBody: TbFunction = this.dataKeyFormGroup.get('postFuncBody').value; + if (usePostProcessing && !isNotEmptyTbFunction(postFuncBody)) { this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); - } else if (!usePostProcessing && postFuncBody && postFuncBody.length) { + } else if (!usePostProcessing && isNotEmptyTbFunction(postFuncBody)) { this.dataKeyFormGroup.get('postFuncBody').patchValue(null); } }); @@ -317,7 +318,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con writeValue(value: DataKey): void { this.modelValue = value; - if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { + if (isNotEmptyTbFunction(this.modelValue.postFuncBody)) { this.modelValue.usePostProcessing = true; } if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) { @@ -466,13 +467,15 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con return key => key.toLowerCase().startsWith(lowercaseQuery); } - public validateOnSubmit() { + public validateOnSubmit(): Observable { if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) { - this.funcBodyEdit.validateOnSubmit(); + return this.funcBodyEdit.validateOnSubmit(); } else if ((this.modelValue.type === DataKeyType.timeseries || this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value && this.postFuncBodyEdit) { - this.postFuncBodyEdit.validateOnSubmit(); + return this.postFuncBodyEdit.validateOnSubmit(); + } else { + return of(null); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index b5a081836d..86256a53bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -51,7 +51,7 @@ import cssjs from '@core/css/css'; import { sortItems } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; -import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, 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 { debounceTime, distinctUntilChanged, map, take, takeUntil, tap } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; @@ -1196,20 +1196,32 @@ class AlarmsDatasource implements DataSource { private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; - private readonly usedShowCellActionFunction: boolean; + private usedShowCellActionFunction: boolean; + private inited = false; constructor(private subscription: IWidgetSubscription, private dataKeys: Array, private ngZone: NgZone, private widgetContext: WidgetContext, - actionCellDescriptors: AlarmWidgetActionDescriptor[]) { - this.cellButtonActions = actionCellDescriptors.concat(getTableCellButtonActions(widgetContext)); - this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + private actionCellDescriptors: AlarmWidgetActionDescriptor[]) { if (this.widgetContext.settings.reserveSpaceForHiddenAction) { this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); } } + private init(): Observable { + if (this.inited) { + return of(null); + } + return getTableCellButtonActions(this.widgetContext).pipe( + tap(actions => { + this.cellButtonActions = this.actionCellDescriptors.concat(actions); + this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + this.inited = true; + }) + ); + } + connect(collectionViewer: CollectionViewer): Observable> { return this.alarmsSubject.asObservable(); } @@ -1237,47 +1249,49 @@ class AlarmsDatasource implements DataSource { } updateAlarms() { - const subscriptionAlarms = this.subscription.alarms; - let alarms = new Array(); - let maxCellButtonAction = 0; - let isEmptySelection = false; - const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; - subscriptionAlarms.data.forEach((alarmData) => { - const alarm = this.alarmDataToInfo(alarmData); - alarms.push(alarm); - if (dynamicWidthCellButtonActions && alarm.actionCellButtons.length > maxCellButtonAction) { - maxCellButtonAction = alarm.actionCellButtons.length; + this.init().subscribe(() => { + const subscriptionAlarms = this.subscription.alarms; + let alarms = new Array(); + let maxCellButtonAction = 0; + let isEmptySelection = false; + const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; + subscriptionAlarms.data.forEach((alarmData) => { + const alarm = this.alarmDataToInfo(alarmData); + alarms.push(alarm); + if (dynamicWidthCellButtonActions && alarm.actionCellButtons.length > maxCellButtonAction) { + maxCellButtonAction = alarm.actionCellButtons.length; + } + }); + if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && alarms.length) { + maxCellButtonAction = alarms[0].actionCellButtons.length; } - }); - if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && alarms.length) { - maxCellButtonAction = alarms[0].actionCellButtons.length; - } - if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { - const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; - alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); - } - if (this.selection.hasValue()) { - const alarmIds = alarms.map((alarm) => alarm.id.id); - const toRemove = this.selection.selected.filter(alarm => alarmIds.indexOf(alarm.id.id) === -1); - this.selection.deselect(...toRemove); - if (this.selection.isEmpty()) { - isEmptySelection = true; + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); } - } - const alarmsPageData: PageData = { - data: alarms, - totalPages: subscriptionAlarms.totalPages, - totalElements: subscriptionAlarms.totalElements, - hasNext: subscriptionAlarms.hasNext - }; - this.ngZone.run(() => { - if (isEmptySelection) { - this.onSelectionModeChanged(false); + if (this.selection.hasValue()) { + const alarmIds = alarms.map((alarm) => alarm.id.id); + const toRemove = this.selection.selected.filter(alarm => alarmIds.indexOf(alarm.id.id) === -1); + this.selection.deselect(...toRemove); + if (this.selection.isEmpty()) { + isEmptySelection = true; + } } - this.alarmsSubject.next(alarms); - this.pageDataSubject.next(alarmsPageData); - this.countCellButtonAction = maxCellButtonAction; - this.dataLoading = false; + const alarmsPageData: PageData = { + data: alarms, + totalPages: subscriptionAlarms.totalPages, + totalElements: subscriptionAlarms.totalElements, + hasNext: subscriptionAlarms.hasNext + }; + this.ngZone.run(() => { + if (isEmptySelection) { + this.onSelectionModeChanged(false); + } + this.alarmsSubject.next(alarms); + this.pageDataSubject.next(alarmsPageData); + this.countCellButtonAction = maxCellButtonAction; + this.dataLoading = false; + }); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 8c44ea759a..2f4c55beab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -46,11 +46,11 @@ import { deepClone, hashCode, isDefined, isNumber, isObject, isUndefined } from import cssjs from '@core/css/css'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { BehaviorSubject, fromEvent, merge, Observable, Subject } from 'rxjs'; +import { BehaviorSubject, fromEvent, merge, Observable, of, Subject } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map, takeUntil, tap } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, SortDirection } from '@angular/material/sort'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -789,7 +789,8 @@ class EntityDatasource implements DataSource { private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; - private readonly usedShowCellActionFunction: boolean; + private usedShowCellActionFunction: boolean; + private inited = false; constructor( private translate: TranslateService, @@ -798,13 +799,24 @@ class EntityDatasource implements DataSource { private ngZone: NgZone, private widgetContext: WidgetContext ) { - this.cellButtonActions = getTableCellButtonActions(widgetContext); - this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); if (this.widgetContext.settings.reserveSpaceForHiddenAction) { this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); } } + private init(): Observable { + if (this.inited) { + return of(null); + } + return getTableCellButtonActions(this.widgetContext).pipe( + tap(actions => { + this.cellButtonActions = actions + this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + this.inited = true; + }) + ); + } + connect(collectionViewer: CollectionViewer): Observable> { return this.entitiesSubject.asObservable(); } @@ -828,36 +840,38 @@ class EntityDatasource implements DataSource { } dataUpdated() { - const datasourcesPageData = this.subscription.datasourcePages[0]; - const dataPageData = this.subscription.dataPages[0]; - let entities = new Array(); - let maxCellButtonAction = 0; - const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; - datasourcesPageData.data.forEach((datasource, index) => { - const entity = this.datasourceToEntityData(datasource, dataPageData.data[index]); - entities.push(entity); - if (dynamicWidthCellButtonActions && entity.actionCellButtons.length > maxCellButtonAction) { - maxCellButtonAction = entity.actionCellButtons.length; + this.init().subscribe(() => { + const datasourcesPageData = this.subscription.datasourcePages[0]; + const dataPageData = this.subscription.dataPages[0]; + let entities = new Array(); + let maxCellButtonAction = 0; + const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; + datasourcesPageData.data.forEach((datasource, index) => { + const entity = this.datasourceToEntityData(datasource, dataPageData.data[index]); + entities.push(entity); + if (dynamicWidthCellButtonActions && entity.actionCellButtons.length > maxCellButtonAction) { + maxCellButtonAction = entity.actionCellButtons.length; + } + }); + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); } - }); - if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { - const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; - entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); - } - if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) { - maxCellButtonAction = entities[0].actionCellButtons.length; - } - const entitiesPageData: PageData = { - data: entities, - totalPages: datasourcesPageData.totalPages, - totalElements: datasourcesPageData.totalElements, - hasNext: datasourcesPageData.hasNext - }; - this.ngZone.run(() => { - this.entitiesSubject.next(entities); - this.pageDataSubject.next(entitiesPageData); - this.countCellButtonAction = maxCellButtonAction; - this.dataLoading = false; + if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) { + maxCellButtonAction = entities[0].actionCellButtons.length; + } + const entitiesPageData: PageData = { + data: entities, + totalPages: datasourcesPageData.totalPages, + totalElements: datasourcesPageData.totalElements, + hasNext: datasourcesPageData.hasNext + }; + this.ngZone.run(() => { + this.entitiesSubject.next(entities); + this.pageDataSubject.next(entitiesPageData); + this.countCellButtonAction = maxCellButtonAction; + this.dataLoading = false; + }); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 3e200481fe..b31b9d9e5a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -26,6 +26,9 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; +import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; @@ -59,7 +62,7 @@ export type ShowCellButtonActionFunction = (ctx: WidgetContext, data: EntityData export interface TableCellButtonActionDescriptor extends WidgetActionDescriptor { useShowActionCellButtonFunction: boolean; - showActionCellButtonFunction: ShowCellButtonActionFunction; + showActionCellButtonFunction: CompiledTbFunction; } export interface EntityData { @@ -314,20 +317,26 @@ export function getColumnSelectionAvailability(keySettings: TableWidgetDataKeySe return !(isDefined(keySettings.columnSelectionToDisplay) && keySettings.columnSelectionToDisplay === 'disabled'); } -export function getTableCellButtonActions(widgetContext: WidgetContext): TableCellButtonActionDescriptor[] { - return widgetContext.actionsApi.getActionDescriptors('actionCellButton').map(descriptor => { +export function getTableCellButtonActions(widgetContext: WidgetContext): Observable { + const actions$ = widgetContext.actionsApi.getActionDescriptors('actionCellButton').map(descriptor => { let useShowActionCellButtonFunction = descriptor.useShowWidgetActionFunction || false; - let showActionCellButtonFunction: ShowCellButtonActionFunction = null; - if (useShowActionCellButtonFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { - try { - showActionCellButtonFunction = - new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowCellButtonActionFunction; - } catch (e) { - useShowActionCellButtonFunction = false; - } + let showActionCellButtonFunction$: Observable>; + if (useShowActionCellButtonFunction && isNotEmptyTbFunction(descriptor.showWidgetActionFunction)) { + showActionCellButtonFunction$ = compileTbFunction(widgetContext.http, descriptor.showWidgetActionFunction, 'widgetContext', 'data'); + } else { + showActionCellButtonFunction$ = of(null); } - return {...descriptor, showActionCellButtonFunction, useShowActionCellButtonFunction}; + return showActionCellButtonFunction$.pipe( + catchError(() => { return of(null) }), + map(showActionCellButtonFunction => { + if (!showActionCellButtonFunction) { + useShowActionCellButtonFunction = false; + } + return {...descriptor, showActionCellButtonFunction, useShowActionCellButtonFunction}; + }) + ); }); + return actions$.length ? forkJoin(actions$) : of([]); } export function checkHasActions(cellButtonActions: TableCellButtonActionDescriptor[]): boolean { @@ -348,7 +357,7 @@ function filterTableCellButtonAction(widgetContext: WidgetContext, action: TableCellButtonActionDescriptor, data: EntityData | AlarmDataInfo | FormattedData): boolean { if (action.useShowActionCellButtonFunction) { try { - return action.showActionCellButtonFunction(widgetContext, data); + return action.showActionCellButtonFunction.execute(widgetContext, data); } catch (e) { console.warn('Failed to execute showActionCellButtonFunction', e); return false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 7c232eb071..0e83f81961 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -58,7 +58,17 @@ import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/s import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; -import { catchError, debounceTime, distinctUntilChanged, map, skip, startWith, takeUntil } from 'rxjs/operators'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + skip, + startWith, + switchMap, + takeUntil, + tap +} from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -828,7 +838,8 @@ class TimeseriesDatasource implements DataSource { private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; - private readonly usedShowCellActionFunction: boolean; + private usedShowCellActionFunction: boolean; + private inited = false; constructor( private source: TimeseriesTableSource, @@ -837,14 +848,25 @@ class TimeseriesDatasource implements DataSource { private datePipe: DatePipe, private widgetContext: WidgetContext ) { - this.cellButtonActions = getTableCellButtonActions(widgetContext); - this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); if (this.widgetContext.settings.reserveSpaceForHiddenAction) { this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); } this.source.timeseriesDatasource = this; } + private init(): Observable { + if (this.inited) { + return of(null); + } + return getTableCellButtonActions(this.widgetContext).pipe( + tap(actions => { + this.cellButtonActions = actions + this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + this.inited = true; + }) + ); + } + connect(collectionViewer: CollectionViewer): Observable> { if (this.rowsSubject.isStopped) { this.rowsSubject.isStopped = false; @@ -886,68 +908,74 @@ class TimeseriesDatasource implements DataSource { } private updateSourceData() { - this.source.data = this.convertData(this.source.rawData, this.source.latestRawData); - this.allRowsSubject.next(this.source.data); - } - - private convertData(data: DatasourceData[], latestData: DatasourceData[]): TimeseriesRow[] { - const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; - for (let d = 0; d < data.length; d++) { - const columnData = data[d].data; - columnData.forEach((cellData) => { - const timestamp = cellData[0]; - let row = rowsMap[timestamp]; - if (!row) { - row = { - formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) - }; - if (this.cellButtonActions.length) { - if (this.usedShowCellActionFunction) { - const parsedData = formattedDataFormDatasourceData(data, undefined, timestamp); - row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions, - parsedData[0], this.reserveSpaceForHiddenAction); - row.hasActions = checkHasActions(row.actionCellButtons); - } else { - row.hasActions = true; - row.actionCellButtons = this.cellButtonActions; + this.convertData(this.source.rawData, this.source.latestRawData).subscribe((data) => { + this.source.data = data; + this.allRowsSubject.next(this.source.data); + }); + } + + private convertData(data: DatasourceData[], latestData: DatasourceData[]): Observable { + return this.init().pipe( + map(() => { + const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; + for (let d = 0; d < data.length; d++) { + const columnData = data[d].data; + columnData.forEach((cellData) => { + const timestamp = cellData[0]; + let row = rowsMap[timestamp]; + if (!row) { + row = { + formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) + }; + if (this.cellButtonActions.length) { + if (this.usedShowCellActionFunction) { + const parsedData = formattedDataFormDatasourceData(data, undefined, timestamp); + row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions, + parsedData[0], this.reserveSpaceForHiddenAction); + row.hasActions = checkHasActions(row.actionCellButtons); + } else { + row.hasActions = true; + row.actionCellButtons = this.cellButtonActions; + } + } + row[0] = timestamp; + for (let c = 0; c < (data.length + latestData.length); c++) { + row[c + 1] = undefined; + } + rowsMap[timestamp] = row; } - } - row[0] = timestamp; - for (let c = 0; c < (data.length + latestData.length); c++) { - row[c + 1] = undefined; - } - rowsMap[timestamp] = row; + row[d + 1] = cellData[1]; + }); } - row[d + 1] = cellData[1]; - }); - } - let rows: TimeseriesRow[] = []; - if (this.hideEmptyLines) { - for (const t of Object.keys(rowsMap)) { - let hideLine = true; - for (let c = 0; (c < data.length) && hideLine; c++) { - if (rowsMap[t][c + 1]) { - hideLine = false; + let rows: TimeseriesRow[] = []; + if (this.hideEmptyLines) { + for (const t of Object.keys(rowsMap)) { + let hideLine = true; + for (let c = 0; (c < data.length) && hideLine; c++) { + if (rowsMap[t][c + 1]) { + hideLine = false; + } + } + if (!hideLine) { + rows.push(rowsMap[t]); + } } + } else { + rows = Object.keys(rowsMap).map(itm => rowsMap[itm]); } - if (!hideLine) { - rows.push(rowsMap[t]); + for (let d = 0; d < latestData.length; d++) { + const columnData = latestData[d].data; + if (columnData.length) { + const value = columnData[0][1]; + rows.forEach((row) => { + row[data.length + d + 1] = value; + }); + } } - } - } else { - rows = Object.keys(rowsMap).map(itm => rowsMap[itm]); - } - for (let d = 0; d < latestData.length; d++) { - const columnData = latestData[d].data; - if (columnData.length) { - const value = columnData[0][1]; - rows.forEach((row) => { - row[data.length + d + 1] = value; - }); - } - } - return rows; + return rows; + }) + ); } isEmpty(): Observable { @@ -963,19 +991,23 @@ class TimeseriesDatasource implements DataSource { } private fetchRows(pageLink: PageLink): Observable> { - return this.allRows$.pipe( - map((data) => { - const fetchData = pageLink.filterData(data); - if (this.cellButtonActions.length) { - let maxCellButtonAction: number; - if (this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction) { - maxCellButtonAction = Math.max(...fetchData.data.map(tsRow => tsRow.actionCellButtons.length)); - } else { - maxCellButtonAction = this.cellButtonActions.length; - } - this.countCellButtonAction = maxCellButtonAction; - } - return fetchData; + return this.init().pipe( + switchMap(() => { + return this.allRows$.pipe( + map((data) => { + const fetchData = pageLink.filterData(data); + if (this.cellButtonActions.length) { + let maxCellButtonAction: number; + if (this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction) { + maxCellButtonAction = Math.max(...fetchData.data.map(tsRow => tsRow.actionCellButtons.length)); + } else { + maxCellButtonAction = this.cellButtonActions.length; + } + this.countCellButtonAction = maxCellButtonAction; + } + return fetchData; + }) + ); }) ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index a9634c24a8..92fc7d7621 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -120,7 +120,8 @@ import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; import { MODULES_MAP } from '@shared/models/constants'; import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; -import { compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; +import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; +import { HttpClient } from '@angular/common/http'; @Component({ selector: 'tb-widget', @@ -209,7 +210,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, private mobileService: MobileService, private raf: RafService, private ngZone: NgZone, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private http: HttpClient) { super(store); this.cssParser.testMode = false; } @@ -270,37 +272,49 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, }; this.widgetContext.customHeaderActions = []; + const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); - headerActionsDescriptors.forEach((descriptor) => - { + + const customHeaderActions$ = headerActionsDescriptors.map((descriptor) => { let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false; - let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null; - if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { - try { - showWidgetHeaderActionFunction = - new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction; - } catch (e) { - useShowWidgetHeaderActionFunction = false; - } + let showWidgetHeaderActionFunction$: Observable>; + if (useShowWidgetHeaderActionFunction && isNotEmptyTbFunction(descriptor.showWidgetActionFunction)) { + showWidgetHeaderActionFunction$ = compileTbFunction(this.http, descriptor.showWidgetActionFunction, 'widgetContext', 'data'); + } else { + showWidgetHeaderActionFunction$ = of(null); } - const headerAction: WidgetHeaderAction = { - name: descriptor.name, - displayName: descriptor.displayName, - icon: descriptor.icon, - descriptor, - useShowWidgetHeaderActionFunction, - showWidgetHeaderActionFunction, - onAction: $event => { - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); - } - }; - this.widgetContext.customHeaderActions.push(headerAction); + return showWidgetHeaderActionFunction$.pipe( + catchError(() => { return of(null) }), + map(showWidgetHeaderActionFunction => { + if (!showWidgetHeaderActionFunction) { + useShowWidgetHeaderActionFunction = false; + } + const headerAction: WidgetHeaderAction = { + name: descriptor.name, + displayName: descriptor.displayName, + icon: descriptor.icon, + descriptor, + useShowWidgetHeaderActionFunction, + showWidgetHeaderActionFunction, + onAction: $event => { + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + } + }; + return headerAction; + }) + ); }); + if (customHeaderActions$.length) { + forkJoin(customHeaderActions$).subscribe((customHeaderActions) => { + this.widgetContext.customHeaderActions.push(...customHeaderActions); + }); + } + this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); this.subscriptionContext.timeService = this.timeService; this.subscriptionContext.deviceService = this.deviceService; @@ -1110,7 +1124,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetActionType.custom: const customFunction = descriptor.customFunction; if (isNotEmptyTbFunction(customFunction)) { - compileTbFunction(this.widgetContext.http, customFunction, '$event', 'widgetContext', 'entityId', + compileTbFunction(this.http, customFunction, '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel').subscribe( { next: (compiled) => { @@ -1143,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ next: () => { if (isNotEmptyTbFunction(customPrettyFunction)) { - compileTbFunction(this.widgetContext.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', + compileTbFunction(this.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe( { next: (compiled) => { diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 6658f0b8a4..12c00597d5 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -686,7 +686,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean { if (action.useShowWidgetHeaderActionFunction) { try { - return action.showWidgetHeaderActionFunction(this.widgetContext, data); + return action.showWidgetHeaderActionFunction.execute(this.widgetContext, data); } catch (e) { console.warn('Failed to execute showWidgetHeaderActionFunction', e); return false; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 971c7db6f7..03af49d6d2 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -105,6 +105,7 @@ import { UserId } from '@shared/models/id/user-id'; import { UserSettingsService } from '@core/http/user-settings.service'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { UtilsService } from '@core/services/utils.service'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; export interface IWidgetAction { name: string; @@ -118,7 +119,7 @@ export interface WidgetHeaderAction extends IWidgetAction { displayName: string; descriptor: WidgetActionDescriptor; useShowWidgetHeaderActionFunction: boolean; - showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction; + showWidgetHeaderActionFunction: CompiledTbFunction; } export interface WidgetAction extends IWidgetAction { diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index b3b8ffe9e6..647ab6113d 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -42,11 +42,12 @@ import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { beautifyJs } from '@shared/models/beautify.models'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { loadModulesCompleter, TbFunction } from '@shared/models/js-function.models'; +import { compileTbFunction, loadModulesCompleter, TbFunction } from '@shared/models/js-function.models'; import { TbPopoverService } from '@shared/components/popover.service'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; +import { map, Observable, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; @Component({ selector: 'tb-js-func', @@ -322,23 +323,29 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, ); } - validateOnSubmit(): void { + validateOnSubmit(): Observable { if (!this.disabled) { this.cleanupJsErrors(); - this.functionValid = this.validateJsFunc(); - if (!this.functionValid) { - this.propagateValue(this.modelValue); - this.cd.markForCheck(); - this.store.dispatch(new ActionNotificationShow( - { - message: this.validationError, - type: 'error', - target: this.toastTargetId, - verticalPosition: 'bottom', - horizontalPosition: 'left' - })); - this.errorShowed = true; - } + return this.validateJsFunc().pipe( + map((valid) => { + this.functionValid = valid; + if (!this.functionValid) { + this.propagateValue(this.modelValue); + this.cd.markForCheck(); + this.store.dispatch(new ActionNotificationShow( + { + message: this.validationError, + type: 'error', + target: this.toastTargetId, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + this.errorShowed = true; + } + }) + ); + } else { + return of(null); } } @@ -347,83 +354,95 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, this.jsEditor?.focus(); } - private validateJsFunc(): boolean { - try { - const toValidate = new Function(this.functionArgsString, this.modelValue); - if (this.noValidate) { - return true; - } - if (this.validationArgs) { - let res: any; - let validationError: any; - for (const validationArg of this.validationArgs) { - try { - res = toValidate.apply(this, validationArg); - validationError = null; - break; - } catch (e) { - validationError = e; - } - } - if (validationError) { - throw validationError; + private validateJsFunc(): Observable { + let toCompile: TbFunction; + if (this.withModules && this.modules && Object.keys(this.modules).length) { + toCompile = { + body: this.modelValue, + modules: this.modules + }; + } else { + toCompile = this.modelValue; + } + const args = this.functionArgs || []; + return compileTbFunction(this.http, toCompile, ...args).pipe( + map(toValidate => { + if (this.noValidate) { + return true; } - if (this.resultType !== 'nocheck') { - if (this.resultType === 'any') { - if (isUndefined(res)) { - this.validationError = this.translate.instant('js-func.no-return-error'); - return false; + if (this.validationArgs) { + let res: any; + let validationError: any; + for (const validationArg of this.validationArgs) { + try { + res = toValidate.apply(this, validationArg); + validationError = null; + break; + } catch (e) { + validationError = e; } - } else { - const resType = typeof res; - if (resType !== this.resultType) { - this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); - return false; + } + if (validationError) { + throw validationError; + } + if (this.resultType !== 'nocheck') { + if (this.resultType === 'any') { + if (isUndefined(res)) { + this.validationError = this.translate.instant('js-func.no-return-error'); + return false; + } + } else { + const resType = typeof res; + if (resType !== this.resultType) { + this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); + return false; + } } } + return true; + } else { + return true; } - return true; - } else { - return true; - } - } catch (e) { - const details = this.utils.parseException(e); - let errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } - if (details.message) { - errorInfo += ' ' + details.message; - } - if (details.lineNumber) { - errorInfo += '
Line ' + details.lineNumber; - if (details.columnNumber) { - errorInfo += ' column ' + details.columnNumber; + }), + catchError((e) => { + const details = this.utils.parseException(e); + let errorInfo = 'Error:'; + if (details.name) { + errorInfo += ' ' + details.name + ':'; } - errorInfo += ' of script.'; - } - this.validationError = errorInfo; - if (details.lineNumber) { - const line = details.lineNumber - 1; - let column = 0; - if (details.columnNumber) { - column = details.columnNumber; + if (details.message) { + errorInfo += ' ' + details.message; } - const errorMarkerId = this.jsEditor.session.addMarker(new Range(line, 0, line, Infinity), - 'ace_active-line', 'screenLine'); - this.errorMarkers.push(errorMarkerId); - const annotations = this.jsEditor.session.getAnnotations(); - const errorAnnotation: Ace.Annotation = { - row: line, - column, - text: details.message, - type: 'error' - }; - this.errorAnnotationId = annotations.push(errorAnnotation) - 1; - this.jsEditor.session.setAnnotations(annotations); - } - return false; - } + if (details.lineNumber) { + errorInfo += '
Line ' + details.lineNumber; + if (details.columnNumber) { + errorInfo += ' column ' + details.columnNumber; + } + errorInfo += ' of script.'; + } + this.validationError = errorInfo; + if (details.lineNumber) { + const line = details.lineNumber - 1; + let column = 0; + if (details.columnNumber) { + column = details.columnNumber; + } + const errorMarkerId = this.jsEditor.session.addMarker(new Range(line, 0, line, Infinity), + 'ace_active-line', 'screenLine'); + this.errorMarkers.push(errorMarkerId); + const annotations = this.jsEditor.session.getAnnotations(); + const errorAnnotation: Ace.Annotation = { + row: line, + column, + text: details.message, + type: 'error' + }; + this.errorAnnotationId = annotations.push(errorAnnotation) - 1; + this.jsEditor.session.setAnnotations(annotations); + } + return of(false); + }) + ); } private cleanupJsErrors(): void { @@ -449,6 +468,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, writeValue(value: TbFunction): void { if (isUndefinedOrNull(value) || typeof value === 'string') { this.modelValue = value as any; + this.modules = null; } else { this.modelValue = value.body; this.modules = value.modules; 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 1b75111f5f..474796b74b 100644 --- a/ui-ngx/src/app/shared/models/js-function.models.ts +++ b/ui-ngx/src/app/shared/models/js-function.models.ts @@ -39,11 +39,11 @@ export const isNotEmptyTbFunction = (tbFunction: TbFunction): boolean => { return tbFunction.body && tbFunction.body.trim().length > 0; } } else { - return true; + return false; } } -export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable => { +export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable> => { let functionBody: string; let functionArgs: string[]; let modules: {[alias: string]: string }; @@ -59,7 +59,7 @@ export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...a return loadFunctionModules(http, modules).pipe( map((compiledModules) => { const compiledFunction = new Function(...functionArgs, functionBody); - return new CompiledTbFunction(compiledFunction, compiledModules); + return new CompiledTbFunction(compiledFunction, compiledModules); }) ); } @@ -188,13 +188,17 @@ const loadModuleCompletion = (http: HttpClient, moduleLink: string): Observable< ); } -export class CompiledTbFunction { +type GenericFunction = (...args: any[]) => any; + +export class CompiledTbFunction { + + public execute: T = this.executeImpl.bind(this); constructor(private compiledFunction: Function, private compiledModules: System.Module[]) { } - execute(...args: any[]): any { + private executeImpl(...args: any[]): any { let functionArgs: any[]; if (this.compiledModules?.length) { functionArgs = args ? args.concat(this.compiledModules) : this.compiledModules; @@ -292,8 +296,15 @@ const loadFunctionModuleSource = (http: HttpClient, moduleLink: string): Observa const getFunctionArguments = (func: Function): string[] => { const fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, ''); - let result = new Array(...fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g)); - if (result === null) - result = []; - return result; + const firstBracketIndex = fnStr.indexOf('('); + const secondBracketIndex = fnStr.indexOf(')'); + if (firstBracketIndex === -1 || secondBracketIndex === -1 || (secondBracketIndex - firstBracketIndex) <= 1) { + return []; + } + const match = fnStr.slice(firstBracketIndex+1, secondBracketIndex).match(/([^\s,]+)/g); + if (match) { + return new Array(...match); + } else { + return []; + } } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 718262dd53..3c91c4dd1f 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -335,7 +335,7 @@ export interface KeyInfo { label?: string; color?: string; funcBody?: string; - postFuncBody?: string; + postFuncBody?: TbFunction; units?: string; decimals?: number; } @@ -694,7 +694,7 @@ export interface WidgetActionDescriptor extends WidgetAction { icon: string; displayName?: string; useShowWidgetActionFunction?: boolean; - showWidgetActionFunction?: string; + showWidgetActionFunction?: TbFunction; columnIndex?: number; }