Commit a17c5b77578e8bfed9545f91ea39796adfd864d2
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.
Showing
9 changed files
with
233 additions
and
46 deletions
... | ... | @@ -130,6 +130,7 @@ export class AlarmDataSubscription { |
130 | 130 | this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; |
131 | 131 | } |
132 | 132 | |
133 | + this.subscriber.setTsOffset(this.subsTw.tsOffset); | |
133 | 134 | this.subscriber.subscriptionCommands.push(this.alarmDataCommand); |
134 | 135 | |
135 | 136 | this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { |
... | ... | @@ -143,8 +144,11 @@ export class AlarmDataSubscription { |
143 | 144 | this.subscriber.subscribe(); |
144 | 145 | |
145 | 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 | 150 | const pageData: PageData<AlarmData> = { |
147 | - data: [{...simulatedAlarm, entityId: '1', latest: {}}], | |
151 | + data: [{...alarm, entityId: '1', latest: {}}], | |
148 | 152 | hasNext: false, |
149 | 153 | totalElements: 1, |
150 | 154 | totalPages: 1 | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 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 | 19 | import { |
20 | 20 | EntityData, |
21 | 21 | EntityDataPageLink, |
... | ... | @@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions { |
74 | 74 | keyFilters?: Array<KeyFilter>; |
75 | 75 | additionalKeyFilters?: Array<KeyFilter>; |
76 | 76 | subscriptionTimewindow?: SubscriptionTimewindow; |
77 | + latestTsOffset?: number; | |
77 | 78 | } |
78 | 79 | |
79 | 80 | export class EntityDataSubscription { |
... | ... | @@ -95,6 +96,7 @@ export class EntityDataSubscription { |
95 | 96 | private entityDataResolveSubject: Subject<EntityDataLoadResult>; |
96 | 97 | private pageData: PageData<EntityData>; |
97 | 98 | private subsTw: SubscriptionTimewindow; |
99 | + private latestTsOffset: number; | |
98 | 100 | private dataAggregators: Array<DataAggregator>; |
99 | 101 | private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}; |
100 | 102 | private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; |
... | ... | @@ -177,6 +179,7 @@ export class EntityDataSubscription { |
177 | 179 | this.started = true; |
178 | 180 | this.dataResolved = true; |
179 | 181 | this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; |
182 | + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
180 | 183 | this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
181 | 184 | isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); |
182 | 185 | this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
... | ... | @@ -237,7 +240,12 @@ export class EntityDataSubscription { |
237 | 240 | }; |
238 | 241 | |
239 | 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 | 251 | this.subscriber.subscriptionCommands.push(this.dataCommand); |
... | ... | @@ -276,9 +284,15 @@ export class EntityDataSubscription { |
276 | 284 | this.subscriber.subscriptionCommands = [this.dataCommand]; |
277 | 285 | } |
278 | 286 | }); |
279 | - | |
280 | 287 | this.subscriber.subscribe(); |
281 | 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 | 296 | const entityData: EntityData = { |
283 | 297 | entityId: { |
284 | 298 | id: NULL_UUID, |
... | ... | @@ -289,7 +303,7 @@ export class EntityDataSubscription { |
289 | 303 | }; |
290 | 304 | const name = DatasourceType.function; |
291 | 305 | entityData.latest[EntityKeyType.ENTITY_FIELD] = { |
292 | - name: {ts: Date.now(), value: name} | |
306 | + name: {ts: Date.now() + tsOffset, value: name} | |
293 | 307 | }; |
294 | 308 | const pageData: PageData<EntityData> = { |
295 | 309 | data: [entityData], |
... | ... | @@ -299,7 +313,9 @@ export class EntityDataSubscription { |
299 | 313 | }; |
300 | 314 | this.onPageData(pageData); |
301 | 315 | } else if (this.datasourceType === DatasourceType.entityCount) { |
316 | + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
302 | 317 | this.subscriber = new TelemetrySubscriber(this.telemetryService); |
318 | + this.subscriber.setTsOffset(this.latestTsOffset); | |
303 | 319 | this.countCommand = new EntityCountCmd(); |
304 | 320 | let keyFilters = this.entityDataSubscriptionOptions.keyFilters; |
305 | 321 | if (this.entityDataSubscriptionOptions.additionalKeyFilters) { |
... | ... | @@ -332,13 +348,13 @@ export class EntityDataSubscription { |
332 | 348 | latest: { |
333 | 349 | [EntityKeyType.ENTITY_FIELD]: { |
334 | 350 | name: { |
335 | - ts: Date.now(), | |
351 | + ts: Date.now() + this.latestTsOffset, | |
336 | 352 | value: DatasourceType.entityCount |
337 | 353 | } |
338 | 354 | }, |
339 | 355 | [EntityKeyType.COUNT]: { |
340 | 356 | [countKey.name]: { |
341 | - ts: Date.now(), | |
357 | + ts: Date.now() + this.latestTsOffset, | |
342 | 358 | value: entityCountUpdate.count + '' |
343 | 359 | } |
344 | 360 | } |
... | ... | @@ -359,7 +375,7 @@ export class EntityDataSubscription { |
359 | 375 | latest: { |
360 | 376 | [EntityKeyType.COUNT]: { |
361 | 377 | [countKey.name]: { |
362 | - ts: Date.now(), | |
378 | + ts: Date.now() + this.latestTsOffset, | |
363 | 379 | value: entityCountUpdate.count + '' |
364 | 380 | } |
365 | 381 | } |
... | ... | @@ -384,6 +400,7 @@ export class EntityDataSubscription { |
384 | 400 | return; |
385 | 401 | } |
386 | 402 | this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; |
403 | + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
387 | 404 | this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
388 | 405 | isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); |
389 | 406 | this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
... | ... | @@ -394,22 +411,38 @@ export class EntityDataSubscription { |
394 | 411 | if (this.datasourceType === DatasourceType.entity) { |
395 | 412 | this.subsCommand = new EntityDataCmd(); |
396 | 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 | 426 | this.subscriber.subscriptionCommands = [this.subsCommand]; |
400 | 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 | 435 | } else if (this.datasourceType === DatasourceType.function) { |
403 | 436 | this.startFunction(); |
404 | 437 | } |
405 | 438 | this.started = true; |
406 | 439 | } |
407 | 440 | |
408 | - private prepareSubscriptionCommands() { | |
441 | + private prepareSubscriptionCommands(cmd: EntityDataCmd) { | |
409 | 442 | if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { |
410 | 443 | if (this.tsFields.length > 0) { |
411 | 444 | if (this.history) { |
412 | - this.subsCommand.historyCmd = { | |
445 | + cmd.historyCmd = { | |
413 | 446 | keys: this.tsFields.map(key => key.key), |
414 | 447 | startTs: this.subsTw.fixedWindow.startTimeMs, |
415 | 448 | endTs: this.subsTw.fixedWindow.endTimeMs, |
... | ... | @@ -419,7 +452,7 @@ export class EntityDataSubscription { |
419 | 452 | fetchLatestPreviousPoint: this.subsTw.aggregation.stateData |
420 | 453 | }; |
421 | 454 | } else { |
422 | - this.subsCommand.tsCmd = { | |
455 | + cmd.tsCmd = { | |
423 | 456 | keys: this.tsFields.map(key => key.key), |
424 | 457 | startTs: this.subsTw.startTs, |
425 | 458 | timeWindow: this.subsTw.aggregation.timeWindow, |
... | ... | @@ -430,10 +463,9 @@ export class EntityDataSubscription { |
430 | 463 | }; |
431 | 464 | } |
432 | 465 | } |
433 | - this.subscriber.setTsOffset(this.subsTw.tsOffset); | |
434 | 466 | } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { |
435 | 467 | if (this.latestValues.length > 0) { |
436 | - this.subsCommand.latestCmd = { | |
468 | + cmd.latestCmd = { | |
437 | 469 | keys: this.latestValues |
438 | 470 | }; |
439 | 471 | } |
... | ... | @@ -783,7 +815,7 @@ export class EntityDataSubscription { |
783 | 815 | } else { |
784 | 816 | prevSeries = [0, 0]; |
785 | 817 | } |
786 | - const time = Date.now(); | |
818 | + const time = Date.now() + this.latestTsOffset; | |
787 | 819 | const value = dataKey.func(time, prevSeries[1]); |
788 | 820 | const series: [number, any] = [time, value]; |
789 | 821 | this.datasourceData[0][dataKey.key].data = [series]; |
... | ... | @@ -838,6 +870,10 @@ export class EntityDataSubscription { |
838 | 870 | endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs + |
839 | 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 | 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 | 32 | export interface EntityDataListener { |
33 | 33 | subscriptionType: widgetType; |
34 | 34 | subscriptionTimewindow?: SubscriptionTimewindow; |
35 | + latestTsOffset?: number; | |
35 | 36 | configDatasource: Datasource; |
36 | 37 | configDatasourceIndex: number; |
37 | 38 | dataLoaded: (pageData: PageData<EntityData>, |
... | ... | @@ -92,6 +93,8 @@ export class EntityDataService { |
92 | 93 | if (listener.subscription) { |
93 | 94 | if (listener.subscriptionType === widgetType.timeseries) { |
94 | 95 | listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); |
96 | + } else if (listener.subscriptionType === widgetType.latest) { | |
97 | + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; | |
95 | 98 | } |
96 | 99 | listener.subscription.start(); |
97 | 100 | } |
... | ... | @@ -118,6 +121,8 @@ export class EntityDataService { |
118 | 121 | listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); |
119 | 122 | if (listener.subscriptionType === widgetType.timeseries) { |
120 | 123 | listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); |
124 | + } else if (listener.subscriptionType === widgetType.latest) { | |
125 | + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; | |
121 | 126 | } |
122 | 127 | return listener.subscription.subscribe(); |
123 | 128 | } | ... | ... |
... | ... | @@ -39,8 +39,10 @@ import { HttpErrorResponse } from '@angular/common/http'; |
39 | 39 | import { |
40 | 40 | calculateIntervalEndTime, |
41 | 41 | calculateIntervalStartTime, |
42 | + calculateTsOffset, | |
42 | 43 | createSubscriptionTimewindow, |
43 | - createTimewindowForComparison, getCurrentTime, | |
44 | + createTimewindowForComparison, | |
45 | + getCurrentTime, | |
44 | 46 | SubscriptionTimewindow, |
45 | 47 | Timewindow, |
46 | 48 | toHistoryTimewindow, |
... | ... | @@ -79,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription { |
79 | 81 | timeWindow: WidgetTimewindow; |
80 | 82 | originalTimewindow: Timewindow; |
81 | 83 | timeWindowConfig: Timewindow; |
84 | + timezone: string; | |
82 | 85 | subscriptionTimewindow: SubscriptionTimewindow; |
83 | 86 | useDashboardTimewindow: boolean; |
87 | + tsOffset = 0; | |
84 | 88 | |
85 | 89 | hasDataPageLink: boolean; |
86 | 90 | singleEntity: boolean; |
... | ... | @@ -213,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription { |
213 | 217 | this.timeWindow = {}; |
214 | 218 | this.useDashboardTimewindow = options.useDashboardTimewindow; |
215 | 219 | this.stateData = options.stateData; |
220 | + if (this.type === widgetType.latest) { | |
221 | + this.timezone = options.dashboardTimewindow.timezone; | |
222 | + this.updateTsOffset(); | |
223 | + } | |
216 | 224 | if (this.useDashboardTimewindow) { |
217 | 225 | this.timeWindowConfig = deepClone(options.dashboardTimewindow); |
218 | 226 | } else { |
... | ... | @@ -578,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription { |
578 | 586 | if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { |
579 | 587 | this.timeWindowConfig = deepClone(newDashboardTimewindow); |
580 | 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 | 601 | updateDataVisibility(index: number): void { |
... | ... | @@ -815,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
815 | 828 | configDatasource: datasource, |
816 | 829 | configDatasourceIndex: datasourceIndex, |
817 | 830 | subscriptionTimewindow: this.subscriptionTimewindow, |
831 | + latestTsOffset: this.tsOffset, | |
818 | 832 | dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { |
819 | 833 | this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); |
820 | 834 | }, |
... | ... | @@ -882,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription { |
882 | 896 | } |
883 | 897 | |
884 | 898 | private dataSubscribe() { |
899 | + this.updateDataTimewindow(); | |
885 | 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 | 902 | this.onDataUpdated(); |
890 | - } | |
891 | 903 | } |
892 | 904 | const forceUpdate = !this.datasources.length; |
905 | + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length; | |
893 | 906 | this.entityDataListeners.forEach((listener) => { |
894 | 907 | if (this.comparisonEnabled && listener.configDatasource.isAdditional) { |
895 | 908 | listener.subscriptionTimewindow = this.timewindowForComparison; |
896 | 909 | } else { |
897 | 910 | listener.subscriptionTimewindow = this.subscriptionTimewindow; |
911 | + listener.latestTsOffset = this.tsOffset; | |
898 | 912 | } |
899 | 913 | this.ctx.entityDataService.startSubscription(listener); |
900 | 914 | }); |
901 | 915 | if (forceUpdate) { |
902 | 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 | 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 | 1131 | private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { |
1106 | 1132 | if (subscriptionTimewindow) { |
1107 | 1133 | this.subscriptionTimewindow = subscriptionTimewindow; | ... | ... |
... | ... | @@ -133,7 +133,7 @@ |
133 | 133 | <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval" |
134 | 134 | (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> |
135 | 135 | </section> |
136 | - <section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval"> | |
136 | + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval"> | |
137 | 137 | <div class="limit-slider-container" |
138 | 138 | fxLayout="row" fxLayoutAlign="start center"> |
139 | 139 | <span translate>aggregation.limit</span> |
... | ... | @@ -184,7 +184,8 @@ |
184 | 184 | (ngModelChange)="onHideTimezoneChanged()"></mat-checkbox> |
185 | 185 | </section> |
186 | 186 | <tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone" |
187 | - formControlName="timezone"> | |
187 | + localBrowserTimezonePlaceholderOnEmpty="true" | |
188 | + formControlName="timezone"> | |
188 | 189 | </tb-timezone-select> |
189 | 190 | </div> |
190 | 191 | <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> | ... | ... |
... | ... | @@ -23,7 +23,8 @@ import { AppState } from '@app/core/core.state'; |
23 | 23 | import { TranslateService } from '@ngx-translate/core'; |
24 | 24 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
25 | 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 | 29 | @Component({ |
29 | 30 | selector: 'tb-timezone-select', |
... | ... | @@ -68,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
68 | 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 | 81 | @Input() |
72 | 82 | disabled: boolean; |
73 | 83 | |
... | ... | @@ -81,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
81 | 91 | |
82 | 92 | private dirty = false; |
83 | 93 | |
94 | + private localBrowserTimezoneInfoPlaceholder: TimezoneInfo; | |
95 | + | |
96 | + private timezones: Array<TimezoneInfo>; | |
97 | + | |
84 | 98 | private propagateChange = (v: any) => { }; |
85 | 99 | |
86 | 100 | constructor(private store: Store<AppState>, |
... | ... | @@ -146,7 +160,11 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
146 | 160 | } |
147 | 161 | } else { |
148 | 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 | 169 | this.dirty = true; |
152 | 170 | } |
... | ... | @@ -162,11 +180,17 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
162 | 180 | if (this.ignoreClosePanel) { |
163 | 181 | this.ignoreClosePanel = false; |
164 | 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 | 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 | 211 | fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { |
188 | 212 | this.searchText = searchText; |
189 | 213 | if (searchText && searchText.length) { |
190 | - return of(getTimezones().filter((timezoneInfo) => | |
214 | + return of(this.loadTimezones().filter((timezoneInfo) => | |
191 | 215 | timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase()))); |
192 | 216 | } |
193 | - return of(getTimezones()); | |
217 | + return of(this.loadTimezones()); | |
194 | 218 | } |
195 | 219 | |
196 | 220 | clear() { |
... | ... | @@ -200,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
200 | 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 | 30 | TsValue |
31 | 31 | } from '@shared/models/query/query.models'; |
32 | 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 | 37 | export enum DataKeyType { |
35 | 38 | timeseries = 'timeseries', |
... | ... | @@ -446,7 +449,9 @@ export class EntityDataUpdate extends DataUpdate<EntityData> { |
446 | 449 | for (const key of Object.keys(entityData.timeseries)) { |
447 | 450 | const tsValues = entityData.timeseries[key]; |
448 | 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 | 460 | const keyTypeValues = entityData.latest[entityKeyType]; |
456 | 461 | for (const key of Object.keys(keyTypeValues)) { |
457 | 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 | 476 | export class AlarmDataUpdate extends DataUpdate<AlarmData> { |
... | ... | @@ -473,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> { |
473 | 482 | this.allowedEntities = msg.allowedEntities; |
474 | 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 | 529 | export class EntityCountUpdate extends CmdUpdate { |
... | ... | @@ -500,7 +551,7 @@ export class TelemetrySubscriber { |
500 | 551 | |
501 | 552 | private zone: NgZone; |
502 | 553 | |
503 | - private tsOffset = 0; | |
554 | + private tsOffset = undefined; | |
504 | 555 | |
505 | 556 | public subscriptionCommands: Array<WebsocketCmd>; |
506 | 557 | |
... | ... | @@ -556,8 +607,14 @@ export class TelemetrySubscriber { |
556 | 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 | 620 | public onData(message: SubscriptionUpdate) { |
... | ... | @@ -598,6 +655,9 @@ export class TelemetrySubscriber { |
598 | 655 | } |
599 | 656 | |
600 | 657 | public onAlarmData(message: AlarmDataUpdate) { |
658 | + if (this.tsOffset) { | |
659 | + message.prepareData(this.tsOffset); | |
660 | + } | |
601 | 661 | if (this.zone) { |
602 | 662 | this.zone.run( |
603 | 663 | () => { | ... | ... |
... | ... | @@ -318,6 +318,16 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, |
318 | 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 | 331 | export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, |
322 | 332 | timeService: TimeService): SubscriptionTimewindow { |
323 | 333 | const subscriptionTimewindow: SubscriptionTimewindow = { |
... | ... | @@ -329,13 +339,8 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num |
329 | 339 | type: AggregationType.AVG |
330 | 340 | }, |
331 | 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 | 344 | let aggTimewindow = 0; |
340 | 345 | if (stateData) { |
341 | 346 | subscriptionTimewindow.aggregation.type = AggregationType.NONE; |
... | ... | @@ -407,6 +412,7 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num |
407 | 412 | endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate) |
408 | 413 | }; |
409 | 414 | aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; |
415 | + subscriptionTimewindow.quickInterval = timewindow.history.quickInterval; | |
410 | 416 | } else { |
411 | 417 | subscriptionTimewindow.fixedWindow = { |
412 | 418 | startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset, |
... | ... | @@ -768,6 +774,11 @@ export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, |
768 | 774 | return foundTimezone; |
769 | 775 | } |
770 | 776 | |
777 | +export function getDefaultTimezoneInfo(): TimezoneInfo { | |
778 | + const userTimezone = getDefaultTimezone(); | |
779 | + return getTimezoneInfo(userTimezone); | |
780 | +} | |
781 | + | |
771 | 782 | export function getDefaultTimezone(): string { |
772 | 783 | if (!defaultTimezone) { |
773 | 784 | defaultTimezone = monentTz.tz.guess(); | ... | ... |
... | ... | @@ -1988,7 +1988,8 @@ |
1988 | 1988 | "timezone": "Timezone", |
1989 | 1989 | "select-timezone": "Select timezone", |
1990 | 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 | 1994 | "queue": { |
1994 | 1995 | "select_name": "Select queue name", | ... | ... |