UI: Add aggregation configuration to TimeSeries dataKey for latest widget
This commit is contained in:
parent
6445fec439
commit
9f178105cf
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
[dashboard]="data.dashboard"
|
||||
[aliasController]="data.aliasController"
|
||||
[widget]="data.widget"
|
||||
[widgetType]="data.widgetType"
|
||||
[showPostProcessing]="data.showPostProcessing"
|
||||
[callbacks]="data.callbacks"
|
||||
formControlName="dataKey">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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/>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -65,4 +65,11 @@
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-chip-label {
|
||||
.tb-agg-func {
|
||||
font-style: italic;
|
||||
color: #0c959c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user