Browse Source

UI: Update timeseries aggregation data structures. Implement timeseries aggregation delta calculation.

pull/7288/head
Igor Kulikov 4 years ago
parent
commit
b5383aa421
  1. 169
      ui-ngx/src/app/core/api/data-aggregator.ts
  2. 426
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  3. 30
      ui-ngx/src/app/core/api/entity-data.service.ts
  4. 72
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  5. 74
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss
  6. 108
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  7. 7
      ui-ngx/src/app/shared/models/query/query.models.ts
  8. 8
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  9. 20
      ui-ngx/src/app/shared/models/widget.models.ts
  10. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json

169
ui-ngx/src/app/core/api/data-aggregator.ts

@ -16,8 +16,7 @@
import {
AggKey,
AggSubscriptionData,
SubscriptionData
IndexedSubscriptionData,
} from '@app/shared/models/telemetry/telemetry.models';
import {
AggregationType,
@ -32,7 +31,7 @@ import { UtilsService } from '@core/services/utils.service';
import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import Timeout = NodeJS.Timeout;
export declare type onAggregatedData = (data: AggSubscriptionData, detectChanges: boolean) => void;
export declare type onAggregatedData = (data: IndexedSubscriptionData, detectChanges: boolean) => void;
interface AggData {
count: number;
@ -71,12 +70,12 @@ class AggDataMap {
}
class AggregationMap {
aggMap: {[aggKey: string]: AggDataMap} = {};
aggMap: {[id: number]: AggDataMap} = {};
detectRangeChanged(): boolean {
let changed = false;
for (const aggKey of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[aggKey];
for (const id of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[id];
if (aggDataMap.rangeChanged) {
changed = true;
aggDataMap.rangeChanged = false;
@ -86,8 +85,8 @@ class AggregationMap {
}
clearRangeChangedFlags() {
for (const aggKey of Object.keys(this.aggMap)) {
this.aggMap[aggKey].rangeChanged = false;
for (const id of Object.keys(this.aggMap)) {
this.aggMap[id].rangeChanged = false;
}
}
}
@ -146,19 +145,18 @@ export class DataAggregator {
private utils: UtilsService,
private ignoreDataUpdateOnIntervalTick: boolean) {
this.tsKeys.forEach((key) => {
if (!this.dataBuffer[key.agg]) {
this.dataBuffer[key.agg] = {};
if (!this.dataBuffer[key.id]) {
this.dataBuffer[key.id] = [];
}
this.dataBuffer[key.agg][key.key] = [];
});
if (this.subsTw.aggregation.stateData) {
this.lastPrevKvPairData = {};
}
}
private dataBuffer: AggSubscriptionData = {};
private data: AggSubscriptionData;
private readonly lastPrevKvPairData: {[aggKey: string]: [number, any]};
private dataBuffer: IndexedSubscriptionData = [];
private data: IndexedSubscriptionData;
private readonly lastPrevKvPairData: {[id: number]: [number, any]};
private aggregationMap: AggregationMap;
@ -201,17 +199,6 @@ export class DataAggregator {
}
}
private static aggKeyToString(aggKey: AggKey): string {
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;
@ -241,7 +228,7 @@ export class DataAggregator {
this.aggregationMap = null;
}
public onData(data: AggSubscriptionData, update: boolean, history: boolean, detectChanges: boolean) {
public onData(data: IndexedSubscriptionData, update: boolean, history: boolean, detectChanges: boolean) {
this.updatedData = true;
if (!this.dataReceived || this.resetPending) {
let updateIntervalScheduledTime = true;
@ -330,24 +317,24 @@ export class DataAggregator {
}
}
private updateData(): {[aggType: string]: SubscriptionData} {
this.dataBuffer = {};
private updateData(): IndexedSubscriptionData {
this.dataBuffer = [];
this.tsKeys.forEach((key) => {
if (!this.dataBuffer[key.agg]) {
this.dataBuffer[key.agg] = {};
if (!this.dataBuffer[key.id]) {
this.dataBuffer[key.id] = [];
}
this.dataBuffer[key.agg][key.key] = [];
});
for (const aggKeyString of Object.keys(this.aggregationMap.aggMap)) {
const aggKeyData = this.aggregationMap.aggMap[aggKeyString];
const aggKey = DataAggregator.aggKeyFromString(aggKeyString);
for (const idStr of Object.keys(this.aggregationMap.aggMap)) {
const id = Number(idStr);
const aggKeyData = this.aggregationMap.aggMap[id];
const aggKey = this.aggKeyById(id);
const noAggregation = aggKey.agg === AggregationType.NONE;
let keyData = this.dataBuffer[aggKey.agg][aggKey.key];
let keyData = this.dataBuffer[id];
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];
(!this.lastPrevKvPairData[id] || this.lastPrevKvPairData[id][0] < aggTimestamp)) {
this.lastPrevKvPairData[id] = [aggTimestamp, aggData.aggValue];
}
aggKeyData.delete(aggTimestamp);
this.updatedData = true;
@ -358,12 +345,12 @@ export class DataAggregator {
});
keyData.sort((set1, set2) => set1[0] - set2[0]);
if (this.subsTw.aggregation.stateData) {
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[aggKeyString]));
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[id]));
}
if (keyData.length > this.subsTw.aggregation.limit) {
keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit);
}
this.dataBuffer[aggKey.agg][aggKey.key] = keyData;
this.dataBuffer[id] = keyData;
}
return this.dataBuffer;
}
@ -396,69 +383,71 @@ export class DataAggregator {
}
}
private processAggregatedData(data: AggSubscriptionData): AggregationMap {
private processAggregatedData(data: IndexedSubscriptionData): AggregationMap {
const aggregationMap = new AggregationMap();
for (const aggTypeString of Object.keys(data)) {
const aggType = AggregationType[aggTypeString];
for (const idStr of Object.keys(data)) {
const id = Number(idStr);
const aggKey = this.aggKeyById(id);
const aggType = aggKey.agg;
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);
});
let aggKeyData = aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap();
aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
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(data: AggSubscriptionData) {
for (const aggTypeString of Object.keys(data)) {
const aggType = AggregationType[aggTypeString];
private updateAggregatedData(data: IndexedSubscriptionData) {
for (const idStr of Object.keys(data)) {
const id = Number(idStr);
const aggKey = this.aggKeyById(id);
const aggType = aggKey.agg;
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);
}
});
let aggKeyData = this.aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap();
this.aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[id];
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);
}
});
}
}
private aggKeyById(id: number): AggKey {
return this.tsKeys.find(key => key.id === id);
}
}

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

