diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 133447022b..5d04936733 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -283,10 +283,12 @@ public class DefaultTbClusterService implements TbClusterService { byte[] msgBytes = encodingService.encode(msg); TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); Set tbRuleEngineServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE)); - if (msg.getEntityId().getEntityType().equals(EntityType.TENANT) - || msg.getEntityId().getEntityType().equals(EntityType.TENANT_PROFILE) - || msg.getEntityId().getEntityType().equals(EntityType.DEVICE_PROFILE) - || msg.getEntityId().getEntityType().equals(EntityType.API_USAGE_STATE)) { + EntityType entityType = msg.getEntityId().getEntityType(); + if (entityType.equals(EntityType.TENANT) + || entityType.equals(EntityType.TENANT_PROFILE) + || entityType.equals(EntityType.DEVICE_PROFILE) + || entityType.equals(EntityType.API_USAGE_STATE) + || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); for (String serviceId : tbCoreServices) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index f34048c5ad..b422f1cd4a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -76,12 +76,17 @@ public interface AlarmRepository extends CrudRepository { @Param("searchText") String searchText, Pageable pageable); - @Query("SELECT alarm.severity FROM AlarmEntity alarm" + - " WHERE alarm.tenantId = :tenantId" + - " AND alarm.originatorId = :entityId" + - " AND ((:status) IS NULL OR alarm.status in (:status))") + @Query(value = "SELECT a.severity FROM AlarmEntity a " + + "LEFT JOIN RelationEntity re ON a.id = re.toId " + + "AND re.relationTypeGroup = 'ALARM' " + + "AND re.toType = 'ALARM' " + + "AND re.fromId = :affectedEntityId " + + "AND re.fromType = :affectedEntityType " + + "WHERE a.tenantId = :tenantId " + + "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))") Set findAlarmSeverities(@Param("tenantId") UUID tenantId, - @Param("entityId") UUID entityId, - @Param("status") Set status); - + @Param("affectedEntityId") UUID affectedEntityId, + @Param("affectedEntityType") String affectedEntityType, + @Param("alarmStatuses") Set alarmStatuses); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 722928e1b8..bfca088c69 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -123,7 +123,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status) { - return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status); + public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set statuses) { + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses); } } diff --git a/ui-ngx/src/app/core/api/alarm-data-subscription.ts b/ui-ngx/src/app/core/api/alarm-data-subscription.ts index c7db10025d..7a8df1f704 100644 --- a/ui-ngx/src/app/core/api/alarm-data-subscription.ts +++ b/ui-ngx/src/app/core/api/alarm-data-subscription.ts @@ -130,6 +130,7 @@ export class AlarmDataSubscription { this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; } + this.subscriber.setTsOffset(this.subsTw.tsOffset); this.subscriber.subscriptionCommands.push(this.alarmDataCommand); this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { @@ -143,8 +144,11 @@ export class AlarmDataSubscription { this.subscriber.subscribe(); } else if (this.datasourceType === DatasourceType.function) { + const alarm = deepClone(simulatedAlarm); + alarm.createdTime += this.subsTw.tsOffset; + alarm.startTs += this.subsTw.tsOffset; const pageData: PageData = { - data: [{...simulatedAlarm, entityId: '1', latest: {}}], + data: [{...alarm, entityId: '1', latest: {}}], hasNext: false, totalElements: 1, totalPages: 1 diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index a57ad25d93..e31d821efd 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -15,7 +15,12 @@ /// import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; -import { AggregationType } from '@shared/models/time/time.models'; +import { + AggregationType, + calculateIntervalEndTime, + calculateIntervalStartTime, getCurrentTime, + QuickTimeInterval, SubscriptionTimewindow +} from '@shared/models/time/time.models'; import { UtilsService } from '@core/services/utils.service'; import { deepClone } from '@core/utils'; import Timeout = NodeJS.Timeout; @@ -73,33 +78,29 @@ export class DataAggregator { private resetPending = false; private updatedData = false; - private noAggregation = this.aggregationType === AggregationType.NONE; - private aggregationTimeout = Math.max(this.interval, 1000); + private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE; + private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000); private readonly aggFunction: AggFunction; private intervalTimeoutHandle: Timeout; private intervalScheduledTime: number; + private startTs = this.subsTw.startTs + this.subsTw.tsOffset; private endTs: number; private elapsed: number; constructor(private onDataCb: onAggregatedData, private tsKeyNames: string[], - private startTs: number, - private limit: number, - private aggregationType: AggregationType, - private timeWindow: number, - private interval: number, - private stateData: boolean, + private subsTw: SubscriptionTimewindow, private utils: UtilsService, private ignoreDataUpdateOnIntervalTick: boolean) { this.tsKeyNames.forEach((key) => { this.dataBuffer[key] = []; }); - if (this.stateData) { + if (this.subsTw.aggregation.stateData) { this.lastPrevKvPairData = {}; } - switch (this.aggregationType) { + switch (this.subsTw.aggregation.type) { case AggregationType.MIN: this.aggFunction = min; break; @@ -129,18 +130,21 @@ export class DataAggregator { return prevOnDataCb; } - public reset(startTs: number, timeWindow: number, interval: number) { + public reset(subsTw: SubscriptionTimewindow) { if (this.intervalTimeoutHandle) { clearTimeout(this.intervalTimeoutHandle); this.intervalTimeoutHandle = null; } + this.subsTw = subsTw; this.intervalScheduledTime = this.utils.currentPerfTime(); - this.startTs = startTs; - this.timeWindow = timeWindow; - this.interval = interval; - this.endTs = this.startTs + this.timeWindow; + this.startTs = this.subsTw.startTs + this.subsTw.tsOffset; + if (this.subsTw.quickInterval) { + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset; + } else { + this.endTs = this.startTs + this.subsTw.aggregation.timeWindow; + } this.elapsed = 0; - this.aggregationTimeout = Math.max(this.interval, 1000); + this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000); this.resetPending = true; this.updatedData = false; this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); @@ -161,7 +165,11 @@ export class DataAggregator { if (!this.dataReceived) { this.elapsed = 0; this.dataReceived = true; - this.endTs = this.startTs + this.timeWindow; + if (this.subsTw.quickInterval) { + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, null, this.subsTw.timezone) + this.subsTw.tsOffset; + } else { + this.endTs = this.startTs + this.subsTw.aggregation.timeWindow; + } } if (this.resetPending) { this.resetPending = false; @@ -195,12 +203,19 @@ export class DataAggregator { this.intervalTimeoutHandle = null; } if (!history) { - const delta = Math.floor(this.elapsed / this.interval); + const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval); if (delta || !this.data) { - this.startTs += delta * this.interval; - this.endTs += delta * this.interval; + const tickTs = delta * this.subsTw.aggregation.interval; + if (this.subsTw.quickInterval) { + const currentDate = getCurrentTime(this.subsTw.timezone); + this.startTs = calculateIntervalStartTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; + this.endTs = calculateIntervalEndTime(this.subsTw.quickInterval, currentDate) + this.subsTw.tsOffset; + } else { + this.startTs += tickTs; + this.endTs += tickTs; + } this.data = this.updateData(); - this.elapsed = this.elapsed - delta * this.interval; + this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval; } } else { this.data = this.updateData(); @@ -223,7 +238,7 @@ export class DataAggregator { let keyData = this.dataBuffer[key]; aggKeyData.forEach((aggData, aggTimestamp) => { if (aggTimestamp <= this.startTs) { - if (this.stateData && + if (this.subsTw.aggregation.stateData && (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; } @@ -235,11 +250,11 @@ export class DataAggregator { } }); keyData.sort((set1, set2) => set1[0] - set2[0]); - if (this.stateData) { + if (this.subsTw.aggregation.stateData) { this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key])); } - if (keyData.length > this.limit) { - keyData = keyData.slice(keyData.length - this.limit); + if (keyData.length > this.subsTw.aggregation.limit) { + keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit); } this.dataBuffer[key] = keyData; } @@ -275,7 +290,7 @@ export class DataAggregator { } private processAggregatedData(data: SubscriptionData): AggregationMap { - const isCount = this.aggregationType === AggregationType.COUNT; + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; const aggregationMap: AggregationMap = {}; for (const key of Object.keys(data)) { let aggKeyData = aggregationMap[key]; @@ -300,7 +315,7 @@ export class DataAggregator { } private updateAggregatedData(data: SubscriptionData) { - const isCount = this.aggregationType === AggregationType.COUNT; + const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; for (const key of Object.keys(data)) { let aggKeyData = this.aggregationMap[key]; if (!aggKeyData) { @@ -312,7 +327,8 @@ export class DataAggregator { const timestamp = kvPair[0]; const value = this.convertValue(kvPair[1]); const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + - Math.floor((timestamp - this.startTs) / this.interval) * this.interval + this.interval / 2); + Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) * + this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2); let aggData = aggKeyData.get(aggTimestamp); if (!aggData) { aggData = { diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index dbc9bd2adb..7a26a5373d 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -15,7 +15,7 @@ /// import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; -import { AggregationType, SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models'; import { EntityData, EntityDataPageLink, @@ -74,6 +74,7 @@ export interface EntityDataSubscriptionOptions { keyFilters?: Array; additionalKeyFilters?: Array; subscriptionTimewindow?: SubscriptionTimewindow; + latestTsOffset?: number; } export class EntityDataSubscription { @@ -95,6 +96,7 @@ export class EntityDataSubscription { private entityDataResolveSubject: Subject; private pageData: PageData; private subsTw: SubscriptionTimewindow; + private latestTsOffset: number; private dataAggregators: Array; private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {}; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; @@ -177,6 +179,7 @@ export class EntityDataSubscription { this.started = true; this.dataResolved = true; this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && @@ -238,6 +241,11 @@ export class EntityDataSubscription { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { this.prepareSubscriptionCommands(this.dataCommand); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + this.subscriber.setTsOffset(this.latestTsOffset); + } } this.subscriber.subscriptionCommands.push(this.dataCommand); @@ -256,8 +264,8 @@ export class EntityDataSubscription { if (this.started) { const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && - !this.history && this.tsFields.length) { - const newSubsTw: SubscriptionTimewindow = this.listener.updateRealtimeSubscription(); + !this.history && this.tsFields.length) { + const newSubsTw = this.listener.updateRealtimeSubscription(); this.subsTw = newSubsTw; targetCommand.tsCmd.startTs = this.subsTw.startTs; targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; @@ -266,18 +274,25 @@ export class EntityDataSubscription { targetCommand.tsCmd.agg = this.subsTw.aggregation.type; targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; this.dataAggregators.forEach((dataAggregator) => { - dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval); + dataAggregator.reset(newSubsTw); }); } + this.subscriber.setTsOffset(this.subsTw.tsOffset); targetCommand.query = this.dataCommand.query; this.subscriber.subscriptionCommands = [targetCommand]; } else { this.subscriber.subscriptionCommands = [this.dataCommand]; } }); - this.subscriber.subscribe(); } else if (this.datasourceType === DatasourceType.function) { + let tsOffset = 0; + if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) { + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; + } + const entityData: EntityData = { entityId: { id: NULL_UUID, @@ -288,7 +303,7 @@ export class EntityDataSubscription { }; const name = DatasourceType.function; entityData.latest[EntityKeyType.ENTITY_FIELD] = { - name: {ts: Date.now(), value: name} + name: {ts: Date.now() + tsOffset, value: name} }; const pageData: PageData = { data: [entityData], @@ -298,7 +313,9 @@ export class EntityDataSubscription { }; this.onPageData(pageData); } else if (this.datasourceType === DatasourceType.entityCount) { + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.subscriber.setTsOffset(this.latestTsOffset); this.countCommand = new EntityCountCmd(); let keyFilters = this.entityDataSubscriptionOptions.keyFilters; if (this.entityDataSubscriptionOptions.additionalKeyFilters) { @@ -331,13 +348,13 @@ export class EntityDataSubscription { latest: { [EntityKeyType.ENTITY_FIELD]: { name: { - ts: Date.now(), + ts: Date.now() + this.latestTsOffset, value: DatasourceType.entityCount } }, [EntityKeyType.COUNT]: { [countKey.name]: { - ts: Date.now(), + ts: Date.now() + this.latestTsOffset, value: entityCountUpdate.count + '' } } @@ -358,7 +375,7 @@ export class EntityDataSubscription { latest: { [EntityKeyType.COUNT]: { [countKey.name]: { - ts: Date.now(), + ts: Date.now() + this.latestTsOffset, value: entityCountUpdate.count + '' } } @@ -383,6 +400,7 @@ export class EntityDataSubscription { return; } this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && @@ -394,10 +412,26 @@ export class EntityDataSubscription { this.subsCommand = new EntityDataCmd(); this.subsCommand.cmdId = this.dataCommand.cmdId; this.prepareSubscriptionCommands(this.subsCommand); - if (!this.subsCommand.isEmpty()) { + let latestTsOffsetChanged = false; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + latestTsOffsetChanged = this.subscriber.setTsOffset(this.latestTsOffset); + } + if (latestTsOffsetChanged) { + if (this.listener.initialPageDataChanged) { + this.listener.initialPageDataChanged(this.pageData); + } + } else if (!this.subsCommand.isEmpty()) { this.subscriber.subscriptionCommands = [this.subsCommand]; this.subscriber.update(); } + } else if (this.datasourceType === DatasourceType.entityCount) { + if (this.subscriber.setTsOffset(this.latestTsOffset)) { + if (this.listener.initialPageDataChanged) { + this.listener.initialPageDataChanged(this.pageData); + } + } } else if (this.datasourceType === DatasourceType.function) { this.startFunction(); } @@ -745,12 +779,7 @@ export class EntityDataSubscription { this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb); }, tsKeyNames, - subsTw.startTs, - subsTw.aggregation.limit, - subsTw.aggregation.type, - subsTw.aggregation.timeWindow, - subsTw.aggregation.interval, - subsTw.aggregation.stateData, + subsTw, this.utils, this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick ); @@ -786,7 +815,7 @@ export class EntityDataSubscription { } else { prevSeries = [0, 0]; } - const time = Date.now(); + const time = Date.now() + this.latestTsOffset; const value = dataKey.func(time, prevSeries[1]); const series: [number, any] = [time, value]; this.datasourceData[0][dataKey.key].data = [series]; @@ -827,7 +856,8 @@ export class EntityDataSubscription { startTime = dataKey.lastUpdateTime + this.frequency; endTime = dataKey.lastUpdateTime + deltaElapsed; } else { - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs; + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs + + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency; if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) { const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit; @@ -835,8 +865,14 @@ export class EntityDataSubscription { } } } else { - startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs; - endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs; + startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs + + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; + endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs + + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; + } + if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) { + const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; + endTime = Math.min(currentTime, endTime); } } generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime); diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index d9df28baba..7f45cee73f 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -32,6 +32,7 @@ import { Observable, of } from 'rxjs'; export interface EntityDataListener { subscriptionType: widgetType; subscriptionTimewindow?: SubscriptionTimewindow; + latestTsOffset?: number; configDatasource: Datasource; configDatasourceIndex: number; dataLoaded: (pageData: PageData, @@ -92,6 +93,8 @@ export class EntityDataService { if (listener.subscription) { if (listener.subscriptionType === widgetType.timeseries) { listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); + } else if (listener.subscriptionType === widgetType.latest) { + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; } listener.subscription.start(); } @@ -118,6 +121,8 @@ export class EntityDataService { listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); if (listener.subscriptionType === widgetType.timeseries) { listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); + } else if (listener.subscriptionType === widgetType.latest) { + listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; } return listener.subscription.subscribe(); } diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index ce36f57e10..ba79e8f09a 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -37,8 +37,12 @@ import { } from '@app/shared/models/widget.models'; import { HttpErrorResponse } from '@angular/common/http'; import { + calculateIntervalEndTime, + calculateIntervalStartTime, + calculateTsOffset, createSubscriptionTimewindow, createTimewindowForComparison, + getCurrentTime, SubscriptionTimewindow, Timewindow, toHistoryTimewindow, @@ -77,8 +81,10 @@ export class WidgetSubscription implements IWidgetSubscription { timeWindow: WidgetTimewindow; originalTimewindow: Timewindow; timeWindowConfig: Timewindow; + timezone: string; subscriptionTimewindow: SubscriptionTimewindow; useDashboardTimewindow: boolean; + tsOffset = 0; hasDataPageLink: boolean; singleEntity: boolean; @@ -211,6 +217,10 @@ export class WidgetSubscription implements IWidgetSubscription { this.timeWindow = {}; this.useDashboardTimewindow = options.useDashboardTimewindow; this.stateData = options.stateData; + if (this.type === widgetType.latest) { + this.timezone = options.dashboardTimewindow.timezone; + this.updateTsOffset(); + } if (this.useDashboardTimewindow) { this.timeWindowConfig = deepClone(options.dashboardTimewindow); } else { @@ -576,11 +586,16 @@ export class WidgetSubscription implements IWidgetSubscription { if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { this.timeWindowConfig = deepClone(newDashboardTimewindow); this.update(); - return true; + } + } + } else if (this.type === widgetType.latest) { + if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) { + this.timezone = newDashboardTimewindow.timezone; + if (this.updateTsOffset()) { + this.update(); } } } - return false; } updateDataVisibility(index: number): void { @@ -813,6 +828,7 @@ export class WidgetSubscription implements IWidgetSubscription { configDatasource: datasource, configDatasourceIndex: datasourceIndex, subscriptionTimewindow: this.subscriptionTimewindow, + latestTsOffset: this.tsOffset, dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => { this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true); }, @@ -837,9 +853,11 @@ export class WidgetSubscription implements IWidgetSubscription { if (this.alarmDataListener) { this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); } + if (this.timeWindowConfig) { this.updateRealtimeSubscription(); } + this.alarmDataListener = { subscriptionTimewindow: this.subscriptionTimewindow, alarmSource: this.alarmSource, @@ -878,25 +896,28 @@ export class WidgetSubscription implements IWidgetSubscription { } private dataSubscribe() { + this.updateDataTimewindow(); if (!this.hasDataPageLink) { - if (this.type === widgetType.timeseries && this.timeWindowConfig) { - this.updateDataTimewindow(); - if (this.subscriptionTimewindow.fixedWindow) { + if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) { this.onDataUpdated(); - } } const forceUpdate = !this.datasources.length; + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length; this.entityDataListeners.forEach((listener) => { if (this.comparisonEnabled && listener.configDatasource.isAdditional) { listener.subscriptionTimewindow = this.timewindowForComparison; } else { listener.subscriptionTimewindow = this.subscriptionTimewindow; + listener.latestTsOffset = this.tsOffset; } this.ctx.entityDataService.startSubscription(listener); }); if (forceUpdate) { this.onDataUpdated(); } + if (notifyDataLoaded) { + this.notifyDataLoaded(); + } } } @@ -1080,15 +1101,33 @@ export class WidgetSubscription implements IWidgetSubscription { private updateTimewindow() { this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000; + this.timeWindow.timezone = this.subscriptionTimewindow.timezone; if (this.subscriptionTimewindow.realtimeWindowMs) { - this.timeWindow.maxTime = moment().valueOf() + this.timeWindow.stDiff; - this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; + if (this.subscriptionTimewindow.quickInterval) { + const currentDate = getCurrentTime(this.subscriptionTimewindow.timezone); + this.timeWindow.maxTime = calculateIntervalEndTime( + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset; + this.timeWindow.minTime = calculateIntervalStartTime( + this.subscriptionTimewindow.quickInterval, currentDate) + this.subscriptionTimewindow.tsOffset; + } else { + this.timeWindow.maxTime = moment().valueOf() + this.subscriptionTimewindow.tsOffset + this.timeWindow.stDiff; + this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs; + } } else if (this.subscriptionTimewindow.fixedWindow) { - this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs; - this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs; + this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs + this.subscriptionTimewindow.tsOffset; + this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs + this.subscriptionTimewindow.tsOffset; } } + private updateTsOffset(): boolean { + const newOffset = calculateTsOffset(this.timezone); + if (this.tsOffset !== newOffset) { + this.tsOffset = newOffset; + return true; + } + return false; + } + private updateRealtimeSubscription(subscriptionTimewindow?: SubscriptionTimewindow): SubscriptionTimewindow { if (subscriptionTimewindow) { this.subscriptionTimewindow = subscriptionTimewindow; @@ -1103,12 +1142,13 @@ export class WidgetSubscription implements IWidgetSubscription { private updateComparisonTimewindow() { this.comparisonTimeWindow.interval = this.timewindowForComparison.aggregation.interval || 1000; + this.comparisonTimeWindow.timezone = this.timewindowForComparison.timezone; if (this.timewindowForComparison.realtimeWindowMs) { this.comparisonTimeWindow.maxTime = moment(this.timeWindow.maxTime).subtract(1, this.timeForComparison).valueOf(); - this.comparisonTimeWindow.minTime = this.comparisonTimeWindow.maxTime - this.timewindowForComparison.realtimeWindowMs; + this.comparisonTimeWindow.minTime = moment(this.timeWindow.minTime).subtract(1, this.timeForComparison).valueOf(); } else if (this.timewindowForComparison.fixedWindow) { - this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs; - this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs; + this.comparisonTimeWindow.maxTime = this.timewindowForComparison.fixedWindow.endTimeMs + this.timewindowForComparison.tsOffset; + this.comparisonTimeWindow.minTime = this.timewindowForComparison.fixedWindow.startTimeMs + this.timewindowForComparison.tsOffset; } } @@ -1335,7 +1375,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.onDataUpdated(); } - private alarmsUpdated(_updated: Array, alarms: PageData) { + private alarmsUpdated(updated: Array, alarms: PageData) { this.alarmsLoaded(alarms, 0, 0); } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 1cc501a0d1..762b7149a2 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -91,6 +91,7 @@ direction="left" tooltipPosition="below" aggregation="true" + timezone="true" [(ngModel)]="dashboardCtx.dashboardTimewindow"> diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index da735c1567..c1dd69ecf5 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -55,7 +55,13 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; +import { + calculateIntervalEndTime, + calculateIntervalStartTime, + getCurrentTime, + HistoryWindowType, + Timewindow +} from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; @@ -296,6 +302,10 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn const currentTime = Date.now(); timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; timePageLink.endTime = currentTime; + } else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) { + const currentDate = getCurrentTime(); + timePageLink.startTime = calculateIntervalStartTime(this.timewindow.history.quickInterval, currentDate); + timePageLink.endTime = calculateIntervalEndTime(this.timewindow.history.quickInterval, currentDate); } else { timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts index 5c9a1cd0aa..d5c9f02876 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts @@ -94,11 +94,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator, items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) }); this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { - getDefaultTimezone().subscribe((defaultTimezone) => { - this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); - this.updateValidators(type, true); - this.alarmScheduleForm.updateValueAndValidity(); - }); + const defaultTimezone = getDefaultTimezone(); + this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); + this.updateValidators(type, true); + this.alarmScheduleForm.updateValueAndValidity(); }); this.alarmScheduleForm.valueChanges.subscribe(() => { this.updateModel(); diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html new file mode 100644 index 0000000000..c9f367c574 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.html @@ -0,0 +1,27 @@ + +
+ + timewindow.interval + + + {{ timeIntervalTranslationMap.get(interval) | translate}} + + + +
diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss new file mode 100644 index 0000000000..97c400525e --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss @@ -0,0 +1,19 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + min-width: 364px; +} diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts new file mode 100644 index 0000000000..3eab8c428d --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2021 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-quick-time-interval', + templateUrl: './quick-time-interval.component.html', + styleUrls: ['./quick-time-interval.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => QuickTimeIntervalComponent), + multi: true + } + ] +}) +export class QuickTimeIntervalComponent implements OnInit, ControlValueAccessor { + + private allIntervals = Object.values(QuickTimeInterval); + + modelValue: QuickTimeInterval; + timeIntervalTranslationMap = QuickTimeIntervalTranslationMap; + + rendered = false; + + @Input() disabled: boolean; + + @Input() onlyCurrentInterval = false; + + private propagateChange = (_: any) => {}; + + constructor() { + } + + get intervals() { + if (this.onlyCurrentInterval) { + return this.allIntervals.filter(interval => interval.startsWith('CURRENT_')); + } + return this.allIntervals; + } + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(interval: QuickTimeInterval): void { + this.modelValue = interval; + } + + onIntervalChange() { + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index e4ddaa0f56..57c596bbc2 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -21,16 +21,43 @@ -
- -
+
+
+ + +
+
+
+ + +
+ timewindow.last + +
+
+ +
+ timewindow.interval + +
+
+
+
+
+
@@ -65,6 +92,17 @@ style="padding-top: 8px;">
+ +
+ timewindow.interval + +
+
@@ -95,7 +133,7 @@ -
+
aggregation.limit @@ -139,6 +177,17 @@ predefinedName="aggregation.group-interval">
+
+
+ + +
+ + +