Commit 43578c7e584da5f487eaabde42a777ab25a1c044

Authored by Andrii Shvaika
2 parents 171b991a 0a27101e

Merge branch 'master' of github.com:thingsboard/thingsboard

Showing 25 changed files with 1007 additions and 217 deletions
... ... @@ -283,10 +283,12 @@ public class DefaultTbClusterService implements TbClusterService {
283 283 byte[] msgBytes = encodingService.encode(msg);
284 284 TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
285 285 Set<String> tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE));
286   - if (msg.getEntityId().getEntityType().equals(EntityType.TENANT)
287   - || msg.getEntityId().getEntityType().equals(EntityType.TENANT_PROFILE)
288   - || msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE)
289   - || msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) {
  286 + EntityType entityType = msg.getEntityId().getEntityType();
  287 + if (entityType.equals(EntityType.TENANT)
  288 + || entityType.equals(EntityType.TENANT_PROFILE)
  289 + || entityType.equals(EntityType.DEVICE_PROFILE)
  290 + || entityType.equals(EntityType.API_USAGE_STATE)
  291 + || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)) {
290 292 TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
291 293 Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
292 294 for (String serviceId : tbCoreServices) {
... ...
... ... @@ -76,12 +76,17 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
76 76 @Param("searchText") String searchText,
77 77 Pageable pageable);
78 78
79   - @Query("SELECT alarm.severity FROM AlarmEntity alarm" +
80   - " WHERE alarm.tenantId = :tenantId" +
81   - " AND alarm.originatorId = :entityId" +
82   - " AND ((:status) IS NULL OR alarm.status in (:status))")
  79 + @Query(value = "SELECT a.severity FROM AlarmEntity a " +
  80 + "LEFT JOIN RelationEntity re ON a.id = re.toId " +
  81 + "AND re.relationTypeGroup = 'ALARM' " +
  82 + "AND re.toType = 'ALARM' " +
  83 + "AND re.fromId = :affectedEntityId " +
  84 + "AND re.fromType = :affectedEntityType " +
  85 + "WHERE a.tenantId = :tenantId " +
  86 + "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " +
  87 + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))")
83 88 Set<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId,
84   - @Param("entityId") UUID entityId,
85   - @Param("status") Set<AlarmStatus> status);
86   -
  89 + @Param("affectedEntityId") UUID affectedEntityId,
  90 + @Param("affectedEntityType") String affectedEntityType,
  91 + @Param("alarmStatuses") Set<AlarmStatus> alarmStatuses);
87 92 }
... ...
... ... @@ -123,7 +123,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
123 123 }
124 124
125 125 @Override
126   - public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status) {
127   - return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status);
  126 + public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> statuses) {
  127 + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses);
