Commit 7cfa352a60dc3323c4987df6656a4cc0deeef3c6

Authored by Igor Kulikov
2 parents 59f85f1c 3ed49e3f

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

... ... @@ -15,7 +15,12 @@
15 15 ///
16 16
17 17 import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models';
18   -import { AggregationType } from '@shared/models/time/time.models';
  18 +import {
  19 + AggregationType,
  20 + calculateIntervalEndTime,
  21 + calculateIntervalStartTime, getCurrentTime,
  22 + QuickTimeInterval, SubscriptionTimewindow
  23 +} from '@shared/models/time/time.models';
19 24 import { UtilsService } from '@core/services/utils.service';
20 25 import { deepClone } from '@core/utils';
21 26 import Timeout = NodeJS.Timeout;
... ... @@ -73,33 +78,29 @@ export class DataAggregator {
73 78 private resetPending = false;
74 79 private updatedData = false;
75 80
76   - private noAggregation = this.aggregationType === AggregationType.NONE;
77   - private aggregationTimeout = Math.max(this.interval, 1000);
  81 + private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE;
  82 + private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
78 83 private readonly aggFunction: AggFunction;
79 84
80 85 private intervalTimeoutHandle: Timeout;
81 86 private intervalScheduledTime: number;
82 87
  88 + private startTs = this.subsTw.startTs + this.subsTw.tsOffset;
83 89 private endTs: number;
84 90 private elapsed: number;
85 91
86 92 constructor(private onDataCb: onAggregatedData,
87 93 private tsKeyNames: string[],
88   - private startTs: number,
89   - private limit: number,
90   - private aggregationType: AggregationType,
91   - private timeWindow: number,
92   - private interval: number,
93   - private stateData: boolean,
  94 + private subsTw: SubscriptionTimewindow,
94 95 private utils: UtilsService,
95 96 private ignoreDataUpdateOnIntervalTick: boolean) {
96 97 this.tsKeyNames.forEach((key) => {
97 98 this.dataBuffer[key] = [];
98 99 });
99   - if (this.stateData) {
  100 + if (this.subsTw.aggregation.stateData) {
100 101 this.lastPrevKvPairData = {};
101 102 }
102   - switch (this.aggregationType) {
  103 + switch (this.subsTw.aggregation.type) {
103 104 case AggregationType.MIN:
104 105 this.aggFunction = min;
105 106 break;
... ... @@ -129,18 +130,21 @@ export class DataAggregator {
129 130 return prevOnDataCb;
130 131 }
131 132
132   - public reset(startTs: number, timeWindow: number, interval: number) {
  133 + public reset(subsTw: SubscriptionTimewindow) {
133 134 if (this.intervalTimeoutHandle) {
134 135 clearTimeout(this.intervalTimeoutHandle);
135 136 this.intervalTimeoutHandle = null;
136 137 }
  138 + this.subsTw = subsTw;
137 139 this.intervalScheduledTime = this.utils.currentPerfTime();
138   - this.startTs = startTs;
139   - this.timeWindow = timeWindow;
140   - this.interval = interval;
141   - this.endTs = this.startTs + this.timeWindow;
  140 + this.startTs = this.subsTw.startTs + this.subsTw.tsOffset;
  141 + if (this.subsTw.quickInterval) {
  142 + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset;
  143 + } else {
  144 + this.endTs = this.startTs + this.subsTw.aggregation.timeWindow;
  145 + }
142 146 this.elapsed = 0;
143   - this.aggregationTimeout = Math.max(this.interval, 1000);
  147 + this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
144 148 this.resetPending = true;
145 149 this.updatedData = false;
146 150 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
... ... @@ -161,7 +165,11 @@ export class DataAggregator {
161 165 if (!this.dataReceived) {
162 166 this.elapsed = 0;
163 167 this.dataReceived = true;
164   - this.endTs = this.startTs + this.timeWindow;
  168 + if (this.subsTw.quickInterval) {
  169 + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset;
  170 + } else {
  171 + this.endTs = this.startTs + this.subsTw.aggregation.timeWindow;
  172 + }
165 173 }
166 174 if (this.resetPending) {
167 175 this.resetPending = false;
... ... @@ -195,12 +203,19 @@ export class DataAggregator {
195 203 this.intervalTimeoutHandle = null;
196 204 }
197 205 if (!history) {
198   - const delta = Math.floor(this.elapsed / this.interval);
  206 + const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval);
199 207 if (delta || !this.data) {
200   - this.startTs += delta * this.interval;
201   - this.endTs += delta * this.interval;
  208 + const tickTs = delta * this.subsTw.aggregation.interval;
  209 + if (this.subsTw.quickInterval) {
  210 + const currentDate = getCurrentTime(this.subsTw.timezone);
  211 + this.startTs = calculateIntervalStartTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
  212 + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset;
  213 + } else {
  214 + this.startTs += tickTs;
  215 + this.endTs += tickTs;
  216 + }
202 217 this.data = this.updateData();
203   - this.elapsed = this.elapsed - delta * this.interval;
  218 + this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval;
204 219 }
205 220 } else {
206 221 this.data = this.updateData();
... ... @@ -223,7 +238,7 @@ export class DataAggregator {
223 238 let keyData = this.dataBuffer[key];
224 239 aggKeyData.forEach((aggData, aggTimestamp) => {
225 240 if (aggTimestamp <= this.startTs) {
226   - if (this.stateData &&
  241 + if (this.subsTw.aggregation.stateData &&
227 242 (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
228 243 this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
229 244 }
... ... @@ -235,11 +250,11 @@ export class DataAggregator {
235 250 }
236 251 });
237 252 keyData.sort((set1, set2) => set1[0] - set2[0]);
238   - if (this.stateData) {
  253 + if (this.subsTw.aggregation.stateData) {
239 254 this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key]));
240 255 }
241   - if (keyData.length > this.limit) {
242   - keyData = keyData.slice(keyData.length - this.limit);
  256 + if (keyData.length > this.subsTw.aggregation.limit) {
  257 + keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit);
243 258 }
244 259 this.dataBuffer[key] = keyData;
245 260 }
... ... @@ -275,7 +290,7 @@ export class DataAggregator {
275 290 }
276 291
277 292 private processAggregatedData(data: SubscriptionData): AggregationMap {
278   - const isCount = this.aggregationType === AggregationType.COUNT;
  293 + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
279 294 const aggregationMap: AggregationMap = {};
280 295 for (const key of Object.keys(data)) {
281 296 let aggKeyData = aggregationMap[key];
... ... @@ -300,7 +315,7 @@ export class DataAggregator {
300 315 }
301 316
302 317 private updateAggregatedData(data: SubscriptionData) {
303   - const isCount = this.aggregationType === AggregationType.COUNT;
  318 + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
304 319 for (const key of Object.keys(data)) {
305 320 let aggKeyData = this.aggregationMap[key];
306 321 if (!aggKeyData) {
... ... @@ -312,7 +327,8 @@ export class DataAggregator {
312 327 const timestamp = kvPair[0];
313 328 const value = this.convertValue(kvPair[1]);
314 329 const aggTimestamp = this.noAggregation ? timestamp : (this.startTs +
315   - Math.floor((timestamp - this.startTs) / this.interval) * this.interval + this.interval / 2);
  330 + Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) *
  331 + this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2);
316 332 let aggData = aggKeyData.get(aggTimestamp);
317 333 if (!aggData) {
318 334 aggData = {
... ...
... ... @@ -237,7 +237,7 @@ export class EntityDataSubscription {
237 237 };
238 238
239 239 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
240   - this.prepareSubscriptionCommands(this.dataCommand);
  240 + this.prepareSubscriptionCommands();
241 241 }
242 242
243 243 this.subscriber.subscriptionCommands.push(this.dataCommand);
... ... @@ -256,8 +256,8 @@ export class EntityDataSubscription {
256 256 if (this.started) {
257 257 const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand;
258 258 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries &&
259   - !this.history && this.tsFields.length) {
260   - const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription();
  259 + !this.history && this.tsFields.length) {
  260 + const newSubsTw = this.listener.updateRealtimeSubscription();
261 261 this.subsTw = newSubsTw;
262 262 targetCommand.tsCmd.startTs = this.subsTw.startTs;
263 263 targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
... ... @@ -266,9 +266,10 @@ export class EntityDataSubscription {
266 266 targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
267 267 targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
268 268 this.dataAggregators.forEach((dataAggregator) => {
269   - dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
  269 + dataAggregator.reset(newSubsTw);
270 270 });
271 271 }
  272 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
272 273 targetCommand.query = this.dataCommand.query;
273 274 this.subscriber.subscriptionCommands = [targetCommand];
274 275 } else {
... ... @@ -393,7 +394,7 @@ export class EntityDataSubscription {
393 394 if (this.datasourceType === DatasourceType.entity) {
394 395 this.subsCommand = new EntityDataCmd();
395 396 this.subsCommand.cmdId = this.dataCommand.cmdId;
396   - this.prepareSubscriptionCommands(this.subsCommand);
  397 + this.prepareSubscriptionCommands();
397 398 if (!this.subsCommand.isEmpty()) {
398 399 this.subscriber.subscriptionCommands = [this.subsCommand];
399 400 this.subscriber.update();
... ... @@ -404,11 +405,11 @@ export class EntityDataSubscription {
404 405 this.started = true;
405 406 }
406 407
407   - private prepareSubscriptionCommands(cmd: EntityDataCmd) {
  408 + private prepareSubscriptionCommands() {
408 409 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
409 410 if (this.tsFields.length > 0) {
410 411 if (this.history) {
411   - cmd.historyCmd = {
  412 + this.subsCommand.historyCmd = {
412 413 keys: this.tsFields.map(key => key.key),
413 414 startTs: this.subsTw.fixedWindow.startTimeMs,
414 415 endTs: this.subsTw.fixedWindow.endTimeMs,
... ... @@ -418,7 +419,7 @@ export class EntityDataSubscription {
418 419 fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
419 420 };
420 421 } else {
421   - cmd.tsCmd = {
  422 + this.subsCommand.tsCmd = {
422 423 keys: this.tsFields.map(key => key.key),
423 424 startTs: this.subsTw.startTs,
424 425 timeWindow: this.subsTw.aggregation.timeWindow,
... ... @@ -429,9 +430,10 @@ export class EntityDataSubscription {
429 430 };
430 431 }
431 432 }
  433 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
432 434 } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
433 435 if (this.latestValues.length > 0) {
434   - cmd.latestCmd = {
  436 + this.subsCommand.latestCmd = {
435 437 keys: this.latestValues
436 438 };
437 439 }
... ... @@ -745,12 +747,7 @@ export class EntityDataSubscription {
745 747 this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb);
746 748 },
747 749 tsKeyNames,
748   - subsTw.startTs,
749   - subsTw.aggregation.limit,
750   - subsTw.aggregation.type,
751   - subsTw.aggregation.timeWindow,
752   - subsTw.aggregation.interval,
753   - subsTw.aggregation.stateData,
  750 + subsTw,
754 751 this.utils,
755 752 this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick
756 753 );
... ... @@ -827,7 +824,8 @@ export class EntityDataSubscription {
827 824 startTime = dataKey.lastUpdateTime + this.frequency;
828 825 endTime = dataKey.lastUpdateTime + deltaElapsed;
829 826 } else {
830   - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs;
  827 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs +
  828 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
831 829 endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
832 830 if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
833 831 const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
... ... @@ -835,8 +833,10 @@ export class EntityDataSubscription {
835 833 }
836 834 }
837 835 } else {
838   - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs;
839   - endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs;
  836 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs +
  837 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  838 + endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
  839 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
840 840 }
841 841 }
842 842 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
... ...
... ... @@ -37,8 +37,10 @@ import {
37 37 } from '@app/shared/models/widget.models';
38 38 import { HttpErrorResponse } from '@angular/common/http';
39 39 import {
  40 + calculateIntervalEndTime,
  41 + calculateIntervalStartTime,
40 42 createSubscriptionTimewindow,
41   - createTimewindowForComparison,
  43 + createTimewindowForComparison, getCurrentTime,
42 44 SubscriptionTimewindow,
43 45 Timewindow,
44 46 toHistoryTimewindow,
... ... @@ -837,9 +839,11 @@ export class WidgetSubscription implements IWidgetSubscription {
837 839 if (this.alarmDataListener) {
838 840 this.ctx.alarmDataService.stopSubscription(this.alarmDataListener);
839 841 }
  842 +
840 843 if (this.timeWindowConfig) {
841 844 this.updateRealtimeSubscription();
842 845 }
  846 +
843 847 this.alarmDataListener = {
844 848 subscriptionTimewindow: this.subscriptionTimewindow,
845 849 alarmSource: this.alarmSource,
... ... @@ -1080,12 +1084,21 @@ export class WidgetSubscription implements IWidgetSubscription {
1080 1084
1081 1085 private updateTimewindow() {
1082 1086 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
  1087 + this.timeWindow.timezone = this.subscriptionTimewindow.timezone;
1083 1088 if (this.subscriptionTimewindow.realtimeWindowMs) {
1084   - this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff;
1085   - this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
  1089 + if (this.subscriptionTimewindow.quickInterval) {
  1090 + const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone);
  1091 + this.timeWindow.maxTime = calculateIntervalEndTime(
  1092 + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
  1093 + this.timeWindow.minTime = calculateIntervalStartTime(
  1094 + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
  1095 + } else {
  1096 + this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff;
  1097 + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
  1098 + }
1086 1099 } else if (this.subscriptionTimewindow.fixedWindow) {
1087   - this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
1088   - this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs;
  1100 + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs + this.subscriptionTimewindow.tsOffset;
  1101 + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs + this.subscriptionTimewindow.tsOffset;
1089 1102 }
1090 1103 }
1091 1104
... ... @@ -1103,12 +1116,13 @@ export class WidgetSubscription implements IWidgetSubscription {
1103 1116
1104 1117 private updateComparisonTimewindow() {
1105 1118 this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000;
  1119 + this.comparisonTimeWindow.timezone = this.timewindowForComparison.timezone;
1106 1120 if (this.timewindowForComparison.realtimeWindowMs) {
1107 1121 this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf();
1108   - this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs;
  1122 + this.comparisonTimeWindow.minTime = moment(this.timeWindow.minTime).subtract(1, this.timeForComparison).valueOf();
1109 1123 } else if (this.timewindowForComparison.fixedWindow) {
1110   - this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs;
1111   - this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs;
  1124 + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs + this.timewindowForComparison.tsOffset;
  1125 + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs + this.timewindowForComparison.tsOffset;
1112 1126 }
1113 1127 }
1114 1128
... ... @@ -1335,7 +1349,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1335 1349 this.onDataUpdated();
1336 1350 }
1337 1351
1338   - private alarmsUpdated(_updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
  1352 + private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
1339 1353 this.alarmsLoaded(alarms, 0, 0);
1340 1354 }
1341 1355
... ...
... ... @@ -91,6 +91,7 @@
91 91 direction="left"
92 92 tooltipPosition="below"
93 93 aggregation="true"
  94 + timezone="true"
94 95 [(ngModel)]="dashboardCtx.dashboardTimewindow">
95 96 </tb-timewindow>
96 97 <tb-filters-edit [fxShow]="!isEdit && displayFilters()"
... ...
... ... @@ -95,6 +95,7 @@
95 95 <tb-timewindow *ngIf="widget.hasTimewindow"
96 96 #timewindowComponent
97 97 aggregation="{{widget.hasAggregation}}"
  98 + timezone="true"
98 99 [isEdit]="isEdit"
99 100 [(ngModel)]="widgetComponent.widget.config.timewindow"
100 101 (ngModelChange)="widgetComponent.onTimewindowChanged($event)">
... ...
... ... @@ -55,7 +55,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models';
55 55 import { DialogService } from '@core/services/dialog.service';
56 56 import { AddEntityDialogComponent } from './add-entity-dialog.component';
57 57 import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
58   -import { HistoryWindowType, Timewindow } from '@shared/models/time/time.models';
  58 +import {
  59 + calculateIntervalEndTime,
  60 + calculateIntervalStartTime,
  61 + getCurrentTime,
  62 + HistoryWindowType,
  63 + Timewindow
  64 +} from '@shared/models/time/time.models';
59 65 import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
60 66 import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
61 67 import { isDefined, isUndefined } from '@core/utils';
... ... @@ -296,6 +302,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
296 302 const currentTime = Date.now();
297 303 timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
298 304 timePageLink.endTime = currentTime;
  305 + } else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) {
  306 + const currentDate = getCurrentTime();
  307 + timePageLink.startTime = calculateIntervalStartTime(this.timewindow.history.quickInterval, currentDate);
  308 + timePageLink.endTime = calculateIntervalEndTime(this.timewindow.history.quickInterval, currentDate);
299 309 } else {
300 310 timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
301 311 timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;
... ...
... ... @@ -94,11 +94,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
94 94 items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
95 95 });
96 96 this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
97   - getDefaultTimezone().subscribe((defaultTimezone) => {
98   - this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false});
99   - this.updateValidators(type, true);
100   - this.alarmScheduleForm.updateValueAndValidity();
101   - });
  97 + const defaultTimezone = getDefaultTimezone();
  98 + this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false});
  99 + this.updateValidators(type, true);
  100 + this.alarmScheduleForm.updateValueAndValidity();
102 101 });
103 102 this.alarmScheduleForm.valueChanges.subscribe(() => {
104 103 this.updateModel();
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section class="interval-section" fxLayout="row" fxFlex>
  19 + <mat-form-field fxFlex>
  20 + <mat-label translate>timewindow.interval</mat-label>
  21 + <mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
  22 + <mat-option *ngFor="let interval of intervals" [value]="interval">
  23 + {{ timeIntervalTranslationMap.get(interval) | translate}}
  24 + </mat-option>
  25 + </mat-select>
  26 + </mat-form-field>
  27 +</section>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +:host {
  18 + min-width: 364px;
  19 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models';
  20 +
  21 +@Component({
  22 + selector: 'tb-quick-time-interval',
  23 + templateUrl: './quick-time-interval.component.html',
  24 + styleUrls: ['./quick-time-interval.component.scss'],
  25 + providers: [
  26 + {
  27 + provide: NG_VALUE_ACCESSOR,
  28 + useExisting: forwardRef(() => QuickTimeIntervalComponent),
  29 + multi: true
  30 + }
  31 + ]
  32 +})
  33 +export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor {
  34 +
  35 + private allIntervals = Object.values(QuickTimeInterval);
  36 +
  37 + modelValue: QuickTimeInterval;
  38 + timeIntervalTranslationMap = QuickTimeIntervalTranslationMap;
  39 +
  40 + rendered = false;
  41 +
  42 + @Input() disabled: boolean;
  43 +
  44 + @Input() onlyCurrentInterval = false;
  45 +
  46 + private propagateChange = (_: any) => {};
  47 +
  48 + constructor() {
  49 + }
  50 +
  51 + get intervals() {
  52 + if (this.onlyCurrentInterval) {
  53 + return this.allIntervals.filter(interval => interval.startsWith('CURRENT_'));
  54 + }
  55 + return this.allIntervals;
  56 + }
  57 +
  58 + ngOnInit(): void {
  59 + }
  60 +
  61 + registerOnChange(fn: any): void {
  62 + this.propagateChange = fn;
  63 + }
  64 +
  65 + registerOnTouched(fn: any): void {
  66 + }
  67 +
  68 + setDisabledState(isDisabled: boolean): void {
  69 + this.disabled = isDisabled;
  70 + }
  71 +
  72 + writeValue(interval: QuickTimeInterval): void {
  73 + this.modelValue = interval;
  74 + }
  75 +
  76 + onIntervalChange() {
  77 + this.propagateChange(this.modelValue);
  78 + }
  79 +}
... ...
... ... @@ -21,16 +21,43 @@
21 21 <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}"
22 22 (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab">
23 23 <mat-tab label="{{ 'timewindow.realtime' | translate }}">
24   - <div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column">
25   - <tb-timeinterval
26   - [(hideFlag)]="timewindow.hideInterval"
27   - (hideFlagChange)="onHideIntervalChanged()"
28   - [isEdit]="isEdit"
29   - formControlName="timewindowMs"
30   - predefinedName="timewindow.last"
31   - [required]="timewindow.selectedTab === timewindowTypes.REALTIME"
32   - style="padding-top: 8px;"></tb-timeinterval>
33   - </div>
  24 + <section fxLayout="row">
  25 + <section *ngIf="isEdit" fxLayout="column" style="padding-top: 8px; padding-left: 16px;">
  26 + <label class="tb-small hide-label" translate>timewindow.hide</label>
  27 + <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
  28 + (ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
  29 + </section>
  30 + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
  31 + <div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
  32 + <mat-radio-group formControlName="realtimeType">
  33 + <mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
  34 + <section fxLayout="column">
  35 + <span translate>timewindow.last</span>
  36 + <tb-timeinterval
  37 + formControlName="timewindowMs"
  38 + predefinedName="timewindow.last"
  39 + [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
  40 + [required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
  41 + timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
  42 + style="padding-top: 8px;"></tb-timeinterval>
  43 + </section>
  44 + </mat-radio-button>
  45 + <mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
  46 + <section fxLayout="column">
  47 + <span translate>timewindow.interval</span>
  48 + <tb-quick-time-interval
  49 + formControlName="quickInterval"
  50 + onlyCurrentInterval="true"
  51 + [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
  52 + [required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
  53 + timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
  54 + style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
  55 + </section>
  56 + </mat-radio-button>
  57 + </mat-radio-group>
  58 + </div>
  59 + </section>
  60 + </section>
34 61 </mat-tab>
35 62 <mat-tab label="{{ 'timewindow.history' | translate }}">
36 63 <section fxLayout="row">
... ... @@ -65,6 +92,17 @@
65 92 style="padding-top: 8px;"></tb-datetime-period>
66 93 </section>
67 94 </mat-radio-button>
  95 + <mat-radio-button [value]="historyTypes.INTERVAL" color="primary">
  96 + <section fxLayout="column">
  97 + <span translate>timewindow.interval</span>
  98 + <tb-quick-time-interval
  99 + formControlName="quickInterval"
  100 + [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
  101 + [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
  102 + timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
  103 + style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
  104 + </section>
  105 + </mat-radio-button>
68 106 </mat-radio-group>
69 107 </div>
70 108 </section>
... ... @@ -139,6 +177,16 @@
139 177 predefinedName="aggregation.group-interval">
140 178 </tb-timeinterval>
141 179 </div>
  180 + <div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row">
  181 + <section fxLayout="column" [fxShow]="isEdit">
  182 + <label class="tb-small hide-label" translate>timewindow.hide</label>
  183 + <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone"
  184 + (ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
  185 + </section>
  186 + <tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
  187 + formControlName="timezone">
  188 + </tb-timezone-select>
  189 + </div>
142 190 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
143 191 <button type="button"
144 192 mat-button
... ...
... ... @@ -20,6 +20,8 @@ import {
20 20 AggregationType,
21 21 DAY,
22 22 HistoryWindowType,
  23 + quickTimeIntervalPeriod,
  24 + RealtimeWindowType,
23 25 Timewindow,
24 26 TimewindowType
25 27 } from '@shared/models/time/time.models';
... ... @@ -36,6 +38,7 @@ export interface TimewindowPanelData {
36 38 historyOnly: boolean;
37 39 timewindow: Timewindow;
38 40 aggregation: boolean;
  41 + timezone: boolean;
39 42 isEdit: boolean;
40 43 }
41 44
... ... @@ -50,6 +53,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
50 53
51 54 aggregation = false;
52 55
  56 + timezone = false;
  57 +
53 58 isEdit = false;
54 59
55 60 timewindow: Timewindow;
... ... @@ -60,6 +65,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
60 65
61 66 historyTypes = HistoryWindowType;
62 67
  68 + realtimeTypes = RealtimeWindowType;
  69 +
63 70 timewindowTypes = TimewindowType;
64 71
65 72 aggregationTypes = AggregationType;
... ... @@ -78,6 +85,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
78 85 this.historyOnly = data.historyOnly;
79 86 this.timewindow = data.timewindow;
80 87 this.aggregation = data.aggregation;
  88 + this.timezone = data.timezone;
81 89 this.isEdit = data.isEdit;
82 90 }
83 91
... ... @@ -85,10 +93,16 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
85 93 const hideInterval = this.timewindow.hideInterval || false;
86 94 const hideAggregation = this.timewindow.hideAggregation || false;
87 95 const hideAggInterval = this.timewindow.hideAggInterval || false;
  96 + const hideTimezone = this.timewindow.hideTimezone || false;
88 97
89 98 this.timewindowForm = this.fb.group({
90 99 realtime: this.fb.group(
91 100 {
  101 + realtimeType: this.fb.control({
  102 + value: this.timewindow.realtime && typeof this.timewindow.realtime.realtimeType !== 'undefined'
  103 + ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
  104 + disabled: hideInterval
  105 + }),
92 106 timewindowMs: [
93 107 this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
94 108 ? this.timewindow.realtime.timewindowMs : null
... ... @@ -96,7 +110,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
96 110 interval: [
97 111 this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
98 112 ? this.timewindow.realtime.interval : null
99   - ]
  113 + ],
  114 + quickInterval: this.fb.control({
  115 + value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined'
  116 + ? this.timewindow.realtime.quickInterval : null,
  117 + disabled: hideInterval
  118 + })
100 119 }
101 120 ),
102 121 history: this.fb.group(
... ... @@ -119,6 +138,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
119 138 value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined'
120 139 ? this.timewindow.history.fixedTimewindow : null,
121 140 disabled: hideInterval
  141 + }),
  142 + quickInterval: this.fb.control({
  143 + value: this.timewindow.history && typeof this.timewindow.history.quickInterval !== 'undefined'
  144 + ? this.timewindow.history.quickInterval : null,
  145 + disabled: hideInterval
122 146 })
123 147 }
124 148 ),
... ... @@ -135,21 +159,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
135 159 disabled: hideAggInterval
136 160 }, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())])
137 161 }
138   - )
  162 + ),
  163 + timezone: this.fb.control({
  164 + value: this.timewindow.timezone !== 'undefined'
  165 + ? this.timewindow.timezone : null,
  166 + disabled: hideTimezone
  167 + })
139 168 });
140 169 }
141 170
142 171 update() {
143 172 const timewindowFormValue = this.timewindowForm.getRawValue();
144 173 this.timewindow.realtime = {
  174 + realtimeType: timewindowFormValue.realtime.realtimeType,
145 175 timewindowMs: timewindowFormValue.realtime.timewindowMs,
  176 + quickInterval: timewindowFormValue.realtime.quickInterval,
146 177 interval: timewindowFormValue.realtime.interval
147 178 };
148 179 this.timewindow.history = {
149 180 historyType: timewindowFormValue.history.historyType,
150 181 timewindowMs: timewindowFormValue.history.timewindowMs,
151 182 interval: timewindowFormValue.history.interval,
152   - fixedTimewindow: timewindowFormValue.history.fixedTimewindow
  183 + fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
  184 + quickInterval: timewindowFormValue.history.quickInterval,
153 185 };
154 186 if (this.aggregation) {
155 187 this.timewindow.aggregation = {
... ... @@ -157,6 +189,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
157 189 limit: timewindowFormValue.aggregation.limit
158 190 };
159 191 }
  192 + if (this.timezone) {
  193 + this.timewindow.timezone = timewindowFormValue.timezone;
  194 + }
160 195 this.result = this.timewindow;
161 196 this.overlayRef.dispose();
162 197 }
... ... @@ -174,11 +209,23 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
174 209 }
175 210
176 211 minRealtimeAggInterval() {
177   - return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value);
  212 + return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow());
178 213 }
179 214
180 215 maxRealtimeAggInterval() {
181   - return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value);
  216 + return this.timeService.maxIntervalLimit(this.currentRealtimeTimewindow());
  217 + }
  218 +
  219 + currentRealtimeTimewindow(): number {
  220 + const timeWindowFormValue = this.timewindowForm.getRawValue();
  221 + switch (timeWindowFormValue.realtime.realtimeType) {
  222 + case RealtimeWindowType.LAST_INTERVAL:
  223 + return timeWindowFormValue.realtime.timewindowMs;
  224 + case RealtimeWindowType.INTERVAL:
  225 + return quickTimeIntervalPeriod(timeWindowFormValue.realtime.quickInterval);
  226 + default:
  227 + return DAY;
  228 + }
182 229 }
183 230
184 231 minHistoryAggInterval() {
... ... @@ -193,6 +240,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
193 240 const timewindowFormValue = this.timewindowForm.getRawValue();
194 241 if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
195 242 return timewindowFormValue.history.timewindowMs;
  243 + } else if (timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL) {
  244 + return quickTimeIntervalPeriod(timewindowFormValue.history.quickInterval);
196 245 } else if (timewindowFormValue.history.fixedTimewindow) {
197 246 return timewindowFormValue.history.fixedTimewindow.endTimeMs -
198 247 timewindowFormValue.history.fixedTimewindow.startTimeMs;
... ... @@ -206,10 +255,18 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
206 255 this.timewindowForm.get('history.historyType').disable({emitEvent: false});
207 256 this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false});
208 257 this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false});
  258 + this.timewindowForm.get('history.quickInterval').disable({emitEvent: false});
  259 + this.timewindowForm.get('realtime.realtimeType').disable({emitEvent: false});
  260 + this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false});
  261 + this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false});
209 262 } else {
210 263 this.timewindowForm.get('history.historyType').enable({emitEvent: false});
211 264 this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false});
212 265 this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false});
  266 + this.timewindowForm.get('history.quickInterval').enable({emitEvent: false});
  267 + this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false});
  268 + this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
  269 + this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
