From f28297b7aedd5d0aaa5207704f982d44c35af826 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 13 Sep 2022 14:07:56 +0300 Subject: [PATCH] UI: latest data aggregation improvements --- ui-ngx/src/app/core/api/data-aggregator.ts | 242 ++++++------ .../app/core/api/entity-data-subscription.ts | 347 +++++++++--------- .../core/services/dashboard-utils.service.ts | 4 + .../dashboard-page.component.ts | 3 +- .../widget/data-key-config.component.ts | 20 +- .../lib/entities-table-widget.component.ts | 3 +- .../widget/widget-config.component.html | 1 + .../widget/widget-container.component.html | 1 + .../home/models/dashboard-component.models.ts | 6 + .../time/quick-time-interval.component.scss | 25 ++ .../time/quick-time-interval.component.ts | 1 + .../time/timewindow-panel.component.html | 63 +++- .../time/timewindow-panel.component.ts | 53 ++- .../components/time/timewindow.component.ts | 14 +- .../app/shared/models/query/query.models.ts | 1 - .../models/telemetry/telemetry.models.ts | 5 +- .../src/app/shared/models/time/time.models.ts | 15 +- ui-ngx/src/app/shared/models/widget.models.ts | 2 +- 18 files changed, 469 insertions(+), 337 deletions(-) create mode 100644 ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index a17f59dcb0..697fa2489a 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -14,7 +14,11 @@ /// limitations under the License. /// -import { AggKey, SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; +import { + AggKey, + AggSubscriptionData, + SubscriptionData +} from '@app/shared/models/telemetry/telemetry.models'; import { AggregationType, calculateIntervalComparisonEndTime, @@ -28,7 +32,7 @@ import { UtilsService } from '@core/services/utils.service'; import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; import Timeout = NodeJS.Timeout; -export declare type onAggregatedData = (aggType: AggregationType, data: SubscriptionData, detectChanges: boolean) => void; +export declare type onAggregatedData = (data: AggSubscriptionData, detectChanges: boolean) => void; interface AggData { count: number; @@ -67,30 +71,23 @@ class AggDataMap { } class AggregationMap { - aggMap: {[aggType: string]: {[key: string]: AggDataMap}} = {}; + aggMap: {[aggKey: string]: AggDataMap} = {}; detectRangeChanged(): boolean { let changed = false; - for (const aggType of Object.keys(this.aggMap)) { - const aggKeyMap = this.aggMap[aggType]; - for (const key of Object.keys(aggKeyMap)) { - const aggDataMap = aggKeyMap[key]; - if (aggDataMap.rangeChanged) { - changed = true; - aggDataMap.rangeChanged = false; - } + for (const aggKey of Object.keys(this.aggMap)) { + const aggDataMap = this.aggMap[aggKey]; + if (aggDataMap.rangeChanged) { + changed = true; + aggDataMap.rangeChanged = false; } } return changed; } clearRangeChangedFlags() { - for (const aggType of Object.keys(this.aggMap)) { - const aggKeyMap = this.aggMap[aggType]; - for (const key of Object.keys(aggKeyMap)) { - const aggDataMap = aggKeyMap[key]; - aggDataMap.rangeChanged = false; - } + for (const aggKey of Object.keys(this.aggMap)) { + this.aggMap[aggKey].rangeChanged = false; } } } @@ -145,7 +142,6 @@ export class DataAggregator { constructor(private onDataCb: onAggregatedData, private tsKeys: AggKey[], private isLatestDataAgg: boolean, - private isFloatingLatestDataAgg: boolean, private subsTw: SubscriptionTimewindow, private utils: UtilsService, private ignoreDataUpdateOnIntervalTick: boolean) { @@ -160,8 +156,8 @@ export class DataAggregator { } } - private dataBuffer: {[aggType: string]: SubscriptionData} = {}; - private data: {[aggType: string]: SubscriptionData}; + private dataBuffer: AggSubscriptionData = {}; + private data: AggSubscriptionData; private readonly lastPrevKvPairData: {[aggKey: string]: [number, any]}; private aggregationMap: AggregationMap; @@ -209,6 +205,13 @@ export class DataAggregator { return `${aggKey.key}_${aggKey.agg}`; } + private static aggKeyFromString(aggKeyString: string): AggKey { + const separatorIndex = aggKeyString.lastIndexOf('_'); + const key = aggKeyString.substring(0, separatorIndex); + const agg = AggregationType[aggKeyString.substring(separatorIndex + 1)]; + return { key, agg }; + } + public updateOnDataCb(newOnDataCb: onAggregatedData): onAggregatedData { const prevOnDataCb = this.onDataCb; this.onDataCb = newOnDataCb; @@ -238,8 +241,7 @@ export class DataAggregator { this.aggregationMap = null; } - public onData(aggType: AggregationType, - data: SubscriptionDataHolder, update: boolean, history: boolean, detectChanges: boolean) { + public onData(data: AggSubscriptionData, update: boolean, history: boolean, detectChanges: boolean) { this.updatedData = true; if (!this.dataReceived || this.resetPending) { let updateIntervalScheduledTime = true; @@ -252,11 +254,11 @@ export class DataAggregator { this.resetPending = false; updateIntervalScheduledTime = false; } - this.aggregationMap = new AggregationMap(); if (update) { - this.updateAggregatedData(aggType, data.data); + this.aggregationMap = new AggregationMap(); + this.updateAggregatedData(data); } else { - this.processAggregatedData(aggType, data.data); + this.aggregationMap = this.processAggregatedData(data); } if (updateIntervalScheduledTime) { this.intervalScheduledTime = this.utils.currentPerfTime(); @@ -264,11 +266,7 @@ export class DataAggregator { this.aggregationMap.clearRangeChangedFlags(); this.onInterval(history, detectChanges); } else { - if (this.aggregationMap.aggMap[aggType]) { - this.updateAggregatedData(aggType, data.data); - } else { - this.processAggregatedData(aggType, data.data); - } + this.updateAggregatedData(data); if (history) { this.intervalScheduledTime = this.utils.currentPerfTime(); this.onInterval(history, detectChanges); @@ -324,9 +322,7 @@ export class DataAggregator { this.data = this.updateData(); } if (this.onDataCb && (!this.ignoreDataUpdateOnIntervalTick || this.updatedData)) { - for (const aggType of Object.keys(this.data)) { - this.onDataCb(AggregationType[aggType], this.data[aggType], detectChanges); - } + this.onDataCb(this.data, detectChanges); this.updatedData = false; } if (!history) { @@ -342,52 +338,32 @@ export class DataAggregator { } this.dataBuffer[key.agg][key.key] = []; }); - for (const aggType of Object.keys(this.aggregationMap.aggMap)) { - for (const key of Object.keys(this.aggregationMap.aggMap[aggType])) { - const aggKeyData = this.aggregationMap.aggMap[aggType][key]; - const noAggregation = AggregationType[aggType] === AggregationType.NONE; - const aggKeyString = DataAggregator.aggKeyToString({agg: AggregationType[aggType], key}); - let keyData = this.dataBuffer[aggType][key]; - aggKeyData.forEach((aggData, aggTimestamp) => { - if (aggTimestamp < this.startTs) { - if (this.subsTw.aggregation.stateData && - (!this.lastPrevKvPairData[aggKeyString] || this.lastPrevKvPairData[aggKeyString][0] < aggTimestamp)) { - this.lastPrevKvPairData[aggKeyString] = [aggTimestamp, aggData.aggValue]; - } - aggKeyData.delete(aggTimestamp); - this.updatedData = true; - } else if (aggTimestamp < this.endTs || noAggregation) { - const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; - keyData.push(kvPair); - } - }); - keyData.sort((set1, set2) => set1[0] - set2[0]); - if (this.isFloatingLatestDataAgg) { - if (keyData.length) { - const timestamp = this.startTs + (this.endTs - this.startTs) / 2; - const first = keyData[0]; - const aggData: AggData = { - aggValue: AggregationType[aggType] === AggregationType.COUNT ? 1 : first[1], - sum: first[1], - count: 1 - }; - const aggFunction = DataAggregator.getAggFunction(AggregationType[aggType]); - for (let i = 1; i < keyData.length; i++) { - const kvPair = keyData[i]; - aggFunction(aggData, kvPair[1]); - } - this.dataBuffer[aggType][key] = [[timestamp, aggData.aggValue]]; + for (const aggKeyString of Object.keys(this.aggregationMap.aggMap)) { + const aggKeyData = this.aggregationMap.aggMap[aggKeyString]; + const aggKey = DataAggregator.aggKeyFromString(aggKeyString); + const noAggregation = aggKey.agg === AggregationType.NONE; + let keyData = this.dataBuffer[aggKey.agg][aggKey.key]; + aggKeyData.forEach((aggData, aggTimestamp) => { + if (aggTimestamp < this.startTs) { + if (this.subsTw.aggregation.stateData && + (!this.lastPrevKvPairData[aggKeyString] || this.lastPrevKvPairData[aggKeyString][0] < aggTimestamp)) { + this.lastPrevKvPairData[aggKeyString] = [aggTimestamp, aggData.aggValue]; } - } else { - if (this.subsTw.aggregation.stateData) { - this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[aggKeyString])); - } - if (keyData.length > this.subsTw.aggregation.limit) { - keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit); - } - this.dataBuffer[aggType][key] = keyData; + aggKeyData.delete(aggTimestamp); + this.updatedData = true; + } else if (aggTimestamp < this.endTs || noAggregation) { + const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; + keyData.push(kvPair); } + }); + keyData.sort((set1, set2) => set1[0] - set2[0]); + if (this.subsTw.aggregation.stateData) { + this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[aggKeyString])); } + if (keyData.length > this.subsTw.aggregation.limit) { + keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit); + } + this.dataBuffer[aggKey.agg][aggKey.key] = keyData; } return this.dataBuffer; } @@ -420,68 +396,68 @@ export class DataAggregator { } } - private processAggregatedData(aggType: AggregationType, data: SubscriptionData) { - const isCount = aggType === AggregationType.COUNT; - const noAggregation = aggType === AggregationType.NONE || this.isFloatingLatestDataAgg; - let aggDataByKey = this.aggregationMap.aggMap[aggType]; - if (!aggDataByKey) { - aggDataByKey = {}; - this.aggregationMap.aggMap[aggType] = aggDataByKey; - } - for (const key of Object.keys(data)) { - let aggKeyData = aggDataByKey[key]; - if (!aggKeyData) { - aggKeyData = new AggDataMap(); - aggDataByKey[key] = aggKeyData; + private processAggregatedData(data: AggSubscriptionData): AggregationMap { + const aggregationMap = new AggregationMap(); + for (const aggTypeString of Object.keys(data)) { + const aggType = AggregationType[aggTypeString]; + const isCount = aggType === AggregationType.COUNT; + const noAggregation = aggType === AggregationType.NONE; + for (const key of Object.keys(data[aggType])) { + const aggKey = DataAggregator.aggKeyToString({key, agg: aggType}); + let aggKeyData = aggregationMap.aggMap[aggKey]; + if (!aggKeyData) { + aggKeyData = new AggDataMap(); + aggregationMap.aggMap[aggKey] = aggKeyData; + } + const keyData = data[aggType][key]; + keyData.forEach((kvPair) => { + const timestamp = kvPair[0]; + const value = DataAggregator.convertValue(kvPair[1], noAggregation); + const tsKey = timestamp; + const aggData = { + count: isCount ? value : isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1, + sum: value, + aggValue: value + }; + aggKeyData.set(tsKey, aggData); + }); } - const keyData = data[key]; - keyData.forEach((kvPair) => { - const timestamp = kvPair[0]; - const value = DataAggregator.convertValue(kvPair[1], noAggregation); - const tsKey = timestamp; - const aggData = { - count: isCount ? value : isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1, - sum: value, - aggValue: value - }; - aggKeyData.set(tsKey, aggData); - }); } + return aggregationMap; } - private updateAggregatedData(aggType: AggregationType, data: SubscriptionData) { - const isCount = aggType === AggregationType.COUNT; - const noAggregation = aggType === AggregationType.NONE || this.isFloatingLatestDataAgg; - let aggDataByKey = this.aggregationMap.aggMap[aggType]; - if (!aggDataByKey) { - aggDataByKey = {}; - this.aggregationMap.aggMap[aggType] = aggDataByKey; - } - for (const key of Object.keys(data)) { - let aggKeyData = aggDataByKey[key]; - if (!aggKeyData) { - aggKeyData = new AggDataMap(); - aggDataByKey[key] = aggKeyData; - } - const keyData = data[key]; - keyData.forEach((kvPair) => { - const timestamp = kvPair[0]; - const value = DataAggregator.convertValue(kvPair[1], noAggregation); - const aggTimestamp = noAggregation ? timestamp : (this.startTs + - Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) * - this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2); - let aggData = aggKeyData.get(aggTimestamp); - if (!aggData || this.isFloatingLatestDataAgg) { - aggData = { - count: isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1, - sum: value, - aggValue: isCount ? 1 : value - }; - aggKeyData.set(aggTimestamp, aggData); - } else { - DataAggregator.getAggFunction(aggType)(aggData, value); + private updateAggregatedData(data: AggSubscriptionData) { + for (const aggTypeString of Object.keys(data)) { + const aggType = AggregationType[aggTypeString]; + const isCount = aggType === AggregationType.COUNT; + const noAggregation = aggType === AggregationType.NONE; + for (const key of Object.keys(data[aggType])) { + const aggKey = DataAggregator.aggKeyToString({key, agg: aggType}); + let aggKeyData = this.aggregationMap.aggMap[aggKey]; + if (!aggKeyData) { + aggKeyData = new AggDataMap(); + this.aggregationMap.aggMap[aggKey] = aggKeyData; } - }); + const keyData = data[aggType][key]; + keyData.forEach((kvPair) => { + const timestamp = kvPair[0]; + const value = DataAggregator.convertValue(kvPair[1], noAggregation); + const aggTimestamp = noAggregation ? timestamp : (this.startTs + + Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) * + this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2); + let aggData = aggKeyData.get(aggTimestamp); + if (!aggData) { + aggData = { + count: isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1, + sum: value, + aggValue: isCount ? 1 : value + }; + aggKeyData.set(aggTimestamp, aggData); + } else { + DataAggregator.getAggFunction(aggType)(aggData, value); + } + }); + } } } 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 3378ee8bd2..0d6c534e4c 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -29,11 +29,11 @@ import { } from '@shared/models/query/query.models'; import { AggKey, + AggSubscriptionData, DataKeyType, EntityCountCmd, EntityDataCmd, SubscriptionData, - SubscriptionDataHolder, TelemetryService, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; @@ -46,8 +46,8 @@ 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 { EntityId } from '@shared/models/id/entity-id'; -import Timeout = NodeJS.Timeout; import _ from 'lodash'; +import Timeout = NodeJS.Timeout; declare type DataKeyFunction = (time: number, prevValue: any) => any; declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any; @@ -94,6 +94,7 @@ export class EntityDataSubscription { private entityDataSubscriptionOptions = this.listener.subscriptionOptions; private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; private history: boolean; + private isFloatingTimewindow: boolean; private realtime: boolean; private subscriber: TelemetrySubscriber; @@ -108,6 +109,7 @@ export class EntityDataSubscription { private entityDataResolveSubject: Subject; private pageData: PageData; + private data: Array>; private subsTw: SubscriptionTimewindow; private latestTsOffset: number; private dataAggregators: Array; @@ -304,19 +306,28 @@ export class EntityDataSubscription { this.subscriber.reconnect$.subscribe(() => { if (this.started) { const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && - !this.history && this.tsFields.length) { + 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; - targetCommand.tsCmd.startTs = this.subsTw.startTs; - targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; - targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; - 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.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { + targetCommand.tsCmd.startTs = this.subsTw.startTs; + targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; + 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); + }); + } } if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { this.subscriber.setTsOffset(this.subsTw.tsOffset); @@ -485,9 +496,12 @@ export class EntityDataSubscription { isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + this.isFloatingTimewindow = this.entityDataSubscriptionOptions.subscriptionTimewindow && + !this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval && !this.history; } private prepareSubscriptionCommands(cmd: EntityDataCmd) { + let latestValuesKeys: EntityKey[] = []; if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.tsFields.length > 0) { if (this.history) { @@ -512,17 +526,9 @@ export class EntityDataSubscription { }; } } - if (this.latestValues.length > 0) { - cmd.latestCmd = { - keys: this.latestValues - }; - } + latestValuesKeys = this.latestValues; } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { - if (this.latestValues.length > 0) { - cmd.latestCmd = { - keys: this.latestValues - }; - } + latestValuesKeys = this.latestValues; } if (this.aggTsValues.length > 0) { if (this.history) { @@ -531,15 +537,24 @@ export class EntityDataSubscription { startTs: this.subsTw.fixedWindow.startTimeMs, endTs: this.subsTw.fixedWindow.endTimeMs }; - } else { + } else if (!this.isFloatingTimewindow) { cmd.aggTsCmd = { keys: this.aggTsValues, startTs: this.subsTw.startTs, - timeWindow: this.subsTw.aggregation.timeWindow, - floating: !this.subsTw.quickInterval + timeWindow: this.subsTw.aggregation.timeWindow }; + if (latestValuesKeys.length > 0) { + const tsKeys = this.aggTsValues.map(key => key.key); + latestValuesKeys = latestValuesKeys.filter(latestKey => latestKey.type !== EntityKeyType.TIME_SERIES + || !tsKeys.includes(latestKey.key)); + } } } + if (latestValuesKeys.length > 0) { + cmd.latestCmd = { + keys: latestValuesKeys + }; + } } private startFunction() { @@ -595,30 +610,50 @@ export class EntityDataSubscription { for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { if (this.datasourceType === DatasourceType.function) { this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys, - false, false, DataKeyType.function, dataIndex, this.notifyListener.bind(this)); + false, DataKeyType.function, dataIndex, this.notifyListener.bind(this)); } else { this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys, - false, false, DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this)); + false, DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this)); } } } } if (this.aggTsValues && this.aggTsValues.length) { - const aggLatestTimewindow = deepClone(this.subsTw); - aggLatestTimewindow.aggregation.stateData = false; - const isFloatingLatestDataAgg = !aggLatestTimewindow.quickInterval && !this.history; - if (!isFloatingLatestDataAgg) { + if (!this.isFloatingTimewindow) { + const aggLatestTimewindow = deepClone(this.subsTw); + aggLatestTimewindow.aggregation.stateData = false; aggLatestTimewindow.aggregation.interval = aggLatestTimewindow.aggregation.timeWindow; - } - for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { - this.tsLatestDataAggregators[dataIndex] = this.createRealtimeDataAggregator(aggLatestTimewindow, this.aggTsValues, - true, isFloatingLatestDataAgg, - DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this)); + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + this.tsLatestDataAggregators[dataIndex] = this.createRealtimeDataAggregator(aggLatestTimewindow, this.aggTsValues, + true, DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this)); + } + } else { + const tsKeysByAggType = _.groupBy(this.aggTsValues, value => value.agg); + const aggSubscriptionData: AggSubscriptionData = {}; + for (const aggTypeString of Object.keys(tsKeysByAggType)) { + const tsKeys = tsKeysByAggType[aggTypeString]; + const latestTsAggSubsciptionData: SubscriptionData = {}; + for (const tsKey of tsKeys) { + latestTsAggSubsciptionData[tsKey.key] = [[0, 'Not supported!']]; + } + aggSubscriptionData[aggTypeString] = latestTsAggSubsciptionData; + } + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + this.onAggData(aggSubscriptionData, DataKeyType.timeseries, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, true, + (data1, dataIndex1, dataKeyIndex) => { + if (!this.data[dataIndex1]) { + this.data[dataIndex1] = []; + } + this.data[dataIndex1][dataKeyIndex] = data1; + }); + } } } } private resetData() { + this.data = []; this.datasourceData = []; this.entityIdToDataIndex = {}; for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { @@ -675,15 +710,14 @@ export class EntityDataSubscription { } else if (isInitialData) { this.resetData(); } - const data: Array> = []; for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { const entityData = pageData.data[dataIndex]; this.processEntityData(entityData, dataIndex, false, (data1, dataIndex1, dataKeyIndex) => { - if (!data[dataIndex1]) { - data[dataIndex1] = []; + if (!this.data[dataIndex1]) { + this.data[dataIndex1] = []; } - data[dataIndex1][dataKeyIndex] = data1; + this.data[dataIndex1][dataKeyIndex] = data1; } ); } @@ -692,7 +726,7 @@ export class EntityDataSubscription { this.entityDataResolveSubject.next( { pageData, - data, + data: this.data, datasourceIndex: this.listener.configDatasourceIndex, pageLink: this.entityDataSubscriptionOptions.pageLink } @@ -700,7 +734,7 @@ export class EntityDataSubscription { this.entityDataResolveSubject.complete(); } else { if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.listener.dataLoaded(pageData, data, + this.listener.dataLoaded(pageData, this.data, this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink); } if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) { @@ -710,7 +744,7 @@ export class EntityDataSubscription { this.entityDataResolveSubject.next( { pageData, - data, + data: this.data, datasourceIndex: this.listener.configDatasourceIndex, pageLink: this.entityDataSubscriptionOptions.pageLink } @@ -740,46 +774,20 @@ export class EntityDataSubscription { if (this.entityDataSubscriptionOptions.type === widgetType.latest || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (entityData.aggLatest) { - for (const aggTypeString of Object.keys(entityData.aggLatest)) { - const subscriptionData = this.toSubscriptionData(entityData.aggLatest[aggTypeString], false); - if (this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { - const dataAggregator = this.tsLatestDataAggregators[dataIndex]; - let prevDataCb; - if (!isUpdate) { - prevDataCb = dataAggregator.updateOnDataCb((aggType, data, detectChanges) => { - this.onData(aggType, data, DataKeyType.timeseries, dataIndex, detectChanges, - this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); - }); - } - dataAggregator.onData(AggregationType[aggTypeString], {data: subscriptionData}, false, this.history, true); - if (prevDataCb) { - dataAggregator.updateOnDataCb(prevDataCb); - } - } - } - } - if (entityData.aggFloating) { - const subscriptionData = this.toSubscriptionData(entityData.aggFloating, true); - const keys: string[] = Object.keys(subscriptionData); - const aggTsKeys = this.aggTsValues.filter(key => keys.includes(key.key)); - if (aggTsKeys.length && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { + if (this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { const dataAggregator = this.tsLatestDataAggregators[dataIndex]; + const aggSubscriptionData: AggSubscriptionData = {}; + for (const aggTypeString of Object.keys(entityData.aggLatest)) { + aggSubscriptionData[aggTypeString] = this.toSubscriptionData(entityData.aggLatest[aggTypeString], false); + } let prevDataCb; if (!isUpdate) { - prevDataCb = dataAggregator.updateOnDataCb((aggType, data, detectChanges) => { - this.onData(aggType, data, DataKeyType.timeseries, dataIndex, detectChanges, - this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => { + this.onAggData(data, DataKeyType.timeseries, dataIndex, detectChanges, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, true, dataUpdatedCb); }); } - const tsKeysByAggType = _.groupBy(aggTsKeys, value => value.agg); - for (const aggTypeString of Object.keys(tsKeysByAggType)) { - const tsKeys = tsKeysByAggType[aggTypeString]; - const latestTsAggSubsciptionData: SubscriptionData = {}; - for (const tsKey of tsKeys) { - latestTsAggSubsciptionData[tsKey.key] = subscriptionData[tsKey.key]; - } - dataAggregator.onData(AggregationType[aggTypeString], {data: latestTsAggSubsciptionData}, false, this.history, true); - } + dataAggregator.onData(aggSubscriptionData, false, this.history, true); if (prevDataCb) { dataAggregator.updateOnDataCb(prevDataCb); } @@ -797,25 +805,27 @@ export class EntityDataSubscription { for (const latestTsKey of latestTsKeys) { latestTsSubsciptionData[latestTsKey.key] = subscriptionData[latestTsKey.key]; } - this.onData(null, latestTsSubsciptionData, dataKeyType, dataIndex, true, - this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + this.onData(latestTsSubsciptionData, dataKeyType, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, false, dataUpdatedCb); } const aggTsKeys = this.aggTsValues.filter(key => keys.includes(key.key)); - if (aggTsKeys.length && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { + if (!this.history && aggTsKeys.length && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { const dataAggregator = this.tsLatestDataAggregators[dataIndex]; const tsKeysByAggType = _.groupBy(aggTsKeys, value => value.agg); + const aggSubscriptionData: AggSubscriptionData = {}; for (const aggTypeString of Object.keys(tsKeysByAggType)) { const tsKeys = tsKeysByAggType[aggTypeString]; const latestTsAggSubsciptionData: SubscriptionData = {}; for (const tsKey of tsKeys) { latestTsAggSubsciptionData[tsKey.key] = subscriptionData[tsKey.key]; } - dataAggregator.onData(AggregationType[aggTypeString], {data: latestTsAggSubsciptionData}, true, false, true); + aggSubscriptionData[aggTypeString] = latestTsAggSubsciptionData; } + dataAggregator.onData(aggSubscriptionData, true, false, true); } } else { - this.onData(null, subscriptionData, dataKeyType, dataIndex, true, - this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + this.onData(subscriptionData, dataKeyType, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, false, dataUpdatedCb); } } } @@ -824,89 +834,99 @@ export class EntityDataSubscription { const subscriptionData = this.toSubscriptionData(entityData.timeseries, true); if (this.dataAggregators && this.dataAggregators[dataIndex]) { const dataAggregator = this.dataAggregators[dataIndex]; + const aggSubscriptionData: AggSubscriptionData = {}; + aggSubscriptionData[this.subsTw.aggregation.type] = subscriptionData; let prevDataCb; if (!isUpdate) { - prevDataCb = dataAggregator.updateOnDataCb((aggType, data, detectChanges) => { - this.onData(null, data, this.datasourceType === DatasourceType.function ? - DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, dataUpdatedCb); + prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => { + this.onAggData(data, this.datasourceType === DatasourceType.function ? + DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, false, dataUpdatedCb); }); } - dataAggregator.onData(this.subsTw.aggregation.type, {data: subscriptionData}, false, this.history, true); + dataAggregator.onData(aggSubscriptionData, false, this.history, true); if (prevDataCb) { dataAggregator.updateOnDataCb(prevDataCb); } } else if (!this.history && !isUpdate) { - this.onData(null, - subscriptionData, DataKeyType.timeseries, dataIndex, true, false, dataUpdatedCb); + this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, false, false, dataUpdatedCb); } } } - private onData(aggType: AggregationType | undefined | null, - sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean, - isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) { - for (const keyName of Object.keys(sourceData)) { - const keyData = sourceData[keyName]; - const aggSuffix = aggType && aggType !== AggregationType.NONE ? `_${aggType.toLowerCase()}` : ''; - const key = `${keyName}_${type}${aggSuffix}${isTsLatest ? '_latest' : ''}`; - const dataKeyList = this.dataKeys[key] as Array; - for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { - const datasourceKey = `${key}_${keyIndex}`; - if (this.datasourceData[dataIndex][datasourceKey].data) { - const dataKey = dataKeyList[keyIndex]; - const data: DataSet = []; - let prevSeries: [number, any]; - let prevOrigSeries: [number, any]; - let datasourceKeyData: DataSet; - let datasourceOrigKeyData: DataSet; - let update = false; - if (this.realtime && !isTsLatest) { - datasourceKeyData = []; - datasourceOrigKeyData = []; - } else { - datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data; - datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data; - } - if (datasourceKeyData.length > 0) { - prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; - prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1]; - } else { - prevSeries = [0, 0]; - prevOrigSeries = [0, 0]; - } - this.datasourceOrigData[dataIndex][datasourceKey].data = []; - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) { - keyData.forEach((keySeries) => { - let series = keySeries; - const time = series[0]; - this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]); - let value = EntityDataSubscription.convertValue(series[1]); - if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); - } - prevOrigSeries = [series[0], series[1]]; - series = [series[0], value]; - data.push([series[0], series[1]]); - prevSeries = [series[0], series[1]]; - }); - update = true; - } else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) { - if (keyData.length > 0) { - let series = keyData[0]; - const time = series[0]; - this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]); - let value = EntityDataSubscription.convertValue(series[1]); - if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean, + isTsLatest: boolean, isAggLatest: boolean, dataUpdatedCb: DataUpdatedCb) { + const aggSubscriptionData: AggSubscriptionData = {}; + aggSubscriptionData[AggregationType.NONE] = sourceData; + this.onAggData(aggSubscriptionData, type, dataIndex, detectChanges, isTsLatest, isAggLatest, dataUpdatedCb); + } + + private onAggData(sourceData: AggSubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean, + isTsLatest: boolean, isAggLatest: boolean, dataUpdatedCb: DataUpdatedCb) { + for (const aggTypeString of Object.keys(sourceData)) { + const aggType = AggregationType[aggTypeString]; + const aggSuffix = isAggLatest ? (aggType !== AggregationType.NONE ? `_${aggType.toLowerCase()}` : '') : ''; + for (const keyName of Object.keys(sourceData[aggType])) { + const keyData = sourceData[aggType][keyName]; + const key = `${keyName}_${type}${aggSuffix}${isTsLatest ? '_latest' : ''}`; + const dataKeyList = this.dataKeys[key] as Array; + for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { + const datasourceKey = `${key}_${keyIndex}`; + if (this.datasourceData[dataIndex][datasourceKey].data) { + const dataKey = dataKeyList[keyIndex]; + const data: DataSet = []; + let prevSeries: [number, any]; + let prevOrigSeries: [number, any]; + let datasourceKeyData: DataSet; + let datasourceOrigKeyData: DataSet; + let update = false; + if (this.realtime && !isTsLatest) { + datasourceKeyData = []; + datasourceOrigKeyData = []; + } else { + datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data; + datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data; + } + if (datasourceKeyData.length > 0) { + prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; + prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1]; + } else { + prevSeries = [0, 0]; + prevOrigSeries = [0, 0]; + } + this.datasourceOrigData[dataIndex][datasourceKey].data = []; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) { + keyData.forEach((keySeries) => { + let series = keySeries; + const time = series[0]; + this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]); + let value = EntityDataSubscription.convertValue(series[1]); + if (dataKey.postFunc) { + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + } + prevOrigSeries = [series[0], series[1]]; + series = [series[0], value]; + data.push([series[0], series[1]]); + prevSeries = [series[0], series[1]]; + }); + update = true; + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) { + if (keyData.length > 0) { + let series = keyData[0]; + const time = series[0]; + this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]); + let value = EntityDataSubscription.convertValue(series[1]); + if (dataKey.postFunc) { + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + } + series = [time, value]; + data.push([series[0], series[1]]); } - series = [time, value]; - data.push([series[0], series[1]]); + update = true; + } + if (update) { + this.datasourceData[dataIndex][datasourceKey].data = data; + dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest); } - update = true; - } - if (update) { - this.datasourceData[dataIndex][datasourceKey].data = data; - dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest); } } } @@ -934,18 +954,16 @@ export class EntityDataSubscription { private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow, tsKeys: Array, isLatestDataAgg: boolean, - isFloatingLatestDataAgg: boolean, dataKeyType: DataKeyType, dataIndex: number, dataUpdatedCb: DataUpdatedCb): DataAggregator { return new DataAggregator( - (aggType, data, detectChanges) => { - this.onData(isLatestDataAgg ? aggType : null, data, dataKeyType, dataIndex, detectChanges, - isLatestDataAgg && (this.entityDataSubscriptionOptions.type === widgetType.timeseries), dataUpdatedCb); + (data, detectChanges) => { + this.onAggData(data, dataKeyType, dataIndex, detectChanges, + isLatestDataAgg && (this.entityDataSubscriptionOptions.type === widgetType.timeseries), isLatestDataAgg, dataUpdatedCb); }, tsKeys, isLatestDataAgg, - isFloatingLatestDataAgg, subsTw, this.utils, this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick @@ -1030,9 +1048,9 @@ export class EntityDataSubscription { let startTime: number; let endTime: number; let delta: number; - const generatedData: SubscriptionDataHolder = { - data: {} - }; + const aggType = this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type; + const generatedData: AggSubscriptionData = {}; + generatedData[aggType] = {}; if (!this.history) { delta = Math.floor(this.tickElapsed / this.frequency); } @@ -1065,11 +1083,10 @@ export class EntityDataSubscription { endTime = Math.min(currentTime, endTime); } } - generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, startTime, endTime); + generatedData[aggType][`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, startTime, endTime); } if (this.dataAggregators && this.dataAggregators.length) { - this.dataAggregators[0].onData(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type, - generatedData, true, this.history, detectChanges); + this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges); } if (!this.history) { diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index c157ca869e..90238ebaba 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -34,6 +34,7 @@ import { Datasource, DatasourceType, Widget, widgetType } from '@app/shared/mode import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { EntityId } from '@app/shared/models/id/entity-id'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; @Injectable({ providedIn: 'root' @@ -215,6 +216,9 @@ export class DashboardUtilsService { delete datasource.deviceAliasId; } }); + if (widget.type === widgetType.latest) { + widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService); + } // Temp workaround if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { widget.typeAlias = 'basic_timeseries'; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 4ebaccb774..c145794163 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1062,7 +1062,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC dataKeys: config.alarmSource.dataKeys || [] }; } - const newWidget: Widget = { + let newWidget: Widget = { isSystemType: widget.isSystemType, bundleAlias: widget.bundleAlias, typeAlias: widgetTypeInfo.alias, @@ -1076,6 +1076,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC row: 0, col: 0 }; + newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget); if (widgetTypeInfo.typeParameters.useCustomDatasources) { this.addWidgetToDashboard(newWidget); } else { diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index c737090999..4085e5dca2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -18,12 +18,7 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { - DataKey, - dataKeyAggregationTypeHintTranslationMap, - Widget, - widgetType -} from '@shared/models/widget.models'; +import { DataKey, dataKeyAggregationTypeHintTranslationMap, Widget, widgetType } from '@shared/models/widget.models'; import { ControlValueAccessor, FormBuilder, @@ -184,6 +179,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con postFuncBody: [null, []] }); + this.dataKeyFormGroup.get('aggregationType').valueChanges.subscribe( + (aggType) => { + if (!this.dataKeyFormGroup.get('label').dirty) { + let newLabel = this.dataKeyFormGroup.get('name').value; + if (aggType !== AggregationType.NONE) { + const prefix = this.translate.instant(aggregationTranslations.get(aggType)); + newLabel = prefix + ' ' + newLabel; + } + this.dataKeyFormGroup.get('label').patchValue(newLabel); + } + } + ); + this.dataKeyFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 5cd01900bd..8685fb8e86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -110,6 +110,7 @@ import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; +import { AggregationType } from '@shared/models/time/time.models'; interface EntitiesTableWidgetSettings extends TableWidgetSettings { entitiesTitle: string; @@ -428,7 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); dataKey.title = dataKey.label; dataKey.def = 'def' + this.columns.length; - dataKey.sortable = !dataKey.usePostProcessing; + dataKey.sortable = !dataKey.usePostProcessing && (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE); const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.entityField && !isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index a2b73ccd49..1659ee636d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -34,6 +34,7 @@ style="padding-right: 8px;">widget-config.timewindow diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index a6520e02bd..9f99ffbf29 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -46,6 +46,7 @@
- + -
- timewindow.last - +
+
+ + +
+
+ timewindow.last + +
-
- timewindow.interval - +
+
+ + +
+
+ timewindow.interval + +
+ +
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 09fa20aaa3..67ee2ac72b 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -36,6 +36,7 @@ export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelDat export interface TimewindowPanelData { historyOnly: boolean; + quickIntervalOnly: boolean; timewindow: Timewindow; aggregation: boolean; timezone: boolean; @@ -51,6 +52,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { historyOnly = false; + quickIntervalOnly = false; + aggregation = false; timezone = false; @@ -83,6 +86,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { public viewContainerRef: ViewContainerRef) { super(store); this.historyOnly = data.historyOnly; + this.quickIntervalOnly = data.quickIntervalOnly; this.timewindow = data.timewindow; this.aggregation = data.aggregation; this.timezone = data.timezone; @@ -91,6 +95,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { ngOnInit(): void { const hideInterval = this.timewindow.hideInterval || false; + const hideLastInterval = this.timewindow.hideLastInterval || false; + const hideQuickInterval = this.timewindow.hideQuickInterval || false; const hideAggregation = this.timewindow.hideAggregation || false; const hideAggInterval = this.timewindow.hideAggInterval || false; const hideTimezone = this.timewindow.hideTimezone || false; @@ -103,10 +109,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, disabled: hideInterval }), - timewindowMs: [ - this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' - ? this.timewindow.realtime.timewindowMs : null - ], + timewindowMs: this.fb.control({ + value: this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' + ? this.timewindow.realtime.timewindowMs : null, + disabled: hideInterval || hideLastInterval + }), interval: [ this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' ? this.timewindow.realtime.interval : null @@ -114,7 +121,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { quickInterval: this.fb.control({ value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined' ? this.timewindow.realtime.quickInterval : null, - disabled: hideInterval + disabled: hideInterval || hideQuickInterval }) } ), @@ -289,8 +296,40 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); this.timewindowForm.get('history.quickInterval').enable({emitEvent: false}); this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false}); - this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); - this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); + if (!this.timewindow.hideLastInterval) { + this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); + } + if (!this.timewindow.hideQuickInterval) { + this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); + } + } + this.timewindowForm.markAsDirty(); + } + + onHideLastIntervalChanged() { + if (this.timewindow.hideLastInterval) { + this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false}); + if (!this.timewindow.hideQuickInterval) { + this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.INTERVAL); + } + } else { + if (!this.timewindow.hideInterval) { + this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); + } + } + this.timewindowForm.markAsDirty(); + } + + onHideQuickIntervalChanged() { + if (this.timewindow.hideQuickInterval) { + this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false}); + if (!this.timewindow.hideLastInterval) { + this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.LAST_INTERVAL); + } + } else { + if (!this.timewindow.hideInterval) { + this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); + } } this.timewindowForm.markAsDirty(); } diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index 6f34131241..951f157a59 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -82,6 +82,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces return this.historyOnlyValue; } + quickIntervalOnlyValue = false; + + @Input() + set quickIntervalOnly(val) { + this.quickIntervalOnlyValue = coerceBooleanProperty(val); + } + + get quickIntervalOnly() { + return this.quickIntervalOnlyValue; + } + aggregationValue = false; @Input() @@ -240,6 +251,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces { timewindow: deepClone(this.innerValue), historyOnly: this.historyOnly, + quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, timezone: this.timezone, isEdit: this.isEdit @@ -278,7 +290,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces } writeValue(obj: Timewindow): void { - this.innerValue = initModelFromDefaultTimewindow(obj, this.timeService); + this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.timeService); this.timewindowDisabled = this.isTimewindowDisabled(); this.updateDisplayValue(); } diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index e55beaf7e4..95c756f7f9 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -768,7 +768,6 @@ export interface EntityData { latest: {[entityKeyType: string]: {[key: string]: TsValue}}; timeseries: {[key: string]: Array}; aggLatest?: {[aggType: string]: {[key: string]: TsValue}}; - aggFloating?: {[key: string]: Array}; } export interface AlarmData extends AlarmInfo { diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index e519fd4a25..04d98567cf 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -188,7 +188,6 @@ export interface AggTimeSeriesCmd { keys: Array; startTs: number; timeWindow: number; - floating: boolean; } export class EntityDataCmd implements WebsocketCmd { @@ -315,6 +314,10 @@ export interface SubscriptionData { [key: string]: [number, any, number?][]; } +export interface AggSubscriptionData { + [aggType: string]: SubscriptionData; +} + export interface SubscriptionDataHolder { data: SubscriptionData; } diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 2f90d39ca1..d657d5cda4 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -96,6 +96,8 @@ export interface Timewindow { displayValue?: string; displayTimezoneAbbr?: string; hideInterval?: boolean; + hideQuickInterval?: boolean; + hideLastInterval?: boolean; hideAggregation?: boolean; hideAggInterval?: boolean; hideTimezone?: boolean; @@ -188,6 +190,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { return { displayValue: '', hideInterval: false, + hideLastInterval: false, + hideQuickInterval: false, hideAggregation: false, hideAggInterval: false, hideTimezone: false, @@ -223,10 +227,12 @@ function getTimewindowType(timewindow: Timewindow): TimewindowType { } } -export function initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow { +export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow { const model = defaultTimewindow(timeService); if (value) { model.hideInterval = value.hideInterval; + model.hideLastInterval = value.hideLastInterval; + model.hideQuickInterval = value.hideQuickInterval; model.hideAggregation = value.hideAggregation; model.hideAggInterval = value.hideAggInterval; model.hideTimezone = value.hideTimezone; @@ -281,6 +287,9 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T } model.timezone = value.timezone; } + if (quickIntervalOnly) { + model.realtime.realtimeType = RealtimeWindowType.INTERVAL; + } return model; } @@ -304,6 +313,8 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, } return { hideInterval: timewindow.hideInterval || false, + hideLastInterval: timewindow.hideLastInterval || false, + hideQuickInterval: timewindow.hideQuickInterval || false, hideAggregation: timewindow.hideAggregation || false, hideAggInterval: timewindow.hideAggInterval || false, hideTimezone: timewindow.hideTimezone || false, @@ -694,6 +705,8 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { const cloned: Timewindow = {}; cloned.hideInterval = timewindow.hideInterval || false; + cloned.hideLastInterval = timewindow.hideLastInterval || false; + cloned.hideQuickInterval = timewindow.hideQuickInterval || false; cloned.hideAggregation = timewindow.hideAggregation || false; cloned.hideAggInterval = timewindow.hideAggInterval || false; cloned.hideTimezone = timewindow.hideTimezone || false; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 3d0b1196a5..9efb47758d 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -338,7 +338,7 @@ export interface Datasource { export function datasourcesHasAggregation(datasources?: Array): boolean { if (datasources) { const foundDatasource = datasources.find(datasource => { - const found = datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && + const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && key.aggregationType && key.aggregationType !== AggregationType.NONE); return !!found; });