128 128 }
129 129 }
... ...
... ... @@ -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,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 = {
... ...
... ... @@ -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 &&
... ... @@ -238,6 +241,11 @@ export class EntityDataSubscription {
238 241
239 242 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
240 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);
... ... @@ -256,8 +264,8 @@ export class EntityDataSubscription {
256 264 if (this.started) {
257 265 const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand;
258 266 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries &&
259   - !this.history && this.tsFields.length) {
260   - const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription();
  267 + !this.history && this.tsFields.length) {
  268 + const newSubsTw = this.listener.updateRealtimeSubscription();
261 269 this.subsTw = newSubsTw;
262 270 targetCommand.tsCmd.startTs = this.subsTw.startTs;
263 271 targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
... ... @@ -266,18 +274,25 @@ export class EntityDataSubscription {
266 274 targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
267 275 targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
268 276 this.dataAggregators.forEach((dataAggregator) => {
269   - dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
  277 + dataAggregator.reset(newSubsTw);
270 278 });
271 279 }
  280 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
272 281 targetCommand.query = this.dataCommand.query;
273 282 this.subscriber.subscriptionCommands = [targetCommand];
274 283 } else {
275 284 this.subscriber.subscriptionCommands = [this.dataCommand];
276 285 }
277 286 });
278   -
279 287 this.subscriber.subscribe();
280 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 +
281 296 const entityData: EntityData = {
282 297 entityId: {
283 298 id: NULL_UUID,
... ... @@ -288,7 +303,7 @@ export class EntityDataSubscription {
288 303 };
289 304 const name = DatasourceType.function;
290 305 entityData.latest[EntityKeyType.ENTITY_FIELD] = {
291   - name: {ts: Date.now(), value: name}
  306 + name: {ts: Date.now() + tsOffset, value: name}
292 307 };
293 308 const pageData: PageData<EntityData> = {
294 309 data: [entityData],
... ... @@ -298,7 +313,9 @@ export class EntityDataSubscription {
298 313 };
299 314 this.onPageData(pageData);
300 315 } else if (this.datasourceType === DatasourceType.entityCount) {
  316 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
301 317 this.subscriber = new TelemetrySubscriber(this.telemetryService);
  318 + this.subscriber.setTsOffset(this.latestTsOffset);
302 319 this.countCommand = new EntityCountCmd();
303 320 let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
304 321 if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
... ... @@ -331,13 +348,13 @@ export class EntityDataSubscription {
331 348 latest: {
332 349 [EntityKeyType.ENTITY_FIELD]: {
333 350 name: {
334   - ts: Date.now(),
  351 + ts: Date.now() + this.latestTsOffset,
335 352 value: DatasourceType.entityCount
336 353 }
337 354 },
338 355 [EntityKeyType.COUNT]: {
339 356 [countKey.name]: {
340   - ts: Date.now(),
  357 + ts: Date.now() + this.latestTsOffset,
341 358 value: entityCountUpdate.count + ''
342 359 }
343 360 }
... ... @@ -358,7 +375,7 @@ export class EntityDataSubscription {
358 375 latest: {
359 376 [EntityKeyType.COUNT]: {
360 377 [countKey.name]: {
361   - ts: Date.now(),
  378 + ts: Date.now() + this.latestTsOffset,
362 379 value: entityCountUpdate.count + ''
363 380 }
364 381 }
... ... @@ -383,6 +400,7 @@ export class EntityDataSubscription {
383 400 return;
384 401 }
385 402 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  403 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
386 404 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
387 405 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
388 406 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
... ... @@ -394,10 +412,26 @@ export class EntityDataSubscription {
394 412 this.subsCommand = new EntityDataCmd();
395 413 this.subsCommand.cmdId = this.dataCommand.cmdId;
396 414 this.prepareSubscriptionCommands(this.subsCommand);
397   - if (!this.subsCommand.isEmpty()) {
  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()) {
398 426 this.subscriber.subscriptionCommands = [this.subsCommand];
399 427 this.subscriber.update();
400 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 + }
401 435 } else if (this.datasourceType === DatasourceType.function) {
402 436 this.startFunction();
403 437 }
... ... @@ -745,12 +779,7 @@ export class EntityDataSubscription {
745 779 this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb);
746 780 },
747 781 tsKeyNames,
748   - subsTw.startTs,
749   - subsTw.aggregation.limit,
750   - subsTw.aggregation.type,
751   - subsTw.aggregation.timeWindow,
752   - subsTw.aggregation.interval,
753   - subsTw.aggregation.stateData,
  782 + subsTw,
754 783 this.utils,
755 784 this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick
756 785 );
... ... @@ -786,7 +815,7 @@ export class EntityDataSubscription {
786 815 } else {
787 816 prevSeries = [0, 0];
788 817 }
789   - const time = Date.now();
  818 + const time = Date.now() + this.latestTsOffset;
790 819 const value = dataKey.func(time, prevSeries[1]);
791 820 const series: [number, any] = [time, value];
792 821 this.datasourceData[0][dataKey.key].data = [series];
... ... @@ -827,7 +856,8 @@ export class EntityDataSubscription {
827 856 startTime = dataKey.lastUpdateTime + this.frequency;
828 857 endTime = dataKey.lastUpdateTime + deltaElapsed;
829 858 } else {
830   - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs;
  859 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs +
  860 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
831 861 endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
832 862 if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
833 863 const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
... ... @@ -835,8 +865,14 @@ export class EntityDataSubscription {
835 865 }
836 866 }
837 867 } else {
838   - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs;
839   - endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs;
  868 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs +
  869 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  870 + endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
  871 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  872 + }
  873 + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) {
  874 + const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  875 + endTime = Math.min(currentTime, endTime);
840 876 }
841 877 }
842 878 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
... ...
... ... @@ -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 }
... ...
... ... @@ -37,8 +37,12 @@ import {
37 37 } from '@app/shared/models/widget.models';
38 38 import { HttpErrorResponse } from '@angular/common/http';
39 39 import {
  40 + calculateIntervalEndTime,
  41 + calculateIntervalStartTime,
  42 + calculateTsOffset,
40 43 createSubscriptionTimewindow,
41 44 createTimewindowForComparison,
  45 + getCurrentTime,
42 46 SubscriptionTimewindow,
43 47 Timewindow,
44 48 toHistoryTimewindow,
... ... @@ -77,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription {
77 81 timeWindow: WidgetTimewindow;
78 82 originalTimewindow: Timewindow;
79 83 timeWindowConfig: Timewindow;
  84 + timezone: string;
80 85 subscriptionTimewindow: SubscriptionTimewindow;
81 86 useDashboardTimewindow: boolean;
  87 + tsOffset = 0;
82 88
83 89 hasDataPageLink: boolean;
84 90 singleEntity: boolean;
... ... @@ -211,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription {
211 217 this.timeWindow = {};
212 218 this.useDashboardTimewindow = options.useDashboardTimewindow;
213 219 this.stateData = options.stateData;
  220 + if (this.type === widgetType.latest) {
  221 + this.timezone = options.dashboardTimewindow.timezone;
  222 + this.updateTsOffset();
  223 + }
214 224 if (this.useDashboardTimewindow) {
215 225 this.timeWindowConfig = deepClone(options.dashboardTimewindow);
216 226 } else {
... ... @@ -576,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription {
576 586 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
577 587 this.timeWindowConfig = deepClone(newDashboardTimewindow);
578 588 this.update();
579   - 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();
580 596 }
581 597 }
582 598 }
583   - return false;
584 599 }
585 600
586 601 updateDataVisibility(index: number): void {
... ... @@ -813,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription {
813 828 configDatasource: datasource,
814 829 configDatasourceIndex: datasourceIndex,
815 830 subscriptionTimewindow: this.subscriptionTimewindow,
  831 + latestTsOffset: this.tsOffset,
816 832 dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => {
817 833 this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true);
818 834 },
... ... @@ -837,9 +853,11 @@ export class WidgetSubscription implements IWidgetSubscription {
837 853 if (this.alarmDataListener) {
838 854 this.ctx.alarmDataService.stopSubscription(this.alarmDataListener);
839 855 }
  856 +
840 857 if (this.timeWindowConfig) {
841 858 this.updateRealtimeSubscription();
842 859 }
  860 +
843 861 this.alarmDataListener = {
844 862 subscriptionTimewindow: this.subscriptionTimewindow,
845 863 alarmSource: this.alarmSource,
... ... @@ -878,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription {
878 896 }
879 897
880 898 private dataSubscribe() {
  899 + this.updateDataTimewindow();
881 900 if (!this.hasDataPageLink) {
882   - if (this.type === widgetType.timeseries && this.timeWindowConfig) {
883   - this.updateDataTimewindow();
884   - if (this.subscriptionTimewindow.fixedWindow) {
  901 + if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
885 902 this.onDataUpdated();
886   - }
887 903 }
888 904 const forceUpdate = !this.datasources.length;
  905 + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length;
889 906 this.entityDataListeners.forEach((listener) => {
890 907 if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
891 908 listener.subscriptionTimewindow = this.timewindowForComparison;
892 909 } else {
893 910 listener.subscriptionTimewindow = this.subscriptionTimewindow;
  911 + listener.latestTsOffset = this.tsOffset;
894 912 }
895 913 this.ctx.entityDataService.startSubscription(listener);
896 914 });
897 915 if (forceUpdate) {
898 916 this.onDataUpdated();
899 917 }
  918 + if (notifyDataLoaded) {
  919 + this.notifyDataLoaded();
  920 + }
900 921 }
901 922 }
902 923
... ... @@ -1080,15 +1101,33 @@ export class WidgetSubscription implements IWidgetSubscription {
1080 1101
1081 1102 private updateTimewindow() {
1082 1103 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
  1104 + this.timeWindow.timezone = this.subscriptionTimewindow.timezone;
1083 1105 if (this.subscriptionTimewindow.realtimeWindowMs) {
1084   - this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff;
1085   - this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
  1106 + if (this.subscriptionTimewindow.quickInterval) {
  1107 + const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone);
  1108 + this.timeWindow.maxTime = calculateIntervalEndTime(
  1109 + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
  1110 + this.timeWindow.minTime = calculateIntervalStartTime(
  1111 + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset;
  1112 + } else {
  1113 + this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff;
  1114 + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
  1115 + }
1086 1116 } else if (this.subscriptionTimewindow.fixedWindow) {
1087   - this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
1088   - this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs;
  1117 + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs + this.subscriptionTimewindow.tsOffset;
  1118 + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs + this.subscriptionTimewindow.tsOffset;
1089 1119 }
1090 1120 }
1091 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 +
1092 1131 private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow {
1093 1132 if (subscriptionTimewindow) {
1094 1133 this.subscriptionTimewindow = subscriptionTimewindow;
... ... @@ -1103,12 +1142,13 @@ export class WidgetSubscription implements IWidgetSubscription {
1103 1142
1104 1143 private updateComparisonTimewindow() {
1105 1144 this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000;
  1145 + this.comparisonTimeWindow.timezone = this.timewindowForComparison.timezone;
1106 1146 if (this.timewindowForComparison.realtimeWindowMs) {
1107 1147 this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf();
1108   - this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs;
  1148 + this.comparisonTimeWindow.minTime = moment(this.timeWindow.minTime).subtract(1, this.timeForComparison).valueOf();
1109 1149 } else if (this.timewindowForComparison.fixedWindow) {
1110   - this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs;
1111   - this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs;
  1150 + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs + this.timewindowForComparison.tsOffset;
  1151 + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs + this.timewindowForComparison.tsOffset;
1112 1152 }
1113 1153 }
1114 1154
... ... @@ -1335,7 +1375,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1335 1375 this.onDataUpdated();
1336 1376 }
1337 1377
1338   - private alarmsUpdated(_updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
  1378 + private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
1339 1379 this.alarmsLoaded(alarms, 0, 0);
1340 1380 }
1341 1381
... ...
... ... @@ -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>
... ... @@ -95,7 +133,7 @@
95 133 <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
96 134 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
97 135 </section>
98   - <section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval">
  136 + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
99 137 <div class="limit-slider-container"
100 138 fxLayout="row" fxLayoutAlign="start center">
101 139 <span translate>aggregation.limit</span>
... ... @@ -139,6 +177,17 @@
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 + localBrowserTimezonePlaceholderOnEmpty="true"
  188 + formControlName="timezone">
  189 + </tb-timezone-select>
  190 + </div>
142 191 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
143 192 <button type="button"
144 193 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 }
... ...
... ... @@ -34,7 +34,7 @@
34 34 (click)="openEditMode()"
35 35 matTooltip="{{ 'timewindow.edit' | translate }}"
36 36 [matTooltipPosition]="tooltipPosition">
37   - {{innerValue?.displayValue}}
  37 + {{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span>
38 38 </span>
39 39 <button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
40 40 type="button"
... ...
... ... @@ -27,5 +27,9 @@
27 27 pointer-events: all;
28 28 cursor: pointer;
29 29 }
  30 +
  31 + .timezone-abbr {
  32 + font-weight: 500;
  33 + }
30 34 }
31 35 }
... ...
... ... @@ -31,8 +31,11 @@ import { TranslateService } from '@ngx-translate/core';
31 31 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
32 32 import {
33 33 cloneSelectedTimewindow,
  34 + getTimezoneInfo,
34 35 HistoryWindowType,
35 36 initModelFromDefaultTimewindow,
  37 + QuickTimeIntervalTranslationMap,
  38 + RealtimeWindowType,
36 39 Timewindow,
37 40 TimewindowType
38 41 } from '@shared/models/time/time.models';
... ... @@ -49,7 +52,7 @@ import { BreakpointObserver } from '@angular/cdk/layout';
49 52 import { WINDOW } from '@core/services/window.service';
50 53 import { TimeService } from '@core/services/time.service';
51 54 import { TooltipPosition } from '@angular/material/tooltip';
52   -import { deepClone } from '@core/utils';
  55 +import { deepClone, isDefinedAndNotNull } from '@core/utils';
53 56 import { coerceBooleanProperty } from '@angular/cdk/coercion';
54 57
55 58 // @dynamic
... ... @@ -89,6 +92,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
89 92 return this.aggregationValue;
90 93 }
91 94
  95 + timezoneValue = false;
  96 +
  97 + @Input()
  98 + set timezone(val) {
  99 + this.timezoneValue = coerceBooleanProperty(val);
  100 + }
  101 +
  102 + get timezone() {
  103 + return this.timezoneValue;
  104 + }
  105 +
92 106 isToolbarValue = false;
93 107
94 108 @Input()
... ... @@ -169,7 +183,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
169 183 });
170 184 if (isGtXs) {
171 185 config.minWidth = '417px';
172   - config.maxHeight = '440px';
  186 + config.maxHeight = '500px';
173 187 const panelHeight = 375;
174 188 const panelWidth = 417;
175 189 const el = this.timewindowPanelOrigin.elementRef.nativeElement;
... ... @@ -225,6 +239,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
225 239 timewindow: deepClone(this.innerValue),
226 240 historyOnly: this.historyOnly,
227 241 aggregation: this.aggregation,
  242 + timezone: this.timezone,
228 243 isEdit: this.isEdit
229 244 }
230 245 );
... ... @@ -272,20 +287,32 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
272 287
273 288 updateDisplayValue() {
274 289 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);
  290 + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ';
  291 + if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
  292 + this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval));
  293 + } else {
  294 + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
  295 + this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
  296 + }
