Commit 43578c7e584da5f487eaabde42a777ab25a1c044
Merge branch 'master' of github.com:thingsboard/thingsboard
Showing
25 changed files
with
1007 additions
and
217 deletions
... | ... | @@ -283,10 +283,12 @@ public class DefaultTbClusterService implements TbClusterService { |
283 | 283 | byte[] msgBytes = encodingService.encode(msg); |
284 | 284 | TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); |
285 | 285 | Set<String> tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)); |
286 | - if (msg.getEntityId().getEntityType().equals(EntityType.TENANT) | |
287 | - || msg.getEntityId().getEntityType().equals(EntityType.TENANT_PROFILE) | |
288 | - || msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE) | |
289 | - || msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) { | |
286 | + EntityType entityType = msg.getEntityId().getEntityType(); | |
287 | + if (entityType.equals(EntityType.TENANT) | |
288 | + || entityType.equals(EntityType.TENANT_PROFILE) | |
289 | + || entityType.equals(EntityType.DEVICE_PROFILE) | |
290 | + || entityType.equals(EntityType.API_USAGE_STATE) | |
291 | + || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)) { | |
290 | 292 | TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); |
291 | 293 | Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); |
292 | 294 | for (String serviceId : tbCoreServices) { | ... | ... |
... | ... | @@ -76,12 +76,17 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> { |
76 | 76 | @Param("searchText") String searchText, |
77 | 77 | Pageable pageable); |
78 | 78 | |
79 | - @Query("SELECT alarm.severity FROM AlarmEntity alarm" + | |
80 | - " WHERE alarm.tenantId = :tenantId" + | |
81 | - " AND alarm.originatorId = :entityId" + | |
82 | - " AND ((:status) IS NULL OR alarm.status in (:status))") | |
79 | + @Query(value = "SELECT a.severity FROM AlarmEntity a " + | |
80 | + "LEFT JOIN RelationEntity re ON a.id = re.toId " + | |
81 | + "AND re.relationTypeGroup = 'ALARM' " + | |
82 | + "AND re.toType = 'ALARM' " + | |
83 | + "AND re.fromId = :affectedEntityId " + | |
84 | + "AND re.fromType = :affectedEntityType " + | |
85 | + "WHERE a.tenantId = :tenantId " + | |
86 | + "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + | |
87 | + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))") | |
83 | 88 | Set<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId, |
84 | - @Param("entityId") UUID entityId, | |
85 | - @Param("status") Set<AlarmStatus> status); | |
86 | - | |
89 | + @Param("affectedEntityId") UUID affectedEntityId, | |
90 | + @Param("affectedEntityType") String affectedEntityType, | |
91 | + @Param("alarmStatuses") Set<AlarmStatus> alarmStatuses); | |
87 | 92 | } | ... | ... |
... | ... | @@ -123,7 +123,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A |
123 | 123 | } |
124 | 124 | |
125 | 125 | @Override |
126 | - public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status) { | |
127 | - return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status); | |
126 | + public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> statuses) { | |
127 | + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses); | |
128 | 128 | } |
129 | 129 | } | ... | ... |
... | ... | @@ -130,6 +130,7 @@ export class AlarmDataSubscription { |
130 | 130 | this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; |
131 | 131 | } |
132 | 132 | |
133 | + this.subscriber.setTsOffset(this.subsTw.tsOffset); | |
133 | 134 | this.subscriber.subscriptionCommands.push(this.alarmDataCommand); |
134 | 135 | |
135 | 136 | this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { |
... | ... | @@ -143,8 +144,11 @@ export class AlarmDataSubscription { |
143 | 144 | this.subscriber.subscribe(); |
144 | 145 | |
145 | 146 | } else if (this.datasourceType === DatasourceType.function) { |
147 | + const alarm = deepClone(simulatedAlarm); | |
148 | + alarm.createdTime += this.subsTw.tsOffset; | |
149 | + alarm.startTs += this.subsTw.tsOffset; | |
146 | 150 | const pageData: PageData<AlarmData> = { |
147 | - data: [{...simulatedAlarm, entityId: '1', latest: {}}], | |
151 | + data: [{...alarm, entityId: '1', latest: {}}], | |
148 | 152 | hasNext: false, |
149 | 153 | totalElements: 1, |
150 | 154 | totalPages: 1 | ... | ... |
... | ... | @@ -15,7 +15,12 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; |
18 | -import { AggregationType } from '@shared/models/time/time.models'; | |
18 | +import { | |
19 | + AggregationType, | |
20 | + calculateIntervalEndTime, | |
21 | + calculateIntervalStartTime, getCurrentTime, | |
22 | + QuickTimeInterval, SubscriptionTimewindow | |
23 | +} from '@shared/models/time/time.models'; | |
19 | 24 | import { UtilsService } from '@core/services/utils.service'; |
20 | 25 | import { deepClone } from '@core/utils'; |
21 | 26 | import Timeout = NodeJS.Timeout; |
... | ... | @@ -73,33 +78,29 @@ export class DataAggregator { |
73 | 78 | private resetPending = false; |
74 | 79 | private updatedData = false; |
75 | 80 | |
76 | - private noAggregation = this.aggregationType === AggregationType.NONE; | |
77 | - private aggregationTimeout = Math.max(this.interval, 1000); | |
81 | + private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE; | |
82 | + private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000); | |
78 | 83 | private readonly aggFunction: AggFunction; |
79 | 84 | |
80 | 85 | private intervalTimeoutHandle: Timeout; |
81 | 86 | private intervalScheduledTime: number; |
82 | 87 | |
88 | + private startTs = this.subsTw.startTs + this.subsTw.tsOffset; | |
83 | 89 | private endTs: number; |
84 | 90 | private elapsed: number; |
85 | 91 | |
86 | 92 | constructor(private onDataCb: onAggregatedData, |
87 | 93 | private tsKeyNames: string[], |
88 | - private startTs: number, | |
89 | - private limit: number, | |
90 | - private aggregationType: AggregationType, | |
91 | - private timeWindow: number, | |
92 | - private interval: number, | |
93 | - private stateData: boolean, | |
94 | + private subsTw: SubscriptionTimewindow, | |
94 | 95 | private utils: UtilsService, |
95 | 96 | private ignoreDataUpdateOnIntervalTick: boolean) { |
96 | 97 | this.tsKeyNames.forEach((key) => { |
97 | 98 | this.dataBuffer[key] = []; |
98 | 99 | }); |
99 | - if (this.stateData) { | |
100 | + if (this.subsTw.aggregation.stateData) { | |
100 | 101 | this.lastPrevKvPairData = {}; |
101 | 102 | } |
102 | - switch (this.aggregationType) { | |
103 | + switch (this.subsTw.aggregation.type) { | |
103 | 104 | case AggregationType.MIN: |
104 | 105 | this.aggFunction = min; |
105 | 106 | break; |
... | ... | @@ -129,18 +130,21 @@ export class DataAggregator { |
129 | 130 | return prevOnDataCb; |
130 | 131 | } |
131 | 132 | |
132 | - public reset(startTs: number, timeWindow: number, interval: number) { | |
133 | + public reset(subsTw: SubscriptionTimewindow) { | |
133 | 134 | if (this.intervalTimeoutHandle) { |
134 | 135 | clearTimeout(this.intervalTimeoutHandle); |
135 | 136 | this.intervalTimeoutHandle = null; |
136 | 137 | } |
138 | + this.subsTw = subsTw; | |
137 | 139 | this.intervalScheduledTime = this.utils.currentPerfTime(); |
138 | - this.startTs = startTs; | |
139 | - this.timeWindow = timeWindow; | |
140 | - this.interval = interval; | |
141 | - this.endTs = this.startTs + this.timeWindow; | |
140 | + this.startTs = this.subsTw.startTs + this.subsTw.tsOffset; | |
141 | + if (this.subsTw.quickInterval) { | |
142 | + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset; | |
143 | + } else { | |
144 | + this.endTs = this.startTs + this.subsTw.aggregation.timeWindow; | |
145 | + } | |
142 | 146 | this.elapsed = 0; |
143 | - this.aggregationTimeout = Math.max(this.interval, 1000); | |
147 | + this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000); | |
144 | 148 | this.resetPending = true; |
145 | 149 | this.updatedData = false; |
146 | 150 | this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); |
... | ... | @@ -161,7 +165,11 @@ export class DataAggregator { |
161 | 165 | if (!this.dataReceived) { |
162 | 166 | this.elapsed = 0; |
163 | 167 | this.dataReceived = true; |
164 | - this.endTs = this.startTs + this.timeWindow; | |
168 | + if (this.subsTw.quickInterval) { | |
169 | + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset; | |
170 | + } else { | |
171 | + this.endTs = this.startTs + this.subsTw.aggregation.timeWindow; | |
172 | + } | |
165 | 173 | } |
166 | 174 | if (this.resetPending) { |
167 | 175 | this.resetPending = false; |
... | ... | @@ -195,12 +203,19 @@ export class DataAggregator { |
195 | 203 | this.intervalTimeoutHandle = null; |
196 | 204 | } |
197 | 205 | if (!history) { |
198 | - const delta = Math.floor(this.elapsed / this.interval); | |
206 | + const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval); | |
199 | 207 | if (delta || !this.data) { |
200 | - this.startTs += delta * this.interval; | |
201 | - this.endTs += delta * this.interval; | |
208 | + const tickTs = delta * this.subsTw.aggregation.interval; | |
209 | + if (this.subsTw.quickInterval) { | |
210 | + const currentDate = getCurrentTime(this.subsTw.timezone); | |
211 | + this.startTs = calculateIntervalStartTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; | |
212 | + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; | |
213 | + } else { | |
214 | + this.startTs += tickTs; | |
215 | + this.endTs += tickTs; | |
216 | + } | |
202 | 217 | this.data = this.updateData(); |
203 | - this.elapsed = this.elapsed - delta * this.interval; | |
218 | + this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval; | |
204 | 219 | } |
205 | 220 | } else { |
206 | 221 | this.data = this.updateData(); |
... | ... | @@ -223,7 +238,7 @@ export class DataAggregator { |
223 | 238 | let keyData = this.dataBuffer[key]; |
224 | 239 | aggKeyData.forEach((aggData, aggTimestamp) => { |
225 | 240 | if (aggTimestamp <= this.startTs) { |
226 | - if (this.stateData && | |
241 | + if (this.subsTw.aggregation.stateData && | |
227 | 242 | (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { |
228 | 243 | this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; |
229 | 244 | } |
... | ... | @@ -235,11 +250,11 @@ export class DataAggregator { |
235 | 250 | } |
236 | 251 | }); |
237 | 252 | keyData.sort((set1, set2) => set1[0] - set2[0]); |
238 | - if (this.stateData) { | |
253 | + if (this.subsTw.aggregation.stateData) { | |
239 | 254 | this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key])); |
240 | 255 | } |
241 | - if (keyData.length > this.limit) { | |
242 | - keyData = keyData.slice(keyData.length - this.limit); | |
256 | + if (keyData.length > this.subsTw.aggregation.limit) { | |
257 | + keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit); | |
243 | 258 | } |
244 | 259 | this.dataBuffer[key] = keyData; |
245 | 260 | } |
... | ... | @@ -275,7 +290,7 @@ export class DataAggregator { |
275 | 290 | } |
276 | 291 | |
277 | 292 | private processAggregatedData(data: SubscriptionData): AggregationMap { |
278 | - const isCount = this.aggregationType === AggregationType.COUNT; | |
293 | + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; | |
279 | 294 | const aggregationMap: AggregationMap = {}; |
280 | 295 | for (const key of Object.keys(data)) { |
281 | 296 | let aggKeyData = aggregationMap[key]; |
... | ... | @@ -300,7 +315,7 @@ export class DataAggregator { |
300 | 315 | } |
301 | 316 | |
302 | 317 | private updateAggregatedData(data: SubscriptionData) { |
303 | - const isCount = this.aggregationType === AggregationType.COUNT; | |
318 | + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; | |
304 | 319 | for (const key of Object.keys(data)) { |
305 | 320 | let aggKeyData = this.aggregationMap[key]; |
306 | 321 | if (!aggKeyData) { |
... | ... | @@ -312,7 +327,8 @@ export class DataAggregator { |
312 | 327 | const timestamp = kvPair[0]; |
313 | 328 | const value = this.convertValue(kvPair[1]); |
314 | 329 | const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + |
315 | - Math.floor((timestamp - this.startTs) / this.interval) * this.interval + this.interval / 2); | |
330 | + Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) * | |
331 | + this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2); | |
316 | 332 | let aggData = aggKeyData.get(aggTimestamp); |
317 | 333 | if (!aggData) { |
318 | 334 | aggData = { | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; |
18 | -import { AggregationType, SubscriptionTimewindow } from '@shared/models/time/time.models'; | |
18 | +import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models'; | |
19 | 19 | import { |
20 | 20 | EntityData, |
21 | 21 | EntityDataPageLink, |
... | ... | @@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions { |
74 | 74 | keyFilters?: Array<KeyFilter>; |
75 | 75 | additionalKeyFilters?: Array<KeyFilter>; |
76 | 76 | subscriptionTimewindow?: SubscriptionTimewindow; |
77 | + latestTsOffset?: number; | |
77 | 78 | } |
78 | 79 | |
79 | 80 | export class EntityDataSubscription { |
... | ... | @@ -95,6 +96,7 @@ export class EntityDataSubscription { |
95 | 96 | private entityDataResolveSubject: Subject<EntityDataLoadResult>; |
96 | 97 | private pageData: PageData<EntityData>; |
97 | 98 | private subsTw: SubscriptionTimewindow; |
99 | + private latestTsOffset: number; | |
98 | 100 | private dataAggregators: Array<DataAggregator>; |
99 | 101 | private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}; |
100 | 102 | private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; |
... | ... | @@ -177,6 +179,7 @@ export class EntityDataSubscription { |
177 | 179 | this.started = true; |
178 | 180 | this.dataResolved = true; |
179 | 181 | this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; |
182 | + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
180 | 183 | this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
181 | 184 | isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); |
182 | 185 | this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
... | ... | @@ -238,6 +241,11 @@ export class EntityDataSubscription { |
238 | 241 | |
239 | 242 | if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { |
240 | 243 | this.prepareSubscriptionCommands(this.dataCommand); |
244 | + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { | |
245 | + this.subscriber.setTsOffset(this.subsTw.tsOffset); | |
246 | + } else { | |
247 | + this.subscriber.setTsOffset(this.latestTsOffset); | |
248 | + } | |
241 | 249 | } |
242 | 250 | |
243 | 251 | this.subscriber.subscriptionCommands.push(this.dataCommand); |
... | ... | @@ -256,8 +264,8 @@ export class EntityDataSubscription { |
256 | 264 | if (this.started) { |
257 | 265 | const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; |
258 | 266 | if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && |
259 | - !this.history && this.tsFields.length) { | |
260 | - const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription(); | |
267 | + !this.history && this.tsFields.length) { | |
268 | + const newSubsTw = this.listener.updateRealtimeSubscription(); | |
261 | 269 | this.subsTw = newSubsTw; |
262 | 270 | targetCommand.tsCmd.startTs = this.subsTw.startTs; |
263 | 271 | targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; |
... | ... | @@ -266,18 +274,25 @@ export class EntityDataSubscription { |
266 | 274 | targetCommand.tsCmd.agg = this.subsTw.aggregation.type; |
267 | 275 | targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; |
268 | 276 | this.dataAggregators.forEach((dataAggregator) => { |
269 | - dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); | |
277 | + dataAggregator.reset(newSubsTw); | |
270 | 278 | }); |
271 | 279 | } |
280 | + this.subscriber.setTsOffset(this.subsTw.tsOffset); | |
272 | 281 | targetCommand.query = this.dataCommand.query; |
273 | 282 | this.subscriber.subscriptionCommands = [targetCommand]; |
274 | 283 | } else { |
275 | 284 | this.subscriber.subscriptionCommands = [this.dataCommand]; |
276 | 285 | } |
277 | 286 | }); |
278 | - | |
279 | 287 | this.subscriber.subscribe(); |
280 | 288 | } else if (this.datasourceType === DatasourceType.function) { |
289 | + let tsOffset = 0; | |
290 | + if (this.entityDataSubscriptionOptions.type === widgetType.latest) { | |
291 | + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
292 | + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) { | |
293 | + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; | |
294 | + } | |
295 | + | |
281 | 296 | const entityData: EntityData = { |
282 | 297 | entityId: { |
283 | 298 | id: NULL_UUID, |
... | ... | @@ -288,7 +303,7 @@ export class EntityDataSubscription { |
288 | 303 | }; |
289 | 304 | const name = DatasourceType.function; |
290 | 305 | entityData.latest[EntityKeyType.ENTITY_FIELD] = { |
291 | - name: {ts: Date.now(), value: name} | |
306 | + name: {ts: Date.now() + tsOffset, value: name} | |
292 | 307 | }; |
293 | 308 | const pageData: PageData<EntityData> = { |
294 | 309 | data: [entityData], |
... | ... | @@ -298,7 +313,9 @@ export class EntityDataSubscription { |
298 | 313 | }; |
299 | 314 | this.onPageData(pageData); |
300 | 315 | } else if (this.datasourceType === DatasourceType.entityCount) { |
316 | + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
301 | 317 | this.subscriber = new TelemetrySubscriber(this.telemetryService); |
318 | + this.subscriber.setTsOffset(this.latestTsOffset); | |
302 | 319 | this.countCommand = new EntityCountCmd(); |
303 | 320 | let keyFilters = this.entityDataSubscriptionOptions.keyFilters; |
304 | 321 | if (this.entityDataSubscriptionOptions.additionalKeyFilters) { |
... | ... | @@ -331,13 +348,13 @@ export class EntityDataSubscription { |
331 | 348 | latest: { |
332 | 349 | [EntityKeyType.ENTITY_FIELD]: { |
333 | 350 | name: { |
334 | - ts: Date.now(), | |
351 | + ts: Date.now() + this.latestTsOffset, | |
335 | 352 | value: DatasourceType.entityCount |
336 | 353 | } |
337 | 354 | }, |
338 | 355 | [EntityKeyType.COUNT]: { |
339 | 356 | [countKey.name]: { |
340 | - ts: Date.now(), | |
357 | + ts: Date.now() + this.latestTsOffset, | |
341 | 358 | value: entityCountUpdate.count + '' |
342 | 359 | } |
343 | 360 | } |
... | ... | @@ -358,7 +375,7 @@ export class EntityDataSubscription { |
358 | 375 | latest: { |
359 | 376 | [EntityKeyType.COUNT]: { |
360 | 377 | [countKey.name]: { |
361 | - ts: Date.now(), | |
378 | + ts: Date.now() + this.latestTsOffset, | |
362 | 379 | value: entityCountUpdate.count + '' |
363 | 380 | } |
364 | 381 | } |
... | ... | @@ -383,6 +400,7 @@ export class EntityDataSubscription { |
383 | 400 | return; |
384 | 401 | } |
385 | 402 | this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; |
403 | + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; | |
386 | 404 | this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
387 | 405 | isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); |
388 | 406 | this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && |
... | ... | @@ -394,10 +412,26 @@ export class EntityDataSubscription { |
394 | 412 | this.subsCommand = new EntityDataCmd(); |
395 | 413 | this.subsCommand.cmdId = this.dataCommand.cmdId; |
396 | 414 | this.prepareSubscriptionCommands(this.subsCommand); |
397 | - if (!this.subsCommand.isEmpty()) { | |
415 | + let latestTsOffsetChanged = false; | |
416 | + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { | |
417 | + this.subscriber.setTsOffset(this.subsTw.tsOffset); | |
418 | + } else { | |
419 | + latestTsOffsetChanged = this.subscriber.setTsOffset(this.latestTsOffset); | |
420 | + } | |
421 | + if (latestTsOffsetChanged) { | |
422 | + if (this.listener.initialPageDataChanged) { | |
423 | + this.listener.initialPageDataChanged(this.pageData); | |
424 | + } | |
425 | + } else if (!this.subsCommand.isEmpty()) { | |
398 | 426 | this.subscriber.subscriptionCommands = [this.subsCommand]; |
399 | 427 | this.subscriber.update(); |
400 | 428 | } |
429 | + } else if (this.datasourceType === DatasourceType.entityCount) { | |
430 | + if (this.subscriber.setTsOffset(this.latestTsOffset)) { | |
431 | + if (this.listener.initialPageDataChanged) { | |
432 | + this.listener.initialPageDataChanged(this.pageData); | |
433 | + } | |
434 | + } | |
401 | 435 | } else if (this.datasourceType === DatasourceType.function) { |
402 | 436 | this.startFunction(); |
403 | 437 | } |
... | ... | @@ -745,12 +779,7 @@ export class EntityDataSubscription { |
745 | 779 | this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb); |
746 | 780 | }, |
747 | 781 | tsKeyNames, |
748 | - subsTw.startTs, | |
749 | - subsTw.aggregation.limit, | |
750 | - subsTw.aggregation.type, | |
751 | - subsTw.aggregation.timeWindow, | |
752 | - subsTw.aggregation.interval, | |
753 | - subsTw.aggregation.stateData, | |
782 | + subsTw, | |
754 | 783 | this.utils, |
755 | 784 | this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick |
756 | 785 | ); |
... | ... | @@ -786,7 +815,7 @@ export class EntityDataSubscription { |
786 | 815 | } else { |
787 | 816 | prevSeries = [0, 0]; |
788 | 817 | } |
789 | - const time = Date.now(); | |
818 | + const time = Date.now() + this.latestTsOffset; | |
790 | 819 | const value = dataKey.func(time, prevSeries[1]); |
791 | 820 | const series: [number, any] = [time, value]; |
792 | 821 | this.datasourceData[0][dataKey.key].data = [series]; |
... | ... | @@ -827,7 +856,8 @@ export class EntityDataSubscription { |
827 | 856 | startTime = dataKey.lastUpdateTime + this.frequency; |
828 | 857 | endTime = dataKey.lastUpdateTime + deltaElapsed; |
829 | 858 | } else { |
830 | - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs; | |
859 | + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs + | |
860 | + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; | |
831 | 861 | endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency; |
832 | 862 | if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) { |
833 | 863 | const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit; |
... | ... | @@ -835,8 +865,14 @@ export class EntityDataSubscription { |
835 | 865 | } |
836 | 866 | } |
837 | 867 | } else { |
838 | - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs; | |
839 | - endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs; | |
868 | + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs + | |
869 | + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; | |
870 | + endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs + | |
871 | + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; | |
872 | + } | |
873 | + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) { | |
874 | + const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; | |
875 | + endTime = Math.min(currentTime, endTime); | |
840 | 876 | } |
841 | 877 | } |
842 | 878 | generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); | ... | ... |
... | ... | @@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs'; |
32 | 32 | export interface EntityDataListener { |
33 | 33 | subscriptionType: widgetType; |
34 | 34 | subscriptionTimewindow?: SubscriptionTimewindow; |
35 | + latestTsOffset?: number; | |
35 | 36 | configDatasource: Datasource; |
36 | 37 | configDatasourceIndex: number; |
37 | 38 | dataLoaded: (pageData: PageData<EntityData>, |
... | ... | @@ -92,6 +93,8 @@ export class EntityDataService { |
92 | 93 | if (listener.subscription) { |
93 | 94 | if (listener.subscriptionType === widgetType.timeseries) { |
94 | 95 | listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); |
96 | + } else if (listener.subscriptionType === widgetType.latest) { | |
97 | + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; | |
95 | 98 | } |
96 | 99 | listener.subscription.start(); |
97 | 100 | } |
... | ... | @@ -118,6 +121,8 @@ export class EntityDataService { |
118 | 121 | listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); |
119 | 122 | if (listener.subscriptionType === widgetType.timeseries) { |
120 | 123 | listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); |
124 | + } else if (listener.subscriptionType === widgetType.latest) { | |
125 | + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; | |
121 | 126 | } |
122 | 127 | return listener.subscription.subscribe(); |
123 | 128 | } | ... | ... |
... | ... | @@ -37,8 +37,12 @@ import { |
37 | 37 | } from '@app/shared/models/widget.models'; |
38 | 38 | import { HttpErrorResponse } from '@angular/common/http'; |
39 | 39 | import { |
40 | + calculateIntervalEndTime, | |
41 | + calculateIntervalStartTime, | |
42 | + calculateTsOffset, | |
40 | 43 | createSubscriptionTimewindow, |
41 | 44 | createTimewindowForComparison, |
45 | + getCurrentTime, | |
42 | 46 | SubscriptionTimewindow, |
43 | 47 | Timewindow, |
44 | 48 | toHistoryTimewindow, |
... | ... | @@ -77,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription { |
77 | 81 | timeWindow: WidgetTimewindow; |
78 | 82 | originalTimewindow: Timewindow; |
79 | 83 | timeWindowConfig: Timewindow; |
84 | + timezone: string; | |
80 | 85 | subscriptionTimewindow: SubscriptionTimewindow; |
81 | 86 | useDashboardTimewindow: boolean; |
87 | + tsOffset = 0; | |
82 | 88 | |
83 | 89 | hasDataPageLink: boolean; |
84 | 90 | singleEntity: boolean; |
... | ... | @@ -211,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription { |
211 | 217 | this.timeWindow = {}; |
212 | 218 | this.useDashboardTimewindow = options.useDashboardTimewindow; |
213 | 219 | this.stateData = options.stateData; |
220 | + if (this.type === widgetType.latest) { | |
221 | + this.timezone = options.dashboardTimewindow.timezone; | |
222 | + this.updateTsOffset(); | |
223 | + } | |
214 | 224 | if (this.useDashboardTimewindow) { |
215 | 225 | this.timeWindowConfig = deepClone(options.dashboardTimewindow); |
216 | 226 | } else { |
... | ... | @@ -576,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription { |
576 | 586 | if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { |
577 | 587 | this.timeWindowConfig = deepClone(newDashboardTimewindow); |
578 | 588 | this.update(); |
579 | - return true; | |
589 | + } | |
590 | + } | |
591 | + } else if (this.type === widgetType.latest) { | |
592 | + if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) { | |
593 | + this.timezone = newDashboardTimewindow.timezone; | |
594 | + if (this.updateTsOffset()) { | |
595 | + this.update(); | |
580 | 596 | } |
581 | 597 | } |
582 | 598 | } |
583 | - return false; | |
584 | 599 | } |
585 | 600 | |
586 | 601 | updateDataVisibility(index: number): void { |
... | ... | @@ -813,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
813 | 828 | configDatasource: datasource, |
814 | 829 | configDatasourceIndex: datasourceIndex, |
815 | 830 | subscriptionTimewindow: this.subscriptionTimewindow, |
831 | + latestTsOffset: this.tsOffset, | |
816 | 832 | dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { |
817 | 833 | this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); |
818 | 834 | }, |
... | ... | @@ -837,9 +853,11 @@ export class WidgetSubscription implements IWidgetSubscription { |
837 | 853 | if (this.alarmDataListener) { |
838 | 854 | this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); |
839 | 855 | } |
856 | + | |
840 | 857 | if (this.timeWindowConfig) { |
841 | 858 | this.updateRealtimeSubscription(); |
842 | 859 | } |
860 | + | |
843 | 861 | this.alarmDataListener = { |
844 | 862 | subscriptionTimewindow: this.subscriptionTimewindow, |
845 | 863 | alarmSource: this.alarmSource, |
... | ... | @@ -878,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription { |
878 | 896 | } |
879 | 897 | |
880 | 898 | private dataSubscribe() { |
899 | + this.updateDataTimewindow(); | |
881 | 900 | if (!this.hasDataPageLink) { |
882 | - if (this.type === widgetType.timeseries && this.timeWindowConfig) { | |
883 | - this.updateDataTimewindow(); | |
884 | - if (this.subscriptionTimewindow.fixedWindow) { | |
901 | + if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) { | |
885 | 902 | this.onDataUpdated(); |
886 | - } | |
887 | 903 | } |
888 | 904 | const forceUpdate = !this.datasources.length; |
905 | + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length; | |
889 | 906 | this.entityDataListeners.forEach((listener) => { |
890 | 907 | if (this.comparisonEnabled && listener.configDatasource.isAdditional) { |
891 | 908 | listener.subscriptionTimewindow = this.timewindowForComparison; |
892 | 909 | } else { |
893 | 910 | listener.subscriptionTimewindow = this.subscriptionTimewindow; |
911 | + listener.latestTsOffset = this.tsOffset; | |
894 | 912 | } |
895 | 913 | this.ctx.entityDataService.startSubscription(listener); |
896 | 914 | }); |
897 | 915 | if (forceUpdate) { |
898 | 916 | this.onDataUpdated(); |
899 | 917 | } |
918 | + if (notifyDataLoaded) { | |
919 | + this.notifyDataLoaded(); | |
920 | + } | |
900 | 921 | } |
901 | 922 | } |
902 | 923 | |
... | ... | @@ -1080,15 +1101,33 @@ export class WidgetSubscription implements IWidgetSubscription { |
1080 | 1101 | |
1081 | 1102 | private updateTimewindow() { |
1082 | 1103 | this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; |
1104 | + this.timeWindow.timezone = this.subscriptionTimewindow.timezone; | |
1083 | 1105 | if (this.subscriptionTimewindow.realtimeWindowMs) { |
1084 | - this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff; | |
1085 | - this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; | |
1106 | + if (this.subscriptionTimewindow.quickInterval) { | |
1107 | + const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone); | |
1108 | + this.timeWindow.maxTime = calculateIntervalEndTime( | |
1109 | + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset; | |
1110 | + this.timeWindow.minTime = calculateIntervalStartTime( | |
1111 | + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset; | |
1112 | + } else { | |
1113 | + this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff; | |
1114 | + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; | |
1115 | + } | |
1086 | 1116 | } else if (this.subscriptionTimewindow.fixedWindow) { |
1087 | - this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; | |
1088 | - this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; | |
1117 | + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs + this.subscriptionTimewindow.tsOffset; | |
1118 | + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs + this.subscriptionTimewindow.tsOffset; | |
1089 | 1119 | } |
1090 | 1120 | } |
1091 | 1121 | |
1122 | + private updateTsOffset(): boolean { | |
1123 | + const newOffset = calculateTsOffset(this.timezone); | |
1124 | + if (this.tsOffset !== newOffset) { | |
1125 | + this.tsOffset = newOffset; | |
1126 | + return true; | |
1127 | + } | |
1128 | + return false; | |
1129 | + } | |
1130 | + | |
1092 | 1131 | private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { |
1093 | 1132 | if (subscriptionTimewindow) { |
1094 | 1133 | this.subscriptionTimewindow = subscriptionTimewindow; |
... | ... | @@ -1103,12 +1142,13 @@ export class WidgetSubscription implements IWidgetSubscription { |
1103 | 1142 | |
1104 | 1143 | private updateComparisonTimewindow() { |
1105 | 1144 | this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; |
1145 | + this.comparisonTimeWindow.timezone = this.timewindowForComparison.timezone; | |
1106 | 1146 | if (this.timewindowForComparison.realtimeWindowMs) { |
1107 | 1147 | this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); |
1108 | - this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs; | |
1148 | + this.comparisonTimeWindow.minTime = moment(this.timeWindow.minTime).subtract(1, this.timeForComparison).valueOf(); | |
1109 | 1149 | } else if (this.timewindowForComparison.fixedWindow) { |
1110 | - this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs; | |
1111 | - this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs; | |
1150 | + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs + this.timewindowForComparison.tsOffset; | |
1151 | + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs + this.timewindowForComparison.tsOffset; | |
1112 | 1152 | } |
1113 | 1153 | } |
1114 | 1154 | |
... | ... | @@ -1335,7 +1375,7 @@ export class WidgetSubscription implements IWidgetSubscription { |
1335 | 1375 | this.onDataUpdated(); |
1336 | 1376 | } |
1337 | 1377 | |
1338 | - private alarmsUpdated(_updated: Array<AlarmData>, alarms: PageData<AlarmData>) { | |
1378 | + private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) { | |
1339 | 1379 | this.alarmsLoaded(alarms, 0, 0); |
1340 | 1380 | } |
1341 | 1381 | ... | ... |
... | ... | @@ -95,6 +95,7 @@ |
95 | 95 | <tb-timewindow *ngIf="widget.hasTimewindow" |
96 | 96 | #timewindowComponent |
97 | 97 | aggregation="{{widget.hasAggregation}}" |
98 | + timezone="true" | |
98 | 99 | [isEdit]="isEdit" |
99 | 100 | [(ngModel)]="widgetComponent.widget.config.timewindow" |
100 | 101 | (ngModelChange)="widgetComponent.onTimewindowChanged($event)"> | ... | ... |
... | ... | @@ -55,7 +55,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; |
55 | 55 | import { DialogService } from '@core/services/dialog.service'; |
56 | 56 | import { AddEntityDialogComponent } from './add-entity-dialog.component'; |
57 | 57 | import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; |
58 | -import { HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; | |
58 | +import { | |
59 | + calculateIntervalEndTime, | |
60 | + calculateIntervalStartTime, | |
61 | + getCurrentTime, | |
62 | + HistoryWindowType, | |
63 | + Timewindow | |
64 | +} from '@shared/models/time/time.models'; | |
59 | 65 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; |
60 | 66 | import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; |
61 | 67 | import { isDefined, isUndefined } from '@core/utils'; |
... | ... | @@ -296,6 +302,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
296 | 302 | const currentTime = Date.now(); |
297 | 303 | timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; |
298 | 304 | timePageLink.endTime = currentTime; |
305 | + } else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) { | |
306 | + const currentDate = getCurrentTime(); | |
307 | + timePageLink.startTime = calculateIntervalStartTime(this.timewindow.history.quickInterval, currentDate); | |
308 | + timePageLink.endTime = calculateIntervalEndTime(this.timewindow.history.quickInterval, currentDate); | |
299 | 309 | } else { |
300 | 310 | timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; |
301 | 311 | timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; | ... | ... |
... | ... | @@ -94,11 +94,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, |
94 | 94 | items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) |
95 | 95 | }); |
96 | 96 | this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { |
97 | - getDefaultTimezone().subscribe((defaultTimezone) => { | |
98 | - this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); | |
99 | - this.updateValidators(type, true); | |
100 | - this.alarmScheduleForm.updateValueAndValidity(); | |
101 | - }); | |
97 | + const defaultTimezone = getDefaultTimezone(); | |
98 | + this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); | |
99 | + this.updateValidators(type, true); | |
100 | + this.alarmScheduleForm.updateValueAndValidity(); | |
102 | 101 | }); |
103 | 102 | this.alarmScheduleForm.valueChanges.subscribe(() => { |
104 | 103 | this.updateModel(); | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2021 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<section class="interval-section" fxLayout="row" fxFlex> | |
19 | + <mat-form-field fxFlex> | |
20 | + <mat-label translate>timewindow.interval</mat-label> | |
21 | + <mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()"> | |
22 | + <mat-option *ngFor="let interval of intervals" [value]="interval"> | |
23 | + {{ timeIntervalTranslationMap.get(interval) | translate}} | |
24 | + </mat-option> | |
25 | + </mat-select> | |
26 | + </mat-form-field> | |
27 | +</section> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +:host { | |
18 | + min-width: 364px; | |
19 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2021 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, forwardRef, Input, OnInit } from '@angular/core'; | |
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
19 | +import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models'; | |
20 | + | |
21 | +@Component({ | |
22 | + selector: 'tb-quick-time-interval', | |
23 | + templateUrl: './quick-time-interval.component.html', | |
24 | + styleUrls: ['./quick-time-interval.component.scss'], | |
25 | + providers: [ | |
26 | + { | |
27 | + provide: NG_VALUE_ACCESSOR, | |
28 | + useExisting: forwardRef(() => QuickTimeIntervalComponent), | |
29 | + multi: true | |
30 | + } | |
31 | + ] | |
32 | +}) | |
33 | +export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor { | |
34 | + | |
35 | + private allIntervals = Object.values(QuickTimeInterval); | |
36 | + | |
37 | + modelValue: QuickTimeInterval; | |
38 | + timeIntervalTranslationMap = QuickTimeIntervalTranslationMap; | |
39 | + | |
40 | + rendered = false; | |
41 | + | |
42 | + @Input() disabled: boolean; | |
43 | + | |
44 | + @Input() onlyCurrentInterval = false; | |
45 | + | |
46 | + private propagateChange = (_: any) => {}; | |
47 | + | |
48 | + constructor() { | |
49 | + } | |
50 | + | |
51 | + get intervals() { | |
52 | + if (this.onlyCurrentInterval) { | |
53 | + return this.allIntervals.filter(interval => interval.startsWith('CURRENT_')); | |
54 | + } | |
55 | + return this.allIntervals; | |
56 | + } | |
57 | + | |
58 | + ngOnInit(): void { | |
59 | + } | |
60 | + | |
61 | + registerOnChange(fn: any): void { | |
62 | + this.propagateChange = fn; | |
63 | + } | |
64 | + | |
65 | + registerOnTouched(fn: any): void { | |
66 | + } | |
67 | + | |
68 | + setDisabledState(isDisabled: boolean): void { | |
69 | + this.disabled = isDisabled; | |
70 | + } | |
71 | + | |
72 | + writeValue(interval: QuickTimeInterval): void { | |
73 | + this.modelValue = interval; | |
74 | + } | |
75 | + | |
76 | + onIntervalChange() { | |
77 | + this.propagateChange(this.modelValue); | |
78 | + } | |
79 | +} | ... | ... |
... | ... | @@ -21,16 +21,43 @@ |
21 | 21 | <mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}" |
22 | 22 | (selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab"> |
23 | 23 | <mat-tab label="{{ 'timewindow.realtime' | translate }}"> |
24 | - <div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column"> | |
25 | - <tb-timeinterval | |
26 | - [(hideFlag)]="timewindow.hideInterval" | |
27 | - (hideFlagChange)="onHideIntervalChanged()" | |
28 | - [isEdit]="isEdit" | |
29 | - formControlName="timewindowMs" | |
30 | - predefinedName="timewindow.last" | |
31 | - [required]="timewindow.selectedTab === timewindowTypes.REALTIME" | |
32 | - style="padding-top: 8px;"></tb-timeinterval> | |
33 | - </div> | |
24 | + <section fxLayout="row"> | |
25 | + <section *ngIf="isEdit" fxLayout="column" style="padding-top: 8px; padding-left: 16px;"> | |
26 | + <label class="tb-small hide-label" translate>timewindow.hide</label> | |
27 | + <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval" | |
28 | + (ngModelChange)="onHideIntervalChanged()"></mat-checkbox> | |
29 | + </section> | |
30 | + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval"> | |
31 | + <div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;"> | |
32 | + <mat-radio-group formControlName="realtimeType"> | |
33 | + <mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary"> | |
34 | + <section fxLayout="column"> | |
35 | + <span translate>timewindow.last</span> | |
36 | + <tb-timeinterval | |
37 | + formControlName="timewindowMs" | |
38 | + predefinedName="timewindow.last" | |
39 | + [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL" | |
40 | + [required]="timewindow.selectedTab === timewindowTypes.REALTIME && | |
41 | + timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL" | |
42 | + style="padding-top: 8px;"></tb-timeinterval> | |
43 | + </section> | |
44 | + </mat-radio-button> | |
45 | + <mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary"> | |
46 | + <section fxLayout="column"> | |
47 | + <span translate>timewindow.interval</span> | |
48 | + <tb-quick-time-interval | |
49 | + formControlName="quickInterval" | |
50 | + onlyCurrentInterval="true" | |
51 | + [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" | |
52 | + [required]="timewindow.selectedTab === timewindowTypes.REALTIME && | |
53 | + timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" | |
54 | + style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval> | |
55 | + </section> | |
56 | + </mat-radio-button> | |
57 | + </mat-radio-group> | |
58 | + </div> | |
59 | + </section> | |
60 | + </section> | |
34 | 61 | </mat-tab> |
35 | 62 | <mat-tab label="{{ 'timewindow.history' | translate }}"> |
36 | 63 | <section fxLayout="row"> |
... | ... | @@ -65,6 +92,17 @@ |
65 | 92 | style="padding-top: 8px;"></tb-datetime-period> |
66 | 93 | </section> |
67 | 94 | </mat-radio-button> |
95 | + <mat-radio-button [value]="historyTypes.INTERVAL" color="primary"> | |
96 | + <section fxLayout="column"> | |
97 | + <span translate>timewindow.interval</span> | |
98 | + <tb-quick-time-interval | |
99 | + formControlName="quickInterval" | |
100 | + [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" | |
101 | + [required]="timewindow.selectedTab === timewindowTypes.HISTORY && | |
102 | + timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" | |
103 | + style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval> | |
104 | + </section> | |
105 | + </mat-radio-button> | |
68 | 106 | </mat-radio-group> |
69 | 107 | </div> |
70 | 108 | </section> |
... | ... | @@ -95,7 +133,7 @@ |
95 | 133 | <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval" |
96 | 134 | (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> |
97 | 135 | </section> |
98 | - <section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval"> | |
136 | + <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval"> | |
99 | 137 | <div class="limit-slider-container" |
100 | 138 | fxLayout="row" fxLayoutAlign="start center"> |
101 | 139 | <span translate>aggregation.limit</span> |
... | ... | @@ -139,6 +177,17 @@ |
139 | 177 | predefinedName="aggregation.group-interval"> |
140 | 178 | </tb-timeinterval> |
141 | 179 | </div> |
180 | + <div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row"> | |
181 | + <section fxLayout="column" [fxShow]="isEdit"> | |
182 | + <label class="tb-small hide-label" translate>timewindow.hide</label> | |
183 | + <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone" | |
184 | + (ngModelChange)="onHideTimezoneChanged()"></mat-checkbox> | |
185 | + </section> | |
186 | + <tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone" | |
187 | + localBrowserTimezonePlaceholderOnEmpty="true" | |
188 | + formControlName="timezone"> | |
189 | + </tb-timezone-select> | |
190 | + </div> | |
142 | 191 | <div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center"> |
143 | 192 | <button type="button" |
144 | 193 | mat-button | ... | ... |
... | ... | @@ -20,6 +20,8 @@ import { |
20 | 20 | AggregationType, |
21 | 21 | DAY, |
22 | 22 | HistoryWindowType, |
23 | + quickTimeIntervalPeriod, | |
24 | + RealtimeWindowType, | |
23 | 25 | Timewindow, |
24 | 26 | TimewindowType |
25 | 27 | } from '@shared/models/time/time.models'; |
... | ... | @@ -36,6 +38,7 @@ export interface TimewindowPanelData { |
36 | 38 | historyOnly: boolean; |
37 | 39 | timewindow: Timewindow; |
38 | 40 | aggregation: boolean; |
41 | + timezone: boolean; | |
39 | 42 | isEdit: boolean; |
40 | 43 | } |
41 | 44 | |
... | ... | @@ -50,6 +53,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
50 | 53 | |
51 | 54 | aggregation = false; |
52 | 55 | |
56 | + timezone = false; | |
57 | + | |
53 | 58 | isEdit = false; |
54 | 59 | |
55 | 60 | timewindow: Timewindow; |
... | ... | @@ -60,6 +65,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
60 | 65 | |
61 | 66 | historyTypes = HistoryWindowType; |
62 | 67 | |
68 | + realtimeTypes = RealtimeWindowType; | |
69 | + | |
63 | 70 | timewindowTypes = TimewindowType; |
64 | 71 | |
65 | 72 | aggregationTypes = AggregationType; |
... | ... | @@ -78,6 +85,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
78 | 85 | this.historyOnly = data.historyOnly; |
79 | 86 | this.timewindow = data.timewindow; |
80 | 87 | this.aggregation = data.aggregation; |
88 | + this.timezone = data.timezone; | |
81 | 89 | this.isEdit = data.isEdit; |
82 | 90 | } |
83 | 91 | |
... | ... | @@ -85,10 +93,16 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
85 | 93 | const hideInterval = this.timewindow.hideInterval || false; |
86 | 94 | const hideAggregation = this.timewindow.hideAggregation || false; |
87 | 95 | const hideAggInterval = this.timewindow.hideAggInterval || false; |
96 | + const hideTimezone = this.timewindow.hideTimezone || false; | |
88 | 97 | |
89 | 98 | this.timewindowForm = this.fb.group({ |
90 | 99 | realtime: this.fb.group( |
91 | 100 | { |
101 | + realtimeType: this.fb.control({ | |
102 | + value: this.timewindow.realtime && typeof this.timewindow.realtime.realtimeType !== 'undefined' | |
103 | + ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, | |
104 | + disabled: hideInterval | |
105 | + }), | |
92 | 106 | timewindowMs: [ |
93 | 107 | this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' |
94 | 108 | ? this.timewindow.realtime.timewindowMs : null |
... | ... | @@ -96,7 +110,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
96 | 110 | interval: [ |
97 | 111 | this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' |
98 | 112 | ? this.timewindow.realtime.interval : null |
99 | - ] | |
113 | + ], | |
114 | + quickInterval: this.fb.control({ | |
115 | + value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined' | |
116 | + ? this.timewindow.realtime.quickInterval : null, | |
117 | + disabled: hideInterval | |
118 | + }) | |
100 | 119 | } |
101 | 120 | ), |
102 | 121 | history: this.fb.group( |
... | ... | @@ -119,6 +138,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
119 | 138 | value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined' |
120 | 139 | ? this.timewindow.history.fixedTimewindow : null, |
121 | 140 | disabled: hideInterval |
141 | + }), | |
142 | + quickInterval: this.fb.control({ | |
143 | + value: this.timewindow.history && typeof this.timewindow.history.quickInterval !== 'undefined' | |
144 | + ? this.timewindow.history.quickInterval : null, | |
145 | + disabled: hideInterval | |
122 | 146 | }) |
123 | 147 | } |
124 | 148 | ), |
... | ... | @@ -135,21 +159,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
135 | 159 | disabled: hideAggInterval |
136 | 160 | }, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())]) |
137 | 161 | } |
138 | - ) | |
162 | + ), | |
163 | + timezone: this.fb.control({ | |
164 | + value: this.timewindow.timezone !== 'undefined' | |
165 | + ? this.timewindow.timezone : null, | |
166 | + disabled: hideTimezone | |
167 | + }) | |
139 | 168 | }); |
140 | 169 | } |
141 | 170 | |
142 | 171 | update() { |
143 | 172 | const timewindowFormValue = this.timewindowForm.getRawValue(); |
144 | 173 | this.timewindow.realtime = { |
174 | + realtimeType: timewindowFormValue.realtime.realtimeType, | |
145 | 175 | timewindowMs: timewindowFormValue.realtime.timewindowMs, |
176 | + quickInterval: timewindowFormValue.realtime.quickInterval, | |
146 | 177 | interval: timewindowFormValue.realtime.interval |
147 | 178 | }; |
148 | 179 | this.timewindow.history = { |
149 | 180 | historyType: timewindowFormValue.history.historyType, |
150 | 181 | timewindowMs: timewindowFormValue.history.timewindowMs, |
151 | 182 | interval: timewindowFormValue.history.interval, |
152 | - fixedTimewindow: timewindowFormValue.history.fixedTimewindow | |
183 | + fixedTimewindow: timewindowFormValue.history.fixedTimewindow, | |
184 | + quickInterval: timewindowFormValue.history.quickInterval, | |
153 | 185 | }; |
154 | 186 | if (this.aggregation) { |
155 | 187 | this.timewindow.aggregation = { |
... | ... | @@ -157,6 +189,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
157 | 189 | limit: timewindowFormValue.aggregation.limit |
158 | 190 | }; |
159 | 191 | } |
192 | + if (this.timezone) { | |
193 | + this.timewindow.timezone = timewindowFormValue.timezone; | |
194 | + } | |
160 | 195 | this.result = this.timewindow; |
161 | 196 | this.overlayRef.dispose(); |
162 | 197 | } |
... | ... | @@ -174,11 +209,23 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
174 | 209 | } |
175 | 210 | |
176 | 211 | minRealtimeAggInterval() { |
177 | - return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value); | |
212 | + return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow()); | |
178 | 213 | } |
179 | 214 | |
180 | 215 | maxRealtimeAggInterval() { |
181 | - return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value); | |
216 | + return this.timeService.maxIntervalLimit(this.currentRealtimeTimewindow()); | |
217 | + } | |
218 | + | |
219 | + currentRealtimeTimewindow(): number { | |
220 | + const timeWindowFormValue = this.timewindowForm.getRawValue(); | |
221 | + switch (timeWindowFormValue.realtime.realtimeType) { | |
222 | + case RealtimeWindowType.LAST_INTERVAL: | |
223 | + return timeWindowFormValue.realtime.timewindowMs; | |
224 | + case RealtimeWindowType.INTERVAL: | |
225 | + return quickTimeIntervalPeriod(timeWindowFormValue.realtime.quickInterval); | |
226 | + default: | |
227 | + return DAY; | |
228 | + } | |
182 | 229 | } |
183 | 230 | |
184 | 231 | minHistoryAggInterval() { |
... | ... | @@ -193,6 +240,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
193 | 240 | const timewindowFormValue = this.timewindowForm.getRawValue(); |
194 | 241 | if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { |
195 | 242 | return timewindowFormValue.history.timewindowMs; |
243 | + } else if (timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL) { | |
244 | + return quickTimeIntervalPeriod(timewindowFormValue.history.quickInterval); | |
196 | 245 | } else if (timewindowFormValue.history.fixedTimewindow) { |
197 | 246 | return timewindowFormValue.history.fixedTimewindow.endTimeMs - |
198 | 247 | timewindowFormValue.history.fixedTimewindow.startTimeMs; |
... | ... | @@ -206,10 +255,18 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
206 | 255 | this.timewindowForm.get('history.historyType').disable({emitEvent: false}); |
207 | 256 | this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false}); |
208 | 257 | this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false}); |
258 | + this.timewindowForm.get('history.quickInterval').disable({emitEvent: false}); | |
259 | + this.timewindowForm.get('realtime.realtimeType').disable({emitEvent: false}); | |
260 | + this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false}); | |
261 | + this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false}); | |
209 | 262 | } else { |
210 | 263 | this.timewindowForm.get('history.historyType').enable({emitEvent: false}); |
211 | 264 | this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false}); |
212 | 265 | this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); |
266 | + this.timewindowForm.get('history.quickInterval').enable({emitEvent: false}); | |
267 | + this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false}); | |
268 | + this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); | |
269 | + this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); | |
213 | 270 | } |
214 | 271 | this.timewindowForm.markAsDirty(); |
215 | 272 | } |
... | ... | @@ -232,4 +289,13 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { |
232 | 289 | this.timewindowForm.markAsDirty(); |
233 | 290 | } |
234 | 291 | |
292 | + onHideTimezoneChanged() { | |
293 | + if (this.timewindow.hideTimezone) { | |
294 | + this.timewindowForm.get('timezone').disable({emitEvent: false}); | |
295 | + } else { | |
296 | + this.timewindowForm.get('timezone').enable({emitEvent: false}); | |
297 | + } | |
298 | + this.timewindowForm.markAsDirty(); | |
299 | + } | |
300 | + | |
235 | 301 | } | ... | ... |
... | ... | @@ -34,7 +34,7 @@ |
34 | 34 | (click)="openEditMode()" |
35 | 35 | matTooltip="{{ 'timewindow.edit' | translate }}" |
36 | 36 | [matTooltipPosition]="tooltipPosition"> |
37 | - {{innerValue?.displayValue}} | |
37 | + {{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span> | |
38 | 38 | </span> |
39 | 39 | <button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32" |
40 | 40 | type="button" | ... | ... |
... | ... | @@ -31,8 +31,11 @@ import { TranslateService } from '@ngx-translate/core'; |
31 | 31 | import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; |
32 | 32 | import { |
33 | 33 | cloneSelectedTimewindow, |
34 | + getTimezoneInfo, | |
34 | 35 | HistoryWindowType, |
35 | 36 | initModelFromDefaultTimewindow, |
37 | + QuickTimeIntervalTranslationMap, | |
38 | + RealtimeWindowType, | |
36 | 39 | Timewindow, |
37 | 40 | TimewindowType |
38 | 41 | } from '@shared/models/time/time.models'; |
... | ... | @@ -49,7 +52,7 @@ import { BreakpointObserver } from '@angular/cdk/layout'; |
49 | 52 | import { WINDOW } from '@core/services/window.service'; |
50 | 53 | import { TimeService } from '@core/services/time.service'; |
51 | 54 | import { TooltipPosition } from '@angular/material/tooltip'; |
52 | -import { deepClone } from '@core/utils'; | |
55 | +import { deepClone, isDefinedAndNotNull } from '@core/utils'; | |
53 | 56 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
54 | 57 | |
55 | 58 | // @dynamic |
... | ... | @@ -89,6 +92,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces |
89 | 92 | return this.aggregationValue; |
90 | 93 | } |
91 | 94 | |
95 | + timezoneValue = false; | |
96 | + | |
97 | + @Input() | |
98 | + set timezone(val) { | |
99 | + this.timezoneValue = coerceBooleanProperty(val); | |
100 | + } | |
101 | + | |
102 | + get timezone() { | |
103 | + return this.timezoneValue; | |
104 | + } | |
105 | + | |
92 | 106 | isToolbarValue = false; |
93 | 107 | |
94 | 108 | @Input() |
... | ... | @@ -169,7 +183,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces |
169 | 183 | }); |
170 | 184 | if (isGtXs) { |
171 | 185 | config.minWidth = '417px'; |
172 | - config.maxHeight = '440px'; | |
186 | + config.maxHeight = '500px'; | |
173 | 187 | const panelHeight = 375; |
174 | 188 | const panelWidth = 417; |
175 | 189 | const el = this.timewindowPanelOrigin.elementRef.nativeElement; |
... | ... | @@ -225,6 +239,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces |
225 | 239 | timewindow: deepClone(this.innerValue), |
226 | 240 | historyOnly: this.historyOnly, |
227 | 241 | aggregation: this.aggregation, |
242 | + timezone: this.timezone, | |
228 | 243 | isEdit: this.isEdit |
229 | 244 | } |
230 | 245 | ); |
... | ... | @@ -272,20 +287,32 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces |
272 | 287 | |
273 | 288 | updateDisplayValue() { |
274 | 289 | if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) { |
275 | - this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' + | |
276 | - this.translate.instant('timewindow.last-prefix') + ' ' + | |
277 | - this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); | |
290 | + this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - '; | |
291 | + if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) { | |
292 | + this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval)); | |
293 | + } else { | |
294 | + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + | |
295 | + this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); | |
296 | + } | |
278 | 297 | } else { |
279 | 298 | this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; |
280 | 299 | if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { |
281 | 300 | this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + |
282 | 301 | this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); |
302 | + } else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) { | |
303 | + this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval)); | |
283 | 304 | } else { |
284 | 305 | const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss'); |
285 | 306 | const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss'); |
286 | 307 | this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString}); |
287 | 308 | } |
288 | 309 | } |
310 | + if (isDefinedAndNotNull(this.innerValue.timezone) && this.innerValue.timezone !== '') { | |
311 | + this.innerValue.displayValue += ' '; | |
312 | + this.innerValue.displayTimezoneAbbr = getTimezoneInfo(this.innerValue.timezone).abbr; | |
313 | + } else { | |
314 | + this.innerValue.displayTimezoneAbbr = ''; | |
315 | + } | |
289 | 316 | } |
290 | 317 | |
291 | 318 | hideLabel() { | ... | ... |
... | ... | @@ -16,14 +16,15 @@ |
16 | 16 | |
17 | 17 | import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core'; |
18 | 18 | import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; |
19 | -import { Observable } from 'rxjs'; | |
19 | +import { Observable, of } from 'rxjs'; | |
20 | 20 | import { map, mergeMap, share, tap } from 'rxjs/operators'; |
21 | 21 | import { Store } from '@ngrx/store'; |
22 | 22 | import { AppState } from '@app/core/core.state'; |
23 | 23 | import { TranslateService } from '@ngx-translate/core'; |
24 | 24 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
25 | 25 | import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
26 | -import { getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models'; | |
26 | +import { getDefaultTimezoneInfo, getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models'; | |
27 | +import { deepClone } from '@core/utils'; | |
27 | 28 | |
28 | 29 | @Component({ |
29 | 30 | selector: 'tb-timezone-select', |
... | ... | @@ -43,10 +44,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
43 | 44 | |
44 | 45 | defaultTimezoneId: string = null; |
45 | 46 | |
46 | - timezones$ = getTimezones().pipe( | |
47 | - share() | |
48 | - ); | |
49 | - | |
50 | 47 | @Input() |
51 | 48 | set defaultTimezone(timezone: string) { |
52 | 49 | if (this.defaultTimezoneId !== timezone) { |
... | ... | @@ -72,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
72 | 69 | this.userTimezoneByDefaultValue = coerceBooleanProperty(value); |
73 | 70 | } |
74 | 71 | |
72 | + private localBrowserTimezonePlaceholderOnEmptyValue: boolean; | |
73 | + get localBrowserTimezonePlaceholderOnEmpty(): boolean { | |
74 | + return this.localBrowserTimezonePlaceholderOnEmptyValue; | |
75 | + } | |
76 | + @Input() | |
77 | + set localBrowserTimezonePlaceholderOnEmpty(value: boolean) { | |
78 | + this.localBrowserTimezonePlaceholderOnEmptyValue = coerceBooleanProperty(value); | |
79 | + } | |
80 | + | |
75 | 81 | @Input() |
76 | 82 | disabled: boolean; |
77 | 83 | |
... | ... | @@ -85,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
85 | 91 | |
86 | 92 | private dirty = false; |
87 | 93 | |
94 | + private localBrowserTimezoneInfoPlaceholder: TimezoneInfo; | |
95 | + | |
96 | + private timezones: Array<TimezoneInfo>; | |
97 | + | |
88 | 98 | private propagateChange = (v: any) => { }; |
89 | 99 | |
90 | 100 | constructor(private store: Store<AppState>, |
... | ... | @@ -138,23 +148,24 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
138 | 148 | |
139 | 149 | writeValue(value: string | null): void { |
140 | 150 | this.searchText = ''; |
141 | - getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe( | |
142 | - (foundTimezone) => { | |
143 | - if (foundTimezone !== null) { | |
144 | - this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); | |
145 | - if (foundTimezone.id !== value) { | |
146 | - setTimeout(() => { | |
147 | - this.updateView(foundTimezone.id); | |
148 | - }, 0); | |
149 | - } else { | |
150 | - this.modelValue = value; | |
151 | - } | |
152 | - } else { | |
153 | - this.modelValue = null; | |
154 | - this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); | |
155 | - } | |
151 | + const foundTimezone = getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue); | |
152 | + if (foundTimezone !== null) { | |
153 | + this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false}); | |
154 | + if (foundTimezone.id !== value) { | |
155 | + setTimeout(() => { | |
156 | + this.updateView(foundTimezone.id); | |
157 | + }, 0); | |
158 | + } else { | |
159 | + this.modelValue = value; | |
160 | + } | |
161 | + } else { | |
162 | + this.modelValue = null; | |
163 | + if (this.localBrowserTimezonePlaceholderOnEmptyValue) { | |
164 | + this.selectTimezoneFormGroup.get('timezone').patchValue(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: false}); | |
165 | + } else { | |
166 | + this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false}); | |
156 | 167 | } |
157 | - ); | |
168 | + } | |
158 | 169 | this.dirty = true; |
159 | 170 | } |
160 | 171 | |
... | ... | @@ -169,15 +180,19 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
169 | 180 | if (this.ignoreClosePanel) { |
170 | 181 | this.ignoreClosePanel = false; |
171 | 182 | } else { |
172 | - if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) { | |
173 | - getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe( | |
174 | - (defaultTimezoneInfo) => { | |
175 | - if (defaultTimezoneInfo !== null) { | |
176 | - this.ngZone.run(() => { | |
177 | - this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true}); | |
178 | - }); | |
179 | - } | |
180 | - }); | |
183 | + if (!this.modelValue) { | |
184 | + if (this.defaultTimezoneId || this.userTimezoneByDefaultValue) { | |
185 | + const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue); | |
186 | + if (defaultTimezoneInfo !== null) { | |
187 | + this.ngZone.run(() => { | |
188 | + this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true}); | |
189 | + }); | |
190 | + } | |
191 | + } else if (this.localBrowserTimezonePlaceholderOnEmptyValue) { | |
192 | + this.ngZone.run(() => { | |
193 | + this.selectTimezoneFormGroup.get('timezone').reset(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: true}); | |
194 | + }); | |
195 | + } | |
181 | 196 | } |
182 | 197 | } |
183 | 198 | } |
... | ... | @@ -196,12 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
196 | 211 | fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> { |
197 | 212 | this.searchText = searchText; |
198 | 213 | if (searchText && searchText.length) { |
199 | - return getTimezones().pipe( | |
200 | - map((timezones) => timezones.filter((timezoneInfo) => | |
201 | - timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase()))) | |
202 | - ); | |
214 | + return of(this.loadTimezones().filter((timezoneInfo) => | |
215 | + timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase()))); | |
203 | 216 | } |
204 | - return getTimezones(); | |
217 | + return of(this.loadTimezones()); | |
205 | 218 | } |
206 | 219 | |
207 | 220 | clear() { |
... | ... | @@ -211,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af |
211 | 224 | }, 0); |
212 | 225 | } |
213 | 226 | |
227 | + private loadTimezones(): Array<TimezoneInfo> { | |
228 | + if (!this.timezones) { | |
229 | + this.timezones = []; | |
230 | + if (this.localBrowserTimezonePlaceholderOnEmptyValue) { | |
231 | + this.timezones.push(this.getLocalBrowserTimezoneInfoPlaceholder()); | |
232 | + } | |
233 | + this.timezones.push(...getTimezones()); | |
234 | + } | |
235 | + return this.timezones; | |
236 | + } | |
237 | + | |
238 | + private getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo { | |
239 | + if (!this.localBrowserTimezoneInfoPlaceholder) { | |
240 | + this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo()); | |
241 | + this.localBrowserTimezoneInfoPlaceholder.id = null; | |
242 | + this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time'); | |
243 | + } | |
244 | + return this.localBrowserTimezoneInfoPlaceholder; | |
245 | + } | |
214 | 246 | } | ... | ... |
... | ... | @@ -30,6 +30,9 @@ import { |
30 | 30 | TsValue |
31 | 31 | } from '@shared/models/query/query.models'; |
32 | 32 | import { PageData } from '@shared/models/page/page-data'; |
33 | +import { alarmFields } from '@shared/models/alarm.models'; | |
34 | +import { entityFields } from '@shared/models/entity.models'; | |
35 | +import { isUndefined } from '@core/utils'; | |
33 | 36 | |
34 | 37 | export enum DataKeyType { |
35 | 38 | timeseries = 'timeseries', |
... | ... | @@ -430,6 +433,44 @@ export class EntityDataUpdate extends DataUpdate<EntityData> { |
430 | 433 | constructor(msg: EntityDataUpdateMsg) { |
431 | 434 | super(msg); |
432 | 435 | } |
436 | + | |
437 | + public prepareData(tsOffset: number) { | |
438 | + if (this.data) { | |
439 | + this.processEntityData(this.data.data, tsOffset); | |
440 | + } | |
441 | + if (this.update) { | |
442 | + this.processEntityData(this.update, tsOffset); | |
443 | + } | |
444 | + } | |
445 | + | |
446 | + private processEntityData(data: Array<EntityData>, tsOffset: number) { | |
447 | + for (const entityData of data) { | |
448 | + if (entityData.timeseries) { | |
449 | + for (const key of Object.keys(entityData.timeseries)) { | |
450 | + const tsValues = entityData.timeseries[key]; | |
451 | + for (const tsValue of tsValues) { | |
452 | + if (tsValue.ts) { | |
453 | + tsValue.ts += tsOffset; | |
454 | + } | |
455 | + } | |
456 | + } | |
457 | + } | |
458 | + if (entityData.latest) { | |
459 | + for (const entityKeyType of Object.keys(entityData.latest)) { | |
460 | + const keyTypeValues = entityData.latest[entityKeyType]; | |
461 | + for (const key of Object.keys(keyTypeValues)) { | |
462 | + const tsValue = keyTypeValues[key]; | |
463 | + if (tsValue.ts) { | |
464 | + tsValue.ts += tsOffset; | |
465 | + } | |
466 | + if (key === entityFields.createdTime.keyName && tsValue.value) { | |
467 | + tsValue.value = (Number(tsValue.value) + tsOffset) + ''; | |
468 | + } | |
469 | + } | |
470 | + } | |
471 | + } | |
472 | + } | |
473 | + } | |
433 | 474 | } |
434 | 475 | |
435 | 476 | export class AlarmDataUpdate extends DataUpdate<AlarmData> { |
... | ... | @@ -441,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> { |
441 | 482 | this.allowedEntities = msg.allowedEntities; |
442 | 483 | this.totalEntities = msg.totalEntities; |
443 | 484 | } |
485 | + | |
486 | + public prepareData(tsOffset: number) { | |
487 | + if (this.data) { | |
488 | + this.processAlarmData(this.data.data, tsOffset); | |
489 | + } | |
490 | + if (this.update) { | |
491 | + this.processAlarmData(this.update, tsOffset); | |
492 | + } | |
493 | + } | |
494 | + | |
495 | + private processAlarmData(data: Array<AlarmData>, tsOffset: number) { | |
496 | + for (const alarmData of data) { | |
497 | + alarmData.createdTime += tsOffset; | |
498 | + if (alarmData.ackTs) { | |
499 | + alarmData.ackTs += tsOffset; | |
500 | + } | |
501 | + if (alarmData.clearTs) { | |
502 | + alarmData.clearTs += tsOffset; | |
503 | + } | |
504 | + if (alarmData.endTs) { | |
505 | + alarmData.endTs += tsOffset; | |
506 | + } | |
507 | + if (alarmData.latest) { | |
508 | + for (const entityKeyType of Object.keys(alarmData.latest)) { | |
509 | + const keyTypeValues = alarmData.latest[entityKeyType]; | |
510 | + for (const key of Object.keys(keyTypeValues)) { | |
511 | + const tsValue = keyTypeValues[key]; | |
512 | + if (tsValue.ts) { | |
513 | + tsValue.ts += tsOffset; | |
514 | + } | |
515 | + if (key in [entityFields.createdTime.keyName, | |
516 | + alarmFields.startTime.keyName, | |
517 | + alarmFields.endTime.keyName, | |
518 | + alarmFields.ackTime.keyName, | |
519 | + alarmFields.clearTime.keyName] && tsValue.value) { | |
520 | + tsValue.value = (Number(tsValue.value) + tsOffset) + ''; | |
521 | + } | |
522 | + } | |
523 | + } | |
524 | + } | |
525 | + } | |
526 | + } | |
444 | 527 | } |
445 | 528 | |
446 | 529 | export class EntityCountUpdate extends CmdUpdate { |
... | ... | @@ -468,6 +551,8 @@ export class TelemetrySubscriber { |
468 | 551 | |
469 | 552 | private zone: NgZone; |
470 | 553 | |
554 | + private tsOffset = undefined; | |
555 | + | |
471 | 556 | public subscriptionCommands: Array<WebsocketCmd>; |
472 | 557 | |
473 | 558 | public data$ = this.dataSubject.asObservable(); |
... | ... | @@ -522,6 +607,16 @@ export class TelemetrySubscriber { |
522 | 607 | this.reconnectSubject.complete(); |
523 | 608 | } |
524 | 609 | |
610 | + public setTsOffset(tsOffset: number): boolean { | |
611 | + if (this.tsOffset !== tsOffset) { | |
612 | + const changed = !isUndefined(this.tsOffset); | |
613 | + this.tsOffset = tsOffset; | |
614 | + return changed; | |
615 | + } else { | |
616 | + return false; | |
617 | + } | |
618 | + } | |
619 | + | |
525 | 620 | public onData(message: SubscriptionUpdate) { |
526 | 621 | const cmdId = message.subscriptionId; |
527 | 622 | let keys: string[]; |
... | ... | @@ -545,6 +640,9 @@ export class TelemetrySubscriber { |
545 | 640 | } |
546 | 641 | |
547 | 642 | public onEntityData(message: EntityDataUpdate) { |
643 | + if (this.tsOffset) { | |
644 | + message.prepareData(this.tsOffset); | |
645 | + } | |
548 | 646 | if (this.zone) { |
549 | 647 | this.zone.run( |
550 | 648 | () => { |
... | ... | @@ -557,6 +655,9 @@ export class TelemetrySubscriber { |
557 | 655 | } |
558 | 656 | |
559 | 657 | public onAlarmData(message: AlarmDataUpdate) { |
658 | + if (this.tsOffset) { | |
659 | + message.prepareData(this.tsOffset); | |
660 | + } | |
560 | 661 | if (this.zone) { |
561 | 662 | this.zone.run( |
562 | 663 | () => { | ... | ... |
... | ... | @@ -17,9 +17,7 @@ |
17 | 17 | import { TimeService } from '@core/services/time.service'; |
18 | 18 | import { deepClone, isDefined, isUndefined } from '@app/core/utils'; |
19 | 19 | import * as moment_ from 'moment'; |
20 | -import { Observable } from 'rxjs/internal/Observable'; | |
21 | -import { from, of } from 'rxjs'; | |
22 | -import { map, mergeMap, tap } from 'rxjs/operators'; | |
20 | +import * as monentTz from 'moment-timezone'; | |
23 | 21 | |
24 | 22 | const moment = moment_; |
25 | 23 | |
... | ... | @@ -27,6 +25,7 @@ export const SECOND = 1000; |
27 | 25 | export const MINUTE = 60 * SECOND; |
28 | 26 | export const HOUR = 60 * MINUTE; |
29 | 27 | export const DAY = 24 * HOUR; |
28 | +export const WEEK = 7 * DAY; | |
30 | 29 | export const YEAR = DAY * 365; |
31 | 30 | |
32 | 31 | export enum TimewindowType { |
... | ... | @@ -34,14 +33,25 @@ export enum TimewindowType { |
34 | 33 | HISTORY |
35 | 34 | } |
36 | 35 | |
36 | +export enum RealtimeWindowType { | |
37 | + LAST_INTERVAL, | |
38 | + INTERVAL | |
39 | +} | |
40 | + | |
37 | 41 | export enum HistoryWindowType { |
38 | 42 | LAST_INTERVAL, |
39 | - FIXED | |
43 | + FIXED, | |
44 | + INTERVAL | |
40 | 45 | } |
41 | 46 | |
42 | 47 | export interface IntervalWindow { |
43 | 48 | interval?: number; |
44 | 49 | timewindowMs?: number; |
50 | + quickInterval?: QuickTimeInterval; | |
51 | +} | |
52 | + | |
53 | +export interface RealtimeWindow extends IntervalWindow{ | |
54 | + realtimeType?: RealtimeWindowType; | |
45 | 55 | } |
46 | 56 | |
47 | 57 | export interface FixedWindow { |
... | ... | @@ -82,13 +92,16 @@ export interface Aggregation { |
82 | 92 | |
83 | 93 | export interface Timewindow { |
84 | 94 | displayValue?: string; |
95 | + displayTimezoneAbbr?: string; | |
85 | 96 | hideInterval?: boolean; |
86 | 97 | hideAggregation?: boolean; |
87 | 98 | hideAggInterval?: boolean; |
99 | + hideTimezone?: boolean; | |
88 | 100 | selectedTab?: TimewindowType; |
89 | - realtime?: IntervalWindow; | |
101 | + realtime?: RealtimeWindow; | |
90 | 102 | history?: HistoryWindow; |
91 | 103 | aggregation?: Aggregation; |
104 | + timezone?: string; | |
92 | 105 | } |
93 | 106 | |
94 | 107 | export interface SubscriptionAggregation extends Aggregation { |
... | ... | @@ -99,6 +112,9 @@ export interface SubscriptionAggregation extends Aggregation { |
99 | 112 | |
100 | 113 | export interface SubscriptionTimewindow { |
101 | 114 | startTs?: number; |
115 | + quickInterval?: QuickTimeInterval; | |
116 | + timezone?: string; | |
117 | + tsOffset?: number; | |
102 | 118 | realtimeWindowMs?: number; |
103 | 119 | fixedWindow?: FixedWindow; |
104 | 120 | aggregation?: SubscriptionAggregation; |
... | ... | @@ -108,9 +124,46 @@ export interface WidgetTimewindow { |
108 | 124 | minTime?: number; |
109 | 125 | maxTime?: number; |
110 | 126 | interval?: number; |
127 | + timezone?: string; | |
111 | 128 | stDiff?: number; |
112 | 129 | } |
113 | 130 | |
131 | +export enum QuickTimeInterval { | |
132 | + YESTERDAY = 'YESTERDAY', | |
133 | + DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY', | |
134 | + THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK', | |
135 | + PREVIOUS_WEEK = 'PREVIOUS_WEEK', | |
136 | + PREVIOUS_MONTH = 'PREVIOUS_MONTH', | |
137 | + PREVIOUS_YEAR = 'PREVIOUS_YEAR', | |
138 | + CURRENT_HOUR = 'CURRENT_HOUR', | |
139 | + CURRENT_DAY = 'CURRENT_DAY', | |
140 | + CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR', | |
141 | + CURRENT_WEEK = 'CURRENT_WEEK', | |
142 | + CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR', | |
143 | + CURRENT_MONTH = 'CURRENT_MONTH', | |
144 | + CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR', | |
145 | + CURRENT_YEAR = 'CURRENT_YEAR', | |
146 | + CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR' | |
147 | +} | |
148 | + | |
149 | +export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string>([ | |
150 | + [QuickTimeInterval.YESTERDAY, 'timeinterval.predefined.yesterday'], | |
151 | + [QuickTimeInterval.DAY_BEFORE_YESTERDAY, 'timeinterval.predefined.day-before-yesterday'], | |
152 | + [QuickTimeInterval.THIS_DAY_LAST_WEEK, 'timeinterval.predefined.this-day-last-week'], | |
153 | + [QuickTimeInterval.PREVIOUS_WEEK, 'timeinterval.predefined.previous-week'], | |
154 | + [QuickTimeInterval.PREVIOUS_MONTH, 'timeinterval.predefined.previous-month'], | |
155 | + [QuickTimeInterval.PREVIOUS_YEAR, 'timeinterval.predefined.previous-year'], | |
156 | + [QuickTimeInterval.CURRENT_HOUR, 'timeinterval.predefined.current-hour'], | |
157 | + [QuickTimeInterval.CURRENT_DAY, 'timeinterval.predefined.current-day'], | |
158 | + [QuickTimeInterval.CURRENT_DAY_SO_FAR, 'timeinterval.predefined.current-day-so-far'], | |
159 | + [QuickTimeInterval.CURRENT_WEEK, 'timeinterval.predefined.current-week'], | |
160 | + [QuickTimeInterval.CURRENT_WEEK_SO_FAR, 'timeinterval.predefined.current-week-so-far'], | |
161 | + [QuickTimeInterval.CURRENT_MONTH, 'timeinterval.predefined.current-month'], | |
162 | + [QuickTimeInterval.CURRENT_MONTH_SO_FAR, 'timeinterval.predefined.current-month-so-far'], | |
163 | + [QuickTimeInterval.CURRENT_YEAR, 'timeinterval.predefined.current-year'], | |
164 | + [QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far'] | |
165 | +]); | |
166 | + | |
114 | 167 | export function historyInterval(timewindowMs: number): Timewindow { |
115 | 168 | const timewindow: Timewindow = { |
116 | 169 | selectedTab: TimewindowType.HISTORY, |
... | ... | @@ -129,10 +182,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { |
129 | 182 | hideInterval: false, |
130 | 183 | hideAggregation: false, |
131 | 184 | hideAggInterval: false, |
185 | + hideTimezone: false, | |
132 | 186 | selectedTab: TimewindowType.REALTIME, |
133 | 187 | realtime: { |
188 | + realtimeType: RealtimeWindowType.LAST_INTERVAL, | |
134 | 189 | interval: SECOND, |
135 | - timewindowMs: MINUTE | |
190 | + timewindowMs: MINUTE, | |
191 | + quickInterval: QuickTimeInterval.CURRENT_DAY | |
136 | 192 | }, |
137 | 193 | history: { |
138 | 194 | historyType: HistoryWindowType.LAST_INTERVAL, |
... | ... | @@ -141,7 +197,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { |
141 | 197 | fixedTimewindow: { |
142 | 198 | startTimeMs: currentTime - DAY, |
143 | 199 | endTimeMs: currentTime |
144 | - } | |
200 | + }, | |
201 | + quickInterval: QuickTimeInterval.CURRENT_DAY | |
145 | 202 | }, |
146 | 203 | aggregation: { |
147 | 204 | type: AggregationType.AVG, |
... | ... | @@ -157,6 +214,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T |
157 | 214 | model.hideInterval = value.hideInterval; |
158 | 215 | model.hideAggregation = value.hideAggregation; |
159 | 216 | model.hideAggInterval = value.hideAggInterval; |
217 | + model.hideTimezone = value.hideTimezone; | |
160 | 218 | if (isUndefined(value.selectedTab)) { |
161 | 219 | if (value.realtime) { |
162 | 220 | model.selectedTab = TimewindowType.REALTIME; |
... | ... | @@ -170,7 +228,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T |
170 | 228 | if (isDefined(value.realtime.interval)) { |
171 | 229 | model.realtime.interval = value.realtime.interval; |
172 | 230 | } |
173 | - model.realtime.timewindowMs = value.realtime.timewindowMs; | |
231 | + if (isUndefined(value.realtime.realtimeType)) { | |
232 | + if (isDefined(value.realtime.quickInterval)) { | |
233 | + model.realtime.realtimeType = RealtimeWindowType.INTERVAL; | |
234 | + } else { | |
235 | + model.realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL; | |
236 | + } | |
237 | + } else { | |
238 | + model.realtime.realtimeType = value.realtime.realtimeType; | |
239 | + } | |
240 | + if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) { | |
241 | + model.realtime.quickInterval = value.realtime.quickInterval; | |
242 | + } else { | |
243 | + model.realtime.timewindowMs = value.realtime.timewindowMs; | |
244 | + } | |
174 | 245 | } else { |
175 | 246 | if (isDefined(value.history.interval)) { |
176 | 247 | model.history.interval = value.history.interval; |
... | ... | @@ -178,6 +249,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T |
178 | 249 | if (isUndefined(value.history.historyType)) { |
179 | 250 | if (isDefined(value.history.timewindowMs)) { |
180 | 251 | model.history.historyType = HistoryWindowType.LAST_INTERVAL; |
252 | + } else if (isDefined(value.history.quickInterval)) { | |
253 | + model.history.historyType = HistoryWindowType.INTERVAL; | |
181 | 254 | } else { |
182 | 255 | model.history.historyType = HistoryWindowType.FIXED; |
183 | 256 | } |
... | ... | @@ -186,6 +259,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T |
186 | 259 | } |
187 | 260 | if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) { |
188 | 261 | model.history.timewindowMs = value.history.timewindowMs; |
262 | + } else if (model.history.historyType === HistoryWindowType.INTERVAL) { | |
263 | + model.history.quickInterval = value.history.quickInterval; | |
189 | 264 | } else { |
190 | 265 | model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; |
191 | 266 | model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; |
... | ... | @@ -197,6 +272,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T |
197 | 272 | } |
198 | 273 | model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); |
199 | 274 | } |
275 | + model.timezone = value.timezone; | |
200 | 276 | } |
201 | 277 | return model; |
202 | 278 | } |
... | ... | @@ -223,6 +299,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, |
223 | 299 | hideInterval: timewindow.hideInterval || false, |
224 | 300 | hideAggregation: timewindow.hideAggregation || false, |
225 | 301 | hideAggInterval: timewindow.hideAggInterval || false, |
302 | + hideTimezone: timewindow.hideTimezone || false, | |
226 | 303 | selectedTab: TimewindowType.HISTORY, |
227 | 304 | history: { |
228 | 305 | historyType: HistoryWindowType.FIXED, |
... | ... | @@ -235,11 +312,22 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, |
235 | 312 | aggregation: { |
236 | 313 | type: aggType, |
237 | 314 | limit |
238 | - } | |
315 | + }, | |
316 | + timezone: timewindow.timezone | |
239 | 317 | }; |
240 | 318 | return historyTimewindow; |
241 | 319 | } |
242 | 320 | |
321 | +export function calculateTsOffset(timezone?: string): number { | |
322 | + if (timezone) { | |
323 | + const tz = getTimezone(timezone); | |
324 | + const localOffset = moment().utcOffset(); | |
325 | + return (tz.utcOffset() - localOffset) * 60 * 1000; | |
326 | + } else { | |
327 | + return 0; | |
328 | + } | |
329 | +} | |
330 | + | |
243 | 331 | export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean, |
244 | 332 | timeService: TimeService): SubscriptionTimewindow { |
245 | 333 | const subscriptionTimewindow: SubscriptionTimewindow = { |
... | ... | @@ -249,7 +337,9 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num |
249 | 337 | interval: SECOND, |
250 | 338 | limit: timeService.getMaxDatapointsLimit(), |
251 | 339 | type: AggregationType.AVG |
252 | - } | |
340 | + }, | |
341 | + timezone: timewindow.timezone, | |
342 | + tsOffset: calculateTsOffset(timewindow.timezone) | |
253 | 343 | }; |
254 | 344 | let aggTimewindow = 0; |
255 | 345 | if (stateData) { |
... | ... | @@ -267,33 +357,66 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num |
267 | 357 | selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY; |
268 | 358 | } |
269 | 359 | if (selectedTab === TimewindowType.REALTIME) { |
270 | - subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; | |
360 | + let realtimeType = timewindow.realtime.realtimeType; | |
361 | + if (isUndefined(realtimeType)) { | |
362 | + if (isDefined(timewindow.realtime.quickInterval)) { | |
363 | + realtimeType = RealtimeWindowType.INTERVAL; | |
364 | + } else { | |
365 | + realtimeType = RealtimeWindowType.LAST_INTERVAL; | |
366 | + } | |
367 | + } | |
368 | + if (realtimeType === RealtimeWindowType.INTERVAL) { | |
369 | + const currentDate = getCurrentTime(timewindow.timezone); | |
370 | + subscriptionTimewindow.realtimeWindowMs = | |
371 | + getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, currentDate); | |
372 | + subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval; | |
373 | + subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate); | |
374 | + } else { | |
375 | + subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; | |
376 | + subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs; | |
377 | + } | |
271 | 378 | subscriptionTimewindow.aggregation.interval = |
272 | 379 | timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval, |
273 | 380 | subscriptionTimewindow.aggregation.type); |
274 | - subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs; | |
275 | - const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; | |
276 | 381 | aggTimewindow = subscriptionTimewindow.realtimeWindowMs; |
277 | - if (startDiff) { | |
278 | - subscriptionTimewindow.startTs -= startDiff; | |
279 | - aggTimewindow += subscriptionTimewindow.aggregation.interval; | |
382 | + if (realtimeType !== RealtimeWindowType.INTERVAL) { | |
383 | + const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; | |
384 | + if (startDiff) { | |
385 | + subscriptionTimewindow.startTs -= startDiff; | |
386 | + aggTimewindow += subscriptionTimewindow.aggregation.interval; | |
387 | + } | |
280 | 388 | } |
281 | 389 | } else { |
282 | 390 | let historyType = timewindow.history.historyType; |
283 | 391 | if (isUndefined(historyType)) { |
284 | - historyType = isDefined(timewindow.history.timewindowMs) ? HistoryWindowType.LAST_INTERVAL : HistoryWindowType.FIXED; | |
392 | + if (isDefined(timewindow.history.timewindowMs)) { | |
393 | + historyType = HistoryWindowType.LAST_INTERVAL; | |
394 | + } else if (isDefined(timewindow.history.quickInterval)) { | |
395 | + historyType = HistoryWindowType.INTERVAL; | |
396 | + } else { | |
397 | + historyType = HistoryWindowType.FIXED; | |
398 | + } | |
285 | 399 | } |
286 | 400 | if (historyType === HistoryWindowType.LAST_INTERVAL) { |
287 | - const currentTime = Date.now(); | |
401 | + const currentDate = getCurrentTime(timewindow.timezone); | |
402 | + const currentTime = currentDate.valueOf(); | |
288 | 403 | subscriptionTimewindow.fixedWindow = { |
289 | 404 | startTimeMs: currentTime - timewindow.history.timewindowMs, |
290 | 405 | endTimeMs: currentTime |
291 | 406 | }; |
292 | 407 | aggTimewindow = timewindow.history.timewindowMs; |
408 | + } else if (historyType === HistoryWindowType.INTERVAL) { | |
409 | + const currentDate = getCurrentTime(timewindow.timezone); | |
410 | + subscriptionTimewindow.fixedWindow = { | |
411 | + startTimeMs: calculateIntervalStartTime(timewindow.history.quickInterval, currentDate), | |
412 | + endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate) | |
413 | + }; | |
414 | + aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; | |
415 | + subscriptionTimewindow.quickInterval = timewindow.history.quickInterval; | |
293 | 416 | } else { |
294 | 417 | subscriptionTimewindow.fixedWindow = { |
295 | - startTimeMs: timewindow.history.fixedTimewindow.startTimeMs, | |
296 | - endTimeMs: timewindow.history.fixedTimewindow.endTimeMs | |
418 | + startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset, | |
419 | + endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset | |
297 | 420 | }; |
298 | 421 | aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; |
299 | 422 | } |
... | ... | @@ -309,6 +432,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num |
309 | 432 | return subscriptionTimewindow; |
310 | 433 | } |
311 | 434 | |
435 | +function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, currentDate: moment_.Moment): number { | |
436 | + switch (interval) { | |
437 | + case QuickTimeInterval.CURRENT_HOUR: | |
438 | + return HOUR; | |
439 | + case QuickTimeInterval.CURRENT_DAY: | |
440 | + case QuickTimeInterval.CURRENT_DAY_SO_FAR: | |
441 | + return DAY; | |
442 | + case QuickTimeInterval.CURRENT_WEEK: | |
443 | + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: | |
444 | + return WEEK; | |
445 | + case QuickTimeInterval.CURRENT_MONTH: | |
446 | + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: | |
447 | + return currentDate.endOf('month').diff(currentDate.clone().startOf('month')); | |
448 | + case QuickTimeInterval.CURRENT_YEAR: | |
449 | + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: | |
450 | + return currentDate.endOf('year').diff(currentDate.clone().startOf('year')); | |
451 | + } | |
452 | +} | |
453 | + | |
454 | +export function calculateIntervalEndTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number { | |
455 | + currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz); | |
456 | + switch (interval) { | |
457 | + case QuickTimeInterval.YESTERDAY: | |
458 | + currentDate.subtract(1, 'days'); | |
459 | + return currentDate.endOf('day').valueOf(); | |
460 | + case QuickTimeInterval.DAY_BEFORE_YESTERDAY: | |
461 | + currentDate.subtract(2, 'days'); | |
462 | + return currentDate.endOf('day').valueOf(); | |
463 | + case QuickTimeInterval.THIS_DAY_LAST_WEEK: | |
464 | + currentDate.subtract(1, 'weeks'); | |
465 | + return currentDate.endOf('day').valueOf(); | |
466 | + case QuickTimeInterval.PREVIOUS_WEEK: | |
467 | + currentDate.subtract(1, 'weeks'); | |
468 | + return currentDate.endOf('week').valueOf(); | |
469 | + case QuickTimeInterval.PREVIOUS_MONTH: | |
470 | + currentDate.subtract(1, 'months'); | |
471 | + return currentDate.endOf('month').valueOf(); | |
472 | + case QuickTimeInterval.PREVIOUS_YEAR: | |
473 | + currentDate.subtract(1, 'years'); | |
474 | + return currentDate.endOf('year').valueOf(); | |
475 | + case QuickTimeInterval.CURRENT_HOUR: | |
476 | + return currentDate.endOf('hour').valueOf(); | |
477 | + case QuickTimeInterval.CURRENT_DAY: | |
478 | + return currentDate.endOf('day').valueOf(); | |
479 | + case QuickTimeInterval.CURRENT_WEEK: | |
480 | + return currentDate.endOf('week').valueOf(); | |
481 | + case QuickTimeInterval.CURRENT_MONTH: | |
482 | + return currentDate.endOf('month').valueOf(); | |
483 | + case QuickTimeInterval.CURRENT_YEAR: | |
484 | + return currentDate.endOf('year').valueOf(); | |
485 | + case QuickTimeInterval.CURRENT_DAY_SO_FAR: | |
486 | + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: | |
487 | + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: | |
488 | + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: | |
489 | + return currentDate.valueOf(); | |
490 | + } | |
491 | +} | |
492 | + | |
493 | +export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number { | |
494 | + currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz); | |
495 | + switch (interval) { | |
496 | + case QuickTimeInterval.YESTERDAY: | |
497 | + currentDate.subtract(1, 'days'); | |
498 | + return currentDate.startOf('day').valueOf(); | |
499 | + case QuickTimeInterval.DAY_BEFORE_YESTERDAY: | |
500 | + currentDate.subtract(2, 'days'); | |
501 | + return currentDate.startOf('day').valueOf(); | |
502 | + case QuickTimeInterval.THIS_DAY_LAST_WEEK: | |
503 | + currentDate.subtract(1, 'weeks'); | |
504 | + return currentDate.startOf('day').valueOf(); | |
505 | + case QuickTimeInterval.PREVIOUS_WEEK: | |
506 | + currentDate.subtract(1, 'weeks'); | |
507 | + return currentDate.startOf('week').valueOf(); | |
508 | + case QuickTimeInterval.PREVIOUS_MONTH: | |
509 | + currentDate.subtract(1, 'months'); | |
510 | + return currentDate.startOf('month').valueOf(); | |
511 | + case QuickTimeInterval.PREVIOUS_YEAR: | |
512 | + currentDate.subtract(1, 'years'); | |
513 | + return currentDate.startOf('year').valueOf(); | |
514 | + case QuickTimeInterval.CURRENT_HOUR: | |
515 | + return currentDate.startOf('hour').valueOf(); | |
516 | + case QuickTimeInterval.CURRENT_DAY: | |
517 | + case QuickTimeInterval.CURRENT_DAY_SO_FAR: | |
518 | + return currentDate.startOf('day').valueOf(); | |
519 | + case QuickTimeInterval.CURRENT_WEEK: | |
520 | + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: | |
521 | + return currentDate.startOf('week').valueOf(); | |
522 | + case QuickTimeInterval.CURRENT_MONTH: | |
523 | + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: | |
524 | + return currentDate.startOf('month').valueOf(); | |
525 | + case QuickTimeInterval.CURRENT_YEAR: | |
526 | + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: | |
527 | + return currentDate.startOf('year').valueOf(); | |
528 | + } | |
529 | +} | |
530 | + | |
531 | +export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number { | |
532 | + switch (interval) { | |
533 | + case QuickTimeInterval.CURRENT_HOUR: | |
534 | + return HOUR; | |
535 | + case QuickTimeInterval.YESTERDAY: | |
536 | + case QuickTimeInterval.DAY_BEFORE_YESTERDAY: | |
537 | + case QuickTimeInterval.THIS_DAY_LAST_WEEK: | |
538 | + case QuickTimeInterval.CURRENT_DAY: | |
539 | + case QuickTimeInterval.CURRENT_DAY_SO_FAR: | |
540 | + return DAY; | |
541 | + case QuickTimeInterval.PREVIOUS_WEEK: | |
542 | + case QuickTimeInterval.CURRENT_WEEK: | |
543 | + case QuickTimeInterval.CURRENT_WEEK_SO_FAR: | |
544 | + return WEEK; | |
545 | + case QuickTimeInterval.PREVIOUS_MONTH: | |
546 | + case QuickTimeInterval.CURRENT_MONTH: | |
547 | + case QuickTimeInterval.CURRENT_MONTH_SO_FAR: | |
548 | + return DAY * 30; | |
549 | + case QuickTimeInterval.PREVIOUS_YEAR: | |
550 | + case QuickTimeInterval.CURRENT_YEAR: | |
551 | + case QuickTimeInterval.CURRENT_YEAR_SO_FAR: | |
552 | + return YEAR; | |
553 | + } | |
554 | +} | |
555 | + | |
312 | 556 | export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow, |
313 | 557 | timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow { |
314 | 558 | const timewindowForComparison: SubscriptionTimewindow = { |
... | ... | @@ -339,6 +583,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { |
339 | 583 | cloned.hideInterval = timewindow.hideInterval || false; |
340 | 584 | cloned.hideAggregation = timewindow.hideAggregation || false; |
341 | 585 | cloned.hideAggInterval = timewindow.hideAggInterval || false; |
586 | + cloned.hideTimezone = timewindow.hideTimezone || false; | |
342 | 587 | if (isDefined(timewindow.selectedTab)) { |
343 | 588 | cloned.selectedTab = timewindow.selectedTab; |
344 | 589 | if (timewindow.selectedTab === TimewindowType.REALTIME) { |
... | ... | @@ -348,6 +593,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { |
348 | 593 | } |
349 | 594 | } |
350 | 595 | cloned.aggregation = deepClone(timewindow.aggregation); |
596 | + cloned.timezone = timewindow.timezone; | |
351 | 597 | return cloned; |
352 | 598 | } |
353 | 599 | |
... | ... | @@ -358,6 +604,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi |
358 | 604 | cloned.interval = historyWindow.interval; |
359 | 605 | if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) { |
360 | 606 | cloned.timewindowMs = historyWindow.timewindowMs; |
607 | + } else if (historyWindow.historyType === HistoryWindowType.INTERVAL) { | |
608 | + cloned.quickInterval = historyWindow.quickInterval; | |
361 | 609 | } else if (historyWindow.historyType === HistoryWindowType.FIXED) { |
362 | 610 | cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow); |
363 | 611 | } |
... | ... | @@ -375,7 +623,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( |
375 | 623 | { |
376 | 624 | name: 'timeinterval.seconds-interval', |
377 | 625 | translateParams: {seconds: 1}, |
378 | - value: 1 * SECOND | |
626 | + value: SECOND | |
379 | 627 | }, |
380 | 628 | { |
381 | 629 | name: 'timeinterval.seconds-interval', |
... | ... | @@ -400,7 +648,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( |
400 | 648 | { |
401 | 649 | name: 'timeinterval.minutes-interval', |
402 | 650 | translateParams: {minutes: 1}, |
403 | - value: 1 * MINUTE | |
651 | + value: MINUTE | |
404 | 652 | }, |
405 | 653 | { |
406 | 654 | name: 'timeinterval.minutes-interval', |
... | ... | @@ -430,7 +678,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( |
430 | 678 | { |
431 | 679 | name: 'timeinterval.hours-interval', |
432 | 680 | translateParams: {hours: 1}, |
433 | - value: 1 * HOUR | |
681 | + value: HOUR | |
434 | 682 | }, |
435 | 683 | { |
436 | 684 | name: 'timeinterval.hours-interval', |
... | ... | @@ -455,7 +703,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>( |
455 | 703 | { |
456 | 704 | name: 'timeinterval.days-interval', |
457 | 705 | translateParams: {days: 1}, |
458 | - value: 1 * DAY | |
706 | + value: DAY | |
459 | 707 | }, |
460 | 708 | { |
461 | 709 | name: 'timeinterval.days-interval', |
... | ... | @@ -490,65 +738,62 @@ export interface TimezoneInfo { |
490 | 738 | name: string; |
491 | 739 | offset: string; |
492 | 740 | nOffset: number; |
741 | + abbr: string; | |
493 | 742 | } |
494 | 743 | |
495 | 744 | let timezones: TimezoneInfo[] = null; |
496 | 745 | let defaultTimezone: string = null; |
497 | 746 | |
498 | -export function getTimezones(): Observable<TimezoneInfo[]> { | |
499 | - if (timezones) { | |
500 | - return of(timezones); | |
501 | - } else { | |
502 | - return from(import('moment-timezone')).pipe( | |
503 | - map((monentTz) => { | |
504 | - return monentTz.tz.names().map((zoneName) => { | |
505 | - const tz = monentTz.tz(zoneName); | |
506 | - return { | |
507 | - id: zoneName, | |
508 | - name: zoneName.replace(/_/g, ' '), | |
509 | - offset: `UTC${tz.format('Z')}`, | |
510 | - nOffset: tz.utcOffset() | |
511 | - }; | |
512 | - }); | |
513 | - }), | |
514 | - tap((zones) => { | |
515 | - timezones = zones; | |
516 | - }) | |
517 | - ); | |
747 | +export function getTimezones(): TimezoneInfo[] { | |
748 | + if (!timezones) { | |
749 | + timezones = monentTz.tz.names().map((zoneName) => { | |
750 | + const tz = monentTz.tz(zoneName); | |
751 | + return { | |
752 | + id: zoneName, | |
753 | + name: zoneName.replace(/_/g, ' '), | |
754 | + offset: `UTC${tz.format('Z')}`, | |
755 | + nOffset: tz.utcOffset(), | |
756 | + abbr: tz.zoneAbbr() | |
757 | + }; | |
758 | + }); | |
518 | 759 | } |
760 | + return timezones; | |
519 | 761 | } |
520 | 762 | |
521 | -export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): Observable<TimezoneInfo> { | |
522 | - return getTimezones().pipe( | |
523 | - mergeMap((timezoneList) => { | |
524 | - let foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId); | |
525 | - if (!foundTimezone) { | |
526 | - if (userTimezoneByDefault) { | |
527 | - return getDefaultTimezone().pipe( | |
528 | - map((userTimezone) => { | |
529 | - return timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone); | |
530 | - }) | |
531 | - ); | |
532 | - } else if (defaultTimezoneId) { | |
533 | - foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId); | |
534 | - } | |
535 | - } | |
536 | - return of(foundTimezone); | |
537 | - }) | |
538 | - ); | |
763 | +export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo { | |
764 | + const timezoneList = getTimezones(); | |
765 | + let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null; | |
766 | + if (!foundTimezone) { | |
767 | + if (userTimezoneByDefault) { | |
768 | + const userTimezone = getDefaultTimezone(); | |
769 | + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone); | |
770 | + } else if (defaultTimezoneId) { | |
771 | + foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId); | |
772 | + } | |
773 | + } | |
774 | + return foundTimezone; | |
539 | 775 | } |
540 | 776 | |
541 | -export function getDefaultTimezone(): Observable<string> { | |
542 | - if (defaultTimezone) { | |
543 | - return of(defaultTimezone); | |
777 | +export function getDefaultTimezoneInfo(): TimezoneInfo { | |
778 | + const userTimezone = getDefaultTimezone(); | |
779 | + return getTimezoneInfo(userTimezone); | |
780 | +} | |
781 | + | |
782 | +export function getDefaultTimezone(): string { | |
783 | + if (!defaultTimezone) { | |
784 | + defaultTimezone = monentTz.tz.guess(); | |
785 | + } | |
786 | + return defaultTimezone; | |
787 | +} | |
788 | + | |
789 | +export function getCurrentTime(tz?: string): moment_.Moment { | |
790 | + if (tz) { | |
791 | + return moment().tz(tz); | |
544 | 792 | } else { |
545 | - return from(import('moment-timezone')).pipe( | |
546 | - map((monentTz) => { | |
547 | - return monentTz.tz.guess(); | |
548 | - }), | |
549 | - tap((zone) => { | |
550 | - defaultTimezone = zone; | |
551 | - }) | |
552 | - ); | |
793 | + return moment(); | |
553 | 794 | } |
554 | 795 | } |
796 | + | |
797 | +export function getTimezone(tz: string): moment_.Moment { | |
798 | + return moment.tz(tz); | |
799 | +} | ... | ... |
... | ... | @@ -140,6 +140,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select |
140 | 140 | import { FileSizePipe } from '@shared/pipe/file-size.pipe'; |
141 | 141 | import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; |
142 | 142 | import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; |
143 | +import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component'; | |
143 | 144 | |
144 | 145 | @NgModule({ |
145 | 146 | providers: [ |
... | ... | @@ -175,6 +176,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; |
175 | 176 | TimewindowComponent, |
176 | 177 | TimewindowPanelComponent, |
177 | 178 | TimeintervalComponent, |
179 | + QuickTimeIntervalComponent, | |
178 | 180 | DashboardSelectComponent, |
179 | 181 | DashboardSelectPanelComponent, |
180 | 182 | DatetimePeriodComponent, |
... | ... | @@ -302,6 +304,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; |
302 | 304 | TimewindowComponent, |
303 | 305 | TimewindowPanelComponent, |
304 | 306 | TimeintervalComponent, |
307 | + QuickTimeIntervalComponent, | |
305 | 308 | DashboardSelectComponent, |
306 | 309 | DatetimePeriodComponent, |
307 | 310 | DatetimeComponent, | ... | ... |
... | ... | @@ -1988,7 +1988,8 @@ |
1988 | 1988 | "timezone": "Timezone", |
1989 | 1989 | "select-timezone": "Select timezone", |
1990 | 1990 | "no-timezones-matching": "No timezones matching '{{timezone}}' were found.", |
1991 | - "timezone-required": "Timezone is required." | |
1991 | + "timezone-required": "Timezone is required.", | |
1992 | + "browser-time": "Browser Time" | |
1992 | 1993 | }, |
1993 | 1994 | "queue": { |
1994 | 1995 | "select_name": "Select queue name", |
... | ... | @@ -2118,7 +2119,24 @@ |
2118 | 2119 | "hours": "Hours", |
2119 | 2120 | "minutes": "Minutes", |
2120 | 2121 | "seconds": "Seconds", |
2121 | - "advanced": "Advanced" | |
2122 | + "advanced": "Advanced", | |
2123 | + "predefined": { | |
2124 | + "yesterday": "Yesterday", | |
2125 | + "day-before-yesterday": "Day before yesterday", | |
2126 | + "this-day-last-week": "This day last week", | |
2127 | + "previous-week": "Previous week", | |
2128 | + "previous-month": "Previous month", | |
2129 | + "previous-year": "Previous year", | |
2130 | + "current-hour": "Current hour", | |
2131 | + "current-day": "Current day", | |
2132 | + "current-day-so-far": "Current day so far", | |
2133 | + "current-week": "Current week", | |
2134 | + "current-week-so-far": "Current week so far", | |
2135 | + "current-month": "Current month", | |
2136 | + "current-month-so-far": "Current month so far", | |
2137 | + "current-year": "Current year", | |
2138 | + "current-year-so-far": "Current year so far" | |
2139 | + } | |
2122 | 2140 | }, |
2123 | 2141 | "timeunit": { |
2124 | 2142 | "seconds": "Seconds", |
... | ... | @@ -2139,7 +2157,8 @@ |
2139 | 2157 | "date-range": "Date range", |
2140 | 2158 | "last": "Last", |
2141 | 2159 | "time-period": "Time period", |
2142 | - "hide": "Hide" | |
2160 | + "hide": "Hide", | |
2161 | + "interval": "Interval" | |
2143 | 2162 | }, |
2144 | 2163 | "user": { |
2145 | 2164 | "user": "User", | ... | ... |