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
} from '@shared/models/query/query.models';
import {
AggKey,
DataKeyType,
EntityCountCmd,
EntityDataCmd,
@ -55,6 +56,7 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number,
export interface SubscriptionDataKey {
name: string;
type: DataKeyType;
aggregationType?: AggregationType;
funcBody: string;
func?: DataKeyFunction;
postFuncBody: string;
@ -95,6 +97,7 @@ export class EntityDataSubscription {
private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>;
private aggTsValues: Array<AggKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
@ -142,7 +145,8 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
} 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>;
if (!dataKeysList) {
@ -224,13 +228,15 @@ export class EntityDataSubscription {
);
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 })
);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
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 })
);
this.latestValues = this.attrFields.concat(latestTsFields);
@ -238,6 +244,12 @@ export class EntityDataSubscription {
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.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() {

View File

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

View File

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

View File

@ -36,6 +36,7 @@
[dashboard]="data.dashboard"
[aliasController]="data.aliasController"
[widget]="data.widget"
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"
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 { Router } from '@angular/router';
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 { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models';
@ -35,6 +35,7 @@ export interface DataKeyConfigDialogData {
dashboard: Dashboard;
aliasController: IAliasController;
widget: Widget;
widgetType: widgetType;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;

View File

@ -62,6 +62,15 @@
<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">
{{ 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">
<span translate>datakey.data-generation-func</span>
<br/>

View File

@ -18,7 +18,12 @@ 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, Widget } from '@shared/models/widget.models';
import {
DataKey,
dataKeyAggregationTypeHintTranslationMap,
Widget,
widgetType
} from '@shared/models/widget.models';
import {
ControlValueAccessor,
FormBuilder,
@ -43,6 +48,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';
@Component({
selector: 'tb-data-key-config',
@ -65,6 +71,16 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
dataKeyTypes = DataKeyType;
widgetTypes = widgetType;
aggregations = [AggregationType.NONE, ...Object.keys(AggregationType).filter(type => type !== AggregationType.NONE)];
aggregationTypes = AggregationType;
aggregationTypesTranslations = aggregationTranslations;
dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap;
@Input()
entityAliasId: string;
@ -80,6 +96,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
widget: Widget;
@Input()
widgetType: widgetType;
@Input()
dataKeySettingsSchema: any;
@ -155,6 +174,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
this.dataKeyFormGroup = this.fb.group({
name: [null, []],
aggregationType: [null, []],
label: [null, [Validators.required]],
color: [null, [Validators.required]],
units: [null, []],
@ -199,6 +219,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) {
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.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count

View File

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

View File

@ -65,4 +65,11 @@
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 { coerceBooleanProperty } from '@angular/cdk/coercion';
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 { DataKeysCallbacks } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models';
@ -62,6 +62,8 @@ import {
import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
import { Dashboard } from '@shared/models/dashboard.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { AggregationType } from '@shared/models/time/time.models';
@Component({
selector: 'tb-data-keys',
@ -173,6 +175,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private dialogs: DialogService,
private dialog: MatDialog,
private fb: FormBuilder,
private sanitizer: DomSanitizer,
public truncate: TruncatePipe) {
}
@ -424,6 +427,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dashboard: this.dashboard,
aliasController: this.aliasController,
widget: this.widget,
widgetType: this.widgetType,
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks
@ -446,6 +450,36 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
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>> {
if (this.searchText !== searchText || this.latestSearchTextResult === null) {
this.searchText = searchText;

View File

@ -18,7 +18,7 @@
<mat-tab-group class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<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">
<div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow">

View File

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
DataKey,
Datasource,
Datasource, datasourcesHasAggregation,
DatasourceType,
datasourceTypeTranslationMap,
defaultLegendConfig,
@ -42,7 +42,7 @@ import {
Validators
} from '@angular/forms';
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 {
alarmFields,
AlarmSearchStatus,
@ -51,7 +51,7 @@ import {
alarmSeverityTranslations
} from '@shared/models/alarm.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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core';
@ -67,13 +67,14 @@ import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-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 { 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 { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { AggregationType } from '@shared/models/time/time.models';
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -334,10 +335,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
this.dataSettings.addControl('timewindow', this.fb.control(null));
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true));
this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true}));
this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true}));
this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
if (value) {
this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
@ -467,7 +468,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
},
{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) ?
config.useDashboardTimewindow : true;
this.dataSettings.patchValue(
@ -733,6 +734,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
!!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[]>) {
const datasourcesFormArray = this.datasourcesFormArray();
const datasourceForm = datasourcesFormArray.at(event.previousIndex);

View File

@ -15,16 +15,24 @@
///
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 { 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 { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
export interface WidgetsData {
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.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) ?
(!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow)
|| this.widget.config.displayTimewindow)) : false)

View File

@ -173,12 +173,31 @@ export interface TimeSeriesCmd {
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 {
cmdId: number;
query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd;
aggHistoryCmd?: AggEntityHistoryCmd;
aggTsCmd?: AggTimeSeriesCmd;
public isEmpty(): boolean {
return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd;

View File

@ -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 { 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 { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.models';
@ -261,6 +261,7 @@ export function defaultLegendConfig(wType: widgetType): LegendConfig {
export interface KeyInfo {
name: string;
aggregationType?: AggregationType;
label?: string;
color?: string;
funcBody?: string;
@ -269,6 +270,18 @@ export interface KeyInfo {
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 {
type: DataKeyType;
pattern?: string;
@ -322,6 +335,20 @@ export interface Datasource {
[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 {
$datasource: Datasource;
entityName: string;

View File

@ -1039,7 +1039,14 @@
"value-description": "the current value;",
"prev-value-description": "result of the previous function call;",
"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": {
"type": "Datasource type",