278 297 } else {
279 298 this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
280 299 if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
281 300 this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
282 301 this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);
  302 + } else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) {
  303 + this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval));
283 304 } else {
284 305 const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
285 306 const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');
286 307 this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString});
287 308 }
288 309 }
  310 + if (isDefinedAndNotNull(this.innerValue.timezone) && this.innerValue.timezone !== '') {
  311 + this.innerValue.displayValue += ' ';
  312 + this.innerValue.displayTimezoneAbbr = getTimezoneInfo(this.innerValue.timezone).abbr;
  313 + } else {
  314 + this.innerValue.displayTimezoneAbbr = '';
  315 + }
289 316 }
290 317
291 318 hideLabel() {
... ...
... ... @@ -16,14 +16,15 @@
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';
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',
... ... @@ -43,10 +44,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
43 44
44 45 defaultTimezoneId: string = null;
45 46
46   - timezones$ = getTimezones().pipe(
47   - share()
48   - );
49   -
50 47 @Input()
51 48 set defaultTimezone(timezone: string) {
52 49 if (this.defaultTimezoneId !== timezone) {
... ... @@ -72,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
72 69 this.userTimezoneByDefaultValue = coerceBooleanProperty(value);
73 70 }
74 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 +
75 81 @Input()
76 82 disabled: boolean;
77 83
... ... @@ -85,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
85 91
86 92 private dirty = false;
87 93
  94 + private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
  95 +
  96 + private timezones: Array<TimezoneInfo>;
  97 +
88 98 private propagateChange = (v: any) => { };
89 99
90 100 constructor(private store: Store<AppState>,
... ... @@ -138,23 +148,24 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
138 148
139 149 writeValue(value: string | null): void {
140 150 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   - }
  151 + const foundTimezone = getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
  152 + if (foundTimezone !== null) {
  153 + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
  154 + if (foundTimezone.id !== value) {
  155 + setTimeout(() => {
  156 + this.updateView(foundTimezone.id);
  157 + }, 0);
  158 + } else {
  159 + this.modelValue = value;
  160 + }
  161 + } else {
  162 + this.modelValue = null;
  163 + if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
  164 + this.selectTimezoneFormGroup.get('timezone').patchValue(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: false});
  165 + } else {
  166 + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
156 167 }
157   - );
  168 + }
