UI: Add aggregation configuration to TimeSeries dataKey for latest widget

This commit is contained in:
Igor Kulikov 2022-09-08 18:51:13 +03:00
parent 6445fec439
commit 9f178105cf
16 changed files with 246 additions and 48 deletions

View File

@ -28,6 +28,7 @@ import {
TsValue TsValue
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { import {
AggKey,
DataKeyType, DataKeyType,
EntityCountCmd, EntityCountCmd,
EntityDataCmd, EntityDataCmd,
@ -55,6 +56,7 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number,
export interface SubscriptionDataKey { export interface SubscriptionDataKey {
name: string; name: string;
type: DataKeyType; type: DataKeyType;
aggregationType?: AggregationType;
funcBody: string; funcBody: string;
func?: DataKeyFunction; func?: DataKeyFunction;
postFuncBody: string; postFuncBody: string;
@ -95,6 +97,7 @@ export class EntityDataSubscription {
private attrFields: Array<EntityKey>; private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>; private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>; private latestValues: Array<EntityKey>;
private aggTsValues: Array<AggKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>; private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>; private pageData: PageData<EntityData>;
@ -142,7 +145,8 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
} else { } else {
key = `${dataKey.name}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; const aggSuffix = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE ? `_${dataKey.aggregationType.toLowerCase()}` : '';
key = `${dataKey.name}_${dataKey.type}${aggSuffix}${dataKey.latest ? '_latest' : ''}`;
} }
let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>; let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
if (!dataKeysList) { if (!dataKeysList) {
@ -224,13 +228,15 @@ export class EntityDataSubscription {
); );
this.tsFields = this.entityDataSubscriptionOptions.dataKeys. this.tsFields = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries && !dataKey.latest).map( filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
); );
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const latestTsFields = this.entityDataSubscriptionOptions.dataKeys. const latestTsFields = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest).map( filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
); );
this.latestValues = this.attrFields.concat(latestTsFields); this.latestValues = this.attrFields.concat(latestTsFields);
@ -238,6 +244,12 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields); this.latestValues = this.attrFields.concat(this.tsFields);
} }
this.aggTsValues = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE).map(
dataKey => ({ key: dataKey.name, agg: dataKey.aggregationType })
);
this.subscriber = new TelemetrySubscriber(this.telemetryService); this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd(); this.dataCommand = new EntityDataCmd();
@ -498,6 +510,21 @@ export class EntityDataSubscription {
}; };
} }
} }
if (this.aggTsValues.length > 0) {
if (this.history) {
cmd.aggHistoryCmd = {
keys: this.aggTsValues,
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs
};
} else {
cmd.aggTsCmd = {
keys: this.aggTsValues,
startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow
};
}
}
} }
private startFunction() { private startFunction() {

View File

@ -31,6 +31,7 @@ import { Observable, of } from 'rxjs';
export interface EntityDataListener { export interface EntityDataListener {
subscriptionType: widgetType; subscriptionType: widgetType;
useTimewindow?: boolean;
subscriptionTimewindow?: SubscriptionTimewindow; subscriptionTimewindow?: SubscriptionTimewindow;
latestTsOffset?: number; latestTsOffset?: number;
configDatasource: Datasource; configDatasource: Datasource;
@ -93,10 +94,10 @@ export class EntityDataService {
public startSubscription(listener: EntityDataListener) { public startSubscription(listener: EntityDataListener) {
if (listener.subscription) { if (listener.subscription) {
if (listener.subscriptionType === widgetType.timeseries) { if (listener.useTimewindow) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; }
} else if (listener.subscriptionType === widgetType.latest) { if (listener.subscriptionType === widgetType.timeseries || listener.subscriptionType === widgetType.latest) {
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} }
listener.subscription.start(); listener.subscription.start();
@ -122,10 +123,10 @@ export class EntityDataService {
return of(null); return of(null);
} }
listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils);
if (listener.subscriptionType === widgetType.timeseries) { if (listener.useTimewindow) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; }
} else if (listener.subscriptionType === widgetType.latest) { if (listener.subscriptionType === widgetType.timeseries || listener.subscriptionType === widgetType.latest) {
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} }
return listener.subscription.subscribe(); return listener.subscription.subscribe();
@ -176,6 +177,7 @@ export class EntityDataService {
return { return {
name: dataKey.name, name: dataKey.name,
type: dataKey.type, type: dataKey.type,
aggregationType: dataKey.aggregationType,
funcBody: dataKey.funcBody, funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody, postFuncBody: dataKey.postFuncBody,
latest latest

View File

@ -28,6 +28,7 @@ import {
DataSetHolder, DataSetHolder,
Datasource, Datasource,
DatasourceData, DatasourceData,
datasourcesHasAggregation,
DatasourceType, DatasourceType,
LegendConfig, LegendConfig,
LegendData, LegendData,
@ -95,6 +96,7 @@ export class WidgetSubscription implements IWidgetSubscription {
timezone: string; timezone: string;
subscriptionTimewindow: SubscriptionTimewindow; subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean; useDashboardTimewindow: boolean;
useTimewindow: boolean;
tsOffset = 0; tsOffset = 0;
hasDataPageLink: boolean; hasDataPageLink: boolean;
@ -200,6 +202,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.originalTimewindow = null; this.originalTimewindow = null;
this.timeWindow = {}; this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow; this.useDashboardTimewindow = options.useDashboardTimewindow;
this.useTimewindow = true;
if (this.useDashboardTimewindow) { if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow); this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else { } else {
@ -245,15 +248,16 @@ export class WidgetSubscription implements IWidgetSubscription {
this.timeWindow = {}; this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow; this.useDashboardTimewindow = options.useDashboardTimewindow;
this.stateData = options.stateData; this.stateData = options.stateData;
if (this.type === widgetType.latest) { this.useTimewindow = this.type === widgetType.timeseries || datasourcesHasAggregation(this.configuredDatasources);
this.timezone = options.dashboardTimewindow.timezone;
this.updateTsOffset();
}
if (this.useDashboardTimewindow) { if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow); this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else { } else {
this.timeWindowConfig = deepClone(options.timeWindowConfig); this.timeWindowConfig = deepClone(options.timeWindowConfig);
} }
if (this.type === widgetType.latest) {
this.timezone = this.useTimewindow ? this.timeWindowConfig.timezone : options.dashboardTimewindow.timezone;
this.updateTsOffset();
}
this.subscriptionTimewindow = null; this.subscriptionTimewindow = null;
this.comparisonEnabled = options.comparisonEnabled && isHistoryTypeTimewindow(this.timeWindowConfig); this.comparisonEnabled = options.comparisonEnabled && isHistoryTypeTimewindow(this.timeWindowConfig);
@ -443,6 +447,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const resolveResultObservables = this.configuredDatasources.map((datasource, index) => { const resolveResultObservables = this.configuredDatasources.map((datasource, index) => {
const listener: EntityDataListener = { const listener: EntityDataListener = {
subscriptionType: this.type, subscriptionType: this.type,
useTimewindow: this.useTimewindow,
configDatasource: datasource, configDatasource: datasource,
configDatasourceIndex: index, configDatasourceIndex: index,
dataLoaded: (pageData, data1, datasourceIndex, pageLink) => { dataLoaded: (pageData, data1, datasourceIndex, pageLink) => {
@ -626,22 +631,31 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) { onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) { let doUpdate = false;
let isTimewindowTypeChanged = false;
if (this.useTimewindow) {
if (this.useDashboardTimewindow) { if (this.useDashboardTimewindow) {
if (this.type === widgetType.latest) {
if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
this.timezone = newDashboardTimewindow.timezone;
doUpdate = this.updateTsOffset();
}
}
if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow); isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow);
this.timeWindowConfig = deepClone(newDashboardTimewindow); this.timeWindowConfig = deepClone(newDashboardTimewindow);
this.update(isTimewindowTypeChanged); doUpdate = true;
} }
} }
} else if (this.type === widgetType.latest) { } else if (this.type === widgetType.latest) {
if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) { if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
this.timezone = newDashboardTimewindow.timezone; this.timezone = newDashboardTimewindow.timezone;
if (this.updateTsOffset()) { doUpdate = this.updateTsOffset();
this.update();
}
} }
} }
if (doUpdate) {
this.update(isTimewindowTypeChanged);
}
} }
updateDataVisibility(index: number): void { updateDataVisibility(index: number): void {
@ -660,6 +674,12 @@ export class WidgetSubscription implements IWidgetSubscription {
updateTimewindowConfig(newTimewindow: Timewindow): void { updateTimewindowConfig(newTimewindow: Timewindow): void {
if (!this.useDashboardTimewindow) { if (!this.useDashboardTimewindow) {
if (this.type === widgetType.latest) {
if (newTimewindow && this.timezone !== newTimewindow.timezone) {
this.timezone = newTimewindow.timezone;
this.updateTsOffset();
}
}
const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newTimewindow); const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newTimewindow);
this.timeWindowConfig = newTimewindow; this.timeWindowConfig = newTimewindow;
this.update(isTimewindowTypeChanged); this.update(isTimewindowTypeChanged);
@ -874,11 +894,12 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
const datasource = this.configuredDatasources[datasourceIndex]; const datasource = this.configuredDatasources[datasourceIndex];
if (datasource) { if (datasource) {
if (this.type === widgetType.timeseries && this.timeWindowConfig) { if (this.useTimewindow && this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
} }
entityDataListener = { entityDataListener = {
subscriptionType: this.type, subscriptionType: this.type,
useTimewindow: this.useTimewindow,
configDatasource: datasource, configDatasource: datasource,
configDatasourceIndex: datasourceIndex, configDatasourceIndex: datasourceIndex,
subscriptionTimewindow: this.subscriptionTimewindow, subscriptionTimewindow: this.subscriptionTimewindow,
@ -940,7 +961,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateDataTimewindow() { private updateDataTimewindow() {
if (!this.hasDataPageLink) { if (!this.hasDataPageLink) {
if (this.type === widgetType.timeseries && this.timeWindowConfig) { if (this.useTimewindow && this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
if (this.comparisonEnabled) { if (this.comparisonEnabled) {
this.updateSubscriptionForComparison(); this.updateSubscriptionForComparison();
@ -952,11 +973,11 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataSubscribe() { private dataSubscribe() {
this.updateDataTimewindow(); this.updateDataTimewindow();
if (!this.hasDataPageLink) { if (!this.hasDataPageLink) {
if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) { if (this.useTimewindow && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated(); this.onDataUpdated();
} }
const forceUpdate = !this.datasources.length; const forceUpdate = !this.datasources.length;
const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length; const notifyDataLoaded = !this.entityDataListeners.filter((listener) => !!listener.subscription).length;
this.entityDataListeners.forEach((listener) => { this.entityDataListeners.forEach((listener) => {
if (this.comparisonEnabled && listener.configDatasource.isAdditional) { if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
listener.subscriptionTimewindow = this.timewindowForComparison; listener.subscriptionTimewindow = this.timewindowForComparison;

View File

@ -36,6 +36,7 @@
[dashboard]="data.dashboard" [dashboard]="data.dashboard"
[aliasController]="data.aliasController" [aliasController]="data.aliasController"
[widget]="data.widget" [widget]="data.widget"
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing" [showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks" [callbacks]="data.callbacks"
formControlName="dataKey"> formControlName="dataKey">

View File

@ -22,7 +22,7 @@ import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, Widget } from '@shared/models/widget.models'; import { DataKey, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
@ -35,6 +35,7 @@ export interface DataKeyConfigDialogData {
dashboard: Dashboard; dashboard: Dashboard;
aliasController: IAliasController; aliasController: IAliasController;
widget: Widget; widget: Widget;
widgetType: widgetType;
entityAliasId?: string; entityAliasId?: string;
showPostProcessing?: boolean; showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks; callbacks?: DataKeysCallbacks;

View File

@ -62,6 +62,15 @@
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field> </mat-form-field>
</div> </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">
{{ 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 fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function"> <section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function">
<span translate>datakey.data-generation-func</span> <span translate>datakey.data-generation-func</span>
<br/> <br/>

View File

@ -18,7 +18,12 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { DataKey, Widget } from '@shared/models/widget.models'; import {
DataKey,
dataKeyAggregationTypeHintTranslationMap,
Widget,
widgetType
} from '@shared/models/widget.models';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -43,6 +48,7 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { aggregationTranslations, AggregationType } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-data-key-config', selector: 'tb-data-key-config',
@ -65,6 +71,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
dataKeyTypes = DataKeyType; dataKeyTypes = DataKeyType;
widgetTypes = widgetType;
aggregations = [AggregationType.NONE, ...Object.keys(AggregationType).filter(type => type !== AggregationType.NONE)];
aggregationTypes = AggregationType;
aggregationTypesTranslations = aggregationTranslations;
dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap;
@Input() @Input()
entityAliasId: string; entityAliasId: string;
@ -80,6 +96,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input() @Input()
widget: Widget; widget: Widget;
@Input()
widgetType: widgetType;
@Input() @Input()
dataKeySettingsSchema: any; dataKeySettingsSchema: any;
@ -155,6 +174,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
this.dataKeyFormGroup = this.fb.group({ this.dataKeyFormGroup = this.fb.group({
name: [null, []], name: [null, []],
aggregationType: [null, []],
label: [null, [Validators.required]], label: [null, [Validators.required]],
color: [null, [Validators.required]], color: [null, [Validators.required]],
units: [null, []], units: [null, []],
@ -199,6 +219,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) {
this.modelValue.usePostProcessing = true; this.modelValue.usePostProcessing = true;
} }
if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) {
this.modelValue.aggregationType = AggregationType.NONE;
}
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function && this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count this.modelValue.type !== DataKeyType.count

View File

@ -58,12 +58,7 @@
{{key.label}} {{key.label}}
</div> </div>
<div class="tb-chip-separator">: </div> <div class="tb-chip-separator">: </div>
<div class="tb-chip-label"> <div class="tb-chip-label" [innerHTML]="displayDataKeyNameFn(key)"></div>
<strong *ngIf="datasourceType !== datasourceTypes.function && key.postFuncBody; else simpleChipLabel">f({{key.name}})</strong>
<ng-template #simpleChipLabel>
<strong>{{key.name}}</strong>
</ng-template>
</div>
</div> </div>
<button *ngIf="!disabled" <button *ngIf="!disabled"
type="button" type="button"

View File

@ -65,4 +65,11 @@
border-top: 0; border-top: 0;
} }
} }
.tb-chip-label {
.tb-agg-func {
font-style: italic;
color: #0c959c;
}
}
} }

View File

@ -46,7 +46,7 @@ import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, DatasourceType, Widget, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeysCallbacks } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
@ -62,6 +62,8 @@ import {
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { AggregationType } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-data-keys', selector: 'tb-data-keys',
@ -173,6 +175,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private dialogs: DialogService, private dialogs: DialogService,
private dialog: MatDialog, private dialog: MatDialog,
private fb: FormBuilder, private fb: FormBuilder,
private sanitizer: DomSanitizer,
public truncate: TruncatePipe) { public truncate: TruncatePipe) {
} }
@ -424,6 +427,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dashboard: this.dashboard, dashboard: this.dashboard,
aliasController: this.aliasController, aliasController: this.aliasController,
widget: this.widget, widget: this.widget,
widgetType: this.widgetType,
entityAliasId: this.entityAliasId, entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm, showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks callbacks: this.callbacks
@ -446,6 +450,36 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
return key ? key.name : undefined; return key ? key.name : undefined;
} }
displayDataKeyNameFn(key: DataKey): SafeHtml {
let keyName = key.name;
if (this.widgetType === widgetType.latest && key.type === DataKeyType.timeseries
&& key.aggregationType && key.aggregationType !== AggregationType.NONE) {
let aggFuncName: string;
switch (key.aggregationType) {
case AggregationType.MIN:
aggFuncName = 'MIN';
break;
case AggregationType.MAX:
aggFuncName = 'MAX';
break;
case AggregationType.AVG:
aggFuncName = 'AVG';
break;
case AggregationType.SUM:
aggFuncName = 'SUM';
break;
case AggregationType.COUNT:
aggFuncName = 'COUNT';
break;
}
keyName = `<span class="tb-agg-func">${aggFuncName}</span>(${keyName})`;
}
if (this.datasourceType !== DatasourceType.function && key.postFuncBody) {
keyName = `f(${keyName})`;
}
return this.sanitizer.bypassSecurityTrustHtml(`<strong>${keyName}</strong>`);
}
private fetchKeys(searchText?: string): Observable<Array<DataKey>> { private fetchKeys(searchText?: string): Observable<Array<DataKey>> {
if (this.searchText !== searchText || this.latestSearchTextResult === null) { if (this.searchText !== searchText || this.latestSearchTextResult === null) {
this.searchText = searchText; this.searchText = searchText;

View File

@ -18,7 +18,7 @@
<mat-tab-group class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab"> <mat-tab-group class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static"> <mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px"> <div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div *ngIf="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm" fxFlex="100" <div *ngIf="displayTimewindowConfig()" fxFlex="100"
fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"> fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs> <div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow"> <mat-checkbox formControlName="useDashboardTimewindow">

View File

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
DataKey, DataKey,
Datasource, Datasource, datasourcesHasAggregation,
DatasourceType, DatasourceType,
datasourceTypeTranslationMap, datasourceTypeTranslationMap,
defaultLegendConfig, defaultLegendConfig,
@ -42,7 +42,7 @@ import {
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils'; import { deepClone, isDefined, isObject } from '@app/core/utils';
import { import {
alarmFields, alarmFields,
AlarmSearchStatus, AlarmSearchStatus,
@ -51,7 +51,7 @@ import {
alarmSeverityTranslations alarmSeverityTranslations
} from '@shared/models/alarm.models'; } from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; import { EntityAlias } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -67,13 +67,14 @@ import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { WidgetActionsData } from './action/manage-widget-actions.component.models'; import { WidgetActionsData } from './action/manage-widget-actions.component.models';
import { Dashboard, DashboardState } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { Filter, Filters } from '@shared/models/query/query.models'; import { Filter } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { AggregationType } from '@shared/models/time/time.models';
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -334,10 +335,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({}); this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true));
this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true}));
this.dataSettings.addControl('timewindow', this.fb.control(null)); this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true}));
this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => { this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
if (value) { if (value) {
this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
@ -467,7 +468,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}, },
{emitEvent: false} {emitEvent: false}
); );
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true; config.useDashboardTimewindow : true;
this.dataSettings.patchValue( this.dataSettings.patchValue(
@ -733,6 +734,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length);
} }
public displayTimewindowConfig(): boolean {
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
return true;
} else if (this.widgetType === widgetType.latest) {
const datasources = this.dataSettings.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
}
public onDatasourceDrop(event: CdkDragDrop<string[]>) { public onDatasourceDrop(event: CdkDragDrop<string[]>) {
const datasourcesFormArray = this.datasourcesFormArray(); const datasourcesFormArray = this.datasourcesFormArray();
const datasourceForm = datasourcesFormArray.at(event.previousIndex); const datasourceForm = datasourcesFormArray.at(event.previousIndex);

View File

@ -15,16 +15,24 @@
/// ///
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { FormattedData, Widget, WidgetPosition, widgetType } from '@app/shared/models/widget.models'; import {
Datasource,
datasourcesHasAggregation,
FormattedData,
Widget,
WidgetPosition,
widgetType
} from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models'; import { AggregationType, Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils'; import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core'; import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable'; import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
export interface WidgetsData { export interface WidgetsData {
widgets: Array<Widget>; widgets: Array<Widget>;
@ -420,7 +428,14 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true; this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true; this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true;
this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ? let canHaveTimewindow = 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);
}
this.hasTimewindow = canHaveTimewindow ?
(isDefined(this.widget.config.useDashboardTimewindow) ? (isDefined(this.widget.config.useDashboardTimewindow) ?
(!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow) (!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow)
|| this.widget.config.displayTimewindow)) : false) || this.widget.config.displayTimewindow)) : false)

View File

@ -173,12 +173,31 @@ export interface TimeSeriesCmd {
fetchLatestPreviousPoint?: boolean; fetchLatestPreviousPoint?: boolean;
} }
export interface AggKey {
key: string;
agg: AggregationType;
}
export interface AggEntityHistoryCmd {
keys: Array<AggKey>;
startTs: number;
endTs: number;
}
export interface AggTimeSeriesCmd {
keys: Array<AggKey>;
startTs: number;
timeWindow: number;
}
export class EntityDataCmd implements WebsocketCmd { export class EntityDataCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
query?: EntityDataQuery; query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd; historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd; latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd; tsCmd?: TimeSeriesCmd;
aggHistoryCmd?: AggEntityHistoryCmd;
aggTsCmd?: AggTimeSeriesCmd;
public isEmpty(): boolean { public isEmpty(): boolean {
return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd; return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd;

View File

@ -17,7 +17,7 @@
import { BaseData } from '@shared/models/base-data'; import { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { Timewindow } from '@shared/models/time/time.models'; import { AggregationType, Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models'; import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.models'; import { DataKeyType } from './telemetry/telemetry.models';
@ -261,6 +261,7 @@ export function defaultLegendConfig(wType: widgetType): LegendConfig {
export interface KeyInfo { export interface KeyInfo {
name: string; name: string;
aggregationType?: AggregationType;
label?: string; label?: string;
color?: string; color?: string;
funcBody?: string; funcBody?: string;
@ -269,6 +270,18 @@ export interface KeyInfo {
decimals?: number; decimals?: number;
} }
export const dataKeyAggregationTypeHintTranslationMap = new Map<AggregationType, string>(
[
[AggregationType.MIN, 'datakey.aggregation-type-min-hint'],
[AggregationType.MAX, 'datakey.aggregation-type-max-hint'],
[AggregationType.AVG, 'datakey.aggregation-type-avg-hint'],
[AggregationType.SUM, 'datakey.aggregation-type-sum-hint'],
[AggregationType.COUNT, 'datakey.aggregation-type-count-hint'],
[AggregationType.NONE, 'datakey.aggregation-type-none-hint'],
]
);
export interface DataKey extends KeyInfo { export interface DataKey extends KeyInfo {
type: DataKeyType; type: DataKeyType;
pattern?: string; pattern?: string;
@ -322,6 +335,20 @@ export interface Datasource {
[key: string]: any; [key: string]: any;
} }
export function datasourcesHasAggregation(datasources?: Array<Datasource>): boolean {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
const found = datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
key.aggregationType && key.aggregationType !== AggregationType.NONE);
return !!found;
});
if (foundDatasource) {
return true;
}
}
return false;
}
export interface FormattedData { export interface FormattedData {
$datasource: Datasource; $datasource: Datasource;
entityName: string; entityName: string;

View File

@ -1039,7 +1039,14 @@
"value-description": "the current value;", "value-description": "the current value;",
"prev-value-description": "result of the previous function call;", "prev-value-description": "result of the previous function call;",
"time-prev-description": "timestamp of the previous value;", "time-prev-description": "timestamp of the previous value;",
"prev-orig-value-description": "original previous value;" "prev-orig-value-description": "original previous value;",
"aggregation-type": "Aggregation type",
"aggregation-type-none-hint": "Take latest value",
"aggregation-type-min-hint": "Take min value",
"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"
}, },
"datasource": { "datasource": {
"type": "Datasource type", "type": "Datasource type",