Browse Source

UI: latest data aggregation improvements

pull/7288/head
Igor Kulikov 4 years ago
parent
commit
f28297b7ae
  1. 242
      ui-ngx/src/app/core/api/data-aggregator.ts
  2. 347
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  3. 4
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  4. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  5. 20
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  6. 3
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  7. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  8. 1
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  9. 6
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  10. 25
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss
  11. 1
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  12. 63
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  13. 53
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  14. 14
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  15. 1
      ui-ngx/src/app/shared/models/query/query.models.ts
  16. 5
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  17. 15
      ui-ngx/src/app/shared/models/time/time.models.ts
  18. 2
      ui-ngx/src/app/shared/models/widget.models.ts

242
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);
}
});
}
}
}

347
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<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
private data: Array<Array<DataSetHolder>>;
private subsTw: SubscriptionTimewindow;
private latestTsOffset: number;
private dataAggregators: Array<DataAggregator>;
@ -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<Array<DataSetHolder>> = [];
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<SubscriptionDataKey>;
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<SubscriptionDataKey>;
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<AggKey>,
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) {

4
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';

3
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 {

20
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();
});

3
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') {

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

@ -34,6 +34,7 @@
style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true"
isEdit="true"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex formControlName="timewindow"></tb-timewindow>
</section>

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

@ -46,6 +46,7 @@
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
quickIntervalOnly="{{widget.onlyQuickInterval}}"
timezone="true"
[isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow"

6
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -333,6 +333,8 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
hasAggregation: boolean;
onlyQuickInterval: boolean;
style: {[klass: string]: any};
showWidgetTitlePanel: boolean;
@ -429,10 +431,12 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true;
let canHaveTimewindow = false;
let onlyQuickInterval = false;
if (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) {
canHaveTimewindow = true;
} else if (this.widget.type === widgetType.latest) {
canHaveTimewindow = datasourcesHasAggregation(this.widget.config.datasources);
onlyQuickInterval = canHaveTimewindow;
}
this.hasTimewindow = canHaveTimewindow ?
@ -441,6 +445,8 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
|| this.widget.config.displayTimewindow)) : false)
: false;
this.onlyQuickInterval = onlyQuickInterval;
this.hasAggregation = this.widget.type === widgetType.timeseries;
this.style = {

25
ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../scss/constants';
:host {
min-width: 355px;
@media #{$mat-xs} {
min-width: 0;
width: 100%;
}
}

1
ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts

@ -21,6 +21,7 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode
@Component({
selector: 'tb-quick-time-interval',
templateUrl: './quick-time-interval.component.html',
styleUrls: ['./quick-time-interval.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

63
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -29,32 +29,57 @@
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="realtimeType">
<mat-radio-group *ngIf="!quickIntervalOnly" [fxShow]="isEdit || (!timewindow.hideLastInterval && !timewindow.hideQuickInterval)"
formControlName="realtimeType">
<mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideLastInterval"
(ngModelChange)="onHideLastIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</section>
</mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideQuickInterval"
(ngModelChange)="onHideQuickIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</section>
</mat-radio-button>
</mat-radio-group>
<tb-timeinterval *ngIf="!isEdit && !timewindow.hideLastInterval && timewindow.hideQuickInterval"
formControlName="timewindowMs"
predefinedName="timewindow.last"
required
style="padding-top: 8px;"></tb-timeinterval>
<tb-quick-time-interval *ngIf="quickIntervalOnly || !isEdit && timewindow.hideLastInterval && !timewindow.hideQuickInterval"
formControlName="quickInterval"
onlyCurrentInterval="true"
required
style="padding-top: 8px"></tb-quick-time-interval>
</div>
</section>
</section>

53
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -36,6 +36,7 @@ export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('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();
}

14
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();
}

1
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<TsValue>};
aggLatest?: {[aggType: string]: {[key: string]: TsValue}};
aggFloating?: {[key: string]: Array<TsValue>};
}
export interface AlarmData extends AlarmInfo {

5
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -188,7 +188,6 @@ export interface AggTimeSeriesCmd {
keys: Array<AggKey>;
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;
}

15
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;

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

@ -338,7 +338,7 @@ export interface Datasource {
export function datasourcesHasAggregation(datasources?: Array<Datasource>): 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;
});

Loading…
Cancel
Save