213 270 }
214 271 this.timewindowForm.markAsDirty();
215 272 }
... ... @@ -232,4 +289,13 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
232 289 this.timewindowForm.markAsDirty();
233 290 }
234 291
  292 + onHideTimezoneChanged() {
  293 + if (this.timewindow.hideTimezone) {
  294 + this.timewindowForm.get('timezone').disable({emitEvent: false});
  295 + } else {
  296 + this.timewindowForm.get('timezone').enable({emitEvent: false});
  297 + }
  298 + this.timewindowForm.markAsDirty();
  299 + }
  300 +
235 301 }
... ...
... ... @@ -33,6 +33,8 @@ import {
33 33 cloneSelectedTimewindow,
34 34 HistoryWindowType,
35 35 initModelFromDefaultTimewindow,
  36 + QuickTimeIntervalTranslationMap,
  37 + RealtimeWindowType,
36 38 Timewindow,
37 39 TimewindowType
38 40 } from '@shared/models/time/time.models';
... ... @@ -89,6 +91,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
89 91 return this.aggregationValue;
90 92 }
91 93
  94 + timezoneValue = false;
  95 +
  96 + @Input()
  97 + set timezone(val) {
  98 + this.timezoneValue = coerceBooleanProperty(val);
  99 + }
  100 +
  101 + get timezone() {
  102 + return this.timezoneValue;
  103 + }
  104 +
