Commit a17c5b77578e8bfed9545f91ea39796adfd864d2

Authored by Igor Kulikov
1 parent 4b0cf383

Improve timezone offset handling for latest values and alarm widgets. Improve ti…

…mezone selector - add local browser time info on empty value.
@@ -130,6 +130,7 @@ export class AlarmDataSubscription { @@ -130,6 +130,7 @@ export class AlarmDataSubscription {
130 this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; 130 this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs;
131 } 131 }
132 132
  133 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
133 this.subscriber.subscriptionCommands.push(this.alarmDataCommand); 134 this.subscriber.subscriptionCommands.push(this.alarmDataCommand);
134 135
135 this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { 136 this.subscriber.alarmData$.subscribe((alarmDataUpdate) => {
@@ -143,8 +144,11 @@ export class AlarmDataSubscription { @@ -143,8 +144,11 @@ export class AlarmDataSubscription {
143 this.subscriber.subscribe(); 144 this.subscriber.subscribe();
144 145
145 } else if (this.datasourceType === DatasourceType.function) { 146 } else if (this.datasourceType === DatasourceType.function) {
  147 + const alarm = deepClone(simulatedAlarm);
  148 + alarm.createdTime += this.subsTw.tsOffset;
  149 + alarm.startTs += this.subsTw.tsOffset;
146 const pageData: PageData<AlarmData> = { 150 const pageData: PageData<AlarmData> = {
147 - data: [{...simulatedAlarm, entityId: '1', latest: {}}], 151 + data: [{...alarm, entityId: '1', latest: {}}],
148 hasNext: false, 152 hasNext: false,
149 totalElements: 1, 153 totalElements: 1,
150 totalPages: 1 154 totalPages: 1
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 /// 15 ///
16 16
17 import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; 17 import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
18 -import { AggregationType, SubscriptionTimewindow } from '@shared/models/time/time.models'; 18 +import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models';
19 import { 19 import {
20 EntityData, 20 EntityData,
21 EntityDataPageLink, 21 EntityDataPageLink,
@@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions { @@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions {
74 keyFilters?: Array<KeyFilter>; 74 keyFilters?: Array<KeyFilter>;
75 additionalKeyFilters?: Array<KeyFilter>; 75 additionalKeyFilters?: Array<KeyFilter>;
76 subscriptionTimewindow?: SubscriptionTimewindow; 76 subscriptionTimewindow?: SubscriptionTimewindow;
  77 + latestTsOffset?: number;
77 } 78 }
78 79
79 export class EntityDataSubscription { 80 export class EntityDataSubscription {
@@ -95,6 +96,7 @@ export class EntityDataSubscription { @@ -95,6 +96,7 @@ export class EntityDataSubscription {
95 private entityDataResolveSubject: Subject<EntityDataLoadResult>; 96 private entityDataResolveSubject: Subject<EntityDataLoadResult>;
96 private pageData: PageData<EntityData>; 97 private pageData: PageData<EntityData>;
97 private subsTw: SubscriptionTimewindow; 98 private subsTw: SubscriptionTimewindow;
  99 + private latestTsOffset: number;
98 private dataAggregators: Array<DataAggregator>; 100 private dataAggregators: Array<DataAggregator>;
99 private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}; 101 private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
100 private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; 102 private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
@@ -177,6 +179,7 @@ export class EntityDataSubscription { @@ -177,6 +179,7 @@ export class EntityDataSubscription {
177 this.started = true; 179 this.started = true;
178 this.dataResolved = true; 180 this.dataResolved = true;
179 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; 181 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  182 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
180 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && 183 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
181 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); 184 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
182 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && 185 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
@@ -237,7 +240,12 @@ export class EntityDataSubscription { @@ -237,7 +240,12 @@ export class EntityDataSubscription {
237 }; 240 };
238 241
239 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { 242 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
240 - this.prepareSubscriptionCommands(); 243 + this.prepareSubscriptionCommands(this.dataCommand);
  244 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  245 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
  246 + } else {
  247 + this.subscriber.setTsOffset(this.latestTsOffset);
  248 + }
241 } 249 }
242 250
243 this.subscriber.subscriptionCommands.push(this.dataCommand); 251 this.subscriber.subscriptionCommands.push(this.dataCommand);
@@ -276,9 +284,15 @@ export class EntityDataSubscription { @@ -276,9 +284,15 @@ export class EntityDataSubscription {
276 this.subscriber.subscriptionCommands = [this.dataCommand]; 284 this.subscriber.subscriptionCommands = [this.dataCommand];
277 } 285 }
278 }); 286 });
279 -  
280 this.subscriber.subscribe(); 287 this.subscriber.subscribe();
281 } else if (this.datasourceType === DatasourceType.function) { 288 } else if (this.datasourceType === DatasourceType.function) {
  289 + let tsOffset = 0;
  290 + if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  291 + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
  292 + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) {
  293 + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  294 + }
  295 +
282 const entityData: EntityData = { 296 const entityData: EntityData = {
283 entityId: { 297 entityId: {
284 id: NULL_UUID, 298 id: NULL_UUID,
@@ -289,7 +303,7 @@ export class EntityDataSubscription { @@ -289,7 +303,7 @@ export class EntityDataSubscription {
289 }; 303 };
290 const name = DatasourceType.function; 304 const name = DatasourceType.function;
291 entityData.latest[EntityKeyType.ENTITY_FIELD] = { 305 entityData.latest[EntityKeyType.ENTITY_FIELD] = {
292 - name: {ts: Date.now(), value: name} 306 + name: {ts: Date.now() + tsOffset, value: name}
293 }; 307 };
294 const pageData: PageData<EntityData> = { 308 const pageData: PageData<EntityData> = {
295 data: [entityData], 309 data: [entityData],
@@ -299,7 +313,9 @@ export class EntityDataSubscription { @@ -299,7 +313,9 @@ export class EntityDataSubscription {
299 }; 313 };
300 this.onPageData(pageData); 314 this.onPageData(pageData);
301 } else if (this.datasourceType === DatasourceType.entityCount) { 315 } else if (this.datasourceType === DatasourceType.entityCount) {
  316 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
302 this.subscriber = new TelemetrySubscriber(this.telemetryService); 317 this.subscriber = new TelemetrySubscriber(this.telemetryService);
  318 + this.subscriber.setTsOffset(this.latestTsOffset);
303 this.countCommand = new EntityCountCmd(); 319 this.countCommand = new EntityCountCmd();
304 let keyFilters = this.entityDataSubscriptionOptions.keyFilters; 320 let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
305 if (this.entityDataSubscriptionOptions.additionalKeyFilters) { 321 if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
@@ -332,13 +348,13 @@ export class EntityDataSubscription { @@ -332,13 +348,13 @@ export class EntityDataSubscription {
332 latest: { 348 latest: {
333 [EntityKeyType.ENTITY_FIELD]: { 349 [EntityKeyType.ENTITY_FIELD]: {
334 name: { 350 name: {
335 - ts: Date.now(), 351 + ts: Date.now() + this.latestTsOffset,
336 value: DatasourceType.entityCount 352 value: DatasourceType.entityCount
337 } 353 }
338 }, 354 },
339 [EntityKeyType.COUNT]: { 355 [EntityKeyType.COUNT]: {
340 [countKey.name]: { 356 [countKey.name]: {
341 - ts: Date.now(), 357 + ts: Date.now() + this.latestTsOffset,
342 value: entityCountUpdate.count + '' 358 value: entityCountUpdate.count + ''
343 } 359 }
344 } 360 }
@@ -359,7 +375,7 @@ export class EntityDataSubscription { @@ -359,7 +375,7 @@ export class EntityDataSubscription {
359 latest: { 375 latest: {
360 [EntityKeyType.COUNT]: { 376 [EntityKeyType.COUNT]: {
361 [countKey.name]: { 377 [countKey.name]: {
362 - ts: Date.now(), 378 + ts: Date.now() + this.latestTsOffset,
363 value: entityCountUpdate.count + '' 379 value: entityCountUpdate.count + ''
364 } 380 }
365 } 381 }
@@ -384,6 +400,7 @@ export class EntityDataSubscription { @@ -384,6 +400,7 @@ export class EntityDataSubscription {
384 return; 400 return;
385 } 401 }
386 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; 402 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  403 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
387 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && 404 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
388 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); 405 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
389 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && 406 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
@@ -394,22 +411,38 @@ export class EntityDataSubscription { @@ -394,22 +411,38 @@ export class EntityDataSubscription {
394 if (this.datasourceType === DatasourceType.entity) { 411 if (this.datasourceType === DatasourceType.entity) {
395 this.subsCommand = new EntityDataCmd(); 412 this.subsCommand = new EntityDataCmd();
396 this.subsCommand.cmdId = this.dataCommand.cmdId; 413 this.subsCommand.cmdId = this.dataCommand.cmdId;
397 - this.prepareSubscriptionCommands();  
398 - if (!this.subsCommand.isEmpty()) { 414 + this.prepareSubscriptionCommands(this.subsCommand);
  415 + let latestTsOffsetChanged = false;
  416 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  417 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
  418 + } else {
  419 + latestTsOffsetChanged = this.subscriber.setTsOffset(this.latestTsOffset);
  420 + }
  421 + if (latestTsOffsetChanged) {
  422 + if (this.listener.initialPageDataChanged) {
  423 + this.listener.initialPageDataChanged(this.pageData);
  424 + }
  425 + } else if (!this.subsCommand.isEmpty()) {
399 this.subscriber.subscriptionCommands = [this.subsCommand]; 426 this.subscriber.subscriptionCommands = [this.subsCommand];
400 this.subscriber.update(); 427 this.subscriber.update();
401 } 428 }
  429 + } else if (this.datasourceType === DatasourceType.entityCount) {
  430 + if (this.subscriber.setTsOffset(this.latestTsOffset)) {
  431 + if (this.listener.initialPageDataChanged) {
  432 + this.listener.initialPageDataChanged(this.pageData);
  433 + }
  434 + }
402 } else if (this.datasourceType === DatasourceType.function) { 435 } else if (this.datasourceType === DatasourceType.function) {
403 this.startFunction(); 436 this.startFunction();
404 } 437 }
405 this.started = true; 438 this.started = true;
406 } 439 }
407 440
408 - private prepareSubscriptionCommands() { 441 + private prepareSubscriptionCommands(cmd: EntityDataCmd) {
409 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { 442 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
410 if (this.tsFields.length > 0) { 443 if (this.tsFields.length > 0) {
411 if (this.history) { 444 if (this.history) {
412 - this.subsCommand.historyCmd = { 445 + cmd.historyCmd = {
413 keys: this.tsFields.map(key => key.key), 446 keys: this.tsFields.map(key => key.key),
414 startTs: this.subsTw.fixedWindow.startTimeMs, 447 startTs: this.subsTw.fixedWindow.startTimeMs,
415 endTs: this.subsTw.fixedWindow.endTimeMs, 448 endTs: this.subsTw.fixedWindow.endTimeMs,
@@ -419,7 +452,7 @@ export class EntityDataSubscription { @@ -419,7 +452,7 @@ export class EntityDataSubscription {
419 fetchLatestPreviousPoint: this.subsTw.aggregation.stateData 452 fetchLatestPreviousPoint: this.subsTw.aggregation.stateData
420 }; 453 };
421 } else { 454 } else {
422 - this.subsCommand.tsCmd = { 455 + cmd.tsCmd = {
423 keys: this.tsFields.map(key => key.key), 456 keys: this.tsFields.map(key => key.key),
424 startTs: this.subsTw.startTs, 457 startTs: this.subsTw.startTs,
425 timeWindow: this.subsTw.aggregation.timeWindow, 458 timeWindow: this.subsTw.aggregation.timeWindow,
@@ -430,10 +463,9 @@ export class EntityDataSubscription { @@ -430,10 +463,9 @@ export class EntityDataSubscription {
430 }; 463 };
431 } 464 }
432 } 465 }
433 - this.subscriber.setTsOffset(this.subsTw.tsOffset);  
434 } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { 466 } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
435 if (this.latestValues.length > 0) { 467 if (this.latestValues.length > 0) {
436 - this.subsCommand.latestCmd = { 468 + cmd.latestCmd = {
437 keys: this.latestValues 469 keys: this.latestValues
438 }; 470 };
439 } 471 }
@@ -783,7 +815,7 @@ export class EntityDataSubscription { @@ -783,7 +815,7 @@ export class EntityDataSubscription {
783 } else { 815 } else {
784 prevSeries = [0, 0]; 816 prevSeries = [0, 0];
785 } 817 }
786 - const time = Date.now(); 818 + const time = Date.now() + this.latestTsOffset;
787 const value = dataKey.func(time, prevSeries[1]); 819 const value = dataKey.func(time, prevSeries[1]);
788 const series: [number, any] = [time, value]; 820 const series: [number, any] = [time, value];
789 this.datasourceData[0][dataKey.key].data = [series]; 821 this.datasourceData[0][dataKey.key].data = [series];
@@ -838,6 +870,10 @@ export class EntityDataSubscription { @@ -838,6 +870,10 @@ export class EntityDataSubscription {
838 endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs + 870 endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
839 this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; 871 this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
840 } 872 }
  873 + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) {
  874 + const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  875 + endTime = Math.min(currentTime, endTime);
  876 + }
841 } 877 }
842 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); 878 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
843 } 879 }
@@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs'; @@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs';
32 export interface EntityDataListener { 32 export interface EntityDataListener {
33 subscriptionType: widgetType; 33 subscriptionType: widgetType;
34 subscriptionTimewindow?: SubscriptionTimewindow; 34 subscriptionTimewindow?: SubscriptionTimewindow;
  35 + latestTsOffset?: number;
35 configDatasource: Datasource; 36 configDatasource: Datasource;
36 configDatasourceIndex: number; 37 configDatasourceIndex: number;
37 dataLoaded: (pageData: PageData<EntityData>, 38 dataLoaded: (pageData: PageData<EntityData>,
@@ -92,6 +93,8 @@ export class EntityDataService { @@ -92,6 +93,8 @@ export class EntityDataService {
92 if (listener.subscription) { 93 if (listener.subscription) {
93 if (listener.subscriptionType === widgetType.timeseries) { 94 if (listener.subscriptionType === widgetType.timeseries) {
94 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); 95 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  96 + } else if (listener.subscriptionType === widgetType.latest) {
  97 + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
95 } 98 }
96 listener.subscription.start(); 99 listener.subscription.start();
97 } 100 }
@@ -118,6 +121,8 @@ export class EntityDataService { @@ -118,6 +121,8 @@ export class EntityDataService {
118 listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); 121 listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils);
119 if (listener.subscriptionType === widgetType.timeseries) { 122 if (listener.subscriptionType === widgetType.timeseries) {
120 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); 123 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  124 + } else if (listener.subscriptionType === widgetType.latest) {
  125 + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
121 } 126 }
122 return listener.subscription.subscribe(); 127 return listener.subscription.subscribe();
123 } 128 }
@@ -39,8 +39,10 @@ import { HttpErrorResponse } from '@angular/common/http'; @@ -39,8 +39,10 @@ import { HttpErrorResponse } from '@angular/common/http';
39 import { 39 import {
40 calculateIntervalEndTime, 40 calculateIntervalEndTime,
41 calculateIntervalStartTime, 41 calculateIntervalStartTime,
  42 + calculateTsOffset,
42 createSubscriptionTimewindow, 43 createSubscriptionTimewindow,
43 - createTimewindowForComparison, getCurrentTime, 44 + createTimewindowForComparison,
  45 + getCurrentTime,
44 SubscriptionTimewindow, 46 SubscriptionTimewindow,
45 Timewindow, 47 Timewindow,
46 toHistoryTimewindow, 48 toHistoryTimewindow,
@@ -79,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -79,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription {
79 timeWindow: WidgetTimewindow; 81 timeWindow: WidgetTimewindow;
80 originalTimewindow: Timewindow; 82 originalTimewindow: Timewindow;
81 timeWindowConfig: Timewindow; 83 timeWindowConfig: Timewindow;
  84 + timezone: string;
82 subscriptionTimewindow: SubscriptionTimewindow; 85 subscriptionTimewindow: SubscriptionTimewindow;
83 useDashboardTimewindow: boolean; 86 useDashboardTimewindow: boolean;
  87 + tsOffset = 0;
84 88
85 hasDataPageLink: boolean; 89 hasDataPageLink: boolean;
86 singleEntity: boolean; 90 singleEntity: boolean;
@@ -213,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -213,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription {
213 this.timeWindow = {}; 217 this.timeWindow = {};
214 this.useDashboardTimewindow = options.useDashboardTimewindow; 218 this.useDashboardTimewindow = options.useDashboardTimewindow;
215 this.stateData = options.stateData; 219 this.stateData = options.stateData;
  220 + if (this.type === widgetType.latest) {
  221 + this.timezone = options.dashboardTimewindow.timezone;
  222 + this.updateTsOffset();
  223 + }
216 if (this.useDashboardTimewindow) { 224 if (this.useDashboardTimewindow) {
217 this.timeWindowConfig = deepClone(options.dashboardTimewindow); 225 this.timeWindowConfig = deepClone(options.dashboardTimewindow);
218 } else { 226 } else {
@@ -578,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -578,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription {
578 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { 586 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
579 this.timeWindowConfig = deepClone(newDashboardTimewindow); 587 this.timeWindowConfig = deepClone(newDashboardTimewindow);
580 this.update(); 588 this.update();
581 - return true; 589 + }
  590 + }
  591 + } else if (this.type === widgetType.latest) {
  592 + if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
  593 + this.timezone = newDashboardTimewindow.timezone;
  594 + if (this.updateTsOffset()) {
  595 + this.update();
582 } 596 }
583 } 597 }
584 } 598 }
585 - return false;  
586 } 599 }
587 600
588 updateDataVisibility(index: number): void { 601 updateDataVisibility(index: number): void {
@@ -815,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -815,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription {
815 configDatasource: datasource, 828 configDatasource: datasource,
816 configDatasourceIndex: datasourceIndex, 829 configDatasourceIndex: datasourceIndex,
817 subscriptionTimewindow: this.subscriptionTimewindow, 830 subscriptionTimewindow: this.subscriptionTimewindow,
  831 + latestTsOffset: this.tsOffset,
818 dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { 832 dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => {
819 this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); 833 this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true);
820 }, 834 },
@@ -882,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -882,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription {
882 } 896 }
883 897
884 private dataSubscribe() { 898 private dataSubscribe() {
  899 + this.updateDataTimewindow();
885 if (!this.hasDataPageLink) { 900 if (!this.hasDataPageLink) {
886 - if (this.type === widgetType.timeseries && this.timeWindowConfig) {  
887 - this.updateDataTimewindow();  
888 - if (this.subscriptionTimewindow.fixedWindow) { 901 + if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
889 this.onDataUpdated(); 902 this.onDataUpdated();
890 - }  
891 } 903 }
892 const forceUpdate = !this.datasources.length; 904 const forceUpdate = !this.datasources.length;
  905 + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length;
893 this.entityDataListeners.forEach((listener) => { 906 this.entityDataListeners.forEach((listener) => {
894 if (this.comparisonEnabled && listener.configDatasource.isAdditional) { 907 if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
895 listener.subscriptionTimewindow = this.timewindowForComparison; 908 listener.subscriptionTimewindow = this.timewindowForComparison;
896 } else { 909 } else {
897 listener.subscriptionTimewindow = this.subscriptionTimewindow; 910 listener.subscriptionTimewindow = this.subscriptionTimewindow;
  911 + listener.latestTsOffset = this.tsOffset;
898 } 912 }
899 this.ctx.entityDataService.startSubscription(listener); 913 this.ctx.entityDataService.startSubscription(listener);
900 }); 914 });
901 if (forceUpdate) { 915 if (forceUpdate) {
902 this.onDataUpdated(); 916 this.onDataUpdated();
903 } 917 }
  918 + if (notifyDataLoaded) {
  919 + this.notifyDataLoaded();
  920 + }
904 } 921 }
905 } 922 }
906 923
@@ -1102,6 +1119,15 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1102,6 +1119,15 @@ export class WidgetSubscription implements IWidgetSubscription {
1102 } 1119 }
1103 } 1120 }
1104 1121
  1122 + private updateTsOffset(): boolean {
  1123 + const newOffset = calculateTsOffset(this.timezone);
  1124 + if (this.tsOffset !== newOffset) {
  1125 + this.tsOffset = newOffset;
  1126 + return true;
  1127 + }
  1128 + return false;
  1129 + }
  1130 +
1105 private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { 1131 private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow {
1106 if (subscriptionTimewindow) { 1132 if (subscriptionTimewindow) {
1107 this.subscriptionTimewindow = subscriptionTimewindow; 1133 this.subscriptionTimewindow = subscriptionTimewindow;
@@ -133,7 +133,7 @@ @@ -133,7 +133,7 @@
133 <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval" 133 <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
134 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> 134 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
135 </section> 135 </section>
136 - <section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval"> 136 + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
137 <div class="limit-slider-container" 137 <div class="limit-slider-container"
138 fxLayout="row" fxLayoutAlign="start center"> 138 fxLayout="row" fxLayoutAlign="start center">
139 <span translate>aggregation.limit</span> 139 <span translate>aggregation.limit</span>
@@ -184,7 +184,8 @@ @@ -184,7 +184,8 @@
184 (ngModelChange)="onHideTimezoneChanged()"></mat-checkbox> 184 (ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
185 </section> 185 </section>
186 <tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone" 186 <tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
187 - formControlName="timezone"> 187 + localBrowserTimezonePlaceholderOnEmpty="true"
  188 + formControlName="timezone">
188 </tb-timezone-select> 189 </tb-timezone-select>
189 </div> 190 </div>
190 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> 191 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
@@ -23,7 +23,8 @@ import { AppState } from '@app/core/core.state'; @@ -23,7 +23,8 @@ import { AppState } from '@app/core/core.state';
23 import { TranslateService } from '@ngx-translate/core'; 23 import { TranslateService } from '@ngx-translate/core';
24 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 24 import { coerceBooleanProperty } from '@angular/cdk/coercion';
25 import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; 25 import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
26 -import { getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models'; 26 +import { getDefaultTimezoneInfo, getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
  27 +import { deepClone } from '@core/utils';
27 28
28 @Component({ 29 @Component({
29 selector: 'tb-timezone-select', 30 selector: 'tb-timezone-select',
@@ -68,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -68,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
68 this.userTimezoneByDefaultValue = coerceBooleanProperty(value); 69 this.userTimezoneByDefaultValue = coerceBooleanProperty(value);
69 } 70 }
70 71
  72 + private localBrowserTimezonePlaceholderOnEmptyValue: boolean;
  73 + get localBrowserTimezonePlaceholderOnEmpty(): boolean {
  74 + return this.localBrowserTimezonePlaceholderOnEmptyValue;
  75 + }
  76 + @Input()
  77 + set localBrowserTimezonePlaceholderOnEmpty(value: boolean) {
  78 + this.localBrowserTimezonePlaceholderOnEmptyValue = coerceBooleanProperty(value);
  79 + }
  80 +
71 @Input() 81 @Input()
72 disabled: boolean; 82 disabled: boolean;
73 83
@@ -81,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -81,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
81 91
82 private dirty = false; 92 private dirty = false;
83 93
  94 + private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
  95 +
  96 + private timezones: Array<TimezoneInfo>;
  97 +
84 private propagateChange = (v: any) => { }; 98 private propagateChange = (v: any) => { };
85 99
86 constructor(private store: Store<AppState>, 100 constructor(private store: Store<AppState>,
@@ -146,7 +160,11 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -146,7 +160,11 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
146 } 160 }
147 } else { 161 } else {
148 this.modelValue = null; 162 this.modelValue = null;
149 - this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); 163 + if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
  164 + this.selectTimezoneFormGroup.get('timezone').patchValue(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: false});
  165 + } else {
  166 + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
  167 + }
150 } 168 }
151 this.dirty = true; 169 this.dirty = true;
152 } 170 }
@@ -162,11 +180,17 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -162,11 +180,17 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
162 if (this.ignoreClosePanel) { 180 if (this.ignoreClosePanel) {
163 this.ignoreClosePanel = false; 181 this.ignoreClosePanel = false;
164 } else { 182 } else {
165 - if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) {  
166 - const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);  
167 - if (defaultTimezoneInfo !== null) { 183 + if (!this.modelValue) {
  184 + if (this.defaultTimezoneId || this.userTimezoneByDefaultValue) {
  185 + const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
  186 + if (defaultTimezoneInfo !== null) {
  187 + this.ngZone.run(() => {
  188 + this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
  189 + });
  190 + }
  191 + } else if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
168 this.ngZone.run(() => { 192 this.ngZone.run(() => {
169 - this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true}); 193 + this.selectTimezoneFormGroup.get('timezone').reset(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: true});
170 }); 194 });
171 } 195 }
172 } 196 }
@@ -187,10 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -187,10 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
187 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { 211 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
188 this.searchText = searchText; 212 this.searchText = searchText;
189 if (searchText && searchText.length) { 213 if (searchText && searchText.length) {
190 - return of(getTimezones().filter((timezoneInfo) => 214 + return of(this.loadTimezones().filter((timezoneInfo) =>
191 timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase()))); 215 timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())));
192 } 216 }
193 - return of(getTimezones()); 217 + return of(this.loadTimezones());
194 } 218 }
195 219
196 clear() { 220 clear() {
@@ -200,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -200,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
200 }, 0); 224 }, 0);
201 } 225 }
202 226
  227 + private loadTimezones(): Array<TimezoneInfo> {
  228 + if (!this.timezones) {
  229 + this.timezones = [];
  230 + if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
  231 + this.timezones.push(this.getLocalBrowserTimezoneInfoPlaceholder());
  232 + }
  233 + this.timezones.push(...getTimezones());
  234 + }
  235 + return this.timezones;
  236 + }
  237 +
  238 + private getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo {
  239 + if (!this.localBrowserTimezoneInfoPlaceholder) {
  240 + this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo());
  241 + this.localBrowserTimezoneInfoPlaceholder.id = null;
  242 + this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time');
  243 + }
  244 + return this.localBrowserTimezoneInfoPlaceholder;
  245 + }
203 } 246 }
@@ -30,6 +30,9 @@ import { @@ -30,6 +30,9 @@ import {
30 TsValue 30 TsValue
31 } from '@shared/models/query/query.models'; 31 } from '@shared/models/query/query.models';
32 import { PageData } from '@shared/models/page/page-data'; 32 import { PageData } from '@shared/models/page/page-data';
  33 +import { alarmFields } from '@shared/models/alarm.models';
  34 +import { entityFields } from '@shared/models/entity.models';
  35 +import { isUndefined } from '@core/utils';
33 36
34 export enum DataKeyType { 37 export enum DataKeyType {
35 timeseries = 'timeseries', 38 timeseries = 'timeseries',
@@ -446,7 +449,9 @@ export class EntityDataUpdate extends DataUpdate<EntityData> { @@ -446,7 +449,9 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
446 for (const key of Object.keys(entityData.timeseries)) { 449 for (const key of Object.keys(entityData.timeseries)) {
447 const tsValues = entityData.timeseries[key]; 450 const tsValues = entityData.timeseries[key];
448 for (const tsValue of tsValues) { 451 for (const tsValue of tsValues) {
449 - tsValue.ts += tsOffset; 452 + if (tsValue.ts) {
  453 + tsValue.ts += tsOffset;
  454 + }
450 } 455 }
451 } 456 }
452 } 457 }
@@ -455,13 +460,17 @@ export class EntityDataUpdate extends DataUpdate<EntityData> { @@ -455,13 +460,17 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
455 const keyTypeValues = entityData.latest[entityKeyType]; 460 const keyTypeValues = entityData.latest[entityKeyType];
456 for (const key of Object.keys(keyTypeValues)) { 461 for (const key of Object.keys(keyTypeValues)) {
457 const tsValue = keyTypeValues[key]; 462 const tsValue = keyTypeValues[key];
458 - tsValue.ts += tsOffset; 463 + if (tsValue.ts) {
  464 + tsValue.ts += tsOffset;
  465 + }
  466 + if (key === entityFields.createdTime.keyName && tsValue.value) {
  467 + tsValue.value = (Number(tsValue.value) + tsOffset) + '';
  468 + }
459 } 469 }
460 } 470 }
461 } 471 }
462 } 472 }
463 } 473 }
464 -  
465 } 474 }
466 475
467 export class AlarmDataUpdate extends DataUpdate<AlarmData> { 476 export class AlarmDataUpdate extends DataUpdate<AlarmData> {
@@ -473,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> { @@ -473,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
473 this.allowedEntities = msg.allowedEntities; 482 this.allowedEntities = msg.allowedEntities;
474 this.totalEntities = msg.totalEntities; 483 this.totalEntities = msg.totalEntities;
475 } 484 }
  485 +
  486 + public prepareData(tsOffset: number) {
  487 + if (this.data) {
  488 + this.processAlarmData(this.data.data, tsOffset);
  489 + }
  490 + if (this.update) {
  491 + this.processAlarmData(this.update, tsOffset);
  492 + }
  493 + }
  494 +
  495 + private processAlarmData(data: Array<AlarmData>, tsOffset: number) {
  496 + for (const alarmData of data) {
  497 + alarmData.createdTime += tsOffset;
  498 + if (alarmData.ackTs) {
  499 + alarmData.ackTs += tsOffset;
  500 + }
  501 + if (alarmData.clearTs) {
  502 + alarmData.clearTs += tsOffset;
  503 + }
  504 + if (alarmData.endTs) {
  505 + alarmData.endTs += tsOffset;
  506 + }
  507 + if (alarmData.latest) {
  508 + for (const entityKeyType of Object.keys(alarmData.latest)) {
  509 + const keyTypeValues = alarmData.latest[entityKeyType];
  510 + for (const key of Object.keys(keyTypeValues)) {
  511 + const tsValue = keyTypeValues[key];
  512 + if (tsValue.ts) {
  513 + tsValue.ts += tsOffset;
  514 + }
  515 + if (key in [entityFields.createdTime.keyName,
  516 + alarmFields.startTime.keyName,
  517 + alarmFields.endTime.keyName,
  518 + alarmFields.ackTime.keyName,
  519 + alarmFields.clearTime.keyName] && tsValue.value) {
  520 + tsValue.value = (Number(tsValue.value) + tsOffset) + '';
  521 + }
  522 + }
  523 + }
  524 + }
  525 + }
  526 + }
476 } 527 }
477 528
478 export class EntityCountUpdate extends CmdUpdate { 529 export class EntityCountUpdate extends CmdUpdate {
@@ -500,7 +551,7 @@ export class TelemetrySubscriber { @@ -500,7 +551,7 @@ export class TelemetrySubscriber {
500 551
501 private zone: NgZone; 552 private zone: NgZone;
502 553
503 - private tsOffset = 0; 554 + private tsOffset = undefined;
504 555
505 public subscriptionCommands: Array<WebsocketCmd>; 556 public subscriptionCommands: Array<WebsocketCmd>;
506 557
@@ -556,8 +607,14 @@ export class TelemetrySubscriber { @@ -556,8 +607,14 @@ export class TelemetrySubscriber {
556 this.reconnectSubject.complete(); 607 this.reconnectSubject.complete();
557 } 608 }
558 609
559 - public setTsOffset(tsOffset: number) {  
560 - this.tsOffset = tsOffset; 610 + public setTsOffset(tsOffset: number): boolean {
  611 + if (this.tsOffset !== tsOffset) {
  612 + const changed = !isUndefined(this.tsOffset);
  613 + this.tsOffset = tsOffset;
  614 + return changed;
  615 + } else {
  616 + return false;
  617 + }
561 } 618 }
562 619
563 public onData(message: SubscriptionUpdate) { 620 public onData(message: SubscriptionUpdate) {
@@ -598,6 +655,9 @@ export class TelemetrySubscriber { @@ -598,6 +655,9 @@ export class TelemetrySubscriber {
598 } 655 }
599 656
600 public onAlarmData(message: AlarmDataUpdate) { 657 public onAlarmData(message: AlarmDataUpdate) {
  658 + if (this.tsOffset) {
  659 + message.prepareData(this.tsOffset);
  660 + }
601 if (this.zone) { 661 if (this.zone) {
602 this.zone.run( 662 this.zone.run(
603 () => { 663 () => {
@@ -318,6 +318,16 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, @@ -318,6 +318,16 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
318 return historyTimewindow; 318 return historyTimewindow;
319 } 319 }
320 320
  321 +export function calculateTsOffset(timezone?: string): number {
  322 + if (timezone) {
  323 + const tz = getTimezone(timezone);
  324 + const localOffset = moment().utcOffset();
  325 + return (tz.utcOffset() - localOffset) * 60 * 1000;
  326 + } else {
  327 + return 0;
  328 + }
  329 +}
  330 +
321 export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, 331 export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean,
322 timeService: TimeService): SubscriptionTimewindow { 332 timeService: TimeService): SubscriptionTimewindow {
323 const subscriptionTimewindow: SubscriptionTimewindow = { 333 const subscriptionTimewindow: SubscriptionTimewindow = {
@@ -329,13 +339,8 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num @@ -329,13 +339,8 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
329 type: AggregationType.AVG 339 type: AggregationType.AVG
330 }, 340 },
331 timezone: timewindow.timezone, 341 timezone: timewindow.timezone,
332 - tsOffset: 0 342 + tsOffset: calculateTsOffset(timewindow.timezone)
333 }; 343 };
334 - if (timewindow.timezone) {  
335 - const tz = getTimezone(timewindow.timezone);  
336 - const localOffset = moment().utcOffset();  
337 - subscriptionTimewindow.tsOffset = (tz.utcOffset() - localOffset) * 60 * 1000;  
338 - }  
339 let aggTimewindow = 0; 344 let aggTimewindow = 0;
340 if (stateData) { 345 if (stateData) {
341 subscriptionTimewindow.aggregation.type = AggregationType.NONE; 346 subscriptionTimewindow.aggregation.type = AggregationType.NONE;
@@ -407,6 +412,7 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num @@ -407,6 +412,7 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
407 endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate) 412 endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
408 }; 413 };
409 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; 414 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
  415 + subscriptionTimewindow.quickInterval = timewindow.history.quickInterval;
410 } else { 416 } else {
411 subscriptionTimewindow.fixedWindow = { 417 subscriptionTimewindow.fixedWindow = {
412 startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset, 418 startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset,
@@ -768,6 +774,11 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, @@ -768,6 +774,11 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string,
768 return foundTimezone; 774 return foundTimezone;
769 } 775 }
770 776
  777 +export function getDefaultTimezoneInfo(): TimezoneInfo {
  778 + const userTimezone = getDefaultTimezone();
  779 + return getTimezoneInfo(userTimezone);
  780 +}
  781 +
771 export function getDefaultTimezone(): string { 782 export function getDefaultTimezone(): string {
772 if (!defaultTimezone) { 783 if (!defaultTimezone) {
773 defaultTimezone = monentTz.tz.guess(); 784 defaultTimezone = monentTz.tz.guess();
@@ -1988,7 +1988,8 @@ @@ -1988,7 +1988,8 @@
1988 "timezone": "Timezone", 1988 "timezone": "Timezone",
1989 "select-timezone": "Select timezone", 1989 "select-timezone": "Select timezone",
1990 "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", 1990 "no-timezones-matching": "No timezones matching '{{timezone}}' were found.",
1991 - "timezone-required": "Timezone is required." 1991 + "timezone-required": "Timezone is required.",
  1992 + "browser-time": "Browser Time"
1992 }, 1993 },
1993 "queue": { 1994 "queue": {
1994 "select_name": "Select queue name", 1995 "select_name": "Select queue name",