158 169 this.dirty = true;
159 170 }
160 171
... ... @@ -169,15 +180,19 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
169 180 if (this.ignoreClosePanel) {
170 181 this.ignoreClosePanel = false;
171 182 } else {
172   - 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   - });
  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) {
  192 + this.ngZone.run(() => {
  193 + this.selectTimezoneFormGroup.get('timezone').reset(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: true});
  194 + });
  195 + }
181 196 }
182 197 }
183 198 }
... ... @@ -196,12 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
196 211 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
197 212 this.searchText = searchText;
198 213 if (searchText && searchText.length) {
199   - return getTimezones().pipe(
200   - map((timezones) => timezones.filter((timezoneInfo) =>
201   - timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())))
202   - );
  214 + return of(this.loadTimezones().filter((timezoneInfo) =>
  215 + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())));
203 216 }
204   - return getTimezones();
  217 + return of(this.loadTimezones());
205 218 }
206 219
207 220 clear() {
... ... @@ -211,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
211 224 }, 0);
212 225 }
213 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 + }
214 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',
... ... @@ -430,6 +433,44 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
430 433 constructor(msg: EntityDataUpdateMsg) {
431 434 super(msg);
432 435 }
  436 +
  437 + public prepareData(tsOffset: number) {
  438 + if (this.data) {
  439 + this.processEntityData(this.data.data, tsOffset);
  440 + }
  441 + if (this.update) {
  442 + this.processEntityData(this.update, tsOffset);
  443 + }
  444 + }
  445 +
  446 + private processEntityData(data: Array<EntityData>, tsOffset: number) {
  447 + for (const entityData of data) {
  448 + if (entityData.timeseries) {
  449 + for (const key of Object.keys(entityData.timeseries)) {
  450 + const tsValues = entityData.timeseries[key];
  451 + for (const tsValue of tsValues) {
  452 + if (tsValue.ts) {
  453 + tsValue.ts += tsOffset;
  454 + }
  455 + }
  456 + }
  457 + }
  458 + if (entityData.latest) {
  459 + for (const entityKeyType of Object.keys(entityData.latest)) {
  460 + const keyTypeValues = entityData.latest[entityKeyType];
  461 + for (const key of Object.keys(keyTypeValues)) {
  462 + const tsValue = keyTypeValues[key];
  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 + }
  469 + }
  470 + }
  471 + }
  472 + }
  473 + }
433 474 }
434 475
435 476 export class AlarmDataUpdate extends DataUpdate<AlarmData> {
... ... @@ -441,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
441 482 this.allowedEntities = msg.allowedEntities;
442 483 this.totalEntities = msg.totalEntities;
443 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 + }
444 527 }
445 528
446 529 export class EntityCountUpdate extends CmdUpdate {
... ... @@ -468,6 +551,8 @@ export class TelemetrySubscriber {
468 551
469 552 private zone: NgZone;
470 553
  554 + private tsOffset = undefined;
  555 +
471 556 public subscriptionCommands: Array<WebsocketCmd>;
472 557
473 558 public data$ = this.dataSubject.asObservable();
... ... @@ -522,6 +607,16 @@ export class TelemetrySubscriber {
522 607 this.reconnectSubject.complete();
523 608 }
524 609
  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 + }
  618 + }
  619 +