92 105 isToolbarValue = false;
93 106
94 107 @Input()
... ... @@ -169,7 +182,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
169 182 });
170 183 if (isGtXs) {
171 184 config.minWidth = '417px';
172   - config.maxHeight = '440px';
  185 + config.maxHeight = '500px';
173 186 const panelHeight = 375;
174 187 const panelWidth = 417;
175 188 const el = this.timewindowPanelOrigin.elementRef.nativeElement;
... ... @@ -225,6 +238,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
225 238 timewindow: deepClone(this.innerValue),
226 239 historyOnly: this.historyOnly,
227 240 aggregation: this.aggregation,
  241 + timezone: this.timezone,
228 242 isEdit: this.isEdit
229 243 }
230 244 );
... ... @@ -272,14 +286,20 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
272 286
273 287 updateDisplayValue() {
274 288 if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) {
275   - this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' +
276   - this.translate.instant('timewindow.last-prefix') + ' ' +
277   - this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
  289 + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ';
  290 + if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
  291 + this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval));
  292 + } else {
  293 + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
  294 + this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
  295 + }
278 296 } else {
279 297 this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
280 298 if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
281 299 this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
282 300 this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);
  301 + } else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) {
  302 + this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval));
283 303 } else {
284 304 const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
285 305 const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');
... ...
... ... @@ -16,7 +16,7 @@
16 16
17 17 import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
18 18 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
19   -import { Observable } from 'rxjs';
  19 +import { Observable, of } from 'rxjs';
20 20 import { map, mergeMap, share, tap } from 'rxjs/operators';
21 21 import { Store } from '@ngrx/store';
22 22 import { AppState } from '@app/core/core.state';
... ... @@ -43,10 +43,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
43 43
44 44 defaultTimezoneId: string = null;
45 45
46   - timezones$ = getTimezones().pipe(
47   - share()
48   - );
49   -
50 46 @Input()
51 47 set defaultTimezone(timezone: string) {
52 48 if (this.defaultTimezoneId !== timezone) {
... ... @@ -138,23 +134,20 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
138 134
139 135 writeValue(value: string | null): void {
140 136 this.searchText = '';
141   - getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe(
142   - (foundTimezone) => {
143   - if (foundTimezone !== null) {
144   - this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
145   - if (foundTimezone.id !== value) {
146   - setTimeout(() => {
147   - this.updateView(foundTimezone.id);
148   - }, 0);
149   - } else {
150   - this.modelValue = value;
151   - }
152   - } else {
153   - this.modelValue = null;
154   - this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
155   - }
  137 + const foundTimezone = getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
  138 + if (foundTimezone !== null) {
  139 + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
  140 + if (foundTimezone.id !== value) {
  141 + setTimeout(() => {
  142 + this.updateView(foundTimezone.id);
  143 + }, 0);
  144 + } else {
  145 + this.modelValue = value;
156 146 }
157   - );
  147 + } else {
  148 + this.modelValue = null;
  149 + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
  150 + }
158 151 this.dirty = true;
159 152 }
160 153
... ... @@ -170,14 +163,12 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
170 163 this.ignoreClosePanel = false;
171 164 } else {
172 165 if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) {
173   - getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe(
174   - (defaultTimezoneInfo) => {
175   - if (defaultTimezoneInfo !== null) {
176   - this.ngZone.run(() => {
177   - this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
178   - });
179   - }
180   - });
  166 + const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
  167 + if (defaultTimezoneInfo !== null) {
  168 + this.ngZone.run(() => {
  169 + this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
  170 + });
  171 + }
181 172 }
182 173 }
183 174 }
... ... @@ -196,12 +187,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
196 187 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
197 188 this.searchText = searchText;
198 189 if (searchText && searchText.length) {
199   - return getTimezones().pipe(
200   - map((timezones) => timezones.filter((timezoneInfo) =>
201   - timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())))
202   - );
  190 + return of(getTimezones().filter((timezoneInfo) =>
  191 + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())));
