Browse Source

Merge branch 'vvlladd28-feature/timeWindow/quick-interval'

pull/4259/head
Igor Kulikov 6 years ago
parent
commit
7cfa352a60
  1. 74
      ui-ngx/src/app/core/api/data-aggregator.ts
  2. 36
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  3. 32
      ui-ngx/src/app/core/api/widget-subscription.ts
  4. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  5. 1
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  6. 12
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  7. 9
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  8. 27
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.html
  9. 19
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss
  10. 79
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  11. 68
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  12. 76
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  13. 28
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  14. 57
      ui-ngx/src/app/shared/components/time/timezone-select.component.ts
  15. 41
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  16. 377
      ui-ngx/src/app/shared/models/time/time.models.ts
  17. 3
      ui-ngx/src/app/shared/shared.module.ts
  18. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -15,7 +15,12 @@
/// ///
import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models'; import {
AggregationType,
calculateIntervalEndTime,
calculateIntervalStartTime, getCurrentTime,
QuickTimeInterval, SubscriptionTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@ -73,33 +78,29 @@ export class DataAggregator {
private resetPending = false; private resetPending = false;
private updatedData = false; private updatedData = false;
private noAggregation = this.aggregationType === AggregationType.NONE; private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE;
private aggregationTimeout = Math.max(this.interval, 1000); private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
private readonly aggFunction: AggFunction; private readonly aggFunction: AggFunction;
private intervalTimeoutHandle: Timeout; private intervalTimeoutHandle: Timeout;
private intervalScheduledTime: number; private intervalScheduledTime: number;
private startTs = this.subsTw.startTs + this.subsTw.tsOffset;
private endTs: number; private endTs: number;
private elapsed: number; private elapsed: number;
constructor(private onDataCb: onAggregatedData, constructor(private onDataCb: onAggregatedData,
private tsKeyNames: string[], private tsKeyNames: string[],
private startTs: number, private subsTw: SubscriptionTimewindow,
private limit: number,
private aggregationType: AggregationType,
private timeWindow: number,
private interval: number,
private stateData: boolean,
private utils: UtilsService, private utils: UtilsService,
private ignoreDataUpdateOnIntervalTick: boolean) { private ignoreDataUpdateOnIntervalTick: boolean) {
this.tsKeyNames.forEach((key) => { this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = []; this.dataBuffer[key] = [];
}); });
if (this.stateData) { if (this.subsTw.aggregation.stateData) {
this.lastPrevKvPairData = {}; this.lastPrevKvPairData = {};
} }
switch (this.aggregationType) { switch (this.subsTw.aggregation.type) {
case AggregationType.MIN: case AggregationType.MIN:
this.aggFunction = min; this.aggFunction = min;
break; break;
@ -129,18 +130,21 @@ export class DataAggregator {
return prevOnDataCb; return prevOnDataCb;
} }
public reset(startTs: number, timeWindow: number, interval: number) { public reset(subsTw: SubscriptionTimewindow) {
if (this.intervalTimeoutHandle) { if (this.intervalTimeoutHandle) {
clearTimeout(this.intervalTimeoutHandle); clearTimeout(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null; this.intervalTimeoutHandle = null;
} }
this.subsTw = subsTw;
this.intervalScheduledTime = this.utils.currentPerfTime(); this.intervalScheduledTime = this.utils.currentPerfTime();
this.startTs = startTs; this.startTs = this.subsTw.startTs + this.subsTw.tsOffset;
this.timeWindow = timeWindow; if (this.subsTw.quickInterval) {
this.interval = interval; this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset;
this.endTs = this.startTs + this.timeWindow; } else {
this.endTs = this.startTs + this.subsTw.aggregation.timeWindow;
}
this.elapsed = 0; this.elapsed = 0;
this.aggregationTimeout = Math.max(this.interval, 1000); this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
this.resetPending = true; this.resetPending = true;
this.updatedData = false; this.updatedData = false;
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
@ -161,7 +165,11 @@ export class DataAggregator {
if (!this.dataReceived) { if (!this.dataReceived) {
this.elapsed = 0; this.elapsed = 0;
this.dataReceived = true; this.dataReceived = true;
this.endTs = this.startTs + this.timeWindow; if (this.subsTw.quickInterval) {
this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset;
} else {
this.endTs = this.startTs + this.subsTw.aggregation.timeWindow;
}
} }
if (this.resetPending) { if (this.resetPending) {
this.resetPending = false; this.resetPending = false;
@ -195,12 +203,19 @@ export class DataAggregator {
this.intervalTimeoutHandle = null; this.intervalTimeoutHandle = null;
} }
if (!history) { if (!history) {
const delta = Math.floor(this.elapsed / this.interval); const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval);
if (delta || !this.data) { if (delta || !this.data) {
this.startTs += delta * this.interval; const tickTs = delta * this.subsTw.aggregation.interval;
this.endTs += delta * this.interval; if (this.subsTw.quickInterval) {
const currentDate = getCurrentTime(this.subsTw.timezone);
this.startTs = calculateIntervalStartTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
} else {
this.startTs += tickTs;
this.endTs += tickTs;
}
this.data = this.updateData(); this.data = this.updateData();
this.elapsed = this.elapsed - delta * this.interval; this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval;
} }
} else { } else {
this.data = this.updateData(); this.data = this.updateData();
@ -223,7 +238,7 @@ export class DataAggregator {
let keyData = this.dataBuffer[key]; let keyData = this.dataBuffer[key];
aggKeyData.forEach((aggData, aggTimestamp) => { aggKeyData.forEach((aggData, aggTimestamp) => {
if (aggTimestamp <= this.startTs) { if (aggTimestamp <= this.startTs) {
if (this.stateData && if (this.subsTw.aggregation.stateData &&
(!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
} }
@ -235,11 +250,11 @@ export class DataAggregator {
} }
}); });
keyData.sort((set1, set2) => set1[0] - set2[0]); keyData.sort((set1, set2) => set1[0] - set2[0]);
if (this.stateData) { if (this.subsTw.aggregation.stateData) {
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key])); this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key]));
} }
if (keyData.length > this.limit) { if (keyData.length > this.subsTw.aggregation.limit) {
keyData = keyData.slice(keyData.length - this.limit); keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit);
} }
this.dataBuffer[key] = keyData; this.dataBuffer[key] = keyData;
} }
@ -275,7 +290,7 @@ export class DataAggregator {
} }
private processAggregatedData(data: SubscriptionData): AggregationMap { private processAggregatedData(data: SubscriptionData): AggregationMap {
const isCount = this.aggregationType === AggregationType.COUNT; const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
const aggregationMap: AggregationMap = {}; const aggregationMap: AggregationMap = {};
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
let aggKeyData = aggregationMap[key]; let aggKeyData = aggregationMap[key];
@ -300,7 +315,7 @@ export class DataAggregator {
} }
private updateAggregatedData(data: SubscriptionData) { private updateAggregatedData(data: SubscriptionData) {
const isCount = this.aggregationType === AggregationType.COUNT; const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
let aggKeyData = this.aggregationMap[key]; let aggKeyData = this.aggregationMap[key];
if (!aggKeyData) { if (!aggKeyData) {
@ -312,7 +327,8 @@ export class DataAggregator {
const timestamp = kvPair[0]; const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]); const value = this.convertValue(kvPair[1]);
const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + const aggTimestamp = this.noAggregation ? timestamp : (this.startTs +
Math.floor((timestamp - this.startTs) / this.interval) * this.interval + this.interval / 2); Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) *
this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2);
let aggData = aggKeyData.get(aggTimestamp); let aggData = aggKeyData.get(aggTimestamp);
if (!aggData) { if (!aggData) {
aggData = { aggData = {

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

@ -237,7 +237,7 @@ export class EntityDataSubscription {
}; };
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.prepareSubscriptionCommands(this.dataCommand); this.prepareSubscriptionCommands();
} }
this.subscriber.subscriptionCommands.push(this.dataCommand); this.subscriber.subscriptionCommands.push(this.dataCommand);
@ -256,8 +256,8 @@ export class EntityDataSubscription {
if (this.started) { if (this.started) {
const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && if (this.entityDataSubscriptionOptions.type === widgetType.timeseries &&
!this.history && this.tsFields.length) { !this.history && this.tsFields.length) {
const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription(); const newSubsTw = this.listener.updateRealtimeSubscription();
this.subsTw = newSubsTw; this.subsTw = newSubsTw;
targetCommand.tsCmd.startTs = this.subsTw.startTs; targetCommand.tsCmd.startTs = this.subsTw.startTs;
targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
@ -266,9 +266,10 @@ export class EntityDataSubscription {
targetCommand.tsCmd.agg = this.subsTw.aggregation.type; targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
this.dataAggregators.forEach((dataAggregator) => { this.dataAggregators.forEach((dataAggregator) => {
dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); dataAggregator.reset(newSubsTw);
}); });
} }
this.subscriber.setTsOffset(this.subsTw.tsOffset);
targetCommand.query = this.dataCommand.query; targetCommand.query = this.dataCommand.query;
this.subscriber.subscriptionCommands = [targetCommand]; this.subscriber.subscriptionCommands = [targetCommand];
} else { } else {
@ -393,7 +394,7 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.entity) { if (this.datasourceType === DatasourceType.entity) {
this.subsCommand = new EntityDataCmd(); this.subsCommand = new EntityDataCmd();
this.subsCommand.cmdId = this.dataCommand.cmdId; this.subsCommand.cmdId = this.dataCommand.cmdId;
this.prepareSubscriptionCommands(this.subsCommand); this.prepareSubscriptionCommands();
if (!this.subsCommand.isEmpty()) { if (!this.subsCommand.isEmpty()) {
this.subscriber.subscriptionCommands = [this.subsCommand]; this.subscriber.subscriptionCommands = [this.subsCommand];
this.subscriber.update(); this.subscriber.update();
@ -404,11 +405,11 @@ export class EntityDataSubscription {
this.started = true; this.started = true;
} }
private prepareSubscriptionCommands(cmd: EntityDataCmd) { private prepareSubscriptionCommands() {
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.tsFields.length > 0) { if (this.tsFields.length > 0) {
if (this.history) { if (this.history) {
cmd.historyCmd = { this.subsCommand.historyCmd = {
keys: this.tsFields.map(key => key.key), keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.fixedWindow.startTimeMs, startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs, endTs: this.subsTw.fixedWindow.endTimeMs,
@ -418,7 +419,7 @@ export class EntityDataSubscription {
fetchLatestPreviousPoint: this.subsTw.aggregation.stateData fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
}; };
} else { } else {
cmd.tsCmd = { this.subsCommand.tsCmd = {
keys: this.tsFields.map(key => key.key), keys: this.tsFields.map(key => key.key),
startTs: this.subsTw.startTs, startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow, timeWindow: this.subsTw.aggregation.timeWindow,
@ -429,9 +430,10 @@ export class EntityDataSubscription {
}; };
} }
} }
this.subscriber.setTsOffset(this.subsTw.tsOffset);
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) { if (this.latestValues.length > 0) {
cmd.latestCmd = { this.subsCommand.latestCmd = {
keys: this.latestValues keys: this.latestValues
}; };
} }
@ -745,12 +747,7 @@ export class EntityDataSubscription {
this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb); this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb);
}, },
tsKeyNames, tsKeyNames,
subsTw.startTs, subsTw,
subsTw.aggregation.limit,
subsTw.aggregation.type,
subsTw.aggregation.timeWindow,
subsTw.aggregation.interval,
subsTw.aggregation.stateData,
this.utils, this.utils,
this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick
); );
@ -827,7 +824,8 @@ export class EntityDataSubscription {
startTime = dataKey.lastUpdateTime + this.frequency; startTime = dataKey.lastUpdateTime + this.frequency;
endTime = dataKey.lastUpdateTime + deltaElapsed; endTime = dataKey.lastUpdateTime + deltaElapsed;
} else { } else {
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs; startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency; endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) { if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit; const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
@ -835,8 +833,10 @@ export class EntityDataSubscription {
} }
} }
} else { } else {
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs; startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs +
endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs; this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
} }
} }
generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);