525 620 public onData(message: SubscriptionUpdate) {
526 621 const cmdId = message.subscriptionId;
527 622 let keys: string[];
... ... @@ -545,6 +640,9 @@ export class TelemetrySubscriber {
545 640 }
546 641
547 642 public onEntityData(message: EntityDataUpdate) {
  643 + if (this.tsOffset) {
  644 + message.prepareData(this.tsOffset);
  645 + }
548 646 if (this.zone) {
549 647 this.zone.run(
550 648 () => {
... ... @@ -557,6 +655,9 @@ export class TelemetrySubscriber {
557 655 }
558 656
559 657 public onAlarmData(message: AlarmDataUpdate) {
  658 + if (this.tsOffset) {
  659 + message.prepareData(this.tsOffset);
  660 + }
560 661 if (this.zone) {
561 662 this.zone.run(
562 663 () => {
... ...
... ... @@ -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 {
... ... @@ -82,13 +92,16 @@ export interface Aggregation {
82 92
83 93 export interface Timewindow {
84 94 displayValue?: string;
  95 + displayTimezoneAbbr?: string;
85 96 hideInterval?: boolean;
86 97 hideAggregation?: boolean;
87 98 hideAggInterval?: boolean;
  99 + hideTimezone?: boolean;
88 100 selectedTab?: TimewindowType;
89   - realtime?: IntervalWindow;
  101 + realtime?: RealtimeWindow;
90 102 history?: HistoryWindow;
91 103 aggregation?: Aggregation;
  104 + timezone?: string;
92 105 }
93 106
94 107 export interface SubscriptionAggregation extends Aggregation {
... ... @@ -99,6 +112,9 @@ export interface SubscriptionAggregation extends Aggregation {
99 112
100 113 export interface SubscriptionTimewindow {
101 114 startTs?: number;
  115 + quickInterval?: QuickTimeInterval;
  116 + timezone?: string;
  117 + tsOffset?: number;
102 118 realtimeWindowMs?: number;
103 119 fixedWindow?: FixedWindow;
104 120 aggregation?: SubscriptionAggregation;
... ... @@ -108,9 +124,46 @@ export interface WidgetTimewindow {
108 124 minTime?: number;
109 125 maxTime?: number;
110 126 interval?: number;
  127 + timezone?: string;
111 128 stDiff?: number;
112 129 }
113 130
  131 +export enum QuickTimeInterval {
  132 + YESTERDAY = 'YESTERDAY',
  133 + DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY',
  134 + THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK',
  135 + PREVIOUS_WEEK = 'PREVIOUS_WEEK',
  136 + PREVIOUS_MONTH = 'PREVIOUS_MONTH',
  137 + PREVIOUS_YEAR = 'PREVIOUS_YEAR',
  138 + CURRENT_HOUR = 'CURRENT_HOUR',
  139 + CURRENT_DAY = 'CURRENT_DAY',
  140 + CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR',
  141 + CURRENT_WEEK = 'CURRENT_WEEK',
  142 + CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR',
  143 + CURRENT_MONTH = 'CURRENT_MONTH',
  144 + CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR',
  145 + CURRENT_YEAR = 'CURRENT_YEAR',
  146 + CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR'
  147 +}
  148 +
  149 +export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string>([
  150 + [QuickTimeInterval.YESTERDAY, 'timeinterval.predefined.yesterday'],
  151 + [QuickTimeInterval.DAY_BEFORE_YESTERDAY, 'timeinterval.predefined.day-before-yesterday'],
  152 + [QuickTimeInterval.THIS_DAY_LAST_WEEK, 'timeinterval.predefined.this-day-last-week'],
  153 + [QuickTimeInterval.PREVIOUS_WEEK, 'timeinterval.predefined.previous-week'],
  154 + [QuickTimeInterval.PREVIOUS_MONTH, 'timeinterval.predefined.previous-month'],
  155 + [QuickTimeInterval.PREVIOUS_YEAR, 'timeinterval.predefined.previous-year'],
  156 + [QuickTimeInterval.CURRENT_HOUR, 'timeinterval.predefined.current-hour'],
  157 + [QuickTimeInterval.CURRENT_DAY, 'timeinterval.predefined.current-day'],
  158 + [QuickTimeInterval.CURRENT_DAY_SO_FAR, 'timeinterval.predefined.current-day-so-far'],
  159 + [QuickTimeInterval.CURRENT_WEEK, 'timeinterval.predefined.current-week'],
  160 + [QuickTimeInterval.CURRENT_WEEK_SO_FAR, 'timeinterval.predefined.current-week-so-far'],
  161 + [QuickTimeInterval.CURRENT_MONTH, 'timeinterval.predefined.current-month'],
  162 + [QuickTimeInterval.CURRENT_MONTH_SO_FAR, 'timeinterval.predefined.current-month-so-far'],
  163 + [QuickTimeInterval.CURRENT_YEAR, 'timeinterval.predefined.current-year'],
  164 + [QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far']
  165 +]);
  166 +
114 167 export function historyInterval(timewindowMs: number): Timewindow {
115 168 const timewindow: Timewindow = {
116 169 selectedTab: TimewindowType.HISTORY,
... ... @@ -129,10 +182,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
129 182 hideInterval: false,
130 183 hideAggregation: false,
131 184 hideAggInterval: false,
  185 + hideTimezone: false,
132 186 selectedTab: TimewindowType.REALTIME,
133 187 realtime: {
  188 + realtimeType: RealtimeWindowType.LAST_INTERVAL,
134 189 interval: SECOND,
135   - timewindowMs: MINUTE
  190 + timewindowMs: MINUTE,
  191 + quickInterval: QuickTimeInterval.CURRENT_DAY
136 192 },
137 193 history: {
138 194 historyType: HistoryWindowType.LAST_INTERVAL,
... ... @@ -141,7 +197,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
141 197 fixedTimewindow: {
142 198 startTimeMs: currentTime - DAY,
143 199 endTimeMs: currentTime
144   - }
  200 + },
  201 + quickInterval: QuickTimeInterval.CURRENT_DAY
145 202 },
146 203 aggregation: {
147 204 type: AggregationType.AVG,
... ... @@ -157,6 +214,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
157 214 model.hideInterval = value.hideInterval;
158 215 model.hideAggregation = value.hideAggregation;
159 216 model.hideAggInterval = value.hideAggInterval;
  217 + model.hideTimezone = value.hideTimezone;
160 218 if (isUndefined(value.selectedTab)) {
161 219 if (value.realtime) {
162 220 model.selectedTab = TimewindowType.REALTIME;
... ... @@ -170,7 +228,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
170 228 if (isDefined(value.realtime.interval)) {
171 229 model.realtime.interval = value.realtime.interval;
172 230 }
173   - model.realtime.timewindowMs = value.realtime.timewindowMs;
  231 + if (isUndefined(value.realtime.realtimeType)) {
  232 + if (isDefined(value.realtime.quickInterval)) {
  233 + model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
  234 + } else {
  235 + model.realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL;
  236 + }
  237 + } else {
  238 + model.realtime.realtimeType = value.realtime.realtimeType;
  239 + }
  240 + if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
  241 + model.realtime.quickInterval = value.realtime.quickInterval;
  242 + } else {
  243 + model.realtime.timewindowMs = value.realtime.timewindowMs;
  244 + }
174 245 } else {
175 246 if (isDefined(value.history.interval)) {
176 247 model.history.interval = value.history.interval;
... ... @@ -178,6 +249,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
178 249 if (isUndefined(value.history.historyType)) {
179 250 if (isDefined(value.history.timewindowMs)) {
180 251 model.history.historyType = HistoryWindowType.LAST_INTERVAL;
  252 + } else if (isDefined(value.history.quickInterval)) {
  253 + model.history.historyType = HistoryWindowType.INTERVAL;
181 254 } else {
182 255 model.history.historyType = HistoryWindowType.FIXED;
183 256 }
... ... @@ -186,6 +259,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
186 259 }
187 260 if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) {
188 261 model.history.timewindowMs = value.history.timewindowMs;
  262 + } else if (model.history.historyType === HistoryWindowType.INTERVAL) {
  263 + model.history.quickInterval = value.history.quickInterval;
189 264 } else {
190 265 model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
191 266 model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
... ... @@ -197,6 +272,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
197 272 }
198 273 model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
199 274 }
  275 + model.timezone = value.timezone;
200 276 }
201 277 return model;
202 278 }
... ... @@ -223,6 +299,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
223 299 hideInterval: timewindow.hideInterval || false,
224 300 hideAggregation: timewindow.hideAggregation || false,
225 301 hideAggInterval: timewindow.hideAggInterval || false,
  302 + hideTimezone: timewindow.hideTimezone || false,
226 303 selectedTab: TimewindowType.HISTORY,
227 304 history: {
228 305 historyType: HistoryWindowType.FIXED,
... ... @@ -235,11 +312,22 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
235 312 aggregation: {
236 313 type: aggType,
237 314 limit
238   - }
  315 + },
  316 + timezone: timewindow.timezone
239 317 };
240 318 return historyTimewindow;
241 319 }
242 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 +
243 331 export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean,
244 332 timeService: TimeService): SubscriptionTimewindow {
245 333 const subscriptionTimewindow: SubscriptionTimewindow = {
... ... @@ -249,7 +337,9 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
249 337 interval: SECOND,
250 338 limit: timeService.getMaxDatapointsLimit(),
251 339 type: AggregationType.AVG
252   - }
  340 + },
  341 + timezone: timewindow.timezone,
  342 + tsOffset: calculateTsOffset(timewindow.timezone)
253 343 };
254 344 let aggTimewindow = 0;
255 345 if (stateData) {
... ... @@ -267,33 +357,66 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
267 357 selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY;
268 358 }
269 359 if (selectedTab === TimewindowType.REALTIME) {
270   - subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
  360 + let realtimeType = timewindow.realtime.realtimeType;
  361 + if (isUndefined(realtimeType)) {
  362 + if (isDefined(timewindow.realtime.quickInterval)) {
  363 + realtimeType = RealtimeWindowType.INTERVAL;
  364 + } else {
  365 + realtimeType = RealtimeWindowType.LAST_INTERVAL;
  366 + }
  367 + }
  368 + if (realtimeType === RealtimeWindowType.INTERVAL) {
  369 + const currentDate = getCurrentTime(timewindow.timezone);
  370 + subscriptionTimewindow.realtimeWindowMs =
  371 + getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, currentDate);
  372 + subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval;
  373 + subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate);
  374 + } else {
  375 + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
  376 + subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
  377 + }
271 378 subscriptionTimewindow.aggregation.interval =
272 379 timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
273 380 subscriptionTimewindow.aggregation.type);
274   - subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
275   - const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
276 381 aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
277   - if (startDiff) {
278   - subscriptionTimewindow.startTs -= startDiff;
279   - aggTimewindow += subscriptionTimewindow.aggregation.interval;
  382 + if (realtimeType !== RealtimeWindowType.INTERVAL) {
  383 + const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
  384 + if (startDiff) {
  385 + subscriptionTimewindow.startTs -= startDiff;
  386 + aggTimewindow += subscriptionTimewindow.aggregation.interval;
  387 + }
280 388 }
281 389 } else {
282 390 let historyType = timewindow.history.historyType;
283 391 if (isUndefined(historyType)) {
284   - historyType = isDefined(timewindow.history.timewindowMs) ? HistoryWindowType.LAST_INTERVAL : HistoryWindowType.FIXED;
  392 + if (isDefined(timewindow.history.timewindowMs)) {
  393 + historyType = HistoryWindowType.LAST_INTERVAL;
  394 + } else if (isDefined(timewindow.history.quickInterval)) {
  395 + historyType = HistoryWindowType.INTERVAL;
  396 + } else {
  397 + historyType = HistoryWindowType.FIXED;
  398 + }
285 399 }
286 400 if (historyType === HistoryWindowType.LAST_INTERVAL) {
287   - const currentTime = Date.now();
  401 + const currentDate = getCurrentTime(timewindow.timezone);
  402 + const currentTime = currentDate.valueOf();
288 403 subscriptionTimewindow.fixedWindow = {
289 404 startTimeMs: currentTime - timewindow.history.timewindowMs,
290 405 endTimeMs: currentTime
291 406 };
292 407 aggTimewindow = timewindow.history.timewindowMs;
  408 + } else if (historyType === HistoryWindowType.INTERVAL) {
  409 + const currentDate = getCurrentTime(timewindow.timezone);
  410 + subscriptionTimewindow.fixedWindow = {
  411 + startTimeMs: calculateIntervalStartTime(timewindow.history.quickInterval, currentDate),
  412 + endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
  413 + };
  414 + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
  415 + subscriptionTimewindow.quickInterval = timewindow.history.quickInterval;
293 416 } else {
294 417 subscriptionTimewindow.fixedWindow = {
295   - startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
296   - endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
  418 + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset,
  419 + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset
297 420 };
298 421 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
299 422 }
... ... @@ -309,6 +432,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
309 432 return subscriptionTimewindow;
310 433 }
311 434
  435 +function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, currentDate: moment_.Moment): number {
  436 + switch (interval) {
  437 + case QuickTimeInterval.CURRENT_HOUR:
  438 + return HOUR;
  439 + case QuickTimeInterval.CURRENT_DAY:
  440 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  441 + return DAY;
  442 + case QuickTimeInterval.CURRENT_WEEK:
  443 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  444 + return WEEK;
  445 + case QuickTimeInterval.CURRENT_MONTH:
  446 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  447 + return currentDate.endOf('month').diff(currentDate.clone().startOf('month'));
  448 + case QuickTimeInterval.CURRENT_YEAR:
  449 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  450 + return currentDate.endOf('year').diff(currentDate.clone().startOf('year'));
  451 + }
  452 +}
  453 +
  454 +export function calculateIntervalEndTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
  455 + currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
  456 + switch (interval) {
  457 + case QuickTimeInterval.YESTERDAY:
  458 + currentDate.subtract(1, 'days');
  459 + return currentDate.endOf('day').valueOf();
  460 + case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
  461 + currentDate.subtract(2, 'days');
  462 + return currentDate.endOf('day').valueOf();
  463 + case QuickTimeInterval.THIS_DAY_LAST_WEEK:
  464 + currentDate.subtract(1, 'weeks');
  465 + return currentDate.endOf('day').valueOf();
  466 + case QuickTimeInterval.PREVIOUS_WEEK:
  467 + currentDate.subtract(1, 'weeks');
  468 + return currentDate.endOf('week').valueOf();
  469 + case QuickTimeInterval.PREVIOUS_MONTH:
  470 + currentDate.subtract(1, 'months');
  471 + return currentDate.endOf('month').valueOf();
  472 + case QuickTimeInterval.PREVIOUS_YEAR:
  473 + currentDate.subtract(1, 'years');
  474 + return currentDate.endOf('year').valueOf();
  475 + case QuickTimeInterval.CURRENT_HOUR:
  476 + return currentDate.endOf('hour').valueOf();
  477 + case QuickTimeInterval.CURRENT_DAY:
  478 + return currentDate.endOf('day').valueOf();
  479 + case QuickTimeInterval.CURRENT_WEEK:
  480 + return currentDate.endOf('week').valueOf();
  481 + case QuickTimeInterval.CURRENT_MONTH:
  482 + return currentDate.endOf('month').valueOf();
  483 + case QuickTimeInterval.CURRENT_YEAR:
  484 + return currentDate.endOf('year').valueOf();
  485 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  486 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  487 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  488 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  489 + return currentDate.valueOf();
  490 + }
  491 +}
  492 +
  493 +export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
  494 + currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
  495 + switch (interval) {
  496 + case QuickTimeInterval.YESTERDAY:
  497 + currentDate.subtract(1, 'days');
  498 + return currentDate.startOf('day').valueOf();
  499 + case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
  500 + currentDate.subtract(2, 'days');
  501 + return currentDate.startOf('day').valueOf();
  502 + case QuickTimeInterval.THIS_DAY_LAST_WEEK:
  503 + currentDate.subtract(1, 'weeks');
  504 + return currentDate.startOf('day').valueOf();
  505 + case QuickTimeInterval.PREVIOUS_WEEK:
  506 + currentDate.subtract(1, 'weeks');
  507 + return currentDate.startOf('week').valueOf();
  508 + case QuickTimeInterval.PREVIOUS_MONTH:
  509 + currentDate.subtract(1, 'months');
  510 + return currentDate.startOf('month').valueOf();
  511 + case QuickTimeInterval.PREVIOUS_YEAR:
  512 + currentDate.subtract(1, 'years');
  513 + return currentDate.startOf('year').valueOf();
  514 + case QuickTimeInterval.CURRENT_HOUR:
  515 + return currentDate.startOf('hour').valueOf();
  516 + case QuickTimeInterval.CURRENT_DAY:
  517 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  518 + return currentDate.startOf('day').valueOf();
  519 + case QuickTimeInterval.CURRENT_WEEK:
  520 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  521 + return currentDate.startOf('week').valueOf();
  522 + case QuickTimeInterval.CURRENT_MONTH:
  523 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  524 + return currentDate.startOf('month').valueOf();
  525 + case QuickTimeInterval.CURRENT_YEAR:
  526 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  527 + return currentDate.startOf('year').valueOf();
  528 + }
  529 +}
  530 +
  531 +export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
  532 + switch (interval) {
  533 + case QuickTimeInterval.CURRENT_HOUR:
  534 + return HOUR;
  535 + case QuickTimeInterval.YESTERDAY:
  536 + case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
  537 + case QuickTimeInterval.THIS_DAY_LAST_WEEK:
  538 + case QuickTimeInterval.CURRENT_DAY:
  539 + case QuickTimeInterval.CURRENT_DAY_SO_FAR:
  540 + return DAY;
  541 + case QuickTimeInterval.PREVIOUS_WEEK:
  542 + case QuickTimeInterval.CURRENT_WEEK:
  543 + case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
  544 + return WEEK;
  545 + case QuickTimeInterval.PREVIOUS_MONTH:
  546 + case QuickTimeInterval.CURRENT_MONTH:
  547 + case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
  548 + return DAY * 30;
  549 + case QuickTimeInterval.PREVIOUS_YEAR:
  550 + case QuickTimeInterval.CURRENT_YEAR:
  551 + case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
  552 + return YEAR;
  553 + }
  554 +}
  555 +
312 556 export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow,
313 557 timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow {
314 558 const timewindowForComparison: SubscriptionTimewindow = {
... ... @@ -339,6 +583,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
339 583 cloned.hideInterval = timewindow.hideInterval || false;
340 584 cloned.hideAggregation = timewindow.hideAggregation || false;
341 585 cloned.hideAggInterval = timewindow.hideAggInterval || false;
  586 + cloned.hideTimezone = timewindow.hideTimezone || false;
342 587 if (isDefined(timewindow.selectedTab)) {
343 588 cloned.selectedTab = timewindow.selectedTab;
344 589 if (timewindow.selectedTab === TimewindowType.REALTIME) {
... ... @@ -348,6 +593,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
348 593 }
349 594 }
350 595 cloned.aggregation = deepClone(timewindow.aggregation);
  596 + cloned.timezone = timewindow.timezone;
351 597 return cloned;
352 598 }
353 599
... ... @@ -358,6 +604,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi
358 604 cloned.interval = historyWindow.interval;
359 605 if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
360 606 cloned.timewindowMs = historyWindow.timewindowMs;
  607 + } else if (historyWindow.historyType === HistoryWindowType.INTERVAL) {
  608 + cloned.quickInterval = historyWindow.quickInterval;
361 609 } else if (historyWindow.historyType === HistoryWindowType.FIXED) {
362 610 cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
363 611 }
... ... @@ -375,7 +623,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
375 623 {
376 624 name: 'timeinterval.seconds-interval',
377 625 translateParams: {seconds: 1},
378   - value: 1 * SECOND
  626 + value: SECOND
379 627 },
380 628 {
381 629 name: 'timeinterval.seconds-interval',
... ... @@ -400,7 +648,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
400 648 {
401 649 name: 'timeinterval.minutes-interval',
402 650 translateParams: {minutes: 1},
403   - value: 1 * MINUTE
  651 + value: MINUTE
404 652 },
405 653 {
406 654 name: 'timeinterval.minutes-interval',
... ... @@ -430,7 +678,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
430 678 {
431 679 name: 'timeinterval.hours-interval',
432 680 translateParams: {hours: 1},
433   - value: 1 * HOUR
  681 + value: HOUR
434 682 },
435 683 {
436 684 name: 'timeinterval.hours-interval',
... ... @@ -455,7 +703,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
455 703 {
456 704 name: 'timeinterval.days-interval',
457 705 translateParams: {days: 1},
458   - value: 1 * DAY
  706 + value: DAY
459 707 },
460 708 {
461 709 name: 'timeinterval.days-interval',
... ... @@ -490,65 +738,62 @@ export interface TimezoneInfo {
490 738 name: string;
491 739 offset: string;
492 740 nOffset: number;
  741 + abbr: string;
493 742 }
494 743
495 744 let timezones: TimezoneInfo[] = null;
496 745 let defaultTimezone: string = null;
497 746
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   - );
  747 +export function getTimezones(): TimezoneInfo[] {
  748 + if (!timezones) {
  749 + timezones = monentTz.tz.names().map((zoneName) => {
  750 + const tz = monentTz.tz(zoneName);
  751 + return {
  752 + id: zoneName,
  753 + name: zoneName.replace(/_/g, ' '),
  754 + offset: `UTC${tz.format('Z')}`,
  755 + nOffset: tz.utcOffset(),
  756 + abbr: tz.zoneAbbr()
  757 + };
  758 + });
518 759 }
  760 + return timezones;
519 761 }
520 762
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   - );
  763 +export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo {
  764 + const timezoneList = getTimezones();
  765 + let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null;
  766 + if (!foundTimezone) {
  767 + if (userTimezoneByDefault) {
  768 + const userTimezone = getDefaultTimezone();
  769 + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone);
  770 + } else if (defaultTimezoneId) {
  771 + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
  772 + }
  773 + }
  774 + return foundTimezone;
539 775 }
540 776
541   -export function getDefaultTimezone(): Observable<string> {
542   - if (defaultTimezone) {
543   - return of(defaultTimezone);
  777 +export function getDefaultTimezoneInfo(): TimezoneInfo {
  778 + const userTimezone = getDefaultTimezone();
  779 + return getTimezoneInfo(userTimezone);
  780 +}
  781 +
  782 +export function getDefaultTimezone(): string {
  783 + if (!defaultTimezone) {
  784 + defaultTimezone = monentTz.tz.guess();
  785 + }
  786 + return defaultTimezone;
  787 +}
  788 +
  789 +export function getCurrentTime(tz?: string): moment_.Moment {
  790 + if (tz) {
  791 + return moment().tz(tz);
544 792 } 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   - );
  793 + return moment();
553 794 }
554 795 }
  796 +
  797 +export function getTimezone(tz: string): moment_.Moment {
  798 + return moment.tz(tz);
  799 +}
... ...
... ... @@ -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,
... ...
... ... @@ -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",
... ... @@ -2118,7 +2119,24 @@
2118 2119 "hours": "Hours",
2119 2120 "minutes": "Minutes",
2120 2121 "seconds": "Seconds",
2121   - "advanced": "Advanced"
  2122 + "advanced": "Advanced",
  2123 + "predefined": {
  2124 + "yesterday": "Yesterday",
  2125 + "day-before-yesterday": "Day before yesterday",
  2126 + "this-day-last-week": "This day last week",
  2127 + "previous-week": "Previous week",
  2128 + "previous-month": "Previous month",
  2129 + "previous-year": "Previous year",
  2130 + "current-hour": "Current hour",
  2131 + "current-day": "Current day",
  2132 + "current-day-so-far": "Current day so far",
  2133 + "current-week": "Current week",
  2134 + "current-week-so-far": "Current week so far",
  2135 + "current-month": "Current month",
  2136 + "current-month-so-far": "Current month so far",
  2137 + "current-year": "Current year",
  2138 + "current-year-so-far": "Current year so far"
  2139 + }
2122 2140 },
2123 2141 "timeunit": {
2124 2142 "seconds": "Seconds",
... ... @@ -2139,7 +2157,8 @@
2139 2157 "date-range": "Date range",
2140 2158 "last": "Last",
2141 2159 "time-period": "Time period",
2142   - "hide": "Hide"
  2160 + "hide": "Hide",
  2161 + "interval": "Interval"
2143 2162 },
2144 2163 "user": {
2145 2164 "user": "User",
... ...