@ -14,9 +14,16 @@
/// limitations under the License.
///
import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models';
import { ComparisonResultType, DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import {
AggregationType,
ComparisonDuration,
createTimewindowForComparison,
getCurrentTime,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import {
ComparisonTsValue,
EntityData,
EntityDataPageLink,
EntityFilter,
@ -29,10 +36,10 @@ import {
} from '@shared/models/query/query.models';
import {
AggKey,
AggSubscriptionData,
DataKeyType,
EntityCountCmd,
EntityDataCmd,
IndexedSubscriptionData,
SubscriptionData,
TelemetryService,
TelemetrySubscriber
@ -46,7 +53,6 @@ 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 _ from 'lodash';
import Timeout = NodeJS.Timeout;
declare type DataKeyFunction = (time: number, prevValue: any) => any;
@ -58,6 +64,10 @@ export interface SubscriptionDataKey {
name: string;
type: DataKeyType;
aggregationType?: AggregationType;
comparisonEnabled?: boolean;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType;
funcBody: string;
func?: DataKeyFunction;
postFuncBody: string;
@ -106,6 +116,7 @@ export class EntityDataSubscription {
private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>;
private aggTsValues: Array<AggKey>;
private aggTsComparisonValues: Array<AggKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
@ -115,6 +126,7 @@ export class EntityDataSubscription {
private dataAggregators: Array<DataAggregator>;
private tsLatestDataAggregators: Array<DataAggregator>;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
private dataKeysList: SubscriptionDataKey[] = [];
private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}};
private entityIdToDataIndex: {[id: string]: number};
@ -136,9 +148,37 @@ export class EntityDataSubscription {
return val;
}
private static calculateComparisonValue(key: SubscriptionDataKey, comparisonTsValue: ComparisonTsValue): [number, any, number?][] {
let timestamp: number;
let value: any;
switch (key.comparisonResultType) {
case ComparisonResultType.PREVIOUS_VALUE:
timestamp = comparisonTsValue.previous.ts;
value = comparisonTsValue.previous.value;
break;
case ComparisonResultType.DELTA_ABSOLUTE:
case ComparisonResultType.DELTA_PERCENT:
timestamp = comparisonTsValue.previous.ts;
const currentVal = EntityDataSubscription.convertValue(comparisonTsValue.current.value);
const prevVal = EntityDataSubscription.convertValue(comparisonTsValue.previous.value);
if (isNumeric(currentVal) && isNumeric(prevVal)) {
if (key.comparisonResultType === ComparisonResultType.DELTA_ABSOLUTE) {
value = currentVal - prevVal;
} else {
value = (currentVal - prevVal) / prevVal * 100;
}
} else {
value = '';
}
break;
}
return [[timestamp, value]];
}
private initializeSubscription() {
for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) {
const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]);
this.dataKeysList.push(dataKey);
dataKey.index = i;
if (this.datasourceType === DatasourceType.function) {
if (!dataKey.func) {
@ -156,8 +196,8 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
} else {
const aggSuffix = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE ? `_${dataKey.aggregationType.toLowerCase()}` : '';
key = `${dataKey.name}_${dataKey.type}${aggSuffix}${dataKey.latest ? '_latest' : ''}`;
const keyIndexSuffix = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE ? `_${dataKey.index}` : '';
key = `${dataKey.name}_${dataKey.type}${keyIndexSuffix}${dataKey.latest ? '_latest' : ''}`;
}
let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
if (!dataKeysList) {
@ -213,7 +253,7 @@ export class EntityDataSubscription {
}
if (this.datasourceType === DatasourceType.entity) {
const entityFields: Array<EntityKey> =
this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
);
if (!entityFields.find(key => key.key === 'name')) {
@ -235,18 +275,18 @@ export class EntityDataSubscription {
});
}
this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
);
this.tsFields = this.entityDataSubscriptionOptions.dataKeys.
this.tsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const latestTsFields = this.entityDataSubscriptionOptions.dataKeys.
const latestTsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
@ -256,12 +296,19 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields);
}
this.aggTsValues = this.entityDataSubscriptionOptions.dataKeys.
this.aggTsValues = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE).map(
dataKey => ({ key: dataKey.name, agg: dataKey.aggregationType })
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map(
dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType })
);
this.aggTsComparisonValues = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map(
dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType,
previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE })
);
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd();
@ -392,7 +439,7 @@ export class EntityDataSubscription {
entityType: null
};
const countKey = this.entityDataSubscriptionOptions.dataKeys[0];
const countKey = this.dataKeysList[0];
let dataReceived = false;
@ -530,24 +577,30 @@ export class EntityDataSubscription {
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
latestValuesKeys = this.latestValues;
}
if (this.aggTsValues.length > 0) {
if (this.history) {
cmd.aggHistoryCmd = {
keys: this.aggTsValues,
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs
};
} else if (!this.isFloatingTimewindow) {
cmd.aggTsCmd = {
keys: this.aggTsValues,
startTs: this.subsTw.startTs,
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 (this.history && (this.aggTsValues.length > 0 || this.aggTsComparisonValues.length > 0)) {
for (const aggTsComparison of this.aggTsComparisonValues) {
const subscriptionDataKey = this.dataKeyByIndex(aggTsComparison.id);
const timewindowForComparison =
createTimewindowForComparison(this.subsTw, subscriptionDataKey.timeForComparison,
subscriptionDataKey.comparisonCustomIntervalValue);
aggTsComparison.previousStartTs = timewindowForComparison.fixedWindow.startTimeMs;
aggTsComparison.previousEndTs = timewindowForComparison.fixedWindow.endTimeMs;
}
cmd.aggHistoryCmd = {
keys: [...this.aggTsValues, ...this.aggTsComparisonValues],
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs
};
} else if (!this.isFloatingTimewindow && this.aggTsValues.length > 0) {
cmd.aggTsCmd = {
keys: this.aggTsValues,
startTs: this.subsTw.startTs,
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) {
@ -592,29 +645,21 @@ export class EntityDataSubscription {
this.resetData();
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
let tsKeyNames: string[] = [];
let tsKeyIds: number[];
if (this.datasourceType === DatasourceType.function) {
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => {
if (!subscriptionDataKey.latest) {
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
}
});
}
tsKeyIds = this.dataKeysList.filter(key => !key.latest).map(key => key.index);
} else {
tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
tsKeyIds = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map(
dataKey => dataKey.index
);
}
const aggKeys: AggKey[] = tsKeyNames.map(key => ({key, agg: this.subsTw.aggregation.type}));
const aggKeys: AggKey[] = tsKeyIds.map(key => ({id: key, key: key + '', agg: this.subsTw.aggregation.type}));
if (aggKeys.length) {
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
if (this.datasourceType === DatasourceType.function) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys,
false, DataKeyType.function, dataIndex, this.notifyListener.bind(this));
} else {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys,
false, DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
}
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys,
false, dataIndex, this.notifyListener.bind(this));
}
}
}
@ -625,34 +670,35 @@ export class EntityDataSubscription {
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, DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
true, 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,
(data, dataIndex1, dataKeyIndex, detectChanges, isLatest) => {
if (!this.data[dataIndex1]) {
this.data[dataIndex1] = [];
}
this.data[dataIndex1][dataKeyIndex] = data;
if (isUpdate) {
this.notifyListener(data, dataIndex1, dataKeyIndex, detectChanges, isLatest);
}
});
}
this.reportNotSupported(this.aggTsValues, isUpdate);
}
}
if (!this.history && this.aggTsComparisonValues && this.aggTsComparisonValues.length) {
this.reportNotSupported(this.aggTsComparisonValues, isUpdate);
}
}
private reportNotSupported(keys: AggKey[], isUpdate: boolean) {
const indexedData: IndexedSubscriptionData = [];
for (const key of keys) {
indexedData[key.id] = [[0, 'Not supported!']];
}
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
this.onIndexedData(indexedData, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries,
(data, dataIndex1, dataKeyIndex, detectChanges, isLatest) => {
if (!this.data[dataIndex1]) {
this.data[dataIndex1] = [];
}
this.data[dataIndex1][dataKeyIndex] = data;
if (isUpdate) {
this.notifyListener(data, dataIndex1, dataKeyIndex, detectChanges, isLatest);
}
});
}
}
private resetData() {
@ -777,20 +823,29 @@ export class EntityDataSubscription {
if (this.entityDataSubscriptionOptions.type === widgetType.latest ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (entityData.aggLatest) {
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);
const aggData: IndexedSubscriptionData = [];
for (const idStr of Object.keys(entityData.aggLatest)) {
const id = Number(idStr);
const dataKey = this.dataKeyByIndex(id);
const aggLatestData = entityData.aggLatest[id];
if (dataKey.comparisonEnabled) {
const keyData = EntityDataSubscription.calculateComparisonValue(dataKey, aggLatestData);
this.onKeyData(keyData, dataKey.name, id, dataKey.type, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, true, dataUpdatedCb);
} else {
aggData[id] = [[aggLatestData.current.ts, aggLatestData.current.value, aggLatestData.current.count]];
}
}
if (Object.keys(aggData).length > 0 && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) {
const dataAggregator = this.tsLatestDataAggregators[dataIndex];
let prevDataCb;
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onAggData(data, DataKeyType.timeseries, dataIndex, detectChanges,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, true, dataUpdatedCb);
this.onIndexedData(data, dataIndex, detectChanges,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
});
}
dataAggregator.onData(aggSubscriptionData, false, this.history, true);
dataAggregator.onData(aggData, false, this.history, true);
if (prevDataCb) {
dataAggregator.updateOnDataCb(prevDataCb);
}
@ -809,26 +864,20 @@ export class EntityDataSubscription {
latestTsSubsciptionData[latestTsKey.key] = subscriptionData[latestTsKey.key];
}
this.onData(latestTsSubsciptionData, dataKeyType, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, false, dataUpdatedCb);
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
}
const aggTsKeys = this.aggTsValues.filter(key => keys.includes(key.key));
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];
}
aggSubscriptionData[aggTypeString] = latestTsAggSubsciptionData;
const indexedData: IndexedSubscriptionData = [];
for (const aggKey of aggTsKeys) {
indexedData[aggKey.id] = subscriptionData[aggKey.key];
}
dataAggregator.onData(aggSubscriptionData, true, false, true);
dataAggregator.onData(indexedData, true, false, true);
}
} else {
this.onData(subscriptionData, dataKeyType, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, false, dataUpdatedCb);
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
}
}
}
@ -837,100 +886,116 @@ 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;
const keyNames = Object.keys(subscriptionData);
const dataKeys = this.timeseriesDataKeysByKeyNames(keyNames);
const indexedData: IndexedSubscriptionData = [];
for (const dataKey of dataKeys) {
indexedData[dataKey.index] = subscriptionData[dataKey.name];
}
let prevDataCb;
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onAggData(data, this.datasourceType === DatasourceType.function ?
DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, false, dataUpdatedCb);
this.onIndexedData(data, dataIndex, detectChanges, false, dataUpdatedCb);
});
}
dataAggregator.onData(aggSubscriptionData, false, this.history, true);
dataAggregator.onData(indexedData, false, this.history, true);
if (prevDataCb) {
dataAggregator.updateOnDataCb(prevDataCb);
}
} else if (!this.history && !isUpdate) {
this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, false, false, dataUpdatedCb);
this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, false, dataUpdatedCb);
}
}
}
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);
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const key of Object.keys(sourceData)) {
const keyData = sourceData[key];
this.onKeyData(keyData, key, 0, type,
dataIndex, detectChanges, isTsLatest, false, dataUpdatedCb);
}
}
private onIndexedData(sourceData: IndexedSubscriptionData, dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const indexStr of Object.keys(sourceData)) {
const id = Number(indexStr);
const dataKey = this.dataKeyByIndex(id);
const isAggLatest = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE;
const keyData = sourceData[id];
let keyName = dataKey.name;
if (dataKey.type === DataKeyType.function) {
keyName += `_${dataKey.index}`;
}
this.onKeyData(keyData, keyName, id, dataKey.type,
dataIndex, detectChanges, isTsLatest, isAggLatest, dataUpdatedCb);
}
}
private onAggData(sourceData: AggSubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean,
private onKeyData(keyData: [number, any, number?][], keyName: string, id: number, 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]]);
}
update = true;
const keyIdSuffix = isAggLatest ? `_${id}` : '';
const key = `${keyName}_${type}${keyIdSuffix}${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]);
}
if (update) {
this.datasourceData[dataIndex][datasourceKey].data = data;
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest);
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]]);
}
update = true;
}
if (update) {
this.datasourceData[dataIndex][datasourceKey].data = data;
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest);
}
}
}
@ -957,13 +1022,12 @@ export class EntityDataSubscription {
private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow,
tsKeys: Array<AggKey>,
isLatestDataAgg: boolean,
dataKeyType: DataKeyType,
dataIndex: number,
dataUpdatedCb: DataUpdatedCb): DataAggregator {
return new DataAggregator(
(data, detectChanges) => {
this.onAggData(data, dataKeyType, dataIndex, detectChanges,
isLatestDataAgg && (this.entityDataSubscriptionOptions.type === widgetType.timeseries), isLatestDataAgg, dataUpdatedCb);
this.onIndexedData(data, dataIndex, detectChanges,
isLatestDataAgg && (this.entityDataSubscriptionOptions.type === widgetType.timeseries), dataUpdatedCb);
},
tsKeys,
isLatestDataAgg,
@ -973,6 +1037,20 @@ export class EntityDataSubscription {
);
}
private dataKeyByIndex(index: number): SubscriptionDataKey {
return this.dataKeysList.find(key => key.index === index);
}
private timeseriesDataKeysByKeyNames(keyNames: string[]): SubscriptionDataKey[] {
const result: SubscriptionDataKey[] = [];
for (const keyName of keyNames) {
const key = `${keyName}_${DataKeyType.timeseries}`;
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
result.push(...dataKeyList);
}
return result;
}
private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): [number, any][] {
const data: [number, any][] = [];
let prevSeries: [number, any];
@ -1051,9 +1129,7 @@ export class EntityDataSubscription {
let startTime: number;
let endTime: number;
let delta: number;
const aggType = this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type;
const generatedData: AggSubscriptionData = {};
generatedData[aggType] = {};
const generatedData: IndexedSubscriptionData = [];
if (!this.history) {
delta = Math.floor(this.tickElapsed / this.frequency);
}
@ -1086,7 +1162,7 @@ export class EntityDataSubscription {
endTime = Math.min(currentTime, endTime);
}
}
generatedData[aggType][`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, startTime, endTime);
generatedData[dataKey.index] = this.generateSeries(dataKey, startTime, endTime);
}
if (this.dataAggregators && this.dataAggregators.length) {
this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges);

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