32
ui-ngx/src/app/core/api/widget-subscription.ts

@ -37,8 +37,10 @@ import {
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { import {
calculateIntervalEndTime,
calculateIntervalStartTime,
createSubscriptionTimewindow, createSubscriptionTimewindow,
createTimewindowForComparison, createTimewindowForComparison, getCurrentTime,
SubscriptionTimewindow, SubscriptionTimewindow,
Timewindow, Timewindow,
toHistoryTimewindow, toHistoryTimewindow,
@ -837,9 +839,11 @@ export class WidgetSubscription implements IWidgetSubscription {
if (this.alarmDataListener) { if (this.alarmDataListener) {
this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); this.ctx.alarmDataService.stopSubscription(this.alarmDataListener);
} }
if (this.timeWindowConfig) { if (this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
} }
this.alarmDataListener = { this.alarmDataListener = {
subscriptionTimewindow: this.subscriptionTimewindow, subscriptionTimewindow: this.subscriptionTimewindow,
alarmSource: this.alarmSource, alarmSource: this.alarmSource,
@ -1080,12 +1084,21 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateTimewindow() { private updateTimewindow() {
this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
this.timeWindow.timezone = this.subscriptionTimewindow.timezone;
if (this.subscriptionTimewindow.realtimeWindowMs) { if (this.subscriptionTimewindow.realtimeWindowMs) {
this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff; if (this.subscriptionTimewindow.quickInterval) {
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone);
this.timeWindow.maxTime = calculateIntervalEndTime(
this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
this.timeWindow.minTime = calculateIntervalStartTime(
this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
} else {
this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff;
this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
}
} else if (this.subscriptionTimewindow.fixedWindow) { } else if (this.subscriptionTimewindow.fixedWindow) {
this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs + this.subscriptionTimewindow.tsOffset;
this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs + this.subscriptionTimewindow.tsOffset;
} }
} }
@ -1103,12 +1116,13 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateComparisonTimewindow() { private updateComparisonTimewindow() {
this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000;
this.comparisonTimeWindow.timezone = this.timewindowForComparison.timezone;
if (this.timewindowForComparison.realtimeWindowMs) { if (this.timewindowForComparison.realtimeWindowMs) {
this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf();
this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs; this.comparisonTimeWindow.minTime = moment(this.timeWindow.minTime).subtract(1, this.timeForComparison).valueOf();
} else if (this.timewindowForComparison.fixedWindow) { } else if (this.timewindowForComparison.fixedWindow) {
this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs; this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs + this.timewindowForComparison.tsOffset;
this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs; this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs + this.timewindowForComparison.tsOffset;
} }
} }
@ -1335,7 +1349,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.onDataUpdated(); this.onDataUpdated();
} }
private alarmsUpdated(_updated: Array<AlarmData>, alarms: PageData<AlarmData>) { private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
this.alarmsLoaded(alarms, 0, 0); this.alarmsLoaded(alarms, 0, 0);
} }

1
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -91,6 +91,7 @@
direction="left" direction="left"
tooltipPosition="below" tooltipPosition="below"
aggregation="true" aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow"> [(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow> </tb-timewindow>
<tb-filters-edit [fxShow]="!isEdit && displayFilters()" <tb-filters-edit [fxShow]="!isEdit && displayFilters()"

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

@ -95,6 +95,7 @@
<tb-timewindow *ngIf="widget.hasTimewindow" <tb-timewindow *ngIf="widget.hasTimewindow"
#timewindowComponent #timewindowComponent
aggregation="{{widget.hasAggregation}}" aggregation="{{widget.hasAggregation}}"
timezone="true"
[isEdit]="isEdit" [isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow" [(ngModel)]="widgetComponent.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)"> (ngModelChange)="widgetComponent.onTimewindowChanged($event)">

12
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -55,7 +55,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogComponent } from './add-entity-dialog.component';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import {
calculateIntervalEndTime,
calculateIntervalStartTime,
getCurrentTime,
HistoryWindowType,
Timewindow
} from '@shared/models/time/time.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { isDefined, isUndefined } from '@core/utils'; import { isDefined, isUndefined } from '@core/utils';
@ -296,6 +302,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
const currentTime = Date.now(); const currentTime = Date.now();
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
timePageLink.endTime = currentTime; timePageLink.endTime = currentTime;
} else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) {
const currentDate = getCurrentTime();
timePageLink.startTime = calculateIntervalStartTime(this.timewindow.history.quickInterval, currentDate);
timePageLink.endTime = calculateIntervalEndTime(this.timewindow.history.quickInterval, currentDate);
} else { } else {
timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;

9
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts

@ -94,11 +94,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
}); });
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
getDefaultTimezone().subscribe((defaultTimezone) => { const defaultTimezone = getDefaultTimezone();
this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false});
this.updateValidators(type, true); this.updateValidators(type, true);
this.alarmScheduleForm.updateValueAndValidity(); this.alarmScheduleForm.updateValueAndValidity();
});
}); });
this.alarmScheduleForm.valueChanges.subscribe(() => { this.alarmScheduleForm.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();

27
ui-ngx/src/app/shared/components/time/quick-time-interval.component.html

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section class="interval-section" fxLayout="row" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>timewindow.interval</mat-label>
<mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
<mat-option *ngFor="let interval of intervals" [value]="interval">
{{ timeIntervalTranslationMap.get(interval) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</section>

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

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

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

@ -0,0 +1,79 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models';
@Component({
selector: 'tb-quick-time-interval',
templateUrl: './quick-time-interval.component.html',
styleUrls: ['./quick-time-interval.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => QuickTimeIntervalComponent),
multi: true
}
]
})
export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor {
private allIntervals = Object.values(QuickTimeInterval);
modelValue: QuickTimeInterval;
timeIntervalTranslationMap = QuickTimeIntervalTranslationMap;
rendered = false;
@Input() disabled: boolean;
@Input() onlyCurrentInterval = false;
private propagateChange = (_: any) => {};
constructor() {
}
get intervals() {
if (this.onlyCurrentInterval) {
return this.allIntervals.filter(interval => interval.startsWith('CURRENT_'));
}
return this.allIntervals;
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(interval: QuickTimeInterval): void {
this.modelValue = interval;
}
onIntervalChange() {
this.propagateChange(this.modelValue);
}
}

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

@ -21,16 +21,43 @@
<mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}"
(selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab">
<mat-tab label="{{ 'timewindow.realtime' | translate }}"> <mat-tab label="{{ 'timewindow.realtime' | translate }}">
<div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column"> <section fxLayout="row">
<tb-timeinterval <section *ngIf="isEdit" fxLayout="column" style="padding-top: 8px; padding-left: 16px;">
[(hideFlag)]="timewindow.hideInterval" <label class="tb-small hide-label" translate>timewindow.hide</label>
(hideFlagChange)="onHideIntervalChanged()" <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
[isEdit]="isEdit" (ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
formControlName="timewindowMs" </section>
predefinedName="timewindow.last" <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
[required]="timewindow.selectedTab === timewindowTypes.REALTIME" <div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
style="padding-top: 8px;"></tb-timeinterval> <mat-radio-group formControlName="realtimeType">
</div> <mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
</div>
</section>
</section>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'timewindow.history' | translate }}"> <mat-tab label="{{ 'timewindow.history' | translate }}">
<section fxLayout="row"> <section fxLayout="row">
@ -65,6 +92,17 @@
style="padding-top: 8px;"></tb-datetime-period> style="padding-top: 8px;"></tb-datetime-period>
</section> </section>
</mat-radio-button> </mat-radio-button>
<mat-radio-button [value]="historyTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group> </mat-radio-group>
</div> </div>
</section> </section>
@ -139,6 +177,16 @@
predefinedName="aggregation.group-interval"> predefinedName="aggregation.group-interval">
</tb-timeinterval> </tb-timeinterval>
</div> </div>
<div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row">
<section fxLayout="column" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone"
(ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
</section>
<tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
formControlName="timezone">
</tb-timezone-select>
</div>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button" <button type="button"
mat-button mat-button

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

@ -20,6 +20,8 @@ import {
AggregationType, AggregationType,
DAY, DAY,
HistoryWindowType, HistoryWindowType,
quickTimeIntervalPeriod,
RealtimeWindowType,
Timewindow, Timewindow,
TimewindowType TimewindowType
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
@ -36,6 +38,7 @@ export interface TimewindowPanelData {
historyOnly: boolean; historyOnly: boolean;
timewindow: Timewindow; timewindow: Timewindow;
aggregation: boolean; aggregation: boolean;
timezone: boolean;
isEdit: boolean; isEdit: boolean;
} }
@ -50,6 +53,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
aggregation = false; aggregation = false;
timezone = false;
isEdit = false; isEdit = false;
timewindow: Timewindow; timewindow: Timewindow;
@ -60,6 +65,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
historyTypes = HistoryWindowType; historyTypes = HistoryWindowType;
realtimeTypes = RealtimeWindowType;
timewindowTypes = TimewindowType; timewindowTypes = TimewindowType;
aggregationTypes = AggregationType; aggregationTypes = AggregationType;
@ -78,6 +85,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.historyOnly = data.historyOnly; this.historyOnly = data.historyOnly;
this.timewindow = data.timewindow; this.timewindow = data.timewindow;
this.aggregation = data.aggregation; this.aggregation = data.aggregation;
this.timezone = data.timezone;
this.isEdit = data.isEdit; this.isEdit = data.isEdit;
} }
@ -85,10 +93,16 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const hideInterval = this.timewindow.hideInterval || false; const hideInterval = this.timewindow.hideInterval || false;
const hideAggregation = this.timewindow.hideAggregation || false; const hideAggregation = this.timewindow.hideAggregation || false;
const hideAggInterval = this.timewindow.hideAggInterval || false; const hideAggInterval = this.timewindow.hideAggInterval || false;
const hideTimezone = this.timewindow.hideTimezone || false;
this.timewindowForm = this.fb.group({ this.timewindowForm = this.fb.group({
realtime: this.fb.group( realtime: this.fb.group(
{ {
realtimeType: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.realtimeType !== 'undefined'
? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: hideInterval
}),
timewindowMs: [ timewindowMs: [
this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
? this.timewindow.realtime.timewindowMs : null ? this.timewindow.realtime.timewindowMs : null
@ -96,7 +110,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
interval: [ interval: [
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
? this.timewindow.realtime.interval : null ? this.timewindow.realtime.interval : null
] ],
quickInterval: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined'
? this.timewindow.realtime.quickInterval : null,
disabled: hideInterval
})
} }
), ),
history: this.fb.group( history: this.fb.group(
@ -119,6 +138,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined'
? this.timewindow.history.fixedTimewindow : null, ? this.timewindow.history.fixedTimewindow : null,
disabled: hideInterval disabled: hideInterval
}),
quickInterval: this.fb.control({
value: this.timewindow.history && typeof this.timewindow.history.quickInterval !== 'undefined'
? this.timewindow.history.quickInterval : null,
disabled: hideInterval
}) })
} }
), ),
@ -135,21 +159,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
disabled: hideAggInterval disabled: hideAggInterval
}, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]) }, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())])
} }
) ),
timezone: this.fb.control({
value: this.timewindow.timezone !== 'undefined'
? this.timewindow.timezone : null,
disabled: hideTimezone
})
}); });
} }
update() { update() {
const timewindowFormValue = this.timewindowForm.getRawValue(); const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow.realtime = { this.timewindow.realtime = {
realtimeType: timewindowFormValue.realtime.realtimeType,
timewindowMs: timewindowFormValue.realtime.timewindowMs, timewindowMs: timewindowFormValue.realtime.timewindowMs,
quickInterval: timewindowFormValue.realtime.quickInterval,
interval: timewindowFormValue.realtime.interval interval: timewindowFormValue.realtime.interval
}; };
this.timewindow.history = { this.timewindow.history = {
historyType: timewindowFormValue.history.historyType, historyType: timewindowFormValue.history.historyType,
timewindowMs: timewindowFormValue.history.timewindowMs, timewindowMs: timewindowFormValue.history.timewindowMs,
interval: timewindowFormValue.history.interval, interval: timewindowFormValue.history.interval,
fixedTimewindow: timewindowFormValue.history.fixedTimewindow fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
quickInterval: timewindowFormValue.history.quickInterval,
}; };
if (this.aggregation) { if (this.aggregation) {
this.timewindow.aggregation = { this.timewindow.aggregation = {
@ -157,6 +189,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
limit: timewindowFormValue.aggregation.limit limit: timewindowFormValue.aggregation.limit
}; };
} }
if (this.timezone) {
this.timewindow.timezone = timewindowFormValue.timezone;
}
this.result = this.timewindow; this.result = this.timewindow;
this.overlayRef.dispose(); this.overlayRef.dispose();
} }
@ -174,11 +209,23 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
} }
minRealtimeAggInterval() { minRealtimeAggInterval() {
return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value); return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow());
} }
maxRealtimeAggInterval() { maxRealtimeAggInterval() {
return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value); return this.timeService.maxIntervalLimit(this.currentRealtimeTimewindow());
}
currentRealtimeTimewindow(): number {
const timeWindowFormValue = this.timewindowForm.getRawValue();
switch (timeWindowFormValue.realtime.realtimeType) {
case RealtimeWindowType.LAST_INTERVAL:
return timeWindowFormValue.realtime.timewindowMs;
case RealtimeWindowType.INTERVAL:
return quickTimeIntervalPeriod(timeWindowFormValue.realtime.quickInterval);
default:
return DAY;
}
} }
minHistoryAggInterval() { minHistoryAggInterval() {
@ -193,6 +240,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const timewindowFormValue = this.timewindowForm.getRawValue(); const timewindowFormValue = this.timewindowForm.getRawValue();
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
return timewindowFormValue.history.timewindowMs; return timewindowFormValue.history.timewindowMs;
} else if (timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL) {
return quickTimeIntervalPeriod(timewindowFormValue.history.quickInterval);
} else if (timewindowFormValue.history.fixedTimewindow) { } else if (timewindowFormValue.history.fixedTimewindow) {
return timewindowFormValue.history.fixedTimewindow.endTimeMs - return timewindowFormValue.history.fixedTimewindow.endTimeMs -
timewindowFormValue.history.fixedTimewindow.startTimeMs; timewindowFormValue.history.fixedTimewindow.startTimeMs;
@ -206,10 +255,18 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.timewindowForm.get('history.historyType').disable({emitEvent: false}); this.timewindowForm.get('history.historyType').disable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false}); this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false}); this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').disable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').disable({emitEvent: false});
this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false});
this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false});
} else { } else {
this.timewindowForm.get('history.historyType').enable({emitEvent: false}); this.timewindowForm.get('history.historyType').enable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false}); this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').enable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false});
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
} }
this.timewindowForm.markAsDirty(); this.timewindowForm.markAsDirty();
} }
@ -232,4 +289,13 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.timewindowForm.markAsDirty(); this.timewindowForm.markAsDirty();
} }
onHideTimezoneChanged() {
if (this.timewindow.hideTimezone) {
this.timewindowForm.get('timezone').disable({emitEvent: false});
} else {
this.timewindowForm.get('timezone').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
} }

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