203 192 }
204   - return getTimezones();
  193 + return of(getTimezones());
205 194 }
206 195
207 196 clear() {
... ...
... ... @@ -430,6 +430,38 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
430 430 constructor(msg: EntityDataUpdateMsg) {
431 431 super(msg);
432 432 }
  433 +
  434 + public prepareData(tsOffset: number) {
  435 + if (this.data) {
  436 + this.processEntityData(this.data.data, tsOffset);
  437 + }
  438 + if (this.update) {
  439 + this.processEntityData(this.update, tsOffset);
  440 + }
  441 + }
  442 +
  443 + private processEntityData(data: Array<EntityData>, tsOffset: number) {
  444 + for (const entityData of data) {
  445 + if (entityData.timeseries) {
  446 + for (const key of Object.keys(entityData.timeseries)) {
  447 + const tsValues = entityData.timeseries[key];
  448 + for (const tsValue of tsValues) {
  449 + tsValue.ts += tsOffset;
  450 + }
  451 + }
  452 + }
  453 + if (entityData.latest) {
  454 + for (const entityKeyType of Object.keys(entityData.latest)) {
  455 + const keyTypeValues = entityData.latest[entityKeyType];
  456 + for (const key of Object.keys(keyTypeValues)) {
  457 + const tsValue = keyTypeValues[key];
  458 + tsValue.ts += tsOffset;
  459 + }
  460 + }
  461 + }
  462 + }
  463 + }
  464 +
433 465 }
434 466
435 467 export class AlarmDataUpdate extends DataUpdate<AlarmData> {
... ... @@ -468,6 +500,8 @@ export class TelemetrySubscriber {
468 500
469 501 private zone: NgZone;
470 502
  503 + private tsOffset = 0;
  504 +
471 505 public subscriptionCommands: Array<WebsocketCmd>;
472 506
473 507 public data$ = this.dataSubject.asObservable();
... ... @@ -522,6 +556,10 @@ export class TelemetrySubscriber {
522 556 this.reconnectSubject.complete();
523 557 }
524 558
  559 + public setTsOffset(tsOffset: number) {
  560 + this.tsOffset = tsOffset;
  561 + }
  562 +
525 563 public onData(message: SubscriptionUpdate) {
526 564 const cmdId = message.subscriptionId;
527 565 let keys: string[];
... ... @@ -545,6 +583,9 @@ export class TelemetrySubscriber {
545 583 }
546 584
547 585 public onEntityData(message: EntityDataUpdate) {
  586 + if (this.tsOffset) {
  587 + message.prepareData(this.tsOffset);
  588 + }
548 589 if (this.zone) {
549 590 this.zone.run(
550 591 () => {
... ...
... ... @@ -17,9 +17,7 @@
17 17 import { TimeService } from '@core/services/time.service';
18 18 import { deepClone, isDefined, isUndefined } from '@app/core/utils';
19 19 import * as moment_ from 'moment';
20   -import { Observable } from 'rxjs/internal/Observable';
21   -import { from, of } from 'rxjs';
22   -import { map, mergeMap, tap } from 'rxjs/operators';
  20 +import * as monentTz from 'moment-timezone';
23 21
24 22 const moment = moment_;
25 23
... ... @@ -27,6 +25,7 @@ export const SECOND = 1000;
27 25 export const MINUTE = 60 * SECOND;
28 26 export const HOUR = 60 * MINUTE;
29 27 export const DAY = 24 * HOUR;
  28 +export const WEEK = 7 * DAY;
30 29 export const YEAR = DAY * 365;
31 30
32 31 export enum TimewindowType {
... ... @@ -34,14 +33,25 @@ export enum TimewindowType {
34 33 HISTORY
35 34 }
36 35
  36 +export enum RealtimeWindowType {
  37 + LAST_INTERVAL,
  38 + INTERVAL
  39 +}
  40 +
37 41 export enum HistoryWindowType {
38 42 LAST_INTERVAL,
39   - FIXED
  43 + FIXED,
  44 + INTERVAL
40 45 }
41 46
42 47 export interface IntervalWindow {
43 48 interval?: number;
44 49 timewindowMs?: number;
  50 + quickInterval?: QuickTimeInterval;
  51 +}
  52 +
  53 +export interface RealtimeWindow extends IntervalWindow{
  54 + realtimeType?: RealtimeWindowType;
45 55 }
46 56
47 57 export interface FixedWindow {
... ... @@ -85,10 +95,12 @@ export interface Timewindow {
85 95 hideInterval?: boolean;
86 96 hideAggregation?: boolean;
87 97 hideAggInterval?: boolean;
  98 + hideTimezone?: boolean;
88 99 selectedTab?: TimewindowType;
89   - realtime?: IntervalWindow;
  100 + realtime?: RealtimeWindow;
90 101 history?: HistoryWindow;
91 102 aggregation?: Aggregation;
  103 + timezone?: string;
92 104 }
93 105
94 106 export interface SubscriptionAggregation extends Aggregation {
... ... @@ -99,6 +111,9 @@ export interface SubscriptionAggregation extends Aggregation {
99 111
100 112 export interface SubscriptionTimewindow {
101 113 startTs?: number;
  114 + quickInterval?: QuickTimeInterval;
  115 + timezone?: string;
  116 + tsOffset?: number;
102 117 realtimeWindowMs?: number;
103 118 fixedWindow?: FixedWindow;
104 119 aggregation?: SubscriptionAggregation;
... ... @@ -108,9 +123,46 @@ export interface WidgetTimewindow {
108 123 minTime?: number;
109 124 maxTime?: number;
110 125 interval?: number;
  126 + timezone?: string;
111 127 stDiff?: number;
112 128 }
113 129
  130 +export enum QuickTimeInterval {
  131 + YESTERDAY = 'YESTERDAY',
  132 + DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY',
  133 + THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK',
  134 + PREVIOUS_WEEK = 'PREVIOUS_WEEK',
  135 + PREVIOUS_MONTH = 'PREVIOUS_MONTH',
  136 + PREVIOUS_YEAR = 'PREVIOUS_YEAR',
  137 + CURRENT_HOUR = 'CURRENT_HOUR',
  138 + CURRENT_DAY = 'CURRENT_DAY',
  139 + CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR',
  140 + CURRENT_WEEK = 'CURRENT_WEEK',
  141 + CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR',
  142 + CURRENT_MONTH = 'CURRENT_MONTH',
  143 + CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR',
  144 + CURRENT_YEAR = 'CURRENT_YEAR',
  145 + CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR'
  146 +}
  147 +
  148 +export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string>([
  149 + [QuickTimeInterval.YESTERDAY, 'timeinterval.predefined.yesterday'],
  150 + [QuickTimeInterval.DAY_BEFORE_YESTERDAY, 'timeinterval.predefined.day-before-yesterday'],
  151 + [QuickTimeInterval.THIS_DAY_LAST_WEEK, 'timeinterval.predefined.this-day-last-week'],
  152 + [QuickTimeInterval.PREVIOUS_WEEK, 'timeinterval.predefined.previous-week'],
  153 + [QuickTimeInterval.PREVIOUS_MONTH, 'timeinterval.predefined.previous-month'],
  154 + [QuickTimeInterval.PREVIOUS_YEAR, 'timeinterval.predefined.previous-year'],
  155 + [QuickTimeInterval.CURRENT_HOUR, 'timeinterval.predefined.current-hour'],
  156 + [QuickTimeInterval.CURRENT_DAY, 'timeinterval.predefined.current-day'],
  157 + [QuickTimeInterval.CURRENT_DAY_SO_FAR, 'timeinterval.predefined.current-day-so-far'],
  158 + [QuickTimeInterval.CURRENT_WEEK, 'timeinterval.predefined.current-week'],
  159 + [QuickTimeInterval.CURRENT_WEEK_SO_FAR, 'timeinterval.predefined.current-week-so-far'],
  160 + [QuickTimeInterval.CURRENT_MONTH, 'timeinterval.predefined.current-month'],
  161 + [QuickTimeInterval.CURRENT_MONTH_SO_FAR, 'timeinterval.predefined.current-month-so-far'],
  162 + [QuickTimeInterval.CURRENT_YEAR, 'timeinterval.predefined.current-year'],
  163 + [QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far']
  164 +]);
  165 +
114 166 export function historyInterval(timewindowMs: number): Timewindow {
115 167 const timewindow: Timewindow = {
116 168 selectedTab: TimewindowType.HISTORY,
... ... @@ -129,10 +181,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
129 181 hideInterval: false,
130 182 hideAggregation: false,
131 183 hideAggInterval: false,
  184 + hideTimezone: false,
132 185 selectedTab: TimewindowType.REALTIME,
133 186 realtime: {
  187 + realtimeType: RealtimeWindowType.LAST_INTERVAL,
134 188 interval: SECOND,
135   - timewindowMs: MINUTE
  189 + timewindowMs: MINUTE,
  190 + quickInterval: QuickTimeInterval.CURRENT_DAY
136 191 },
137 192 history: {
138 193 historyType: HistoryWindowType.LAST_INTERVAL,
... ... @@ -141,7 +196,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
141 196 fixedTimewindow: {
142 197 startTimeMs: currentTime - DAY,
143 198 endTimeMs: currentTime
144   - }
  199 + },
  200 + quickInterval: QuickTimeInterval.CURRENT_DAY
145 201 },
146 202 aggregation: {
147 203 type: AggregationType.AVG,
... ... @@ -157,6 +213,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
157 213 model.hideInterval = value.hideInterval;
158 214 model.hideAggregation = value.hideAggregation;
159 215 model.hideAggInterval = value.hideAggInterval;
  216 + model.hideTimezone = value.hideTimezone;
160 217 if (isUndefined(value.selectedTab)) {
161 218 if (value.realtime) {
162 219 model.selectedTab = TimewindowType.REALTIME;
... ... @@ -170,7 +227,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
170 227 if (isDefined(value.realtime.interval)) {
171 228 model.realtime.interval = value.realtime.interval;
172 229 }
173   - model.realtime.timewindowMs = value.realtime.timewindowMs;
  230 + if (isUndefined(value.realtime.realtimeType)) {
  231 + if (isDefined(value.realtime.quickInterval)) {
  232 + model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
  233 + } else {
  234 + model.realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL;
  235 + }
  236 + } else {
  237 + model.realtime.realtimeType = value.realtime.realtimeType;
  238 + }
  239 + if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
  240 + model.realtime.quickInterval = value.realtime.quickInterval;
  241 + } else {
  242 + model.realtime.timewindowMs = value.realtime.timewindowMs;
  243 + }
174 244 } else {
175 245 if (isDefined(value.history.interval)) {
176 246 model.history.interval = value.history.interval;
... ... @@ -178,6 +248,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
178 248 if (isUndefined(value.history.historyType)) {
179 249 if (isDefined(value.history.timewindowMs)) {
180 250 model.history.historyType = HistoryWindowType.LAST_INTERVAL;
  251 + } else if (isDefined(value.history.quickInterval)) {
  252 + model.history.historyType = HistoryWindowType.INTERVAL;
181 253 } else {
182 254 model.history.historyType = HistoryWindowType.FIXED;
183 255 }
... ... @@ -186,6 +258,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
186 258 }
187 259 if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) {
188 260 model.history.timewindowMs = value.history.timewindowMs;
  261 + } else if (model.history.historyType === HistoryWindowType.INTERVAL) {
  262 + model.history.quickInterval = value.history.quickInterval;
189 263 } else {
190 264 model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
191 265 model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
... ... @@ -197,6 +271,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
197 271 }
198 272 model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
199 273 }
  274 + model.timezone = value.timezone;
200 275 }
201 276 return model;
202 277 }
... ... @@ -223,6 +298,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
223 298 hideInterval: timewindow.hideInterval || false,
224 299 hideAggregation: timewindow.hideAggregation || false,
225 300 hideAggInterval: timewindow.hideAggInterval || false,
  301 + hideTimezone: timewindow.hideTimezone || false,
226 302 selectedTab: TimewindowType.HISTORY,
227 303 history: {
228 304 historyType: HistoryWindowType.FIXED,
... ... @@ -235,7 +311,8 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
235 311 aggregation: {
236 312 type: aggType,
237 313 limit
238   - }
  314 + },
  315 + timezone: timewindow.timezone
239 316 };
240 317 return historyTimewindow;
241 318 }
... ... @@ -249,8 +326,15 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
249 326 interval: SECOND,
250 327 limit: timeService.getMaxDatapointsLimit(),
251 328 type: AggregationType.AVG
252   - }
  329 + },
  330 + timezone: timewindow.timezone,
  331 + tsOffset: 0
253 332 };
  333 + if (timewindow.timezone) {
  334 + const tz = getTimezone(timewindow.timezone);
  335 + const localOffset = moment().utcOffset();
  336 + subscriptionTimewindow.tsOffset = (tz.utcOffset() - localOffset) * 60 * 1000;
  337 + }
254 338 let aggTimewindow = 0;
255 339 if (stateData) {
256 340 subscriptionTimewindow.aggregation.type = AggregationType.NONE;
... ... @@ -267,33 +351,65 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
267 351 selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY;
268 352 }
269 353 if (selectedTab === TimewindowType.REALTIME) {
270   - subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
  354 + let realtimeType = timewindow.realtime.realtimeType;
  355 + if (isUndefined(realtimeType)) {
  356 + if (isDefined(timewindow.realtime.quickInterval)) {
  357 + realtimeType = RealtimeWindowType.INTERVAL;
  358 + } else {
  359 + realtimeType = RealtimeWindowType.LAST_INTERVAL;
  360 + }
  361 + }
  362 + if (realtimeType === RealtimeWindowType.INTERVAL) {
  363 + const currentDate = getCurrentTime(timewindow.timezone);
  364 + subscriptionTimewindow.realtimeWindowMs =
  365 + getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, currentDate);
  366 + subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval;
  367 + subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate);
  368 + } else {
  369 + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
  370 + subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
  371 + }
271 372 subscriptionTimewindow.aggregation.interval =
272 373 timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
273 374 subscriptionTimewindow.aggregation.type);
274   - subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
275   - const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
276 375 aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
277   - if (startDiff) {
278   - subscriptionTimewindow.startTs -= startDiff;
279   - aggTimewindow += subscriptionTimewindow.aggregation.interval;
  376 + if (realtimeType !== RealtimeWindowType.INTERVAL) {
  377 + const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
  378 + if (startDiff) {
  379 + subscriptionTimewindow.startTs -= startDiff;
  380 + aggTimewindow += subscriptionTimewindow.aggregation.interval;
  381 + }
280 382 }
281 383 } else {
282 384 let historyType = timewindow.history.historyType;
283 385 if (isUndefined(historyType)) {
284   - historyType = isDefined(timewindow.history.timewindowMs) ? HistoryWindowType.LAST_INTERVAL : HistoryWindowType.FIXED;
  386 + if (isDefined(timewindow.history.timewindowMs)) {
  387 + historyType = HistoryWindowType.LAST_INTERVAL;
  388 + } else if (isDefined(timewindow.history.quickInterval)) {
  389 + historyType = HistoryWindowType.INTERVAL;
  390 + } else {
  391 + historyType = HistoryWindowType.FIXED;
  392 + }
285 393 }
286 394 if (historyType === HistoryWindowType.LAST_INTERVAL) {
287   - const currentTime = Date.now();
  395 + const currentDate = getCurrentTime(timewindow.timezone);
  396 + const currentTime = currentDate.valueOf();
288 397 subscriptionTimewindow.fixedWindow = {
289 398 startTimeMs: currentTime - timewindow.history.timewindowMs,
290 399 endTimeMs: currentTime
291 400 };
292 401 aggTimewindow = timewindow.history.timewindowMs;
  402 + } else if (historyType === HistoryWindowType.INTERVAL) {
  403 + const currentDate = getCurrentTime(timewindow.timezone);
  404 + subscriptionTimewindow.fixedWindow = {
  405 + startTimeMs: calculateIntervalStartTime(timewindow.history.quickInterval, currentDate),
  406 + endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
  407 + };
  408 + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
293 409 } else {
294 410 subscriptionTimewindow.fixedWindow = {
295   - startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
296   - endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
  411 + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset,
  412 + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset
297 413 };
298 414 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
299 415 }
... ... @@ -309,6 +425,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
309 425 return subscriptionTimewindow;
310 426 }
311 427
  428 +function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, currentDate: moment_.Moment): number {
  429 + switch (interval) {
  430 + case QuickTimeInterval.CURRENT_HOUR:
  431 + return HOUR;
  432 + case QuickTimeInterval.CURRENT_DAY:
  433 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  434 + return DAY;
  435 + case QuickTimeInterval.CURRENT_WEEK:
  436 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  437 + return WEEK;
  438 + case QuickTimeInterval.CURRENT_MONTH:
  439 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  440 + return currentDate.endOf('month').diff(currentDate.clone().startOf('month'));
  441 + case QuickTimeInterval.CURRENT_YEAR:
  442 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  443 + return currentDate.endOf('year').diff(currentDate.clone().startOf('year'));
  444 + }
  445 +}
  446 +
  447 +export function calculateIntervalEndTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
  448 + currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
  449 + switch (interval) {
  450 + case QuickTimeInterval.YESTERDAY:
  451 + currentDate.subtract(1, 'days');
  452 + return currentDate.endOf('day').valueOf();
  453 + case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
  454 + currentDate.subtract(2, 'days');
  455 + return currentDate.endOf('day').valueOf();
  456 + case QuickTimeInterval.THIS_DAY_LAST_WEEK:
  457 + currentDate.subtract(1, 'weeks');
  458 + return currentDate.endOf('day').valueOf();
  459 + case QuickTimeInterval.PREVIOUS_WEEK:
  460 + currentDate.subtract(1, 'weeks');
  461 + return currentDate.endOf('week').valueOf();
  462 + case QuickTimeInterval.PREVIOUS_MONTH:
  463 + currentDate.subtract(1, 'months');
  464 + return currentDate.endOf('month').valueOf();
  465 + case QuickTimeInterval.PREVIOUS_YEAR:
  466 + currentDate.subtract(1, 'years');
  467 + return currentDate.endOf('year').valueOf();
  468 + case QuickTimeInterval.CURRENT_HOUR:
  469 + return currentDate.endOf('hour').valueOf();
  470 + case QuickTimeInterval.CURRENT_DAY:
  471 + return currentDate.endOf('day').valueOf();
  472 + case QuickTimeInterval.CURRENT_WEEK:
  473 + return currentDate.endOf('week').valueOf();
  474 + case QuickTimeInterval.CURRENT_MONTH:
  475 + return currentDate.endOf('month').valueOf();
  476 + case QuickTimeInterval.CURRENT_YEAR:
  477 + return currentDate.endOf('year').valueOf();
  478 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  479 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  480 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  481 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  482 + return currentDate.valueOf();
  483 + }
  484 +}
  485 +
  486 +export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
  487 + currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
  488 + switch (interval) {
  489 + case QuickTimeInterval.YESTERDAY:
  490 + currentDate.subtract(1, 'days');
  491 + return currentDate.startOf('day').valueOf();
  492 + case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
  493 + currentDate.subtract(2, 'days');
  494 + return currentDate.startOf('day').valueOf();
  495 + case QuickTimeInterval.THIS_DAY_LAST_WEEK:
  496 + currentDate.subtract(1, 'weeks');
  497 + return currentDate.startOf('day').valueOf();
  498 + case QuickTimeInterval.PREVIOUS_WEEK:
  499 + currentDate.subtract(1, 'weeks');
  500 + return currentDate.startOf('week').valueOf();
  501 + case QuickTimeInterval.PREVIOUS_MONTH:
  502 + currentDate.subtract(1, 'months');
  503 + return currentDate.startOf('month').valueOf();
  504 + case QuickTimeInterval.PREVIOUS_YEAR:
  505 + currentDate.subtract(1, 'years');
  506 + return currentDate.startOf('year').valueOf();
  507 + case QuickTimeInterval.CURRENT_HOUR:
  508 + return currentDate.startOf('hour').valueOf();
  509 + case QuickTimeInterval.CURRENT_DAY:
  510 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  511 + return currentDate.startOf('day').valueOf();
  512 + case QuickTimeInterval.CURRENT_WEEK:
  513 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  514 + return currentDate.startOf('week').valueOf();
  515 + case QuickTimeInterval.CURRENT_MONTH:
  516 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  517 + return currentDate.startOf('month').valueOf();
  518 + case QuickTimeInterval.CURRENT_YEAR:
  519 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  520 + return currentDate.startOf('year').valueOf();
  521 + }
  522 +}
  523 +
  524 +export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
  525 + switch (interval) {
  526 + case QuickTimeInterval.CURRENT_HOUR:
  527 + return HOUR;
  528 + case QuickTimeInterval.YESTERDAY:
  529 + case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
  530 + case QuickTimeInterval.THIS_DAY_LAST_WEEK:
  531 + case QuickTimeInterval.CURRENT_DAY:
  532 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  533 + return DAY;
  534 + case QuickTimeInterval.PREVIOUS_WEEK:
  535 + case QuickTimeInterval.CURRENT_WEEK:
  536 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  537 + return WEEK;
  538 + case QuickTimeInterval.PREVIOUS_MONTH:
  539 + case QuickTimeInterval.CURRENT_MONTH:
  540 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  541 + return DAY * 30;
  542 + case QuickTimeInterval.PREVIOUS_YEAR:
  543 + case QuickTimeInterval.CURRENT_YEAR:
  544 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  545 + return YEAR;
  546 + }
  547 +}
  548 +
312 549 export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow,
313 550 timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow {
314 551 const timewindowForComparison: SubscriptionTimewindow = {
... ... @@ -339,6 +576,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
339 576 cloned.hideInterval = timewindow.hideInterval || false;
340 577 cloned.hideAggregation = timewindow.hideAggregation || false;
341 578 cloned.hideAggInterval = timewindow.hideAggInterval || false;
  579 + cloned.hideTimezone = timewindow.hideTimezone || false;
342 580 if (isDefined(timewindow.selectedTab)) {
343 581 cloned.selectedTab = timewindow.selectedTab;
344 582 if (timewindow.selectedTab === TimewindowType.REALTIME) {
... ... @@ -348,6 +586,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
348 586 }
349 587 }
350 588 cloned.aggregation = deepClone(timewindow.aggregation);
  589 + cloned.timezone = timewindow.timezone;
351 590 return cloned;
352 591 }
353 592
... ... @@ -358,6 +597,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi
358 597 cloned.interval = historyWindow.interval;
359 598 if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
360 599 cloned.timewindowMs = historyWindow.timewindowMs;
  600 + } else if (historyWindow.historyType === HistoryWindowType.INTERVAL) {
  601 + cloned.quickInterval = historyWindow.quickInterval;
361 602 } else if (historyWindow.historyType === HistoryWindowType.FIXED) {
362 603 cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
363 604 }
... ... @@ -375,7 +616,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
375 616 {
376 617 name: 'timeinterval.seconds-interval',
377 618 translateParams: {seconds: 1},
378   - value: 1 * SECOND
  619 + value: SECOND
379 620 },
380 621 {
381 622 name: 'timeinterval.seconds-interval',
... ... @@ -400,7 +641,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
400 641 {
401 642 name: 'timeinterval.minutes-interval',
402 643 translateParams: {minutes: 1},
403   - value: 1 * MINUTE
  644 + value: MINUTE
404 645 },
405 646 {
406 647 name: 'timeinterval.minutes-interval',
... ... @@ -430,7 +671,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
430 671 {
431 672 name: 'timeinterval.hours-interval',
432 673 translateParams: {hours: 1},
433   - value: 1 * HOUR
  674 + value: HOUR
434 675 },
435 676 {
436 677 name: 'timeinterval.hours-interval',
... ... @@ -455,7 +696,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
455 696 {
456 697 name: 'timeinterval.days-interval',
457 698 translateParams: {days: 1},
458   - value: 1 * DAY
  699 + value: DAY
459 700 },
460 701 {
461 702 name: 'timeinterval.days-interval',
... ... @@ -495,60 +736,50 @@ export interface TimezoneInfo {
495 736 let timezones: TimezoneInfo[] = null;
496 737 let defaultTimezone: string = null;
497 738
498   -export function getTimezones(): Observable<TimezoneInfo[]> {
499   - if (timezones) {
500   - return of(timezones);
501   - } else {
502   - return from(import('moment-timezone')).pipe(
503   - map((monentTz) => {
504   - return monentTz.tz.names().map((zoneName) => {
505   - const tz = monentTz.tz(zoneName);
506   - return {
507   - id: zoneName,
508   - name: zoneName.replace(/_/g, ' '),
509   - offset: `UTC${tz.format('Z')}`,
510   - nOffset: tz.utcOffset()
511   - };
512   - });
513   - }),
514   - tap((zones) => {
515   - timezones = zones;
516   - })
517   - );
  739 +export function getTimezones(): TimezoneInfo[] {
  740 + if (!timezones) {
  741 + timezones = monentTz.tz.names().map((zoneName) => {
  742 + const tz = monentTz.tz(zoneName);
  743 + return {
  744 + id: zoneName,
  745 + name: zoneName.replace(/_/g, ' '),
  746 + offset: `UTC${tz.format('Z')}`,
  747 + nOffset: tz.utcOffset()
  748 + };
  749 + });
518 750 }
  751 + return timezones;
519 752 }
520 753
521   -export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): Observable<TimezoneInfo> {
522   - return getTimezones().pipe(
523   - mergeMap((timezoneList) => {
524   - let foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId);
525   - if (!foundTimezone) {
526   - if (userTimezoneByDefault) {
527   - return getDefaultTimezone().pipe(
528   - map((userTimezone) => {
529   - return timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone);
530   - })
531   - );
532   - } else if (defaultTimezoneId) {
533   - foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
534   - }
535   - }
536   - return of(foundTimezone);
537   - })
538   - );
  754 +export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo {
  755 + const timezoneList = getTimezones();
  756 + let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null;
  757 + if (!foundTimezone) {
  758 + if (userTimezoneByDefault) {
  759 + const userTimezone = getDefaultTimezone();
  760 + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone);
  761 + } else if (defaultTimezoneId) {
  762 + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
  763 + }
  764 + }
  765 + return foundTimezone;
539 766 }
540 767
541   -export function getDefaultTimezone(): Observable<string> {
542   - if (defaultTimezone) {
543   - return of(defaultTimezone);
  768 +export function getDefaultTimezone(): string {
  769 + if (!defaultTimezone) {
  770 + defaultTimezone = monentTz.tz.guess();
  771 + }
  772 + return defaultTimezone;
  773 +}
  774 +
  775 +export function getCurrentTime(tz?: string): moment_.Moment {
  776 + if (tz) {
  777 + return moment().tz(tz);
544 778 } else {
545   - return from(import('moment-timezone')).pipe(
546   - map((monentTz) => {
547   - return monentTz.tz.guess();
548   - }),
549   - tap((zone) => {
550   - defaultTimezone = zone;
551   - })
552   - );
  779 + return moment();
553 780 }
554 781 }
  782 +
  783 +export function getTimezone(tz: string): moment_.Moment {
  784 + return moment.tz(tz);
  785 +}
... ...
... ... @@ -140,6 +140,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
140 140 import { FileSizePipe } from '@shared/pipe/file-size.pipe';
141 141 import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
142 142 import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
  143 +import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
143 144
144 145 @NgModule({
145 146 providers: [
... ... @@ -175,6 +176,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
175 176 TimewindowComponent,
176 177 TimewindowPanelComponent,
177 178 TimeintervalComponent,
  179 + QuickTimeIntervalComponent,
178 180 DashboardSelectComponent,
179 181 DashboardSelectPanelComponent,
180 182 DatetimePeriodComponent,
... ... @@ -302,6 +304,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
302 304 TimewindowComponent,
303 305 TimewindowPanelComponent,
304 306 TimeintervalComponent,
  307 + QuickTimeIntervalComponent,
305 308 DashboardSelectComponent,
306 309 DatetimePeriodComponent,
307 310 DatetimeComponent,
... ...
... ... @@ -2118,7 +2118,24 @@
2118 2118 "hours": "Hours",
2119 2119 "minutes": "Minutes",
2120 2120 "seconds": "Seconds",
2121   - "advanced": "Advanced"
  2121 + "advanced": "Advanced",
  2122 + "predefined": {
  2123 + "yesterday": "Yesterday",
  2124 + "day-before-yesterday": "Day before yesterday",
  2125 + "this-day-last-week": "This day last week",
  2126 + "previous-week": "Previous week",
  2127 + "previous-month": "Previous month",
  2128 + "previous-year": "Previous year",
  2129 + "current-hour": "Current hour",
  2130 + "current-day": "Current day",
  2131 + "current-day-so-far": "Current day so far",
  2132 + "current-week": "Current week",
  2133 + "current-week-so-far": "Current week so far",
  2134 + "current-month": "Current month",
  2135 + "current-month-so-far": "Current month so far",
  2136 + "current-year": "Current year",
  2137 + "current-year-so-far": "Current year so far"
  2138 + }
2122 2139 },
2123 2140 "timeunit": {
2124 2141 "seconds": "Seconds",
... ... @@ -2139,7 +2156,8 @@
2139 2156 "date-range": "Date range",
2140 2157 "last": "Last",
2141 2158 "time-period": "Time period",
2142   - "hide": "Hide"
  2159 + "hide": "Hide",
  2160 + "interval": "Interval"
2143 2161 },
2144 2162 "user": {
2145 2163 "user": "User",
... ...