Browse Source

UI: Add support of js modules to: show widget header action function, data key post processing function.

pull/12171/head
Igor Kulikov 2 years ago
parent
commit
17af3809f0
  1. 671
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  2. 8
      ui-ngx/src/app/core/api/entity-data.service.ts
  3. 6
      ui-ngx/src/app/core/services/utils.service.ts
  4. 1
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  5. 11
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  6. 1
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  7. 17
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  8. 100
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  9. 82
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  10. 35
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  11. 178
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  12. 74
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  13. 2
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  14. 3
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  15. 194
      ui-ngx/src/app/shared/components/js-func.component.ts
  16. 29
      ui-ngx/src/app/shared/models/js-function.models.ts
  17. 4
      ui-ngx/src/app/shared/models/widget.models.ts

671
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -19,14 +19,16 @@ import {
DataEntry, DataEntry,
DataSet, DataSet,
DataSetHolder, DataSetHolder,
DatasourceType, IndexedData, DatasourceType,
IndexedData,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { import {
AggregationType, AggregationType,
ComparisonDuration, ComparisonDuration,
createTimewindowForComparison, createTimewindowForComparison,
getCurrentTime, IntervalMath, getCurrentTime,
IntervalMath,
SubscriptionTimewindow SubscriptionTimewindow
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { import {
@ -51,7 +53,8 @@ import {
IndexedSubscriptionData, IndexedSubscriptionData,
IntervalType, IntervalType,
NOT_SUPPORTED, NOT_SUPPORTED,
SubscriptionData, SubscriptionDataEntry, SubscriptionData,
SubscriptionDataEntry,
TelemetrySubscriber TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; 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 { 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 { 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 { EntityId } from '@shared/models/id/entity-id';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; 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; import Timeout = NodeJS.Timeout;
declare type DataKeyFunction = (time: number, prevValue: any) => any; declare type DataKeyFunction = (time: number, prevValue: any) => any;
@ -81,8 +91,8 @@ export interface SubscriptionDataKey {
comparisonResultType?: ComparisonResultType; comparisonResultType?: ComparisonResultType;
funcBody: string; funcBody: string;
func?: DataKeyFunction; func?: DataKeyFunction;
postFuncBody: string; postFuncBody: TbFunction;
postFunc?: DataKeyPostFunction; postFunc?: CompiledTbFunction<DataKeyPostFunction>;
index?: number; index?: number;
listIndex?: number; listIndex?: number;
key?: string; key?: string;
@ -109,8 +119,8 @@ export class EntityDataSubscription {
constructor(private listener: EntityDataListener, constructor(private listener: EntityDataListener,
private telemetryService: TelemetryWebsocketService, private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) { private utils: UtilsService,
this.initializeSubscription(); private http: HttpClient) {
} }
private entityDataSubscriptionOptions = this.listener.subscriptionOptions; private entityDataSubscriptionOptions = this.listener.subscriptionOptions;
@ -193,7 +203,7 @@ export class EntityDataSubscription {
return [[timestamp, value]]; return [[timestamp, value]];
} }
private initializeSubscription() { private async initializeSubscription() {
for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) {
const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]);
this.dataKeysList.push(dataKey); this.dataKeysList.push(dataKey);
@ -203,9 +213,10 @@ export class EntityDataSubscription {
dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction; dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction;
} }
} else { } else {
if (dataKey.postFuncBody && !dataKey.postFunc) { if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) {
dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue', try {
dataKey.postFuncBody) as DataKeyPostFunction; dataKey.postFunc = await firstValueFrom(compileTbFunction(this.http, dataKey.postFuncBody, 'time', 'value', 'prevValue', 'timePrev', 'prevOrigValue'));
} catch (e) {}
} }
} }
let key: string; let key: string;
@ -265,354 +276,358 @@ export class EntityDataSubscription {
} }
public subscribe(): Observable<EntityDataLoadResult> { public subscribe(): Observable<EntityDataLoadResult> {
this.entityDataResolveSubject = new ReplaySubject(1); return from(this.initializeSubscription()).pipe(
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { switchMap(() => {
this.started = true; this.entityDataResolveSubject = new ReplaySubject(1);
this.dataResolved = true; if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.prepareSubscriptionTimewindow(); this.started = true;
} this.dataResolved = true;
if (this.datasourceType === DatasourceType.entity) { this.prepareSubscriptionTimewindow();
const entityFields: Array<EntityKey> = }
this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map( if (this.datasourceType === DatasourceType.entity) {
dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) const entityFields: Array<EntityKey> =
); this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
if (!entityFields.find(key => key.key === 'name')) { dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
entityFields.push({ );
type: EntityKeyType.ENTITY_FIELD, if (!entityFields.find(key => key.key === 'name')) {
key: 'name' entityFields.push({
}); type: EntityKeyType.ENTITY_FIELD,
} key: 'name'
if (!entityFields.find(key => key.key === 'label')) { });
entityFields.push({ }
type: EntityKeyType.ENTITY_FIELD, if (!entityFields.find(key => key.key === 'label')) {
key: 'label' entityFields.push({
}); type: EntityKeyType.ENTITY_FIELD,
} key: 'label'
if (!entityFields.find(key => key.key === 'additionalInfo')) { });
entityFields.push({ }
type: EntityKeyType.ENTITY_FIELD, if (!entityFields.find(key => key.key === 'additionalInfo')) {
key: 'additionalInfo' entityFields.push({
}); type: EntityKeyType.ENTITY_FIELD,
} key: 'additionalInfo'
});
}
this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map( this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
); );
this.tsFields = this.dataKeysList. this.tsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map( (!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) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const latestTsFields = this.dataKeysList. const latestTsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest && filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map( (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
); );
this.latestValues = this.attrFields.concat(latestTsFields); this.latestValues = this.attrFields.concat(latestTsFields);
} else { } else {
this.latestValues = this.attrFields.concat(this.tsFields); this.latestValues = this.attrFields.concat(this.tsFields);
} }
this.aggTsValues = this.dataKeysList. this.aggTsValues = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map( dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map(
dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType }) dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType })
); );
this.aggTsComparisonValues = this.dataKeysList. this.aggTsComparisonValues = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map( dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map(
dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType, dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType,
previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE }) previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE })
); );
this.subscriber = new TelemetrySubscriber(this.telemetryService); this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd(); this.dataCommand = new EntityDataCmd();
let keyFilters = this.entityDataSubscriptionOptions.keyFilters; let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
if (this.entityDataSubscriptionOptions.additionalKeyFilters) { if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
if (keyFilters) { if (keyFilters) {
keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters);
} else { } else {
keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters;
} }
} }
this.dataCommand.query = { this.dataCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter, entityFilter: this.entityDataSubscriptionOptions.entityFilter,
pageLink: this.entityDataSubscriptionOptions.pageLink, pageLink: this.entityDataSubscriptionOptions.pageLink,
keyFilters, keyFilters,
entityFields, entityFields,
latestValues: this.latestValues latestValues: this.latestValues
}; };
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.prepareSubscriptionCommands(this.dataCommand); this.prepareSubscriptionCommands(this.dataCommand);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.subscriber.setTsOffset(this.subsTw.tsOffset); this.subscriber.setTsOffset(this.subsTw.tsOffset);
} else { } else {
this.subscriber.setTsOffset(this.latestTsOffset); this.subscriber.setTsOffset(this.latestTsOffset);
} }
} }
this.subscriber.subscriptionCommands.push(this.dataCommand); this.subscriber.subscriptionCommands.push(this.dataCommand);
this.subscriber.entityData$.subscribe( this.subscriber.entityData$.subscribe(
(entityDataUpdate) => { (entityDataUpdate) => {
if (entityDataUpdate.data) { if (entityDataUpdate.data) {
this.onPageData(entityDataUpdate.data); this.onPageData(entityDataUpdate.data);
if (this.prematureUpdates) { if (this.prematureUpdates) {
for (const update of this.prematureUpdates) { for (const update of this.prematureUpdates) {
this.onDataUpdate(update); 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.subscriber.reconnect$.subscribe(() => {
this.prematureUpdates = []; 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 { } 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(() => { const entityData: EntityData = {
if (this.started) { entityId: {
const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; id: NULL_UUID,
if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length || entityType: EntityType.DEVICE
this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) { },
const newSubsTw = this.listener.updateRealtimeSubscription(); timeseries: {},
this.subsTw = newSubsTw; latest: {}
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { };
targetCommand.tsCmd.startTs = this.subsTw.startTs; const name = DatasourceType.function;
targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; entityData.latest[EntityKeyType.ENTITY_FIELD] = {
if (typeof this.subsTw.aggregation.interval === 'number') { name: {ts: Date.now() + tsOffset, value: name}
targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; };
targetCommand.tsCmd.intervalType = IntervalType.MILLISECONDS; const pageData: PageData<EntityData> = {
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<EntityData> = {
data: [entityData],
hasNext: false,
totalElements: 1,
totalPages: 1
};
this.onPageData(pageData);
dataReceived = true;
} else { } 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; this.subscriber.subscribe();
targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow; } else if (this.datasourceType === DatasourceType.alarmCount) {
this.tsLatestDataAggregators.forEach((dataAggregator) => { this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
dataAggregator.reset(newSubsTw); 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.alarmCountCommand.query = {
this.subscriber.setTsOffset(this.subsTw.tsOffset); entityFilter: this.entityDataSubscriptionOptions.entityFilter,
} else { keyFilters
this.subscriber.setTsOffset(this.latestTsOffset); };
if (this.entityDataSubscriptionOptions.alarmFilter) {
this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter};
} }
targetCommand.query = this.dataCommand.query; this.subscriber.subscriptionCommands.push(this.alarmCountCommand);
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<EntityData> = {
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 = { const entityId: EntityId = {
id: NULL_UUID, id: NULL_UUID,
entityType: null entityType: null
}; };
const countKey = this.dataKeysList[0];
let dataReceived = false;
this.subscriber.entityCount$.subscribe( const countKey = this.dataKeysList[0];
(entityCountUpdate) => {
if (!dataReceived) { let dataReceived = false;
const entityData: EntityData = {
entityId, this.subscriber.alarmCount$.subscribe(
latest: { (alarmCountUpdate) => {
[EntityKeyType.ENTITY_FIELD]: { if (!dataReceived) {
name: { const entityData: EntityData = {
ts: Date.now() + this.latestTsOffset, entityId,
value: DatasourceType.entityCount latest: {
} [EntityKeyType.ENTITY_FIELD]: {
}, name: {
[EntityKeyType.COUNT]: { ts: Date.now() + this.latestTsOffset,
[countKey.name]: { value: DatasourceType.alarmCount
ts: Date.now() + this.latestTsOffset, }
value: entityCountUpdate.count + '' },
} [EntityKeyType.COUNT]: {
} [countKey.name]: {
}, ts: Date.now() + this.latestTsOffset,
timeseries: {} value: alarmCountUpdate.count + ''
}; }
const pageData: PageData<EntityData> = { }
data: [entityData], },
hasNext: false, timeseries: {}
totalElements: 1, };
totalPages: 1 const pageData: PageData<EntityData> = {
}; data: [entityData],
this.onPageData(pageData); hasNext: false,
dataReceived = true; totalElements: 1,
} else { totalPages: 1
const update: EntityData[] = [{ };
entityId, this.onPageData(pageData);
latest: { dataReceived = true;
[EntityKeyType.COUNT]: { } else {
[countKey.name]: { const update: EntityData[] = [{
ts: Date.now() + this.latestTsOffset, entityId,
value: entityCountUpdate.count + '' latest: {
} [EntityKeyType.COUNT]: {
} [countKey.name]: {
}, ts: Date.now() + this.latestTsOffset,
timeseries: {} value: alarmCountUpdate.count + ''
}]; }
this.onDataUpdate(update); }
} },
timeseries: {}
}];
this.onDataUpdate(update);
}
}
);
this.subscriber.subscribe();
} }
); if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.subscriber.subscribe(); return of(null);
} 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 { } 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<EntityData> = {
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() { public start() {
@ -1104,7 +1119,7 @@ export class EntityDataSubscription {
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]); this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]);
let value = EntityDataSubscription.convertValue(series[1]); let value = EntityDataSubscription.convertValue(series[1]);
if (dataKey.postFunc) { 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]]; prevOrigSeries = [series[0], series[1], series[2]];
series = [series[0], value, 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]]); this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]);
let value = EntityDataSubscription.convertValue(series[1]); let value = EntityDataSubscription.convertValue(series[1]);
if (dataKey.postFunc) { 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]]; series = [time, value, series[2]];
data.push([series[0], series[1], series[2]]); data.push([series[0], series[1], series[2]]);

8
ui-ngx/src/app/core/api/entity-data.service.ts

@ -28,6 +28,7 @@ import {
SubscriptionDataKey SubscriptionDataKey
} from '@core/api/entity-data-subscription'; } from '@core/api/entity-data-subscription';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
export interface EntityDataListener { export interface EntityDataListener {
subscriptionType: widgetType; subscriptionType: widgetType;
@ -62,7 +63,8 @@ export interface EntityDataLoadResult {
export class EntityDataService { export class EntityDataService {
constructor(private telemetryService: TelemetryWebsocketService, constructor(private telemetryService: TelemetryWebsocketService,
private utils: UtilsService) {} private utils: UtilsService,
private http: HttpClient) {}
private static isUnresolvedDatasource(datasource: Datasource, pageLink: EntityDataPageLink): boolean { private static isUnresolvedDatasource(datasource: Datasource, pageLink: EntityDataPageLink): boolean {
if (datasource.type === DatasourceType.entity) { if (datasource.type === DatasourceType.entity) {
@ -103,7 +105,7 @@ export class EntityDataService {
if (EntityDataService.isUnresolvedDatasource(datasource, datasource.pageLink)) { if (EntityDataService.isUnresolvedDatasource(datasource, datasource.pageLink)) {
return of(null); 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(); return listener.subscription.subscribe();
} }
@ -137,7 +139,7 @@ export class EntityDataService {
listener.configDatasourceIndex, listener.subscriptionOptions.pageLink); listener.configDatasourceIndex, listener.subscriptionOptions.pageLink);
return of(null); 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) { if (listener.useTimewindow) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
} }

6
ui-ngx/src/app/core/services/utils.service.ts

@ -14,9 +14,6 @@
/// limitations under the License. /// limitations under the License.
/// ///
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../src/typings/rawloader.typings.d.ts" />
import { Inject, Injectable, NgZone, Renderer2 } from '@angular/core'; import { Inject, Injectable, NgZone, Renderer2 } from '@angular/core';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { ExceptionData, parseException } from '@app/shared/models/error.models'; 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 { DatePipe, DOCUMENT } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { entityTypeTranslations } from '@shared/models/entity-type.models';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { isNotEmptyTbFunction } from '@shared/models/js-function.models';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -291,7 +289,7 @@ export class UtilsService {
} else if (index > -1) { } else if (index > -1) {
dataKey.color = this.getMaterialColor(index); dataKey.color = this.getMaterialColor(index);
} }
if (keyInfo.postFuncBody && keyInfo.postFuncBody.length) { if (isNotEmptyTbFunction(keyInfo.postFuncBody)) {
dataKey.usePostProcessing = true; dataKey.usePostProcessing = true;
dataKey.postFuncBody = keyInfo.postFuncBody; dataKey.postFuncBody = keyInfo.postFuncBody;
} }

1
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -107,6 +107,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-js-func formControlName="showWidgetActionFunction" <tb-js-func formControlName="showWidgetActionFunction"
functionTitle="{{ 'widget-config.show-action-function' | translate }}" functionTitle="{{ 'widget-config.show-action-function' | translate }}"
withModules
[helpId]="getWidgetActionFunctionHelpId()" [helpId]="getWidgetActionFunctionHelpId()"
[functionArgs]="['widgetContext', 'data']" [functionArgs]="['widgetContext', 'data']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"

11
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts

@ -123,10 +123,11 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
save(): void { save(): void {
this.submitted = true; this.submitted = true;
this.dataKeyConfig.validateOnSubmit(); this.dataKeyConfig.validateOnSubmit().subscribe(() => {
if (this.dataKeyFormGroup.valid) { if (this.dataKeyFormGroup.valid) {
const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value;
this.dialogRef.close(dataKey); this.dialogRef.close(dataKey);
} }
});
} }
} }

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

@ -174,6 +174,7 @@
[functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']" [functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]"
withModules
resultType="any" resultType="any"
helpId="widget/config/datakey_postprocess_fn" helpId="widget/config/datakey_postprocess_fn"
formControlName="postFuncBody"> formControlName="postFuncBody">

17
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 { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models';
@Component({ @Component({
selector: 'tb-data-key-config', 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) => { this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => {
const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; const postFuncBody: TbFunction = this.dataKeyFormGroup.get('postFuncBody').value;
if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { if (usePostProcessing && !isNotEmptyTbFunction(postFuncBody)) {
this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;');
} else if (!usePostProcessing && postFuncBody && postFuncBody.length) { } else if (!usePostProcessing && isNotEmptyTbFunction(postFuncBody)) {
this.dataKeyFormGroup.get('postFuncBody').patchValue(null); this.dataKeyFormGroup.get('postFuncBody').patchValue(null);
} }
}); });
@ -317,7 +318,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
writeValue(value: DataKey): void { writeValue(value: DataKey): void {
this.modelValue = value; this.modelValue = value;
if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { if (isNotEmptyTbFunction(this.modelValue.postFuncBody)) {
this.modelValue.usePostProcessing = true; this.modelValue.usePostProcessing = true;
} }
if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) { 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); return key => key.toLowerCase().startsWith(lowercaseQuery);
} }
public validateOnSubmit() { public validateOnSubmit(): Observable<void> {
if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) { if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) {
this.funcBodyEdit.validateOnSubmit(); return this.funcBodyEdit.validateOnSubmit();
} else if ((this.modelValue.type === DataKeyType.timeseries || } else if ((this.modelValue.type === DataKeyType.timeseries ||
this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value && this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value &&
this.postFuncBodyEdit) { this.postFuncBodyEdit) {
this.postFuncBodyEdit.validateOnSubmit(); return this.postFuncBodyEdit.validateOnSubmit();
} else {
return of(null);
} }
} }

100
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 { sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order'; 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, 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 { debounceTime, distinctUntilChanged, map, take, takeUntil, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
@ -1196,20 +1196,32 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private reserveSpaceForHiddenAction = true; private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[]; private cellButtonActions: TableCellButtonActionDescriptor[];
private readonly usedShowCellActionFunction: boolean; private usedShowCellActionFunction: boolean;
private inited = false;
constructor(private subscription: IWidgetSubscription, constructor(private subscription: IWidgetSubscription,
private dataKeys: Array<DataKey>, private dataKeys: Array<DataKey>,
private ngZone: NgZone, private ngZone: NgZone,
private widgetContext: WidgetContext, private widgetContext: WidgetContext,
actionCellDescriptors: AlarmWidgetActionDescriptor[]) { private actionCellDescriptors: AlarmWidgetActionDescriptor[]) {
this.cellButtonActions = actionCellDescriptors.concat(getTableCellButtonActions(widgetContext));
this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction);
if (this.widgetContext.settings.reserveSpaceForHiddenAction) { if (this.widgetContext.settings.reserveSpaceForHiddenAction) {
this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction);
} }
} }
private init(): Observable<any> {
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<AlarmDataInfo[] | ReadonlyArray<AlarmDataInfo>> { connect(collectionViewer: CollectionViewer): Observable<AlarmDataInfo[] | ReadonlyArray<AlarmDataInfo>> {
return this.alarmsSubject.asObservable(); return this.alarmsSubject.asObservable();
} }
@ -1237,47 +1249,49 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
} }
updateAlarms() { updateAlarms() {
const subscriptionAlarms = this.subscription.alarms; this.init().subscribe(() => {
let alarms = new Array<AlarmDataInfo>(); const subscriptionAlarms = this.subscription.alarms;
let maxCellButtonAction = 0; let alarms = new Array<AlarmDataInfo>();
let isEmptySelection = false; let maxCellButtonAction = 0;
const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; let isEmptySelection = false;
subscriptionAlarms.data.forEach((alarmData) => { const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction;
const alarm = this.alarmDataToInfo(alarmData); subscriptionAlarms.data.forEach((alarmData) => {
alarms.push(alarm); const alarm = this.alarmDataToInfo(alarmData);
if (dynamicWidthCellButtonActions && alarm.actionCellButtons.length > maxCellButtonAction) { alarms.push(alarm);
maxCellButtonAction = alarm.actionCellButtons.length; if (dynamicWidthCellButtonActions && alarm.actionCellButtons.length > maxCellButtonAction) {
maxCellButtonAction = alarm.actionCellButtons.length;
}
});
if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && alarms.length) {
maxCellButtonAction = alarms[0].actionCellButtons.length;
} }
}); if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) {
if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && alarms.length) { const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC;
maxCellButtonAction = alarms[0].actionCellButtons.length; alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc));
}
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.selection.hasValue()) {
const alarmsPageData: PageData<AlarmDataInfo> = { const alarmIds = alarms.map((alarm) => alarm.id.id);
data: alarms, const toRemove = this.selection.selected.filter(alarm => alarmIds.indexOf(alarm.id.id) === -1);
totalPages: subscriptionAlarms.totalPages, this.selection.deselect(...toRemove);
totalElements: subscriptionAlarms.totalElements, if (this.selection.isEmpty()) {
hasNext: subscriptionAlarms.hasNext isEmptySelection = true;
}; }
this.ngZone.run(() => {
if (isEmptySelection) {
this.onSelectionModeChanged(false);
} }
this.alarmsSubject.next(alarms); const alarmsPageData: PageData<AlarmDataInfo> = {
this.pageDataSubject.next(alarmsPageData); data: alarms,
this.countCellButtonAction = maxCellButtonAction; totalPages: subscriptionAlarms.totalPages,
this.dataLoading = false; 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;
});
}); });
} }

82
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 cssjs from '@core/css/css';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 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 { 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 } from 'rxjs/operators'; import { 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';
@ -789,7 +789,8 @@ class EntityDatasource implements DataSource<EntityData> {
private reserveSpaceForHiddenAction = true; private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[]; private cellButtonActions: TableCellButtonActionDescriptor[];
private readonly usedShowCellActionFunction: boolean; private usedShowCellActionFunction: boolean;
private inited = false;
constructor( constructor(
private translate: TranslateService, private translate: TranslateService,
@ -798,13 +799,24 @@ class EntityDatasource implements DataSource<EntityData> {
private ngZone: NgZone, private ngZone: NgZone,
private widgetContext: WidgetContext private widgetContext: WidgetContext
) { ) {
this.cellButtonActions = getTableCellButtonActions(widgetContext);
this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction);
if (this.widgetContext.settings.reserveSpaceForHiddenAction) { if (this.widgetContext.settings.reserveSpaceForHiddenAction) {
this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction);
} }
} }
private init(): Observable<any> {
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<EntityData[] | ReadonlyArray<EntityData>> { connect(collectionViewer: CollectionViewer): Observable<EntityData[] | ReadonlyArray<EntityData>> {
return this.entitiesSubject.asObservable(); return this.entitiesSubject.asObservable();
} }
@ -828,36 +840,38 @@ class EntityDatasource implements DataSource<EntityData> {
} }
dataUpdated() { dataUpdated() {
const datasourcesPageData = this.subscription.datasourcePages[0]; this.init().subscribe(() => {
const dataPageData = this.subscription.dataPages[0]; const datasourcesPageData = this.subscription.datasourcePages[0];
let entities = new Array<EntityData>(); const dataPageData = this.subscription.dataPages[0];
let maxCellButtonAction = 0; let entities = new Array<EntityData>();
const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; let maxCellButtonAction = 0;
datasourcesPageData.data.forEach((datasource, index) => { const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction;
const entity = this.datasourceToEntityData(datasource, dataPageData.data[index]); datasourcesPageData.data.forEach((datasource, index) => {
entities.push(entity); const entity = this.datasourceToEntityData(datasource, dataPageData.data[index]);
if (dynamicWidthCellButtonActions && entity.actionCellButtons.length > maxCellButtonAction) { entities.push(entity);
maxCellButtonAction = entity.actionCellButtons.length; 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 (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) {
if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { maxCellButtonAction = entities[0].actionCellButtons.length;
const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; }
entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); const entitiesPageData: PageData<EntityData> = {
} data: entities,
if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) { totalPages: datasourcesPageData.totalPages,
maxCellButtonAction = entities[0].actionCellButtons.length; totalElements: datasourcesPageData.totalElements,
} hasNext: datasourcesPageData.hasNext
const entitiesPageData: PageData<EntityData> = { };
data: entities, this.ngZone.run(() => {
totalPages: datasourcesPageData.totalPages, this.entitiesSubject.next(entities);
totalElements: datasourcesPageData.totalElements, this.pageDataSubject.next(entitiesPageData);
hasNext: datasourcesPageData.hasNext this.countCellButtonAction = maxCellButtonAction;
}; this.dataLoading = false;
this.ngZone.run(() => { });
this.entitiesSubject.next(entities);
this.pageDataSubject.next(entitiesPageData);
this.countCellButtonAction = maxCellButtonAction;
this.dataLoading = false;
}); });
} }

35
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 { 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 { forkJoin, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile';
@ -59,7 +62,7 @@ export type ShowCellButtonActionFunction = (ctx: WidgetContext, data: EntityData
export interface TableCellButtonActionDescriptor extends WidgetActionDescriptor { export interface TableCellButtonActionDescriptor extends WidgetActionDescriptor {
useShowActionCellButtonFunction: boolean; useShowActionCellButtonFunction: boolean;
showActionCellButtonFunction: ShowCellButtonActionFunction; showActionCellButtonFunction: CompiledTbFunction<ShowCellButtonActionFunction>;
} }
export interface EntityData { export interface EntityData {
@ -314,20 +317,26 @@ export function getColumnSelectionAvailability(keySettings: TableWidgetDataKeySe
return !(isDefined(keySettings.columnSelectionToDisplay) && keySettings.columnSelectionToDisplay === 'disabled'); return !(isDefined(keySettings.columnSelectionToDisplay) && keySettings.columnSelectionToDisplay === 'disabled');
} }
export function getTableCellButtonActions(widgetContext: WidgetContext): TableCellButtonActionDescriptor[] { export function getTableCellButtonActions(widgetContext: WidgetContext): Observable<TableCellButtonActionDescriptor[]> {
return widgetContext.actionsApi.getActionDescriptors('actionCellButton').map(descriptor => { const actions$ = widgetContext.actionsApi.getActionDescriptors('actionCellButton').map(descriptor => {
let useShowActionCellButtonFunction = descriptor.useShowWidgetActionFunction || false; let useShowActionCellButtonFunction = descriptor.useShowWidgetActionFunction || false;
let showActionCellButtonFunction: ShowCellButtonActionFunction = null; let showActionCellButtonFunction$: Observable<CompiledTbFunction<ShowCellButtonActionFunction>>;
if (useShowActionCellButtonFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { if (useShowActionCellButtonFunction && isNotEmptyTbFunction(descriptor.showWidgetActionFunction)) {
try { showActionCellButtonFunction$ = compileTbFunction(widgetContext.http, descriptor.showWidgetActionFunction, 'widgetContext', 'data');
showActionCellButtonFunction = } else {
new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowCellButtonActionFunction; showActionCellButtonFunction$ = of(null);
} catch (e) {
useShowActionCellButtonFunction = false;
}
} }
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 { export function checkHasActions(cellButtonActions: TableCellButtonActionDescriptor[]): boolean {
@ -348,7 +357,7 @@ function filterTableCellButtonAction(widgetContext: WidgetContext,
action: TableCellButtonActionDescriptor, data: EntityData | AlarmDataInfo | FormattedData): boolean { action: TableCellButtonActionDescriptor, data: EntityData | AlarmDataInfo | FormattedData): boolean {
if (action.useShowActionCellButtonFunction) { if (action.useShowActionCellButtonFunction) {
try { try {
return action.showActionCellButtonFunction(widgetContext, data); return action.showActionCellButtonFunction.execute(widgetContext, data);
} catch (e) { } catch (e) {
console.warn('Failed to execute showActionCellButtonFunction', e); console.warn('Failed to execute showActionCellButtonFunction', e);
return false; return false;

178
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 { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, 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 { 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 { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -828,7 +838,8 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
private reserveSpaceForHiddenAction = true; private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[]; private cellButtonActions: TableCellButtonActionDescriptor[];
private readonly usedShowCellActionFunction: boolean; private usedShowCellActionFunction: boolean;
private inited = false;
constructor( constructor(
private source: TimeseriesTableSource, private source: TimeseriesTableSource,
@ -837,14 +848,25 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
private datePipe: DatePipe, private datePipe: DatePipe,
private widgetContext: WidgetContext private widgetContext: WidgetContext
) { ) {
this.cellButtonActions = getTableCellButtonActions(widgetContext);
this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction);
if (this.widgetContext.settings.reserveSpaceForHiddenAction) { if (this.widgetContext.settings.reserveSpaceForHiddenAction) {
this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction);
} }
this.source.timeseriesDatasource = this; this.source.timeseriesDatasource = this;
} }
private init(): Observable<any> {
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<TimeseriesRow[] | ReadonlyArray<TimeseriesRow>> { connect(collectionViewer: CollectionViewer): Observable<TimeseriesRow[] | ReadonlyArray<TimeseriesRow>> {
if (this.rowsSubject.isStopped) { if (this.rowsSubject.isStopped) {
this.rowsSubject.isStopped = false; this.rowsSubject.isStopped = false;
@ -886,68 +908,74 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
} }
private updateSourceData() { private updateSourceData() {
this.source.data = this.convertData(this.source.rawData, this.source.latestRawData); this.convertData(this.source.rawData, this.source.latestRawData).subscribe((data) => {
this.allRowsSubject.next(this.source.data); this.source.data = data;
} 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++) { private convertData(data: DatasourceData[], latestData: DatasourceData[]): Observable<TimeseriesRow[]> {
const columnData = data[d].data; return this.init().pipe(
columnData.forEach((cellData) => { map(() => {
const timestamp = cellData[0]; const rowsMap: {[timestamp: number]: TimeseriesRow} = {};
let row = rowsMap[timestamp]; for (let d = 0; d < data.length; d++) {
if (!row) { const columnData = data[d].data;
row = { columnData.forEach((cellData) => {
formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) const timestamp = cellData[0];
}; let row = rowsMap[timestamp];
if (this.cellButtonActions.length) { if (!row) {
if (this.usedShowCellActionFunction) { row = {
const parsedData = formattedDataFormDatasourceData(data, undefined, timestamp); formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter)
row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions, };
parsedData[0], this.reserveSpaceForHiddenAction); if (this.cellButtonActions.length) {
row.hasActions = checkHasActions(row.actionCellButtons); if (this.usedShowCellActionFunction) {
} else { const parsedData = formattedDataFormDatasourceData(data, undefined, timestamp);
row.hasActions = true; row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions,
row.actionCellButtons = 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[d + 1] = cellData[1];
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];
});
}
let rows: TimeseriesRow[] = []; let rows: TimeseriesRow[] = [];
if (this.hideEmptyLines) { if (this.hideEmptyLines) {
for (const t of Object.keys(rowsMap)) { for (const t of Object.keys(rowsMap)) {
let hideLine = true; let hideLine = true;
for (let c = 0; (c < data.length) && hideLine; c++) { for (let c = 0; (c < data.length) && hideLine; c++) {
if (rowsMap[t][c + 1]) { if (rowsMap[t][c + 1]) {
hideLine = false; hideLine = false;
}
}
if (!hideLine) {
rows.push(rowsMap[t]);
}
} }
} else {
rows = Object.keys(rowsMap).map(itm => rowsMap[itm]);
} }
if (!hideLine) { for (let d = 0; d < latestData.length; d++) {
rows.push(rowsMap[t]); const columnData = latestData[d].data;
if (columnData.length) {
const value = columnData[0][1];
rows.forEach((row) => {
row[data.length + d + 1] = value;
});
}
} }
} return rows;
} 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;
} }
isEmpty(): Observable<boolean> { isEmpty(): Observable<boolean> {
@ -963,19 +991,23 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
} }
private fetchRows(pageLink: PageLink): Observable<PageData<TimeseriesRow>> { private fetchRows(pageLink: PageLink): Observable<PageData<TimeseriesRow>> {
return this.allRows$.pipe( return this.init().pipe(
map((data) => { switchMap(() => {
const fetchData = pageLink.filterData(data); return this.allRows$.pipe(
if (this.cellButtonActions.length) { map((data) => {
let maxCellButtonAction: number; const fetchData = pageLink.filterData(data);
if (this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction) { if (this.cellButtonActions.length) {
maxCellButtonAction = Math.max(...fetchData.data.map(tsRow => tsRow.actionCellButtons.length)); let maxCellButtonAction: number;
} else { if (this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction) {
maxCellButtonAction = this.cellButtonActions.length; maxCellButtonAction = Math.max(...fetchData.data.map(tsRow => tsRow.actionCellButtons.length));
} } else {
this.countCellButtonAction = maxCellButtonAction; maxCellButtonAction = this.cellButtonActions.length;
} }
return fetchData; this.countCellButtonAction = maxCellButtonAction;
}
return fetchData;
})
);
}) })
); );
} }

74
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 { MODULES_MAP } from '@shared/models/constants';
import { IModulesMap } from '@modules/common/modules-map.models'; import { IModulesMap } from '@modules/common/modules-map.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; 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({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -209,7 +210,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
private mobileService: MobileService, private mobileService: MobileService,
private raf: RafService, private raf: RafService,
private ngZone: NgZone, private ngZone: NgZone,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
private http: HttpClient) {
super(store); super(store);
this.cssParser.testMode = false; this.cssParser.testMode = false;
} }
@ -270,37 +272,49 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
}; };
this.widgetContext.customHeaderActions = []; this.widgetContext.customHeaderActions = [];
const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value);
headerActionsDescriptors.forEach((descriptor) =>
{ const customHeaderActions$ = headerActionsDescriptors.map((descriptor) => {
let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false; let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false;
let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null; let showWidgetHeaderActionFunction$: Observable<CompiledTbFunction<ShowWidgetHeaderActionFunction>>;
if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { if (useShowWidgetHeaderActionFunction && isNotEmptyTbFunction(descriptor.showWidgetActionFunction)) {
try { showWidgetHeaderActionFunction$ = compileTbFunction(this.http, descriptor.showWidgetActionFunction, 'widgetContext', 'data');
showWidgetHeaderActionFunction = } else {
new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction; showWidgetHeaderActionFunction$ = of(null);
} catch (e) {
useShowWidgetHeaderActionFunction = false;
}
} }
const headerAction: WidgetHeaderAction = { return showWidgetHeaderActionFunction$.pipe(
name: descriptor.name, catchError(() => { return of(null) }),
displayName: descriptor.displayName, map(showWidgetHeaderActionFunction => {
icon: descriptor.icon, if (!showWidgetHeaderActionFunction) {
descriptor, useShowWidgetHeaderActionFunction = false;
useShowWidgetHeaderActionFunction, }
showWidgetHeaderActionFunction, const headerAction: WidgetHeaderAction = {
onAction: $event => { name: descriptor.name,
const entityInfo = this.getActiveEntityInfo(); displayName: descriptor.displayName,
const entityId = entityInfo ? entityInfo.entityId : null; icon: descriptor.icon,
const entityName = entityInfo ? entityInfo.entityName : null; descriptor,
const entityLabel = entityInfo ? entityInfo.entityLabel : null; useShowWidgetHeaderActionFunction,
this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); showWidgetHeaderActionFunction,
} onAction: $event => {
}; const entityInfo = this.getActiveEntityInfo();
this.widgetContext.customHeaderActions.push(headerAction); 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 = new WidgetSubscriptionContext(this.widgetContext.dashboard);
this.subscriptionContext.timeService = this.timeService; this.subscriptionContext.timeService = this.timeService;
this.subscriptionContext.deviceService = this.deviceService; this.subscriptionContext.deviceService = this.deviceService;
@ -1110,7 +1124,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
case WidgetActionType.custom: case WidgetActionType.custom:
const customFunction = descriptor.customFunction; const customFunction = descriptor.customFunction;
if (isNotEmptyTbFunction(customFunction)) { if (isNotEmptyTbFunction(customFunction)) {
compileTbFunction(this.widgetContext.http, customFunction, '$event', 'widgetContext', 'entityId', compileTbFunction(this.http, customFunction, '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe( 'entityName', 'additionalParams', 'entityLabel').subscribe(
{ {
next: (compiled) => { next: (compiled) => {
@ -1143,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({
next: () => { next: () => {
if (isNotEmptyTbFunction(customPrettyFunction)) { if (isNotEmptyTbFunction(customPrettyFunction)) {
compileTbFunction(this.widgetContext.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', compileTbFunction(this.http, customPrettyFunction, '$event', 'widgetContext', 'entityId',
'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe( 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe(
{ {
next: (compiled) => { next: (compiled) => {

2
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 { private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean {
if (action.useShowWidgetHeaderActionFunction) { if (action.useShowWidgetHeaderActionFunction) {
try { try {
return action.showWidgetHeaderActionFunction(this.widgetContext, data); return action.showWidgetHeaderActionFunction.execute(this.widgetContext, data);
} catch (e) { } catch (e) {
console.warn('Failed to execute showWidgetHeaderActionFunction', e); console.warn('Failed to execute showWidgetHeaderActionFunction', e);
return false; return false;

3
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 { UserSettingsService } from '@core/http/user-settings.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { CompiledTbFunction } from '@shared/models/js-function.models';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -118,7 +119,7 @@ export interface WidgetHeaderAction extends IWidgetAction {
displayName: string; displayName: string;
descriptor: WidgetActionDescriptor; descriptor: WidgetActionDescriptor;
useShowWidgetHeaderActionFunction: boolean; useShowWidgetHeaderActionFunction: boolean;
showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction; showWidgetHeaderActionFunction: CompiledTbFunction<ShowWidgetHeaderActionFunction>;
} }
export interface WidgetAction extends IWidgetAction { export interface WidgetAction extends IWidgetAction {

194
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 { beautifyJs } from '@shared/models/beautify.models';
import { ScriptLanguage } from '@shared/models/rule-node.models'; import { ScriptLanguage } from '@shared/models/rule-node.models';
import { coerceBoolean } from '@shared/decorators/coercion'; 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 { TbPopoverService } from '@shared/components/popover.service';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs'; import { map, Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-js-func', selector: 'tb-js-func',
@ -322,23 +323,29 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
); );
} }
validateOnSubmit(): void { validateOnSubmit(): Observable<void> {
if (!this.disabled) { if (!this.disabled) {
this.cleanupJsErrors(); this.cleanupJsErrors();
this.functionValid = this.validateJsFunc(); return this.validateJsFunc().pipe(
if (!this.functionValid) { map((valid) => {
this.propagateValue(this.modelValue); this.functionValid = valid;
this.cd.markForCheck(); if (!this.functionValid) {
this.store.dispatch(new ActionNotificationShow( this.propagateValue(this.modelValue);
{ this.cd.markForCheck();
message: this.validationError, this.store.dispatch(new ActionNotificationShow(
type: 'error', {
target: this.toastTargetId, message: this.validationError,
verticalPosition: 'bottom', type: 'error',
horizontalPosition: 'left' target: this.toastTargetId,
})); verticalPosition: 'bottom',
this.errorShowed = true; horizontalPosition: 'left'
} }));
this.errorShowed = true;
}
})
);
} else {
return of(null);
} }
} }
@ -347,83 +354,95 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
this.jsEditor?.focus(); this.jsEditor?.focus();
} }
private validateJsFunc(): boolean { private validateJsFunc(): Observable<boolean> {
try { let toCompile: TbFunction;
const toValidate = new Function(this.functionArgsString, this.modelValue); if (this.withModules && this.modules && Object.keys(this.modules).length) {
if (this.noValidate) { toCompile = {
return true; body: this.modelValue,
} modules: this.modules
if (this.validationArgs) { };
let res: any; } else {
let validationError: any; toCompile = this.modelValue;
for (const validationArg of this.validationArgs) { }
try { const args = this.functionArgs || [];
res = toValidate.apply(this, validationArg); return compileTbFunction(this.http, toCompile, ...args).pipe(
validationError = null; map(toValidate => {
break; if (this.noValidate) {
} catch (e) { return true;
validationError = e;
}
}
if (validationError) {
throw validationError;
} }
if (this.resultType !== 'nocheck') { if (this.validationArgs) {
if (this.resultType === 'any') { let res: any;
if (isUndefined(res)) { let validationError: any;
this.validationError = this.translate.instant('js-func.no-return-error'); for (const validationArg of this.validationArgs) {
return false; try {
res = toValidate.apply(this, validationArg);
validationError = null;
break;
} catch (e) {
validationError = e;
} }
} else { }
const resType = typeof res; if (validationError) {
if (resType !== this.resultType) { throw validationError;
this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); }
return false; 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 { catchError((e) => {
return true; const details = this.utils.parseException(e);
} let errorInfo = 'Error:';
} catch (e) { if (details.name) {
const details = this.utils.parseException(e); errorInfo += ' ' + details.name + ':';
let errorInfo = 'Error:';
if (details.name) {
errorInfo += ' ' + details.name + ':';
}
if (details.message) {
errorInfo += ' ' + details.message;
}
if (details.lineNumber) {
errorInfo += '<br>Line ' + details.lineNumber;
if (details.columnNumber) {
errorInfo += ' column ' + details.columnNumber;
} }
errorInfo += ' of script.'; if (details.message) {
} errorInfo += ' ' + details.message;
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), if (details.lineNumber) {
'ace_active-line', 'screenLine'); errorInfo += '<br>Line ' + details.lineNumber;
this.errorMarkers.push(errorMarkerId); if (details.columnNumber) {
const annotations = this.jsEditor.session.getAnnotations(); errorInfo += ' column ' + details.columnNumber;
const errorAnnotation: Ace.Annotation = { }
row: line, errorInfo += ' of script.';
column, }
text: details.message, this.validationError = errorInfo;
type: 'error' if (details.lineNumber) {
}; const line = details.lineNumber - 1;
this.errorAnnotationId = annotations.push(errorAnnotation) - 1; let column = 0;
this.jsEditor.session.setAnnotations(annotations); if (details.columnNumber) {
} column = details.columnNumber;
return false; }
} 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 { private cleanupJsErrors(): void {
@ -449,6 +468,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
writeValue(value: TbFunction): void { writeValue(value: TbFunction): void {
if (isUndefinedOrNull(value) || typeof value === 'string') { if (isUndefinedOrNull(value) || typeof value === 'string') {
this.modelValue = value as any; this.modelValue = value as any;
this.modules = null;
} else { } else {
this.modelValue = value.body; this.modelValue = value.body;
this.modules = value.modules; this.modules = value.modules;

29
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; return tbFunction.body && tbFunction.body.trim().length > 0;
} }
} else { } else {
return true; return false;
} }
} }
export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable<CompiledTbFunction> => { export const compileTbFunction = <T extends GenericFunction>(http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable<CompiledTbFunction<T>> => {
let functionBody: string; let functionBody: string;
let functionArgs: string[]; let functionArgs: string[];
let modules: {[alias: string]: string }; let modules: {[alias: string]: string };
@ -59,7 +59,7 @@ export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...a
return loadFunctionModules(http, modules).pipe( return loadFunctionModules(http, modules).pipe(
map((compiledModules) => { map((compiledModules) => {
const compiledFunction = new Function(...functionArgs, functionBody); const compiledFunction = new Function(...functionArgs, functionBody);
return new CompiledTbFunction(compiledFunction, compiledModules); return new CompiledTbFunction<T>(compiledFunction, compiledModules);
}) })
); );
} }
@ -188,13 +188,17 @@ const loadModuleCompletion = (http: HttpClient, moduleLink: string): Observable<
); );
} }
export class CompiledTbFunction { type GenericFunction = (...args: any[]) => any;
export class CompiledTbFunction<T extends GenericFunction> {
public execute: T = this.executeImpl.bind(this);
constructor(private compiledFunction: Function, constructor(private compiledFunction: Function,
private compiledModules: System.Module[]) { private compiledModules: System.Module[]) {
} }
execute(...args: any[]): any { private executeImpl(...args: any[]): any {
let functionArgs: any[]; let functionArgs: any[];
if (this.compiledModules?.length) { if (this.compiledModules?.length) {
functionArgs = args ? args.concat(this.compiledModules) : this.compiledModules; 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 getFunctionArguments = (func: Function): string[] => {
const fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, ''); const fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '');
let result = new Array<string>(...fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g)); const firstBracketIndex = fnStr.indexOf('(');
if (result === null) const secondBracketIndex = fnStr.indexOf(')');
result = []; if (firstBracketIndex === -1 || secondBracketIndex === -1 || (secondBracketIndex - firstBracketIndex) <= 1) {
return result; return [];
}
const match = fnStr.slice(firstBracketIndex+1, secondBracketIndex).match(/([^\s,]+)/g);
if (match) {
return new Array<string>(...match);
} else {
return [];
}
} }

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

@ -335,7 +335,7 @@ export interface KeyInfo {
label?: string; label?: string;
color?: string; color?: string;
funcBody?: string; funcBody?: string;
postFuncBody?: string; postFuncBody?: TbFunction;
units?: string; units?: string;
decimals?: number; decimals?: number;
} }
@ -694,7 +694,7 @@ export interface WidgetActionDescriptor extends WidgetAction {
icon: string; icon: string;
displayName?: string; displayName?: string;
useShowWidgetActionFunction?: boolean; useShowWidgetActionFunction?: boolean;
showWidgetActionFunction?: string; showWidgetActionFunction?: TbFunction;
columnIndex?: number; columnIndex?: number;
} }

Loading…
Cancel
Save