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,10 +283,12 @@ public class DefaultTbClusterService implements TbClusterService {
283 byte[] msgBytes = encodingService.encode(msg); 283 byte[] msgBytes = encodingService.encode(msg);
284 TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); 284 TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
285 Set<String> tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)); 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 TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); 292 TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
291 Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); 293 Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
292 for (String serviceId : tbCoreServices) { 294 for (String serviceId : tbCoreServices) {
@@ -76,12 +76,17 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> { @@ -76,12 +76,17 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
76 @Param("searchText") String searchText, 76 @Param("searchText") String searchText,
77 Pageable pageable); 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 Set<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId, 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,7 +123,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
123 } 123 }
124 124
125 @Override 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,6 +130,7 @@ export class AlarmDataSubscription {
130 this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; 130 this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs;
131 } 131 }
132 132
  133 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
133 this.subscriber.subscriptionCommands.push(this.alarmDataCommand); 134 this.subscriber.subscriptionCommands.push(this.alarmDataCommand);
134 135
135 this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { 136 this.subscriber.alarmData$.subscribe((alarmDataUpdate) => {
@@ -143,8 +144,11 @@ export class AlarmDataSubscription { @@ -143,8 +144,11 @@ export class AlarmDataSubscription {
143 this.subscriber.subscribe(); 144 this.subscriber.subscribe();
144 145
145 } else if (this.datasourceType === DatasourceType.function) { 146 } else if (this.datasourceType === DatasourceType.function) {
  147 + const alarm = deepClone(simulatedAlarm);
  148 + alarm.createdTime += this.subsTw.tsOffset;
  149 + alarm.startTs += this.subsTw.tsOffset;
146 const pageData: PageData<AlarmData> = { 150 const pageData: PageData<AlarmData> = {
147 - data: [{...simulatedAlarm, entityId: '1', latest: {}}], 151 + data: [{...alarm, entityId: '1', latest: {}}],
148 hasNext: false, 152 hasNext: false,
149 totalElements: 1, 153 totalElements: 1,
150 totalPages: 1 154 totalPages: 1
@@ -15,7 +15,12 @@ @@ -15,7 +15,12 @@
15 /// 15 ///
16 16
17 import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; 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 import { UtilsService } from '@core/services/utils.service'; 24 import { UtilsService } from '@core/services/utils.service';
20 import { deepClone } from '@core/utils'; 25 import { deepClone } from '@core/utils';
21 import Timeout = NodeJS.Timeout; 26 import Timeout = NodeJS.Timeout;
@@ -73,33 +78,29 @@ export class DataAggregator { @@ -73,33 +78,29 @@ export class DataAggregator {
73 private resetPending = false; 78 private resetPending = false;
74 private updatedData = false; 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 private readonly aggFunction: AggFunction; 83 private readonly aggFunction: AggFunction;
79 84
80 private intervalTimeoutHandle: Timeout; 85 private intervalTimeoutHandle: Timeout;
81 private intervalScheduledTime: number; 86 private intervalScheduledTime: number;
82 87
  88 + private startTs = this.subsTw.startTs + this.subsTw.tsOffset;
83 private endTs: number; 89 private endTs: number;
84 private elapsed: number; 90 private elapsed: number;
85 91
86 constructor(private onDataCb: onAggregatedData, 92 constructor(private onDataCb: onAggregatedData,
87 private tsKeyNames: string[], 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 private utils: UtilsService, 95 private utils: UtilsService,
95 private ignoreDataUpdateOnIntervalTick: boolean) { 96 private ignoreDataUpdateOnIntervalTick: boolean) {
96 this.tsKeyNames.forEach((key) => { 97 this.tsKeyNames.forEach((key) => {
97 this.dataBuffer[key] = []; 98 this.dataBuffer[key] = [];
98 }); 99 });
99 - if (this.stateData) { 100 + if (this.subsTw.aggregation.stateData) {
100 this.lastPrevKvPairData = {}; 101 this.lastPrevKvPairData = {};
101 } 102 }
102 - switch (this.aggregationType) { 103 + switch (this.subsTw.aggregation.type) {
103 case AggregationType.MIN: 104 case AggregationType.MIN:
104 this.aggFunction = min; 105 this.aggFunction = min;
105 break; 106 break;
@@ -129,18 +130,21 @@ export class DataAggregator { @@ -129,18 +130,21 @@ export class DataAggregator {
129 return prevOnDataCb; 130 return prevOnDataCb;
130 } 131 }
131 132
132 - public reset(startTs: number, timeWindow: number, interval: number) { 133 + public reset(subsTw: SubscriptionTimewindow) {
133 if (this.intervalTimeoutHandle) { 134 if (this.intervalTimeoutHandle) {
134 clearTimeout(this.intervalTimeoutHandle); 135 clearTimeout(this.intervalTimeoutHandle);
135 this.intervalTimeoutHandle = null; 136 this.intervalTimeoutHandle = null;
136 } 137 }
  138 + this.subsTw = subsTw;
137 this.intervalScheduledTime = this.utils.currentPerfTime(); 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 this.elapsed = 0; 146 this.elapsed = 0;
143 - this.aggregationTimeout = Math.max(this.interval, 1000); 147 + this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
144 this.resetPending = true; 148 this.resetPending = true;
145 this.updatedData = false; 149 this.updatedData = false;
146 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); 150 this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
@@ -161,7 +165,11 @@ export class DataAggregator { @@ -161,7 +165,11 @@ export class DataAggregator {
161 if (!this.dataReceived) { 165 if (!this.dataReceived) {
162 this.elapsed = 0; 166 this.elapsed = 0;
163 this.dataReceived = true; 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 if (this.resetPending) { 174 if (this.resetPending) {
167 this.resetPending = false; 175 this.resetPending = false;
@@ -195,12 +203,19 @@ export class DataAggregator { @@ -195,12 +203,19 @@ export class DataAggregator {
195 this.intervalTimeoutHandle = null; 203 this.intervalTimeoutHandle = null;
196 } 204 }
197 if (!history) { 205 if (!history) {
198 - const delta = Math.floor(this.elapsed / this.interval); 206 + const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval);
199 if (delta || !this.data) { 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 this.data = this.updateData(); 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 } else { 220 } else {
206 this.data = this.updateData(); 221 this.data = this.updateData();
@@ -223,7 +238,7 @@ export class DataAggregator { @@ -223,7 +238,7 @@ export class DataAggregator {
223 let keyData = this.dataBuffer[key]; 238 let keyData = this.dataBuffer[key];
224 aggKeyData.forEach((aggData, aggTimestamp) => { 239 aggKeyData.forEach((aggData, aggTimestamp) => {
225 if (aggTimestamp <= this.startTs) { 240 if (aggTimestamp <= this.startTs) {
226 - if (this.stateData && 241 + if (this.subsTw.aggregation.stateData &&
227 (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { 242 (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
228 this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; 243 this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
229 } 244 }
@@ -235,11 +250,11 @@ export class DataAggregator { @@ -235,11 +250,11 @@ export class DataAggregator {
235 } 250 }
236 }); 251 });
237 keyData.sort((set1, set2) => set1[0] - set2[0]); 252 keyData.sort((set1, set2) => set1[0] - set2[0]);
238 - if (this.stateData) { 253 + if (this.subsTw.aggregation.stateData) {
239 this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key])); 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 this.dataBuffer[key] = keyData; 259 this.dataBuffer[key] = keyData;
245 } 260 }
@@ -275,7 +290,7 @@ export class DataAggregator { @@ -275,7 +290,7 @@ export class DataAggregator {
275 } 290 }
276 291
277 private processAggregatedData(data: SubscriptionData): AggregationMap { 292 private processAggregatedData(data: SubscriptionData): AggregationMap {
278 - const isCount = this.aggregationType === AggregationType.COUNT; 293 + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
279 const aggregationMap: AggregationMap = {}; 294 const aggregationMap: AggregationMap = {};
280 for (const key of Object.keys(data)) { 295 for (const key of Object.keys(data)) {
281 let aggKeyData = aggregationMap[key]; 296 let aggKeyData = aggregationMap[key];
@@ -300,7 +315,7 @@ export class DataAggregator { @@ -300,7 +315,7 @@ export class DataAggregator {
300 } 315 }
301 316
302 private updateAggregatedData(data: SubscriptionData) { 317 private updateAggregatedData(data: SubscriptionData) {
303 - const isCount = this.aggregationType === AggregationType.COUNT; 318 + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
304 for (const key of Object.keys(data)) { 319 for (const key of Object.keys(data)) {
305 let aggKeyData = this.aggregationMap[key]; 320 let aggKeyData = this.aggregationMap[key];
306 if (!aggKeyData) { 321 if (!aggKeyData) {
@@ -312,7 +327,8 @@ export class DataAggregator { @@ -312,7 +327,8 @@ export class DataAggregator {
312 const timestamp = kvPair[0]; 327 const timestamp = kvPair[0];
313 const value = this.convertValue(kvPair[1]); 328 const value = this.convertValue(kvPair[1]);
314 const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + 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 let aggData = aggKeyData.get(aggTimestamp); 332 let aggData = aggKeyData.get(aggTimestamp);
317 if (!aggData) { 333 if (!aggData) {
318 aggData = { 334 aggData = {
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 /// 15 ///
16 16
17 import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; 17 import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
18 -import { AggregationType, SubscriptionTimewindow } from '@shared/models/time/time.models'; 18 +import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models';
19 import { 19 import {
20 EntityData, 20 EntityData,
21 EntityDataPageLink, 21 EntityDataPageLink,
@@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions { @@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions {
74 keyFilters?: Array<KeyFilter>; 74 keyFilters?: Array<KeyFilter>;
75 additionalKeyFilters?: Array<KeyFilter>; 75 additionalKeyFilters?: Array<KeyFilter>;
76 subscriptionTimewindow?: SubscriptionTimewindow; 76 subscriptionTimewindow?: SubscriptionTimewindow;
  77 + latestTsOffset?: number;
77 } 78 }
78 79
79 export class EntityDataSubscription { 80 export class EntityDataSubscription {
@@ -95,6 +96,7 @@ export class EntityDataSubscription { @@ -95,6 +96,7 @@ export class EntityDataSubscription {
95 private entityDataResolveSubject: Subject<EntityDataLoadResult>; 96 private entityDataResolveSubject: Subject<EntityDataLoadResult>;
96 private pageData: PageData<EntityData>; 97 private pageData: PageData<EntityData>;
97 private subsTw: SubscriptionTimewindow; 98 private subsTw: SubscriptionTimewindow;
  99 + private latestTsOffset: number;
98 private dataAggregators: Array<DataAggregator>; 100 private dataAggregators: Array<DataAggregator>;
99 private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}; 101 private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
100 private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; 102 private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
@@ -177,6 +179,7 @@ export class EntityDataSubscription { @@ -177,6 +179,7 @@ export class EntityDataSubscription {
177 this.started = true; 179 this.started = true;
178 this.dataResolved = true; 180 this.dataResolved = true;
179 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; 181 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  182 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
180 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && 183 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
181 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); 184 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
182 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && 185 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
@@ -238,6 +241,11 @@ export class EntityDataSubscription { @@ -238,6 +241,11 @@ export class EntityDataSubscription {
238 241
239 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { 242 if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
240 this.prepareSubscriptionCommands(this.dataCommand); 243 this.prepareSubscriptionCommands(this.dataCommand);
  244 + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
  245 + this.subscriber.setTsOffset(this.subsTw.tsOffset);
  246 + } else {
  247 + this.subscriber.setTsOffset(this.latestTsOffset);
  248 + }
241 } 249 }
242 250
243 this.subscriber.subscriptionCommands.push(this.dataCommand); 251 this.subscriber.subscriptionCommands.push(this.dataCommand);
@@ -256,8 +264,8 @@ export class EntityDataSubscription { @@ -256,8 +264,8 @@ export class EntityDataSubscription {
256 if (this.started) { 264 if (this.started) {
257 const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; 265 const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand;
258 if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && 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 this.subsTw = newSubsTw; 269 this.subsTw = newSubsTw;
262 targetCommand.tsCmd.startTs = this.subsTw.startTs; 270 targetCommand.tsCmd.startTs = this.subsTw.startTs;
263 targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; 271 targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
@@ -266,18 +274,25 @@ export class EntityDataSubscription { @@ -266,18 +274,25 @@ export class EntityDataSubscription {
266 targetCommand.tsCmd.agg = this.subsTw.aggregation.type; 274 targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
267 targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; 275 targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
268 this.dataAggregators.forEach((dataAggregator) => { 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 targetCommand.query = this.dataCommand.query; 281 targetCommand.query = this.dataCommand.query;
273 this.subscriber.subscriptionCommands = [targetCommand]; 282 this.subscriber.subscriptionCommands = [targetCommand];
274 } else { 283 } else {
275 this.subscriber.subscriptionCommands = [this.dataCommand]; 284 this.subscriber.subscriptionCommands = [this.dataCommand];
276 } 285 }
277 }); 286 });
278 -  
279 this.subscriber.subscribe(); 287 this.subscriber.subscribe();
280 } else if (this.datasourceType === DatasourceType.function) { 288 } else if (this.datasourceType === DatasourceType.function) {
  289 + let tsOffset = 0;
  290 + if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
  291 + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
  292 + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) {
  293 + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
  294 + }
  295 +
281 const entityData: EntityData = { 296 const entityData: EntityData = {
282 entityId: { 297 entityId: {
283 id: NULL_UUID, 298 id: NULL_UUID,
@@ -288,7 +303,7 @@ export class EntityDataSubscription { @@ -288,7 +303,7 @@ export class EntityDataSubscription {
288 }; 303 };
289 const name = DatasourceType.function; 304 const name = DatasourceType.function;
290 entityData.latest[EntityKeyType.ENTITY_FIELD] = { 305 entityData.latest[EntityKeyType.ENTITY_FIELD] = {
291 - name: {ts: Date.now(), value: name} 306 + name: {ts: Date.now() + tsOffset, value: name}
292 }; 307 };
293 const pageData: PageData<EntityData> = { 308 const pageData: PageData<EntityData> = {
294 data: [entityData], 309 data: [entityData],
@@ -298,7 +313,9 @@ export class EntityDataSubscription { @@ -298,7 +313,9 @@ export class EntityDataSubscription {
298 }; 313 };
299 this.onPageData(pageData); 314 this.onPageData(pageData);
300 } else if (this.datasourceType === DatasourceType.entityCount) { 315 } else if (this.datasourceType === DatasourceType.entityCount) {
  316 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
301 this.subscriber = new TelemetrySubscriber(this.telemetryService); 317 this.subscriber = new TelemetrySubscriber(this.telemetryService);
  318 + this.subscriber.setTsOffset(this.latestTsOffset);
302 this.countCommand = new EntityCountCmd(); 319 this.countCommand = new EntityCountCmd();
303 let keyFilters = this.entityDataSubscriptionOptions.keyFilters; 320 let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
304 if (this.entityDataSubscriptionOptions.additionalKeyFilters) { 321 if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
@@ -331,13 +348,13 @@ export class EntityDataSubscription { @@ -331,13 +348,13 @@ export class EntityDataSubscription {
331 latest: { 348 latest: {
332 [EntityKeyType.ENTITY_FIELD]: { 349 [EntityKeyType.ENTITY_FIELD]: {
333 name: { 350 name: {
334 - ts: Date.now(), 351 + ts: Date.now() + this.latestTsOffset,
335 value: DatasourceType.entityCount 352 value: DatasourceType.entityCount
336 } 353 }
337 }, 354 },
338 [EntityKeyType.COUNT]: { 355 [EntityKeyType.COUNT]: {
339 [countKey.name]: { 356 [countKey.name]: {
340 - ts: Date.now(), 357 + ts: Date.now() + this.latestTsOffset,
341 value: entityCountUpdate.count + '' 358 value: entityCountUpdate.count + ''
342 } 359 }
343 } 360 }
@@ -358,7 +375,7 @@ export class EntityDataSubscription { @@ -358,7 +375,7 @@ export class EntityDataSubscription {
358 latest: { 375 latest: {
359 [EntityKeyType.COUNT]: { 376 [EntityKeyType.COUNT]: {
360 [countKey.name]: { 377 [countKey.name]: {
361 - ts: Date.now(), 378 + ts: Date.now() + this.latestTsOffset,
362 value: entityCountUpdate.count + '' 379 value: entityCountUpdate.count + ''
363 } 380 }
364 } 381 }
@@ -383,6 +400,7 @@ export class EntityDataSubscription { @@ -383,6 +400,7 @@ export class EntityDataSubscription {
383 return; 400 return;
384 } 401 }
385 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; 402 this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
  403 + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
386 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && 404 this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
387 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); 405 isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
388 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && 406 this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
@@ -394,10 +412,26 @@ export class EntityDataSubscription { @@ -394,10 +412,26 @@ export class EntityDataSubscription {
394 this.subsCommand = new EntityDataCmd(); 412 this.subsCommand = new EntityDataCmd();
395 this.subsCommand.cmdId = this.dataCommand.cmdId; 413 this.subsCommand.cmdId = this.dataCommand.cmdId;
396 this.prepareSubscriptionCommands(this.subsCommand); 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 this.subscriber.subscriptionCommands = [this.subsCommand]; 426 this.subscriber.subscriptionCommands = [this.subsCommand];
399 this.subscriber.update(); 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 } else if (this.datasourceType === DatasourceType.function) { 435 } else if (this.datasourceType === DatasourceType.function) {
402 this.startFunction(); 436 this.startFunction();
403 } 437 }
@@ -745,12 +779,7 @@ export class EntityDataSubscription { @@ -745,12 +779,7 @@ export class EntityDataSubscription {
745 this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb); 779 this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb);
746 }, 780 },
747 tsKeyNames, 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 this.utils, 783 this.utils,
755 this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick 784 this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick
756 ); 785 );
@@ -786,7 +815,7 @@ export class EntityDataSubscription { @@ -786,7 +815,7 @@ export class EntityDataSubscription {
786 } else { 815 } else {
787 prevSeries = [0, 0]; 816 prevSeries = [0, 0];
788 } 817 }
789 - const time = Date.now(); 818 + const time = Date.now() + this.latestTsOffset;
790 const value = dataKey.func(time, prevSeries[1]); 819 const value = dataKey.func(time, prevSeries[1]);
791 const series: [number, any] = [time, value]; 820 const series: [number, any] = [time, value];
792 this.datasourceData[0][dataKey.key].data = [series]; 821 this.datasourceData[0][dataKey.key].data = [series];
@@ -827,7 +856,8 @@ export class EntityDataSubscription { @@ -827,7 +856,8 @@ export class EntityDataSubscription {
827 startTime = dataKey.lastUpdateTime + this.frequency; 856 startTime = dataKey.lastUpdateTime + this.frequency;
828 endTime = dataKey.lastUpdateTime + deltaElapsed; 857 endTime = dataKey.lastUpdateTime + deltaElapsed;
829 } else { 858 } else {
830 - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs; 859 + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs +
  860 + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
831 endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency; 861 endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
832 if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) { 862 if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
833 const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit; 863 const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
@@ -835,8 +865,14 @@ export class EntityDataSubscription { @@ -835,8 +865,14 @@ export class EntityDataSubscription {
835 } 865 }
836 } 866 }
837 } else { 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 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); 878 generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
@@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs'; @@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs';
32 export interface EntityDataListener { 32 export interface EntityDataListener {
33 subscriptionType: widgetType; 33 subscriptionType: widgetType;
34 subscriptionTimewindow?: SubscriptionTimewindow; 34 subscriptionTimewindow?: SubscriptionTimewindow;
  35 + latestTsOffset?: number;
35 configDatasource: Datasource; 36 configDatasource: Datasource;
36 configDatasourceIndex: number; 37 configDatasourceIndex: number;
37 dataLoaded: (pageData: PageData<EntityData>, 38 dataLoaded: (pageData: PageData<EntityData>,
@@ -92,6 +93,8 @@ export class EntityDataService { @@ -92,6 +93,8 @@ export class EntityDataService {
92 if (listener.subscription) { 93 if (listener.subscription) {
93 if (listener.subscriptionType === widgetType.timeseries) { 94 if (listener.subscriptionType === widgetType.timeseries) {
94 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); 95 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  96 + } else if (listener.subscriptionType === widgetType.latest) {
  97 + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
95 } 98 }
96 listener.subscription.start(); 99 listener.subscription.start();
97 } 100 }
@@ -118,6 +121,8 @@ export class EntityDataService { @@ -118,6 +121,8 @@ export class EntityDataService {
118 listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); 121 listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils);
119 if (listener.subscriptionType === widgetType.timeseries) { 122 if (listener.subscriptionType === widgetType.timeseries) {
120 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); 123 listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
  124 + } else if (listener.subscriptionType === widgetType.latest) {
  125 + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
121 } 126 }
122 return listener.subscription.subscribe(); 127 return listener.subscription.subscribe();
123 } 128 }
@@ -37,8 +37,12 @@ import { @@ -37,8 +37,12 @@ import {
37 } from '@app/shared/models/widget.models'; 37 } from '@app/shared/models/widget.models';
38 import { HttpErrorResponse } from '@angular/common/http'; 38 import { HttpErrorResponse } from '@angular/common/http';
39 import { 39 import {
  40 + calculateIntervalEndTime,
  41 + calculateIntervalStartTime,
  42 + calculateTsOffset,
40 createSubscriptionTimewindow, 43 createSubscriptionTimewindow,
41 createTimewindowForComparison, 44 createTimewindowForComparison,
  45 + getCurrentTime,
42 SubscriptionTimewindow, 46 SubscriptionTimewindow,
43 Timewindow, 47 Timewindow,
44 toHistoryTimewindow, 48 toHistoryTimewindow,
@@ -77,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -77,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription {
77 timeWindow: WidgetTimewindow; 81 timeWindow: WidgetTimewindow;
78 originalTimewindow: Timewindow; 82 originalTimewindow: Timewindow;
79 timeWindowConfig: Timewindow; 83 timeWindowConfig: Timewindow;
  84 + timezone: string;
80 subscriptionTimewindow: SubscriptionTimewindow; 85 subscriptionTimewindow: SubscriptionTimewindow;
81 useDashboardTimewindow: boolean; 86 useDashboardTimewindow: boolean;
  87 + tsOffset = 0;
82 88
83 hasDataPageLink: boolean; 89 hasDataPageLink: boolean;
84 singleEntity: boolean; 90 singleEntity: boolean;
@@ -211,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -211,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription {
211 this.timeWindow = {}; 217 this.timeWindow = {};
212 this.useDashboardTimewindow = options.useDashboardTimewindow; 218 this.useDashboardTimewindow = options.useDashboardTimewindow;
213 this.stateData = options.stateData; 219 this.stateData = options.stateData;
  220 + if (this.type === widgetType.latest) {
  221 + this.timezone = options.dashboardTimewindow.timezone;
  222 + this.updateTsOffset();
  223 + }
214 if (this.useDashboardTimewindow) { 224 if (this.useDashboardTimewindow) {
215 this.timeWindowConfig = deepClone(options.dashboardTimewindow); 225 this.timeWindowConfig = deepClone(options.dashboardTimewindow);
216 } else { 226 } else {
@@ -576,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -576,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription {
576 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { 586 if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
577 this.timeWindowConfig = deepClone(newDashboardTimewindow); 587 this.timeWindowConfig = deepClone(newDashboardTimewindow);
578 this.update(); 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 updateDataVisibility(index: number): void { 601 updateDataVisibility(index: number): void {
@@ -813,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -813,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription {
813 configDatasource: datasource, 828 configDatasource: datasource,
814 configDatasourceIndex: datasourceIndex, 829 configDatasourceIndex: datasourceIndex,
815 subscriptionTimewindow: this.subscriptionTimewindow, 830 subscriptionTimewindow: this.subscriptionTimewindow,
  831 + latestTsOffset: this.tsOffset,
816 dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { 832 dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => {
817 this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); 833 this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true);
818 }, 834 },
@@ -837,9 +853,11 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -837,9 +853,11 @@ export class WidgetSubscription implements IWidgetSubscription {
837 if (this.alarmDataListener) { 853 if (this.alarmDataListener) {
838 this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); 854 this.ctx.alarmDataService.stopSubscription(this.alarmDataListener);
839 } 855 }
  856 +
840 if (this.timeWindowConfig) { 857 if (this.timeWindowConfig) {
841 this.updateRealtimeSubscription(); 858 this.updateRealtimeSubscription();
842 } 859 }
  860 +
843 this.alarmDataListener = { 861 this.alarmDataListener = {
844 subscriptionTimewindow: this.subscriptionTimewindow, 862 subscriptionTimewindow: this.subscriptionTimewindow,
845 alarmSource: this.alarmSource, 863 alarmSource: this.alarmSource,
@@ -878,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -878,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription {
878 } 896 }
879 897
880 private dataSubscribe() { 898 private dataSubscribe() {
  899 + this.updateDataTimewindow();
881 if (!this.hasDataPageLink) { 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 this.onDataUpdated(); 902 this.onDataUpdated();
886 - }  
887 } 903 }
888 const forceUpdate = !this.datasources.length; 904 const forceUpdate = !this.datasources.length;
  905 + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length;
889 this.entityDataListeners.forEach((listener) => { 906 this.entityDataListeners.forEach((listener) => {
890 if (this.comparisonEnabled && listener.configDatasource.isAdditional) { 907 if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
891 listener.subscriptionTimewindow = this.timewindowForComparison; 908 listener.subscriptionTimewindow = this.timewindowForComparison;
892 } else { 909 } else {
893 listener.subscriptionTimewindow = this.subscriptionTimewindow; 910 listener.subscriptionTimewindow = this.subscriptionTimewindow;
  911 + listener.latestTsOffset = this.tsOffset;
894 } 912 }
895 this.ctx.entityDataService.startSubscription(listener); 913 this.ctx.entityDataService.startSubscription(listener);
896 }); 914 });
897 if (forceUpdate) { 915 if (forceUpdate) {
898 this.onDataUpdated(); 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,15 +1101,33 @@ export class WidgetSubscription implements IWidgetSubscription {
1080 1101
1081 private updateTimewindow() { 1102 private updateTimewindow() {
1082 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; 1103 this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
  1104 + this.timeWindow.timezone = this.subscriptionTimewindow.timezone;
1083 if (this.subscriptionTimewindow.realtimeWindowMs) { 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 } else if (this.subscriptionTimewindow.fixedWindow) { 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 private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { 1131 private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow {
1093 if (subscriptionTimewindow) { 1132 if (subscriptionTimewindow) {
1094 this.subscriptionTimewindow = subscriptionTimewindow; 1133 this.subscriptionTimewindow = subscriptionTimewindow;
@@ -1103,12 +1142,13 @@ export class WidgetSubscription implements IWidgetSubscription { @@ -1103,12 +1142,13 @@ export class WidgetSubscription implements IWidgetSubscription {
1103 1142
1104 private updateComparisonTimewindow() { 1143 private updateComparisonTimewindow() {
1105 this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; 1144 this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000;
  1145 + this.comparisonTimeWindow.timezone = this.timewindowForComparison.timezone;
1106 if (this.timewindowForComparison.realtimeWindowMs) { 1146 if (this.timewindowForComparison.realtimeWindowMs) {
1107 this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); 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 } else if (this.timewindowForComparison.fixedWindow) { 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,7 +1375,7 @@ export class WidgetSubscription implements IWidgetSubscription {
1335 this.onDataUpdated(); 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 this.alarmsLoaded(alarms, 0, 0); 1379 this.alarmsLoaded(alarms, 0, 0);
1340 } 1380 }
1341 1381
@@ -91,6 +91,7 @@ @@ -91,6 +91,7 @@
91 direction="left" 91 direction="left"
92 tooltipPosition="below" 92 tooltipPosition="below"
93 aggregation="true" 93 aggregation="true"
  94 + timezone="true"
94 [(ngModel)]="dashboardCtx.dashboardTimewindow"> 95 [(ngModel)]="dashboardCtx.dashboardTimewindow">
95 </tb-timewindow> 96 </tb-timewindow>
96 <tb-filters-edit [fxShow]="!isEdit && displayFilters()" 97 <tb-filters-edit [fxShow]="!isEdit && displayFilters()"
@@ -95,6 +95,7 @@ @@ -95,6 +95,7 @@
95 <tb-timewindow *ngIf="widget.hasTimewindow" 95 <tb-timewindow *ngIf="widget.hasTimewindow"
96 #timewindowComponent 96 #timewindowComponent
97 aggregation="{{widget.hasAggregation}}" 97 aggregation="{{widget.hasAggregation}}"
  98 + timezone="true"
98 [isEdit]="isEdit" 99 [isEdit]="isEdit"
99 [(ngModel)]="widgetComponent.widget.config.timewindow" 100 [(ngModel)]="widgetComponent.widget.config.timewindow"
100 (ngModelChange)="widgetComponent.onTimewindowChanged($event)"> 101 (ngModelChange)="widgetComponent.onTimewindowChanged($event)">
@@ -55,7 +55,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; @@ -55,7 +55,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models';
55 import { DialogService } from '@core/services/dialog.service'; 55 import { DialogService } from '@core/services/dialog.service';
56 import { AddEntityDialogComponent } from './add-entity-dialog.component'; 56 import { AddEntityDialogComponent } from './add-entity-dialog.component';
57 import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; 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 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 65 import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
60 import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; 66 import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
61 import { isDefined, isUndefined } from '@core/utils'; 67 import { isDefined, isUndefined } from '@core/utils';
@@ -296,6 +302,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -296,6 +302,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
296 const currentTime = Date.now(); 302 const currentTime = Date.now();
297 timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; 303 timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs;
298 timePageLink.endTime = currentTime; 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 } else { 309 } else {
300 timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; 310 timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
301 timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; 311 timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;
@@ -94,11 +94,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, @@ -94,11 +94,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
94 items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) 94 items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems)
95 }); 95 });
96 this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { 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 this.alarmScheduleForm.valueChanges.subscribe(() => { 102 this.alarmScheduleForm.valueChanges.subscribe(() => {
104 this.updateModel(); 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,16 +21,43 @@
21 <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" 21 <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}"
22 (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> 22 (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab">
23 <mat-tab label="{{ 'timewindow.realtime' | translate }}"> 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 </mat-tab> 61 </mat-tab>
35 <mat-tab label="{{ 'timewindow.history' | translate }}"> 62 <mat-tab label="{{ 'timewindow.history' | translate }}">
36 <section fxLayout="row"> 63 <section fxLayout="row">
@@ -65,6 +92,17 @@ @@ -65,6 +92,17 @@
65 style="padding-top: 8px;"></tb-datetime-period> 92 style="padding-top: 8px;"></tb-datetime-period>
66 </section> 93 </section>
67 </mat-radio-button> 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 </mat-radio-group> 106 </mat-radio-group>
69 </div> 107 </div>
70 </section> 108 </section>
@@ -95,7 +133,7 @@ @@ -95,7 +133,7 @@
95 <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval" 133 <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
96 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> 134 (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
97 </section> 135 </section>
98 - <section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval"> 136 + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
99 <div class="limit-slider-container" 137 <div class="limit-slider-container"
100 fxLayout="row" fxLayoutAlign="start center"> 138 fxLayout="row" fxLayoutAlign="start center">
101 <span translate>aggregation.limit</span> 139 <span translate>aggregation.limit</span>
@@ -139,6 +177,17 @@ @@ -139,6 +177,17 @@
139 predefinedName="aggregation.group-interval"> 177 predefinedName="aggregation.group-interval">
140 </tb-timeinterval> 178 </tb-timeinterval>
141 </div> 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 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> 191 <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
143 <button type="button" 192 <button type="button"
144 mat-button 193 mat-button
@@ -20,6 +20,8 @@ import { @@ -20,6 +20,8 @@ import {
20 AggregationType, 20 AggregationType,
21 DAY, 21 DAY,
22 HistoryWindowType, 22 HistoryWindowType,
  23 + quickTimeIntervalPeriod,
  24 + RealtimeWindowType,
23 Timewindow, 25 Timewindow,
24 TimewindowType 26 TimewindowType
25 } from '@shared/models/time/time.models'; 27 } from '@shared/models/time/time.models';
@@ -36,6 +38,7 @@ export interface TimewindowPanelData { @@ -36,6 +38,7 @@ export interface TimewindowPanelData {
36 historyOnly: boolean; 38 historyOnly: boolean;
37 timewindow: Timewindow; 39 timewindow: Timewindow;
38 aggregation: boolean; 40 aggregation: boolean;
  41 + timezone: boolean;
39 isEdit: boolean; 42 isEdit: boolean;
40 } 43 }
41 44
@@ -50,6 +53,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -50,6 +53,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
50 53
51 aggregation = false; 54 aggregation = false;
52 55
  56 + timezone = false;
  57 +
53 isEdit = false; 58 isEdit = false;
54 59
55 timewindow: Timewindow; 60 timewindow: Timewindow;
@@ -60,6 +65,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -60,6 +65,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
60 65
61 historyTypes = HistoryWindowType; 66 historyTypes = HistoryWindowType;
62 67
  68 + realtimeTypes = RealtimeWindowType;
  69 +
63 timewindowTypes = TimewindowType; 70 timewindowTypes = TimewindowType;
64 71
65 aggregationTypes = AggregationType; 72 aggregationTypes = AggregationType;
@@ -78,6 +85,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -78,6 +85,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
78 this.historyOnly = data.historyOnly; 85 this.historyOnly = data.historyOnly;
79 this.timewindow = data.timewindow; 86 this.timewindow = data.timewindow;
80 this.aggregation = data.aggregation; 87 this.aggregation = data.aggregation;
  88 + this.timezone = data.timezone;
81 this.isEdit = data.isEdit; 89 this.isEdit = data.isEdit;
82 } 90 }
83 91
@@ -85,10 +93,16 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -85,10 +93,16 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
85 const hideInterval = this.timewindow.hideInterval || false; 93 const hideInterval = this.timewindow.hideInterval || false;
86 const hideAggregation = this.timewindow.hideAggregation || false; 94 const hideAggregation = this.timewindow.hideAggregation || false;
87 const hideAggInterval = this.timewindow.hideAggInterval || false; 95 const hideAggInterval = this.timewindow.hideAggInterval || false;
  96 + const hideTimezone = this.timewindow.hideTimezone || false;
88 97
89 this.timewindowForm = this.fb.group({ 98 this.timewindowForm = this.fb.group({
90 realtime: this.fb.group( 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 timewindowMs: [ 106 timewindowMs: [
93 this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' 107 this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
94 ? this.timewindow.realtime.timewindowMs : null 108 ? this.timewindow.realtime.timewindowMs : null
@@ -96,7 +110,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -96,7 +110,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
96 interval: [ 110 interval: [
97 this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' 111 this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
98 ? this.timewindow.realtime.interval : null 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 history: this.fb.group( 121 history: this.fb.group(
@@ -119,6 +138,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -119,6 +138,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
119 value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' 138 value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined'
120 ? this.timewindow.history.fixedTimewindow : null, 139 ? this.timewindow.history.fixedTimewindow : null,
121 disabled: hideInterval 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,21 +159,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
135 disabled: hideAggInterval 159 disabled: hideAggInterval
136 }, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]) 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 update() { 171 update() {
143 const timewindowFormValue = this.timewindowForm.getRawValue(); 172 const timewindowFormValue = this.timewindowForm.getRawValue();
144 this.timewindow.realtime = { 173 this.timewindow.realtime = {
  174 + realtimeType: timewindowFormValue.realtime.realtimeType,
145 timewindowMs: timewindowFormValue.realtime.timewindowMs, 175 timewindowMs: timewindowFormValue.realtime.timewindowMs,
  176 + quickInterval: timewindowFormValue.realtime.quickInterval,
146 interval: timewindowFormValue.realtime.interval 177 interval: timewindowFormValue.realtime.interval
147 }; 178 };
148 this.timewindow.history = { 179 this.timewindow.history = {
149 historyType: timewindowFormValue.history.historyType, 180 historyType: timewindowFormValue.history.historyType,
150 timewindowMs: timewindowFormValue.history.timewindowMs, 181 timewindowMs: timewindowFormValue.history.timewindowMs,
151 interval: timewindowFormValue.history.interval, 182 interval: timewindowFormValue.history.interval,
152 - fixedTimewindow: timewindowFormValue.history.fixedTimewindow 183 + fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
  184 + quickInterval: timewindowFormValue.history.quickInterval,
153 }; 185 };
154 if (this.aggregation) { 186 if (this.aggregation) {
155 this.timewindow.aggregation = { 187 this.timewindow.aggregation = {
@@ -157,6 +189,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -157,6 +189,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
157 limit: timewindowFormValue.aggregation.limit 189 limit: timewindowFormValue.aggregation.limit
158 }; 190 };
159 } 191 }
  192 + if (this.timezone) {
  193 + this.timewindow.timezone = timewindowFormValue.timezone;
  194 + }
160 this.result = this.timewindow; 195 this.result = this.timewindow;
161 this.overlayRef.dispose(); 196 this.overlayRef.dispose();
162 } 197 }
@@ -174,11 +209,23 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -174,11 +209,23 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
174 } 209 }
175 210
176 minRealtimeAggInterval() { 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 maxRealtimeAggInterval() { 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 minHistoryAggInterval() { 231 minHistoryAggInterval() {
@@ -193,6 +240,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -193,6 +240,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
193 const timewindowFormValue = this.timewindowForm.getRawValue(); 240 const timewindowFormValue = this.timewindowForm.getRawValue();
194 if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { 241 if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
195 return timewindowFormValue.history.timewindowMs; 242 return timewindowFormValue.history.timewindowMs;
  243 + } else if (timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL) {
  244 + return quickTimeIntervalPeriod(timewindowFormValue.history.quickInterval);
196 } else if (timewindowFormValue.history.fixedTimewindow) { 245 } else if (timewindowFormValue.history.fixedTimewindow) {
197 return timewindowFormValue.history.fixedTimewindow.endTimeMs - 246 return timewindowFormValue.history.fixedTimewindow.endTimeMs -
198 timewindowFormValue.history.fixedTimewindow.startTimeMs; 247 timewindowFormValue.history.fixedTimewindow.startTimeMs;
@@ -206,10 +255,18 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -206,10 +255,18 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
206 this.timewindowForm.get('history.historyType').disable({emitEvent: false}); 255 this.timewindowForm.get('history.historyType').disable({emitEvent: false});
207 this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false}); 256 this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false});
208 this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false}); 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 } else { 262 } else {
210 this.timewindowForm.get('history.historyType').enable({emitEvent: false}); 263 this.timewindowForm.get('history.historyType').enable({emitEvent: false});
211 this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false}); 264 this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false});
212 this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); 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 this.timewindowForm.markAsDirty(); 271 this.timewindowForm.markAsDirty();
215 } 272 }
@@ -232,4 +289,13 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { @@ -232,4 +289,13 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
232 this.timewindowForm.markAsDirty(); 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,7 +34,7 @@
34 (click)="openEditMode()" 34 (click)="openEditMode()"
35 matTooltip="{{ 'timewindow.edit' | translate }}" 35 matTooltip="{{ 'timewindow.edit' | translate }}"
36 [matTooltipPosition]="tooltipPosition"> 36 [matTooltipPosition]="tooltipPosition">
37 - {{innerValue?.displayValue}} 37 + {{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span>
38 </span> 38 </span>
39 <button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32" 39 <button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
40 type="button" 40 type="button"
@@ -27,5 +27,9 @@ @@ -27,5 +27,9 @@
27 pointer-events: all; 27 pointer-events: all;
28 cursor: pointer; 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,8 +31,11 @@ import { TranslateService } from '@ngx-translate/core';
31 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; 31 import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
32 import { 32 import {
33 cloneSelectedTimewindow, 33 cloneSelectedTimewindow,
  34 + getTimezoneInfo,
34 HistoryWindowType, 35 HistoryWindowType,
35 initModelFromDefaultTimewindow, 36 initModelFromDefaultTimewindow,
  37 + QuickTimeIntervalTranslationMap,
  38 + RealtimeWindowType,
36 Timewindow, 39 Timewindow,
37 TimewindowType 40 TimewindowType
38 } from '@shared/models/time/time.models'; 41 } from '@shared/models/time/time.models';
@@ -49,7 +52,7 @@ import { BreakpointObserver } from '@angular/cdk/layout'; @@ -49,7 +52,7 @@ import { BreakpointObserver } from '@angular/cdk/layout';
49 import { WINDOW } from '@core/services/window.service'; 52 import { WINDOW } from '@core/services/window.service';
50 import { TimeService } from '@core/services/time.service'; 53 import { TimeService } from '@core/services/time.service';
51 import { TooltipPosition } from '@angular/material/tooltip'; 54 import { TooltipPosition } from '@angular/material/tooltip';
52 -import { deepClone } from '@core/utils'; 55 +import { deepClone, isDefinedAndNotNull } from '@core/utils';
53 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 56 import { coerceBooleanProperty } from '@angular/cdk/coercion';
54 57
55 // @dynamic 58 // @dynamic
@@ -89,6 +92,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces @@ -89,6 +92,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
89 return this.aggregationValue; 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 isToolbarValue = false; 106 isToolbarValue = false;
93 107
94 @Input() 108 @Input()
@@ -169,7 +183,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces @@ -169,7 +183,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
169 }); 183 });
170 if (isGtXs) { 184 if (isGtXs) {
171 config.minWidth = '417px'; 185 config.minWidth = '417px';
172 - config.maxHeight = '440px'; 186 + config.maxHeight = '500px';
173 const panelHeight = 375; 187 const panelHeight = 375;
174 const panelWidth = 417; 188 const panelWidth = 417;
175 const el = this.timewindowPanelOrigin.elementRef.nativeElement; 189 const el = this.timewindowPanelOrigin.elementRef.nativeElement;
@@ -225,6 +239,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces @@ -225,6 +239,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
225 timewindow: deepClone(this.innerValue), 239 timewindow: deepClone(this.innerValue),
226 historyOnly: this.historyOnly, 240 historyOnly: this.historyOnly,
227 aggregation: this.aggregation, 241 aggregation: this.aggregation,
  242 + timezone: this.timezone,
228 isEdit: this.isEdit 243 isEdit: this.isEdit
229 } 244 }
230 ); 245 );
@@ -272,20 +287,32 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces @@ -272,20 +287,32 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
272 287
273 updateDisplayValue() { 288 updateDisplayValue() {
274 if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { 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 } else { 297 } else {
279 this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; 298 this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
280 if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { 299 if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
281 this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + 300 this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
282 this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); 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 } else { 304 } else {
284 const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); 305 const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
285 const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); 306 const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');
286 this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString}); 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 hideLabel() { 318 hideLabel() {
@@ -16,14 +16,15 @@ @@ -16,14 +16,15 @@
16 16
17 import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; 17 import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
18 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; 18 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
19 -import { Observable } from 'rxjs'; 19 +import { Observable, of } from 'rxjs';
20 import { map, mergeMap, share, tap } from 'rxjs/operators'; 20 import { map, mergeMap, share, tap } from 'rxjs/operators';
21 import { Store } from '@ngrx/store'; 21 import { Store } from '@ngrx/store';
22 import { AppState } from '@app/core/core.state'; 22 import { AppState } from '@app/core/core.state';
23 import { TranslateService } from '@ngx-translate/core'; 23 import { TranslateService } from '@ngx-translate/core';
24 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 24 import { coerceBooleanProperty } from '@angular/cdk/coercion';
25 import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; 25 import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
26 -import { getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models'; 26 +import { getDefaultTimezoneInfo, getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
  27 +import { deepClone } from '@core/utils';
27 28
28 @Component({ 29 @Component({
29 selector: 'tb-timezone-select', 30 selector: 'tb-timezone-select',
@@ -43,10 +44,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -43,10 +44,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
43 44
44 defaultTimezoneId: string = null; 45 defaultTimezoneId: string = null;
45 46
46 - timezones$ = getTimezones().pipe(  
47 - share()  
48 - );  
49 -  
50 @Input() 47 @Input()
51 set defaultTimezone(timezone: string) { 48 set defaultTimezone(timezone: string) {
52 if (this.defaultTimezoneId !== timezone) { 49 if (this.defaultTimezoneId !== timezone) {
@@ -72,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -72,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
72 this.userTimezoneByDefaultValue = coerceBooleanProperty(value); 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 @Input() 81 @Input()
76 disabled: boolean; 82 disabled: boolean;
77 83
@@ -85,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -85,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
85 91
86 private dirty = false; 92 private dirty = false;
87 93
  94 + private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
  95 +
  96 + private timezones: Array<TimezoneInfo>;
  97 +
88 private propagateChange = (v: any) => { }; 98 private propagateChange = (v: any) => { };
89 99
90 constructor(private store: Store<AppState>, 100 constructor(private store: Store<AppState>,
@@ -138,23 +148,24 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -138,23 +148,24 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
138 148
139 writeValue(value: string | null): void { 149 writeValue(value: string | null): void {
140 this.searchText = ''; 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 this.dirty = true; 169 this.dirty = true;
159 } 170 }
160 171
@@ -169,15 +180,19 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -169,15 +180,19 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
169 if (this.ignoreClosePanel) { 180 if (this.ignoreClosePanel) {
170 this.ignoreClosePanel = false; 181 this.ignoreClosePanel = false;
171 } else { 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,12 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
196 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { 211 fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
197 this.searchText = searchText; 212 this.searchText = searchText;
198 if (searchText && searchText.length) { 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 clear() { 220 clear() {
@@ -211,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af @@ -211,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
211 }, 0); 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,6 +30,9 @@ import {
30 TsValue 30 TsValue
31 } from '@shared/models/query/query.models'; 31 } from '@shared/models/query/query.models';
32 import { PageData } from '@shared/models/page/page-data'; 32 import { PageData } from '@shared/models/page/page-data';
  33 +import { alarmFields } from '@shared/models/alarm.models';
  34 +import { entityFields } from '@shared/models/entity.models';
  35 +import { isUndefined } from '@core/utils';
33 36
34 export enum DataKeyType { 37 export enum DataKeyType {
35 timeseries = 'timeseries', 38 timeseries = 'timeseries',
@@ -430,6 +433,44 @@ export class EntityDataUpdate extends DataUpdate<EntityData> { @@ -430,6 +433,44 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
430 constructor(msg: EntityDataUpdateMsg) { 433 constructor(msg: EntityDataUpdateMsg) {
431 super(msg); 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 export class AlarmDataUpdate extends DataUpdate<AlarmData> { 476 export class AlarmDataUpdate extends DataUpdate<AlarmData> {
@@ -441,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> { @@ -441,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
441 this.allowedEntities = msg.allowedEntities; 482 this.allowedEntities = msg.allowedEntities;
442 this.totalEntities = msg.totalEntities; 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 export class EntityCountUpdate extends CmdUpdate { 529 export class EntityCountUpdate extends CmdUpdate {
@@ -468,6 +551,8 @@ export class TelemetrySubscriber { @@ -468,6 +551,8 @@ export class TelemetrySubscriber {
468 551
469 private zone: NgZone; 552 private zone: NgZone;
470 553
  554 + private tsOffset = undefined;
  555 +
471 public subscriptionCommands: Array<WebsocketCmd>; 556 public subscriptionCommands: Array<WebsocketCmd>;
472 557
473 public data$ = this.dataSubject.asObservable(); 558 public data$ = this.dataSubject.asObservable();
@@ -522,6 +607,16 @@ export class TelemetrySubscriber { @@ -522,6 +607,16 @@ export class TelemetrySubscriber {
522 this.reconnectSubject.complete(); 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 public onData(message: SubscriptionUpdate) { 620 public onData(message: SubscriptionUpdate) {
526 const cmdId = message.subscriptionId; 621 const cmdId = message.subscriptionId;
527 let keys: string[]; 622 let keys: string[];
@@ -545,6 +640,9 @@ export class TelemetrySubscriber { @@ -545,6 +640,9 @@ export class TelemetrySubscriber {
545 } 640 }
546 641
547 public onEntityData(message: EntityDataUpdate) { 642 public onEntityData(message: EntityDataUpdate) {
  643 + if (this.tsOffset) {
  644 + message.prepareData(this.tsOffset);
  645 + }
548 if (this.zone) { 646 if (this.zone) {
549 this.zone.run( 647 this.zone.run(
550 () => { 648 () => {
@@ -557,6 +655,9 @@ export class TelemetrySubscriber { @@ -557,6 +655,9 @@ export class TelemetrySubscriber {
557 } 655 }
558 656
559 public onAlarmData(message: AlarmDataUpdate) { 657 public onAlarmData(message: AlarmDataUpdate) {
  658 + if (this.tsOffset) {
  659 + message.prepareData(this.tsOffset);
  660 + }
560 if (this.zone) { 661 if (this.zone) {
561 this.zone.run( 662 this.zone.run(
562 () => { 663 () => {
@@ -17,9 +17,7 @@ @@ -17,9 +17,7 @@
17 import { TimeService } from '@core/services/time.service'; 17 import { TimeService } from '@core/services/time.service';
18 import { deepClone, isDefined, isUndefined } from '@app/core/utils'; 18 import { deepClone, isDefined, isUndefined } from '@app/core/utils';
19 import * as moment_ from 'moment'; 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 const moment = moment_; 22 const moment = moment_;
25 23
@@ -27,6 +25,7 @@ export const SECOND = 1000; @@ -27,6 +25,7 @@ export const SECOND = 1000;
27 export const MINUTE = 60 * SECOND; 25 export const MINUTE = 60 * SECOND;
28 export const HOUR = 60 * MINUTE; 26 export const HOUR = 60 * MINUTE;
29 export const DAY = 24 * HOUR; 27 export const DAY = 24 * HOUR;
  28 +export const WEEK = 7 * DAY;
30 export const YEAR = DAY * 365; 29 export const YEAR = DAY * 365;
31 30
32 export enum TimewindowType { 31 export enum TimewindowType {
@@ -34,14 +33,25 @@ export enum TimewindowType { @@ -34,14 +33,25 @@ export enum TimewindowType {
34 HISTORY 33 HISTORY
35 } 34 }
36 35
  36 +export enum RealtimeWindowType {
  37 + LAST_INTERVAL,
  38 + INTERVAL
  39 +}
  40 +
37 export enum HistoryWindowType { 41 export enum HistoryWindowType {
38 LAST_INTERVAL, 42 LAST_INTERVAL,
39 - FIXED 43 + FIXED,
  44 + INTERVAL
40 } 45 }
41 46
42 export interface IntervalWindow { 47 export interface IntervalWindow {
43 interval?: number; 48 interval?: number;
44 timewindowMs?: number; 49 timewindowMs?: number;
  50 + quickInterval?: QuickTimeInterval;
  51 +}
  52 +
  53 +export interface RealtimeWindow extends IntervalWindow{
  54 + realtimeType?: RealtimeWindowType;
45 } 55 }
46 56
47 export interface FixedWindow { 57 export interface FixedWindow {
@@ -82,13 +92,16 @@ export interface Aggregation { @@ -82,13 +92,16 @@ export interface Aggregation {
82 92
83 export interface Timewindow { 93 export interface Timewindow {
84 displayValue?: string; 94 displayValue?: string;
  95 + displayTimezoneAbbr?: string;
85 hideInterval?: boolean; 96 hideInterval?: boolean;
86 hideAggregation?: boolean; 97 hideAggregation?: boolean;
87 hideAggInterval?: boolean; 98 hideAggInterval?: boolean;
  99 + hideTimezone?: boolean;
88 selectedTab?: TimewindowType; 100 selectedTab?: TimewindowType;
89 - realtime?: IntervalWindow; 101 + realtime?: RealtimeWindow;
90 history?: HistoryWindow; 102 history?: HistoryWindow;
91 aggregation?: Aggregation; 103 aggregation?: Aggregation;
  104 + timezone?: string;
92 } 105 }
93 106
94 export interface SubscriptionAggregation extends Aggregation { 107 export interface SubscriptionAggregation extends Aggregation {
@@ -99,6 +112,9 @@ export interface SubscriptionAggregation extends Aggregation { @@ -99,6 +112,9 @@ export interface SubscriptionAggregation extends Aggregation {
99 112
100 export interface SubscriptionTimewindow { 113 export interface SubscriptionTimewindow {
101 startTs?: number; 114 startTs?: number;
  115 + quickInterval?: QuickTimeInterval;
  116 + timezone?: string;
  117 + tsOffset?: number;
102 realtimeWindowMs?: number; 118 realtimeWindowMs?: number;
103 fixedWindow?: FixedWindow; 119 fixedWindow?: FixedWindow;
104 aggregation?: SubscriptionAggregation; 120 aggregation?: SubscriptionAggregation;
@@ -108,9 +124,46 @@ export interface WidgetTimewindow { @@ -108,9 +124,46 @@ export interface WidgetTimewindow {
108 minTime?: number; 124 minTime?: number;
109 maxTime?: number; 125 maxTime?: number;
110 interval?: number; 126 interval?: number;
  127 + timezone?: string;
111 stDiff?: number; 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 export function historyInterval(timewindowMs: number): Timewindow { 167 export function historyInterval(timewindowMs: number): Timewindow {
115 const timewindow: Timewindow = { 168 const timewindow: Timewindow = {
116 selectedTab: TimewindowType.HISTORY, 169 selectedTab: TimewindowType.HISTORY,
@@ -129,10 +182,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { @@ -129,10 +182,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
129 hideInterval: false, 182 hideInterval: false,
130 hideAggregation: false, 183 hideAggregation: false,
131 hideAggInterval: false, 184 hideAggInterval: false,
  185 + hideTimezone: false,
132 selectedTab: TimewindowType.REALTIME, 186 selectedTab: TimewindowType.REALTIME,
133 realtime: { 187 realtime: {
  188 + realtimeType: RealtimeWindowType.LAST_INTERVAL,
134 interval: SECOND, 189 interval: SECOND,
135 - timewindowMs: MINUTE 190 + timewindowMs: MINUTE,
  191 + quickInterval: QuickTimeInterval.CURRENT_DAY
136 }, 192 },
137 history: { 193 history: {
138 historyType: HistoryWindowType.LAST_INTERVAL, 194 historyType: HistoryWindowType.LAST_INTERVAL,
@@ -141,7 +197,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { @@ -141,7 +197,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
141 fixedTimewindow: { 197 fixedTimewindow: {
142 startTimeMs: currentTime - DAY, 198 startTimeMs: currentTime - DAY,
143 endTimeMs: currentTime 199 endTimeMs: currentTime
144 - } 200 + },
  201 + quickInterval: QuickTimeInterval.CURRENT_DAY
145 }, 202 },
146 aggregation: { 203 aggregation: {
147 type: AggregationType.AVG, 204 type: AggregationType.AVG,
@@ -157,6 +214,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T @@ -157,6 +214,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
157 model.hideInterval = value.hideInterval; 214 model.hideInterval = value.hideInterval;
158 model.hideAggregation = value.hideAggregation; 215 model.hideAggregation = value.hideAggregation;
159 model.hideAggInterval = value.hideAggInterval; 216 model.hideAggInterval = value.hideAggInterval;
  217 + model.hideTimezone = value.hideTimezone;
160 if (isUndefined(value.selectedTab)) { 218 if (isUndefined(value.selectedTab)) {
161 if (value.realtime) { 219 if (value.realtime) {
162 model.selectedTab = TimewindowType.REALTIME; 220 model.selectedTab = TimewindowType.REALTIME;
@@ -170,7 +228,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T @@ -170,7 +228,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
170 if (isDefined(value.realtime.interval)) { 228 if (isDefined(value.realtime.interval)) {
171 model.realtime.interval = value.realtime.interval; 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 } else { 245 } else {
175 if (isDefined(value.history.interval)) { 246 if (isDefined(value.history.interval)) {
176 model.history.interval = value.history.interval; 247 model.history.interval = value.history.interval;
@@ -178,6 +249,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T @@ -178,6 +249,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
178 if (isUndefined(value.history.historyType)) { 249 if (isUndefined(value.history.historyType)) {
179 if (isDefined(value.history.timewindowMs)) { 250 if (isDefined(value.history.timewindowMs)) {
180 model.history.historyType = HistoryWindowType.LAST_INTERVAL; 251 model.history.historyType = HistoryWindowType.LAST_INTERVAL;
  252 + } else if (isDefined(value.history.quickInterval)) {
  253 + model.history.historyType = HistoryWindowType.INTERVAL;
181 } else { 254 } else {
182 model.history.historyType = HistoryWindowType.FIXED; 255 model.history.historyType = HistoryWindowType.FIXED;
183 } 256 }
@@ -186,6 +259,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T @@ -186,6 +259,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
186 } 259 }
187 if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) { 260 if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) {
188 model.history.timewindowMs = value.history.timewindowMs; 261 model.history.timewindowMs = value.history.timewindowMs;
  262 + } else if (model.history.historyType === HistoryWindowType.INTERVAL) {
  263 + model.history.quickInterval = value.history.quickInterval;
189 } else { 264 } else {
190 model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; 265 model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
191 model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; 266 model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
@@ -197,6 +272,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T @@ -197,6 +272,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
197 } 272 }
198 model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); 273 model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
199 } 274 }
  275 + model.timezone = value.timezone;
200 } 276 }
201 return model; 277 return model;
202 } 278 }
@@ -223,6 +299,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, @@ -223,6 +299,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
223 hideInterval: timewindow.hideInterval || false, 299 hideInterval: timewindow.hideInterval || false,
224 hideAggregation: timewindow.hideAggregation || false, 300 hideAggregation: timewindow.hideAggregation || false,
225 hideAggInterval: timewindow.hideAggInterval || false, 301 hideAggInterval: timewindow.hideAggInterval || false,
  302 + hideTimezone: timewindow.hideTimezone || false,
226 selectedTab: TimewindowType.HISTORY, 303 selectedTab: TimewindowType.HISTORY,
227 history: { 304 history: {
228 historyType: HistoryWindowType.FIXED, 305 historyType: HistoryWindowType.FIXED,
@@ -235,11 +312,22 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, @@ -235,11 +312,22 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
235 aggregation: { 312 aggregation: {
236 type: aggType, 313 type: aggType,
237 limit 314 limit
238 - } 315 + },
  316 + timezone: timewindow.timezone
239 }; 317 };
240 return historyTimewindow; 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 export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, 331 export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean,
244 timeService: TimeService): SubscriptionTimewindow { 332 timeService: TimeService): SubscriptionTimewindow {
245 const subscriptionTimewindow: SubscriptionTimewindow = { 333 const subscriptionTimewindow: SubscriptionTimewindow = {
@@ -249,7 +337,9 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num @@ -249,7 +337,9 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
249 interval: SECOND, 337 interval: SECOND,
250 limit: timeService.getMaxDatapointsLimit(), 338 limit: timeService.getMaxDatapointsLimit(),
251 type: AggregationType.AVG 339 type: AggregationType.AVG
252 - } 340 + },
  341 + timezone: timewindow.timezone,
  342 + tsOffset: calculateTsOffset(timewindow.timezone)
253 }; 343 };
254 let aggTimewindow = 0; 344 let aggTimewindow = 0;
255 if (stateData) { 345 if (stateData) {
@@ -267,33 +357,66 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num @@ -267,33 +357,66 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
267 selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; 357 selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY;
268 } 358 }
269 if (selectedTab === TimewindowType.REALTIME) { 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 subscriptionTimewindow.aggregation.interval = 378 subscriptionTimewindow.aggregation.interval =
272 timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, 379 timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
273 subscriptionTimewindow.aggregation.type); 380 subscriptionTimewindow.aggregation.type);
274 - subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;  
275 - const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;  
276 aggTimewindow = subscriptionTimewindow.realtimeWindowMs; 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 } else { 389 } else {
282 let historyType = timewindow.history.historyType; 390 let historyType = timewindow.history.historyType;
283 if (isUndefined(historyType)) { 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 if (historyType === HistoryWindowType.LAST_INTERVAL) { 400 if (historyType === HistoryWindowType.LAST_INTERVAL) {
287 - const currentTime = Date.now(); 401 + const currentDate = getCurrentTime(timewindow.timezone);
  402 + const currentTime = currentDate.valueOf();
288 subscriptionTimewindow.fixedWindow = { 403 subscriptionTimewindow.fixedWindow = {
289 startTimeMs: currentTime - timewindow.history.timewindowMs, 404 startTimeMs: currentTime - timewindow.history.timewindowMs,
290 endTimeMs: currentTime 405 endTimeMs: currentTime
291 }; 406 };
292 aggTimewindow = timewindow.history.timewindowMs; 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 } else { 416 } else {
294 subscriptionTimewindow.fixedWindow = { 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 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; 421 aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
299 } 422 }
@@ -309,6 +432,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num @@ -309,6 +432,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
309 return subscriptionTimewindow; 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 export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, 556 export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow,
313 timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow { 557 timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow {
314 const timewindowForComparison: SubscriptionTimewindow = { 558 const timewindowForComparison: SubscriptionTimewindow = {
@@ -339,6 +583,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { @@ -339,6 +583,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
339 cloned.hideInterval = timewindow.hideInterval || false; 583 cloned.hideInterval = timewindow.hideInterval || false;
340 cloned.hideAggregation = timewindow.hideAggregation || false; 584 cloned.hideAggregation = timewindow.hideAggregation || false;
341 cloned.hideAggInterval = timewindow.hideAggInterval || false; 585 cloned.hideAggInterval = timewindow.hideAggInterval || false;
  586 + cloned.hideTimezone = timewindow.hideTimezone || false;
342 if (isDefined(timewindow.selectedTab)) { 587 if (isDefined(timewindow.selectedTab)) {
343 cloned.selectedTab = timewindow.selectedTab; 588 cloned.selectedTab = timewindow.selectedTab;
344 if (timewindow.selectedTab === TimewindowType.REALTIME) { 589 if (timewindow.selectedTab === TimewindowType.REALTIME) {
@@ -348,6 +593,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { @@ -348,6 +593,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
348 } 593 }
349 } 594 }
350 cloned.aggregation = deepClone(timewindow.aggregation); 595 cloned.aggregation = deepClone(timewindow.aggregation);
  596 + cloned.timezone = timewindow.timezone;
351 return cloned; 597 return cloned;
352 } 598 }
353 599
@@ -358,6 +604,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi @@ -358,6 +604,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi
358 cloned.interval = historyWindow.interval; 604 cloned.interval = historyWindow.interval;
359 if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) { 605 if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
360 cloned.timewindowMs = historyWindow.timewindowMs; 606 cloned.timewindowMs = historyWindow.timewindowMs;
  607 + } else if (historyWindow.historyType === HistoryWindowType.INTERVAL) {
  608 + cloned.quickInterval = historyWindow.quickInterval;
361 } else if (historyWindow.historyType === HistoryWindowType.FIXED) { 609 } else if (historyWindow.historyType === HistoryWindowType.FIXED) {
362 cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow); 610 cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
363 } 611 }
@@ -375,7 +623,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( @@ -375,7 +623,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
375 { 623 {
376 name: 'timeinterval.seconds-interval', 624 name: 'timeinterval.seconds-interval',
377 translateParams: {seconds: 1}, 625 translateParams: {seconds: 1},
378 - value: 1 * SECOND 626 + value: SECOND
379 }, 627 },
380 { 628 {
381 name: 'timeinterval.seconds-interval', 629 name: 'timeinterval.seconds-interval',
@@ -400,7 +648,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( @@ -400,7 +648,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
400 { 648 {
401 name: 'timeinterval.minutes-interval', 649 name: 'timeinterval.minutes-interval',
402 translateParams: {minutes: 1}, 650 translateParams: {minutes: 1},
403 - value: 1 * MINUTE 651 + value: MINUTE
404 }, 652 },
405 { 653 {
406 name: 'timeinterval.minutes-interval', 654 name: 'timeinterval.minutes-interval',
@@ -430,7 +678,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( @@ -430,7 +678,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
430 { 678 {
431 name: 'timeinterval.hours-interval', 679 name: 'timeinterval.hours-interval',
432 translateParams: {hours: 1}, 680 translateParams: {hours: 1},
433 - value: 1 * HOUR 681 + value: HOUR
434 }, 682 },
435 { 683 {
436 name: 'timeinterval.hours-interval', 684 name: 'timeinterval.hours-interval',
@@ -455,7 +703,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( @@ -455,7 +703,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
455 { 703 {
456 name: 'timeinterval.days-interval', 704 name: 'timeinterval.days-interval',
457 translateParams: {days: 1}, 705 translateParams: {days: 1},
458 - value: 1 * DAY 706 + value: DAY
459 }, 707 },
460 { 708 {
461 name: 'timeinterval.days-interval', 709 name: 'timeinterval.days-interval',
@@ -490,65 +738,62 @@ export interface TimezoneInfo { @@ -490,65 +738,62 @@ export interface TimezoneInfo {
490 name: string; 738 name: string;
491 offset: string; 739 offset: string;
492 nOffset: number; 740 nOffset: number;
  741 + abbr: string;
493 } 742 }
494 743
495 let timezones: TimezoneInfo[] = null; 744 let timezones: TimezoneInfo[] = null;
496 let defaultTimezone: string = null; 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 } else { 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,6 +140,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
140 import { FileSizePipe } from '@shared/pipe/file-size.pipe'; 140 import { FileSizePipe } from '@shared/pipe/file-size.pipe';
141 import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; 141 import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
142 import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; 142 import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
  143 +import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
143 144
144 @NgModule({ 145 @NgModule({
145 providers: [ 146 providers: [
@@ -175,6 +176,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; @@ -175,6 +176,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
175 TimewindowComponent, 176 TimewindowComponent,
176 TimewindowPanelComponent, 177 TimewindowPanelComponent,
177 TimeintervalComponent, 178 TimeintervalComponent,
  179 + QuickTimeIntervalComponent,
178 DashboardSelectComponent, 180 DashboardSelectComponent,
179 DashboardSelectPanelComponent, 181 DashboardSelectPanelComponent,
180 DatetimePeriodComponent, 182 DatetimePeriodComponent,
@@ -302,6 +304,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; @@ -302,6 +304,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
302 TimewindowComponent, 304 TimewindowComponent,
303 TimewindowPanelComponent, 305 TimewindowPanelComponent,
304 TimeintervalComponent, 306 TimeintervalComponent,
  307 + QuickTimeIntervalComponent,
305 DashboardSelectComponent, 308 DashboardSelectComponent,
306 DatetimePeriodComponent, 309 DatetimePeriodComponent,
307 DatetimeComponent, 310 DatetimeComponent,
@@ -1988,7 +1988,8 @@ @@ -1988,7 +1988,8 @@
1988 "timezone": "Timezone", 1988 "timezone": "Timezone",
1989 "select-timezone": "Select timezone", 1989 "select-timezone": "Select timezone",
1990 "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", 1990 "no-timezones-matching": "No timezones matching '{{timezone}}' were found.",
1991 - "timezone-required": "Timezone is required." 1991 + "timezone-required": "Timezone is required.",
  1992 + "browser-time": "Browser Time"
1992 }, 1993 },
1993 "queue": { 1994 "queue": {
1994 "select_name": "Select queue name", 1995 "select_name": "Select queue name",
@@ -2118,7 +2119,24 @@ @@ -2118,7 +2119,24 @@
2118 "hours": "Hours", 2119 "hours": "Hours",
2119 "minutes": "Minutes", 2120 "minutes": "Minutes",
2120 "seconds": "Seconds", 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 "timeunit": { 2141 "timeunit": {
2124 "seconds": "Seconds", 2142 "seconds": "Seconds",
@@ -2139,7 +2157,8 @@ @@ -2139,7 +2157,8 @@
2139 "date-range": "Date range", 2157 "date-range": "Date range",
2140 "last": "Last", 2158 "last": "Last",
2141 "time-period": "Time period", 2159 "time-period": "Time period",
2142 - "hide": "Hide" 2160 + "hide": "Hide",
  2161 + "interval": "Interval"
2143 }, 2162 },
2144 "user": { 2163 "user": {
2145 "user": "User", 2164 "user": "User",