@ -33,6 +33,8 @@ import {
cloneSelectedTimewindow, cloneSelectedTimewindow,
HistoryWindowType, HistoryWindowType,
initModelFromDefaultTimewindow, initModelFromDefaultTimewindow,
QuickTimeIntervalTranslationMap,
RealtimeWindowType,
Timewindow, Timewindow,
TimewindowType TimewindowType
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
@ -89,6 +91,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
return this.aggregationValue; return this.aggregationValue;
} }
timezoneValue = false;
@Input()
set timezone(val) {
this.timezoneValue = coerceBooleanProperty(val);
}
get timezone() {
return this.timezoneValue;
}
isToolbarValue = false; isToolbarValue = false;
@Input() @Input()
@ -169,7 +182,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
}); });
if (isGtXs) { if (isGtXs) {
config.minWidth = '417px'; config.minWidth = '417px';
config.maxHeight = '440px'; config.maxHeight = '500px';
const panelHeight = 375; const panelHeight = 375;
const panelWidth = 417; const panelWidth = 417;
const el = this.timewindowPanelOrigin.elementRef.nativeElement; const el = this.timewindowPanelOrigin.elementRef.nativeElement;
@ -225,6 +238,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
timewindow: deepClone(this.innerValue), timewindow: deepClone(this.innerValue),
historyOnly: this.historyOnly, historyOnly: this.historyOnly,
aggregation: this.aggregation, aggregation: this.aggregation,
timezone: this.timezone,
isEdit: this.isEdit isEdit: this.isEdit
} }
); );
@ -272,14 +286,20 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
updateDisplayValue() { updateDisplayValue() {
if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) {
this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ';
this.translate.instant('timewindow.last-prefix') + ' ' + if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval));
} else {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
}
} else { } else {
this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);
} else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) {
this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval));
} else { } else {
const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');

57
ui-ngx/src/app/shared/components/time/timezone-select.component.ts

@ -16,7 +16,7 @@
import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators'; import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
@ -43,10 +43,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
defaultTimezoneId: string = null; defaultTimezoneId: string = null;
timezones$ = getTimezones().pipe(
share()
);
@Input() @Input()
set defaultTimezone(timezone: string) { set defaultTimezone(timezone: string) {
if (this.defaultTimezoneId !== timezone) { if (this.defaultTimezoneId !== timezone) {
@ -138,23 +134,20 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
writeValue(value: string | null): void { writeValue(value: string | null): void {
this.searchText = ''; this.searchText = '';
getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe( const foundTimezone = getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
(foundTimezone) => { if (foundTimezone !== null) {
if (foundTimezone !== null) { this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); if (foundTimezone.id !== value) {
if (foundTimezone.id !== value) { setTimeout(() => {
setTimeout(() => { this.updateView(foundTimezone.id);
this.updateView(foundTimezone.id); }, 0);
}, 0); } else {
} else { this.modelValue = value;
this.modelValue = value;
}
} else {
this.modelValue = null;
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
}
} }
); } else {
this.modelValue = null;
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
}
this.dirty = true; this.dirty = true;
} }
@ -170,14 +163,12 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
this.ignoreClosePanel = false; this.ignoreClosePanel = false;
} else { } else {
if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) { if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) {
getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe( const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
(defaultTimezoneInfo) => { if (defaultTimezoneInfo !== null) {
if (defaultTimezoneInfo !== null) { this.ngZone.run(() => {
this.ngZone.run(() => { this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true}); });
}); }
}
});
} }
} }
} }
@ -196,12 +187,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
this.searchText = searchText; this.searchText = searchText;
if (searchText && searchText.length) { if (searchText && searchText.length) {
return getTimezones().pipe( return of(getTimezones().filter((timezoneInfo) =>
map((timezones) => timezones.filter((timezoneInfo) => timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())));
timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())))
);
} }
return getTimezones(); return of(getTimezones());
} }
clear() { clear() {

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

@ -430,6 +430,38 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
constructor(msg: EntityDataUpdateMsg) { constructor(msg: EntityDataUpdateMsg) {
super(msg); super(msg);
} }
public prepareData(tsOffset: number) {
if (this.data) {
this.processEntityData(this.data.data, tsOffset);
}
if (this.update) {
this.processEntityData(this.update, tsOffset);
}
}
private processEntityData(data: Array<EntityData>, tsOffset: number) {
for (const entityData of data) {
if (entityData.timeseries) {
for (const key of Object.keys(entityData.timeseries)) {
const tsValues = entityData.timeseries[key];
for (const tsValue of tsValues) {
tsValue.ts += tsOffset;
}
}
}
if (entityData.latest) {
for (const entityKeyType of Object.keys(entityData.latest)) {
const keyTypeValues = entityData.latest[entityKeyType];
for (const key of Object.keys(keyTypeValues)) {
const tsValue = keyTypeValues[key];
tsValue.ts += tsOffset;
}
}
}
}
}
} }
export class AlarmDataUpdate extends DataUpdate<AlarmData> { export class AlarmDataUpdate extends DataUpdate<AlarmData> {
@ -468,6 +500,8 @@ export class TelemetrySubscriber {
private zone: NgZone; private zone: NgZone;
private tsOffset = 0;
public subscriptionCommands: Array<WebsocketCmd>; public subscriptionCommands: Array<WebsocketCmd>;
public data$ = this.dataSubject.asObservable(); public data$ = this.dataSubject.asObservable();
@ -522,6 +556,10 @@ export class TelemetrySubscriber {
this.reconnectSubject.complete(); this.reconnectSubject.complete();
} }
public setTsOffset(tsOffset: number) {
this.tsOffset = tsOffset;
}
public onData(message: SubscriptionUpdate) { public onData(message: SubscriptionUpdate) {
const cmdId = message.subscriptionId; const cmdId = message.subscriptionId;
let keys: string[]; let keys: string[];
@ -545,6 +583,9 @@ export class TelemetrySubscriber {
} }
public onEntityData(message: EntityDataUpdate) { public onEntityData(message: EntityDataUpdate) {
if (this.tsOffset) {
message.prepareData(this.tsOffset);
}
if (this.zone) { if (this.zone) {
this.zone.run( this.zone.run(
() => { () => {

377
ui-ngx/src/app/shared/models/time/time.models.ts

@ -17,9 +17,7 @@
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined, isUndefined } from '@app/core/utils'; import { deepClone, isDefined, isUndefined } from '@app/core/utils';
import * as moment_ from 'moment'; import * as moment_ from 'moment';
import { Observable } from 'rxjs/internal/Observable'; import * as monentTz from 'moment-timezone';
import { from, of } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
const moment = moment_; const moment = moment_;
@ -27,6 +25,7 @@ export const SECOND = 1000;
export const MINUTE = 60 * SECOND; export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE; export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR; export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
export const YEAR = DAY * 365; export const YEAR = DAY * 365;
export enum TimewindowType { export enum TimewindowType {
@ -34,14 +33,25 @@ export enum TimewindowType {
HISTORY HISTORY
} }
export enum RealtimeWindowType {
LAST_INTERVAL,
INTERVAL
}
export enum HistoryWindowType { export enum HistoryWindowType {
LAST_INTERVAL, LAST_INTERVAL,
FIXED FIXED,
INTERVAL
} }
export interface IntervalWindow { export interface IntervalWindow {
interval?: number; interval?: number;
timewindowMs?: number; timewindowMs?: number;
quickInterval?: QuickTimeInterval;
}
export interface RealtimeWindow extends IntervalWindow{
realtimeType?: RealtimeWindowType;
} }
export interface FixedWindow { export interface FixedWindow {
@ -85,10 +95,12 @@ export interface Timewindow {
hideInterval?: boolean; hideInterval?: boolean;
hideAggregation?: boolean; hideAggregation?: boolean;
hideAggInterval?: boolean; hideAggInterval?: boolean;
hideTimezone?: boolean;
selectedTab?: TimewindowType; selectedTab?: TimewindowType;
realtime?: IntervalWindow; realtime?: RealtimeWindow;
history?: HistoryWindow; history?: HistoryWindow;
aggregation?: Aggregation; aggregation?: Aggregation;
timezone?: string;
} }
export interface SubscriptionAggregation extends Aggregation { export interface SubscriptionAggregation extends Aggregation {
@ -99,6 +111,9 @@ export interface SubscriptionAggregation extends Aggregation {
export interface SubscriptionTimewindow { export interface SubscriptionTimewindow {
startTs?: number; startTs?: number;
quickInterval?: QuickTimeInterval;
timezone?: string;
tsOffset?: number;
realtimeWindowMs?: number; realtimeWindowMs?: number;
fixedWindow?: FixedWindow; fixedWindow?: FixedWindow;
aggregation?: SubscriptionAggregation; aggregation?: SubscriptionAggregation;
@ -108,9 +123,46 @@ export interface WidgetTimewindow {
minTime?: number; minTime?: number;
maxTime?: number; maxTime?: number;
interval?: number; interval?: number;
timezone?: string;
stDiff?: number; stDiff?: number;
} }
export enum QuickTimeInterval {
YESTERDAY = 'YESTERDAY',
DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY',
THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK',
PREVIOUS_WEEK = 'PREVIOUS_WEEK',
PREVIOUS_MONTH = 'PREVIOUS_MONTH',
PREVIOUS_YEAR = 'PREVIOUS_YEAR',
CURRENT_HOUR = 'CURRENT_HOUR',
CURRENT_DAY = 'CURRENT_DAY',
CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR',
CURRENT_WEEK = 'CURRENT_WEEK',
CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR',
CURRENT_MONTH = 'CURRENT_MONTH',
CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR',
CURRENT_YEAR = 'CURRENT_YEAR',
CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR'
}
export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string>([
[QuickTimeInterval.YESTERDAY, 'timeinterval.predefined.yesterday'],
[QuickTimeInterval.DAY_BEFORE_YESTERDAY, 'timeinterval.predefined.day-before-yesterday'],
[QuickTimeInterval.THIS_DAY_LAST_WEEK, 'timeinterval.predefined.this-day-last-week'],
[QuickTimeInterval.PREVIOUS_WEEK, 'timeinterval.predefined.previous-week'],
[QuickTimeInterval.PREVIOUS_MONTH, 'timeinterval.predefined.previous-month'],
[QuickTimeInterval.PREVIOUS_YEAR, 'timeinterval.predefined.previous-year'],
[QuickTimeInterval.CURRENT_HOUR, 'timeinterval.predefined.current-hour'],
[QuickTimeInterval.CURRENT_DAY, 'timeinterval.predefined.current-day'],
[QuickTimeInterval.CURRENT_DAY_SO_FAR, 'timeinterval.predefined.current-day-so-far'],
[QuickTimeInterval.CURRENT_WEEK, 'timeinterval.predefined.current-week'],
[QuickTimeInterval.CURRENT_WEEK_SO_FAR, 'timeinterval.predefined.current-week-so-far'],
[QuickTimeInterval.CURRENT_MONTH, 'timeinterval.predefined.current-month'],
[QuickTimeInterval.CURRENT_MONTH_SO_FAR, 'timeinterval.predefined.current-month-so-far'],
[QuickTimeInterval.CURRENT_YEAR, 'timeinterval.predefined.current-year'],
[QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far']
]);
export function historyInterval(timewindowMs: number): Timewindow { export function historyInterval(timewindowMs: number): Timewindow {
const timewindow: Timewindow = { const timewindow: Timewindow = {
selectedTab: TimewindowType.HISTORY, selectedTab: TimewindowType.HISTORY,
@ -129,10 +181,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
hideInterval: false, hideInterval: false,
hideAggregation: false, hideAggregation: false,
hideAggInterval: false, hideAggInterval: false,
hideTimezone: false,
selectedTab: TimewindowType.REALTIME, selectedTab: TimewindowType.REALTIME,
realtime: { realtime: {
realtimeType: RealtimeWindowType.LAST_INTERVAL,
interval: SECOND, interval: SECOND,
timewindowMs: MINUTE timewindowMs: MINUTE,
quickInterval: QuickTimeInterval.CURRENT_DAY
}, },
history: { history: {
historyType: HistoryWindowType.LAST_INTERVAL, historyType: HistoryWindowType.LAST_INTERVAL,
@ -141,7 +196,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
fixedTimewindow: { fixedTimewindow: {
startTimeMs: currentTime - DAY, startTimeMs: currentTime - DAY,
endTimeMs: currentTime endTimeMs: currentTime
} },
quickInterval: QuickTimeInterval.CURRENT_DAY
}, },
aggregation: { aggregation: {
type: AggregationType.AVG, type: AggregationType.AVG,
@ -157,6 +213,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
model.hideInterval = value.hideInterval; model.hideInterval = value.hideInterval;
model.hideAggregation = value.hideAggregation; model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval; model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone;
if (isUndefined(value.selectedTab)) { if (isUndefined(value.selectedTab)) {
if (value.realtime) { if (value.realtime) {
model.selectedTab = TimewindowType.REALTIME; model.selectedTab = TimewindowType.REALTIME;
@ -170,7 +227,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
if (isDefined(value.realtime.interval)) { if (isDefined(value.realtime.interval)) {
model.realtime.interval = value.realtime.interval; model.realtime.interval = value.realtime.interval;
} }
model.realtime.timewindowMs = value.realtime.timewindowMs; if (isUndefined(value.realtime.realtimeType)) {
if (isDefined(value.realtime.quickInterval)) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
} else {
model.realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL;
}
} else {
model.realtime.realtimeType = value.realtime.realtimeType;
}
if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
model.realtime.quickInterval = value.realtime.quickInterval;
} else {
model.realtime.timewindowMs = value.realtime.timewindowMs;
}
} else { } else {
if (isDefined(value.history.interval)) { if (isDefined(value.history.interval)) {
model.history.interval = value.history.interval; model.history.interval = value.history.interval;
@ -178,6 +248,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
if (isUndefined(value.history.historyType)) { if (isUndefined(value.history.historyType)) {
if (isDefined(value.history.timewindowMs)) { if (isDefined(value.history.timewindowMs)) {
model.history.historyType = HistoryWindowType.LAST_INTERVAL; model.history.historyType = HistoryWindowType.LAST_INTERVAL;
} else if (isDefined(value.history.quickInterval)) {
model.history.historyType = HistoryWindowType.INTERVAL;
} else { } else {
model.history.historyType = HistoryWindowType.FIXED; model.history.historyType = HistoryWindowType.FIXED;
} }
@ -186,6 +258,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
} }
if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) { if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) {
model.history.timewindowMs = value.history.timewindowMs; model.history.timewindowMs = value.history.timewindowMs;
} else if (model.history.historyType === HistoryWindowType.INTERVAL) {
model.history.quickInterval = value.history.quickInterval;
} else { } else {
model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
@ -197,6 +271,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
} }
model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
} }
model.timezone = value.timezone;
} }
return model; return model;
} }
@ -223,6 +298,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
hideInterval: timewindow.hideInterval || false, hideInterval: timewindow.hideInterval || false,
hideAggregation: timewindow.hideAggregation || false, hideAggregation: timewindow.hideAggregation || false,
hideAggInterval: timewindow.hideAggInterval || false, hideAggInterval: timewindow.hideAggInterval || false,
hideTimezone: timewindow.hideTimezone || false,
selectedTab: TimewindowType.HISTORY, selectedTab: TimewindowType.HISTORY,
history: { history: {
historyType: HistoryWindowType.FIXED, historyType: HistoryWindowType.FIXED,
@ -235,7 +311,8 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
aggregation: { aggregation: {
type: aggType, type: aggType,
limit limit
} },
timezone: timewindow.timezone
}; };
return historyTimewindow; return historyTimewindow;
} }
@ -249,8 +326,15 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
interval: SECOND, interval: SECOND,
limit: timeService.getMaxDatapointsLimit(), limit: timeService.getMaxDatapointsLimit(),
type: AggregationType.AVG type: AggregationType.AVG
} },
timezone: timewindow.timezone,
tsOffset: 0
}; };
if (timewindow.timezone) {
const tz = getTimezone(timewindow.timezone);
const localOffset = moment().utcOffset();
subscriptionTimewindow.tsOffset = (tz.utcOffset() - localOffset) * 60 * 1000;
}
let aggTimewindow = 0; let aggTimewindow = 0;
if (stateData) { if (stateData) {
subscriptionTimewindow.aggregation.type = AggregationType.NONE; subscriptionTimewindow.aggregation.type = AggregationType.NONE;
@ -267,33 +351,65 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY;
} }
if (selectedTab === TimewindowType.REALTIME) { if (selectedTab === TimewindowType.REALTIME) {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; let realtimeType = timewindow.realtime.realtimeType;
if (isUndefined(realtimeType)) {
if (isDefined(timewindow.realtime.quickInterval)) {
realtimeType = RealtimeWindowType.INTERVAL;
} else {
realtimeType = RealtimeWindowType.LAST_INTERVAL;
}
}
if (realtimeType === RealtimeWindowType.INTERVAL) {
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.realtimeWindowMs =
getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, currentDate);
subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval;
subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate);
} else {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
}
subscriptionTimewindow.aggregation.interval = subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
subscriptionTimewindow.aggregation.type); subscriptionTimewindow.aggregation.type);
subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
aggTimewindow = subscriptionTimewindow.realtimeWindowMs; aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
if (startDiff) { if (realtimeType !== RealtimeWindowType.INTERVAL) {
subscriptionTimewindow.startTs -= startDiff; const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
aggTimewindow += subscriptionTimewindow.aggregation.interval; if (startDiff) {
subscriptionTimewindow.startTs -= startDiff;
aggTimewindow += subscriptionTimewindow.aggregation.interval;
}
} }
} else { } else {
let historyType = timewindow.history.historyType; let historyType = timewindow.history.historyType;
if (isUndefined(historyType)) { if (isUndefined(historyType)) {
historyType = isDefined(timewindow.history.timewindowMs) ? HistoryWindowType.LAST_INTERVAL : HistoryWindowType.FIXED; if (isDefined(timewindow.history.timewindowMs)) {
historyType = HistoryWindowType.LAST_INTERVAL;
} else if (isDefined(timewindow.history.quickInterval)) {
historyType = HistoryWindowType.INTERVAL;
} else {
historyType = HistoryWindowType.FIXED;
}
} }
if (historyType === HistoryWindowType.LAST_INTERVAL) { if (historyType === HistoryWindowType.LAST_INTERVAL) {
const currentTime = Date.now(); const currentDate = getCurrentTime(timewindow.timezone);
const currentTime = currentDate.valueOf();
subscriptionTimewindow.fixedWindow = { subscriptionTimewindow.fixedWindow = {
startTimeMs: currentTime - timewindow.history.timewindowMs, startTimeMs: currentTime - timewindow.history.timewindowMs,
endTimeMs: currentTime endTimeMs: currentTime
}; };
aggTimewindow = timewindow.history.timewindowMs; aggTimewindow = timewindow.history.timewindowMs;
} else if (historyType === HistoryWindowType.INTERVAL) {
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.fixedWindow = {
startTimeMs: calculateIntervalStartTime(timewindow.history.quickInterval, currentDate),
endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
};
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
} else { } else {
subscriptionTimewindow.fixedWindow = { subscriptionTimewindow.fixedWindow = {
startTimeMs: timewindow.history.fixedTimewindow.startTimeMs, startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset,
endTimeMs: timewindow.history.fixedTimewindow.endTimeMs endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset
}; };
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
} }
@ -309,6 +425,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
return subscriptionTimewindow; return subscriptionTimewindow;
} }
function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, currentDate: moment_.Moment): number {
switch (interval) {
case QuickTimeInterval.CURRENT_HOUR:
return HOUR;
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return DAY;
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return WEEK;
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return currentDate.endOf('month').diff(currentDate.clone().startOf('month'));
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.endOf('year').diff(currentDate.clone().startOf('year'));
}
}
export function calculateIntervalEndTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
switch (interval) {
case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
currentDate.subtract(2, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
currentDate.subtract(1, 'months');
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
currentDate.subtract(1, 'years');
return currentDate.endOf('year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
return currentDate.endOf('hour').valueOf();
case QuickTimeInterval.CURRENT_DAY:
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.CURRENT_WEEK:
return currentDate.endOf('week').valueOf();
case QuickTimeInterval.CURRENT_MONTH:
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.CURRENT_YEAR:
return currentDate.endOf('year').valueOf();
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.valueOf();
}
}
export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
switch (interval) {
case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days');
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
currentDate.subtract(2, 'days');
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
currentDate.subtract(1, 'months');
return currentDate.startOf('month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
currentDate.subtract(1, 'years');
return currentDate.startOf('year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
return currentDate.startOf('hour').valueOf();
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return currentDate.startOf('week').valueOf();
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return currentDate.startOf('month').valueOf();
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.startOf('year').valueOf();
}
}
export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
switch (interval) {
case QuickTimeInterval.CURRENT_HOUR:
return HOUR;
case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return DAY;
case QuickTimeInterval.PREVIOUS_WEEK:
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return WEEK;
case QuickTimeInterval.PREVIOUS_MONTH:
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return DAY * 30;
case QuickTimeInterval.PREVIOUS_YEAR:
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return YEAR;
}
}
export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow,
timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow { timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow {
const timewindowForComparison: SubscriptionTimewindow = { const timewindowForComparison: SubscriptionTimewindow = {
@ -339,6 +576,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
cloned.hideInterval = timewindow.hideInterval || false; cloned.hideInterval = timewindow.hideInterval || false;
cloned.hideAggregation = timewindow.hideAggregation || false; cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false; cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || false;
if (isDefined(timewindow.selectedTab)) { if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab; cloned.selectedTab = timewindow.selectedTab;
if (timewindow.selectedTab === TimewindowType.REALTIME) { if (timewindow.selectedTab === TimewindowType.REALTIME) {
@ -348,6 +586,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
} }
} }
cloned.aggregation = deepClone(timewindow.aggregation); cloned.aggregation = deepClone(timewindow.aggregation);
cloned.timezone = timewindow.timezone;
return cloned; return cloned;
} }
@ -358,6 +597,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi
cloned.interval = historyWindow.interval; cloned.interval = historyWindow.interval;
if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) { if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
cloned.timewindowMs = historyWindow.timewindowMs; cloned.timewindowMs = historyWindow.timewindowMs;
} else if (historyWindow.historyType === HistoryWindowType.INTERVAL) {
cloned.quickInterval = historyWindow.quickInterval;
} else if (historyWindow.historyType === HistoryWindowType.FIXED) { } else if (historyWindow.historyType === HistoryWindowType.FIXED) {
cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow); cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
} }
@ -375,7 +616,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{ {
name: 'timeinterval.seconds-interval', name: 'timeinterval.seconds-interval',
translateParams: {seconds: 1}, translateParams: {seconds: 1},
value: 1 * SECOND value: SECOND
}, },
{ {
name: 'timeinterval.seconds-interval', name: 'timeinterval.seconds-interval',
@ -400,7 +641,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{ {
name: 'timeinterval.minutes-interval', name: 'timeinterval.minutes-interval',
translateParams: {minutes: 1}, translateParams: {minutes: 1},
value: 1 * MINUTE value: MINUTE
}, },
{ {
name: 'timeinterval.minutes-interval', name: 'timeinterval.minutes-interval',
@ -430,7 +671,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{ {
name: 'timeinterval.hours-interval', name: 'timeinterval.hours-interval',
translateParams: {hours: 1}, translateParams: {hours: 1},
value: 1 * HOUR value: HOUR
}, },
{ {
name: 'timeinterval.hours-interval', name: 'timeinterval.hours-interval',
@ -455,7 +696,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{ {
name: 'timeinterval.days-interval', name: 'timeinterval.days-interval',
translateParams: {days: 1}, translateParams: {days: 1},
value: 1 * DAY value: DAY
}, },
{ {
name: 'timeinterval.days-interval', name: 'timeinterval.days-interval',
@ -495,60 +736,50 @@ export interface TimezoneInfo {
let timezones: TimezoneInfo[] = null; let timezones: TimezoneInfo[] = null;
let defaultTimezone: string = null; let defaultTimezone: string = null;
export function getTimezones(): Observable<TimezoneInfo[]> { export function getTimezones(): TimezoneInfo[] {
if (timezones) { if (!timezones) {
return of(timezones); timezones = monentTz.tz.names().map((zoneName) => {
} else { const tz = monentTz.tz(zoneName);
return from(import('moment-timezone')).pipe( return {
map((monentTz) => { id: zoneName,
return monentTz.tz.names().map((zoneName) => { name: zoneName.replace(/_/g, ' '),
const tz = monentTz.tz(zoneName); offset: `UTC${tz.format('Z')}`,
return { nOffset: tz.utcOffset()
id: zoneName, };
name: zoneName.replace(/_/g, ' '), });
offset: `UTC${tz.format('Z')}`,
nOffset: tz.utcOffset()
};
});
}),
tap((zones) => {
timezones = zones;
})
);
} }
return timezones;
} }
export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): Observable<TimezoneInfo> { export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo {
return getTimezones().pipe( const timezoneList = getTimezones();
mergeMap((timezoneList) => { let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null;
let foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId); if (!foundTimezone) {
if (!foundTimezone) { if (userTimezoneByDefault) {
if (userTimezoneByDefault) { const userTimezone = getDefaultTimezone();
return getDefaultTimezone().pipe( foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone);
map((userTimezone) => { } else if (defaultTimezoneId) {
return timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone); foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
}) }
); }
} else if (defaultTimezoneId) { return foundTimezone;
foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
}
}
return of(foundTimezone);
})
);
} }
export function getDefaultTimezone(): Observable<string> { export function getDefaultTimezone(): string {
if (defaultTimezone) { if (!defaultTimezone) {
return of(defaultTimezone); defaultTimezone = monentTz.tz.guess();
}
return defaultTimezone;
}
export function getCurrentTime(tz?: string): moment_.Moment {
if (tz) {
return moment().tz(tz);
} else { } else {
return from(import('moment-timezone')).pipe( return moment();
map((monentTz) => {
return monentTz.tz.guess();
}),
tap((zone) => {
defaultTimezone = zone;
})
);
} }
} }
export function getTimezone(tz: string): moment_.Moment {
return moment.tz(tz);
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -140,6 +140,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
import { FileSizePipe } from '@shared/pipe/file-size.pipe'; import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -175,6 +176,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
TimewindowComponent, TimewindowComponent,
TimewindowPanelComponent, TimewindowPanelComponent,
TimeintervalComponent, TimeintervalComponent,
QuickTimeIntervalComponent,
DashboardSelectComponent, DashboardSelectComponent,
DashboardSelectPanelComponent, DashboardSelectPanelComponent,
DatetimePeriodComponent, DatetimePeriodComponent,
@ -302,6 +304,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
TimewindowComponent, TimewindowComponent,
TimewindowPanelComponent, TimewindowPanelComponent,
TimeintervalComponent, TimeintervalComponent,
QuickTimeIntervalComponent,
DashboardSelectComponent, DashboardSelectComponent,
DatetimePeriodComponent, DatetimePeriodComponent,
DatetimeComponent, DatetimeComponent,

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

@ -2118,7 +2118,24 @@
"hours": "Hours", "hours": "Hours",
"minutes": "Minutes", "minutes": "Minutes",
"seconds": "Seconds", "seconds": "Seconds",
"advanced": "Advanced" "advanced": "Advanced",
"predefined": {
"yesterday": "Yesterday",
"day-before-yesterday": "Day before yesterday",
"this-day-last-week": "This day last week",
"previous-week": "Previous week",
"previous-month": "Previous month",
"previous-year": "Previous year",
"current-hour": "Current hour",
"current-day": "Current day",
"current-day-so-far": "Current day so far",
"current-week": "Current week",
"current-week-so-far": "Current week so far",
"current-month": "Current month",
"current-month-so-far": "Current month so far",
"current-year": "Current year",
"current-year-so-far": "Current year so far"
}
}, },
"timeunit": { "timeunit": {
"seconds": "Seconds", "seconds": "Seconds",
@ -2139,7 +2156,8 @@
"date-range": "Date range", "date-range": "Date range",
"last": "Last", "last": "Last",
"time-period": "Time period", "time-period": "Time period",
"hide": "Hide" "hide": "Hide",
"interval": "Interval"
}, },
"user": { "user": {
"user": "User", "user": "User",

Loading…
Cancel
Save