@ -74,6 +74,21 @@ export class EntityDataService {
}
}
private static toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey {
return {
name: dataKey.name,
type: dataKey.type,
aggregationType: dataKey.aggregationType,
comparisonEnabled: dataKey.comparisonEnabled,
timeForComparison: dataKey.timeForComparison,
comparisonCustomIntervalValue: dataKey.comparisonCustomIntervalValue,
comparisonResultType: dataKey.comparisonResultType,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody,
latest
};
}
public prepareSubscription(listener: EntityDataListener,
ignoreDataUpdateOnIntervalTick = false): Observable<EntityDataLoadResult> {
const datasource = listener.configDatasource;
@ -147,11 +162,11 @@ export class EntityDataService {
ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions {
const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
datasource.dataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, false));
subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, false));
});
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, true));
subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, true));
});
}
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
@ -172,15 +187,4 @@ export class EntityDataService {
entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick;
return entityDataSubscriptionOptions;
}
private toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey {
return {
name: dataKey.name,
type: dataKey.type,
aggregationType: dataKey.aggregationType,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody,
latest
};
}
}

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

@ -62,15 +62,69 @@
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field>
</div>
<mat-form-field *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries" style="padding-bottom: 16px;">
<mat-label translate>datakey.aggregation-type</mat-label>
<mat-select formControlName="aggregationType" style="min-width: 150px;">
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
{{ (aggregation === aggregationTypes.NONE ? 'datakey.latest-value' : aggregationTypesTranslations.get(aggregationTypes[aggregation])) | translate }}
</mat-option>
</mat-select>
<mat-hint>{{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }}</mat-hint>
</mat-form-field>
<section *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries" fxLayout="column">
<mat-form-field style="padding-bottom: 16px;">
<mat-label translate>datakey.aggregation-type</mat-label>
<mat-select formControlName="aggregationType" style="min-width: 150px;">
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
{{ (aggregation === aggregationTypes.NONE ? 'datakey.latest-value' : aggregationTypesTranslations.get(aggregationTypes[aggregation])) | translate }}
</mat-option>
</mat-select>
<mat-hint>{{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }}</mat-hint>
</mat-form-field>
<fieldset *ngIf="dataKeyFormGroup.get('aggregationType').value && dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE" class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.comparison-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="dataKeyFormGroup.get('comparisonEnabled').value" [disabled]="!dataKeyFormGroup.get('comparisonEnabled').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="comparisonEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.chart.enable-comparison' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section fxLayout="column" *ngIf="dataKeyFormGroup.get('comparisonEnabled').value">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.time-for-comparison</mat-label>
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
</mat-option>
<mat-option [value]="'days'">
{{ 'widgets.chart.time-for-comparison-days' | translate }}
</mat-option>
<mat-option [value]="'weeks'">
{{ 'widgets.chart.time-for-comparison-weeks' | translate }}
</mat-option>
<mat-option [value]="'months'">
{{ 'widgets.chart.time-for-comparison-months' | translate }}
</mat-option>
<mat-option [value]="'years'">
{{ 'widgets.chart.time-for-comparison-years' | translate }}
</mat-option>
<mat-option [value]="'customInterval'">
{{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="dataKeyFormGroup.get('timeForComparison').value === 'customInterval'" fxFlex class="mat-block">
<mat-label translate>widgets.chart.custom-interval-value</mat-label>
<input required matInput type="number" min="0" formControlName="comparisonCustomIntervalValue">
</mat-form-field>
<mat-form-field style="padding-bottom: 16px;">
<mat-label translate>datakey.comparison-result</mat-label>
<mat-select formControlName="comparisonResultType" style="min-width: 150px;">
<mat-option *ngFor="let comparisonResultType of comparisonResults" [value]="comparisonResultType">
{{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
</ng-template>
</mat-expansion-panel>
</fieldset>
</section>
<section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function">
<span translate>datakey.data-generation-func</span>
<br/>

74
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss

@ -28,6 +28,36 @@
padding-left: 12px;
}
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
legend + * {
display: block;
margin-top: 16px;
}
&.fields-group-slider {
padding: 0;
legend {
margin-left: 16px;
}
> .tb-settings {
margin-top: 0;
padding: 0 16px 8px;
}
}
}
}
}
@ -42,5 +72,49 @@
}
}
}
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
padding: 0;
color: rgba(0, 0, 0, 0.87);
&:hover {
background: none;
}
.mat-expansion-indicator {
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
.mat-slide {
margin: 8px 0;
}
.mat-slide-toggle-content {
white-space: normal;
}
}
}

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

@ -18,7 +18,13 @@ 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 {
ComparisonResultType, comparisonResultTypeTranslationMap,
DataKey,
dataKeyAggregationTypeHintTranslationMap,
Widget,
widgetType
} from '@shared/models/widget.models';
import {
ControlValueAccessor,
FormBuilder,
@ -43,7 +49,7 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co
import { WidgetService } from '@core/http/widget.service';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { aggregationTranslations, AggregationType } from '@shared/models/time/time.models';
import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models';
@Component({
selector: 'tb-data-key-config',
@ -76,6 +82,12 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap;
comparisonResultTypes = ComparisonResultType;
comparisonResults = Object.keys(ComparisonResultType);
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
entityAliasId: string;
@ -170,6 +182,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
this.dataKeyFormGroup = this.fb.group({
name: [null, []],
aggregationType: [null, []],
comparisonEnabled: [null, []],
timeForComparison: [null, [Validators.required]],
comparisonCustomIntervalValue: [null, [Validators.required, Validators.min(1000)]],
comparisonResultType: [null, [Validators.required]],
label: [null, [Validators.required]],
color: [null, [Validators.required]],
units: [null, []],
@ -189,6 +205,19 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
this.dataKeyFormGroup.get('label').patchValue(newLabel);
}
this.updateComparisonValidators();
}
);
this.dataKeyFormGroup.get('comparisonEnabled').valueChanges.subscribe(
() => {
this.updateComparisonValues();
}
);
this.dataKeyFormGroup.get('timeForComparison').valueChanges.subscribe(
() => {
this.updateComparisonValues();
}
);
@ -231,21 +260,82 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
this.modelValue.aggregationType = AggregationType.NONE;
}
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.updateValidators();
if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData
}, {emitEvent: false});
}
}
private updateValidators() {
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count
? [Validators.required] : []);
this.modelValue.type !== DataKeyType.count
? [Validators.required] : []);
if (this.modelValue.type === DataKeyType.count) {
this.dataKeyFormGroup.get('name').disable({emitEvent: false});
} else {
this.dataKeyFormGroup.get('name').enable({emitEvent: false});
}
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData
}, {emitEvent: false});
this.updateComparisonValidators();
}
private updateComparisonValues() {
const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value;
if (comparisonEnabled) {
const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value;
if (!timeForComparison) {
this.dataKeyFormGroup.get('timeForComparison').patchValue('previousInterval', {emitEvent: false});
} else if (timeForComparison === 'customInterval') {
const comparisonCustomIntervalValue = this.dataKeyFormGroup.get('comparisonCustomIntervalValue').value;
if (!comparisonCustomIntervalValue) {
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').patchValue(7200000, {emitEvent: false});
}
}
const comparisonResultType: ComparisonResultType = this.dataKeyFormGroup.get('comparisonResultType').value;
if (!comparisonResultType) {
this.dataKeyFormGroup.get('comparisonResultType').patchValue(ComparisonResultType.DELTA_ABSOLUTE, {emitEvent: false});
}
}
this.updateComparisonValidators();
}
private updateComparisonValidators() {
const aggregationType: AggregationType = this.dataKeyFormGroup.get('aggregationType').value;
if (aggregationType && aggregationType !== AggregationType.NONE) {
this.dataKeyFormGroup.get('comparisonEnabled').enable({emitEvent: false});
const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value;
if (comparisonEnabled) {
this.dataKeyFormGroup.get('timeForComparison').enable({emitEvent: false});
const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value;
if (timeForComparison) {
this.dataKeyFormGroup.get('comparisonResultType').enable({emitEvent: false});
if (timeForComparison === 'customInterval') {
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').enable({emitEvent: false});
} else {
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
} else {
this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
} else {
this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
} else {
this.dataKeyFormGroup.get('comparisonEnabled').disable({emitEvent: false});
this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
this.dataKeyFormGroup.get('comparisonEnabled').updateValueAndValidity({emitEvent: false});
this.dataKeyFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false});
this.dataKeyFormGroup.get('comparisonResultType').updateValueAndValidity({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false});
}
private updateModel() {

7
ui-ngx/src/app/shared/models/query/query.models.ts

@ -763,11 +763,16 @@ export interface TsValue {
count?: number;
}
export interface ComparisonTsValue {
current?: TsValue;
previous?: TsValue;
}
export interface EntityData {
entityId: EntityId;
latest: {[entityKeyType: string]: {[key: string]: TsValue}};
timeseries: {[key: string]: Array<TsValue>};
aggLatest?: {[aggType: string]: {[key: string]: TsValue}};
aggLatest?: {[id: number]: ComparisonTsValue};
}
export interface AlarmData extends AlarmInfo {

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

@ -174,8 +174,12 @@ export interface TimeSeriesCmd {
}
export interface AggKey {
id: number;
key: string;
agg: AggregationType;
previousStartTs?: number;
previousEndTs?: number;
previousValueOnly?: boolean;
}
export interface AggEntityHistoryCmd {
@ -314,8 +318,8 @@ export interface SubscriptionData {
[key: string]: [number, any, number?][];
}
export interface AggSubscriptionData {
[aggType: string]: SubscriptionData;
export interface IndexedSubscriptionData {
[id: number]: [number, any, number?][];
}
export interface SubscriptionDataHolder {

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

@ -17,7 +17,7 @@
import { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { AggregationType, Timewindow } from '@shared/models/time/time.models';
import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.models';
@ -259,9 +259,27 @@ export function defaultLegendConfig(wType: widgetType): LegendConfig {
};
}
export enum ComparisonResultType {
PREVIOUS_VALUE = 'PREVIOUS_VALUE',
DELTA_ABSOLUTE = 'DELTA_ABSOLUTE',
DELTA_PERCENT = 'DELTA_PERCENT'
}
export const comparisonResultTypeTranslationMap = new Map<ComparisonResultType, string>(
[
[ComparisonResultType.PREVIOUS_VALUE, 'datakey.comparison-result-previous-value'],
[ComparisonResultType.DELTA_ABSOLUTE, 'datakey.comparison-result-delta-absolute'],
[ComparisonResultType.DELTA_PERCENT, 'datakey.comparison-result-delta-percent']
]
);
export interface KeyInfo {
name: string;
aggregationType?: AggregationType;
comparisonEnabled?: boolean;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType;
label?: string;
color?: string;
funcBody?: string;

6
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1047,7 +1047,11 @@
"aggregation-type-max-hint": "Take max value",
"aggregation-type-avg-hint": "Calculate average value",
"aggregation-type-sum-hint": "Calculate sum value",
"aggregation-type-count-hint": "Calculate count value"
"aggregation-type-count-hint": "Calculate count value",
"comparison-result": "Comparison result",
"comparison-result-previous-value": "Previous value",
"comparison-result-delta-absolute": "Delta (absolute)",
"comparison-result-delta-percent": "Delta (percent)"
},
"datasource": {
"type": "Datasource type",

Loading…
Cancel
Save