Browse Source

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

pull/4269/head
Andrii Shvaika 5 years ago
parent
commit
43578c7e58
  1. 10
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  2. 19
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  3. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  4. 6
      ui-ngx/src/app/core/api/alarm-data-subscription.ts
  5. 74
      ui-ngx/src/app/core/api/data-aggregator.ts
  6. 76
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  7. 5
      ui-ngx/src/app/core/api/entity-data.service.ts
  8. 68
      ui-ngx/src/app/core/api/widget-subscription.ts
  9. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  10. 1
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  11. 12
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  12. 9
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  13. 27
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.html
  14. 19
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss
  15. 79
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  16. 71
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  17. 76
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  18. 2
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  19. 4
      ui-ngx/src/app/shared/components/time/timewindow.component.scss
  20. 37
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  21. 104
      ui-ngx/src/app/shared/components/time/timezone-select.component.ts
  22. 101
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  23. 391
      ui-ngx/src/app/shared/models/time/time.models.ts
  24. 3
      ui-ngx/src/app/shared/shared.module.ts
  25. 25
      ui-ngx/src/assets/locale/locale.constant-en_US.json

10
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<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> 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<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
for (String serviceId : tbCoreServices) {

19
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -76,12 +76,17 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
@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<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId,
@Param("entityId") UUID entityId,
@Param("status") Set<AlarmStatus> status);
@Param("affectedEntityId") UUID affectedEntityId,
@Param("affectedEntityType") String affectedEntityType,
@Param("alarmStatuses") Set<AlarmStatus> alarmStatuses);
}

4
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -123,7 +123,7 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
}
@Override
public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status) {
return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status);
public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> statuses) {
return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses);
}
}

6
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<AlarmData> = {
data: [{...simulatedAlarm, entityId: '1', latest: {}}],
data: [{...alarm, entityId: '1', latest: {}}],
hasNext: false,
totalElements: 1,
totalPages: 1

74
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 = {

76
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<KeyFilter>;
additionalKeyFilters?: Array<KeyFilter>;
subscriptionTimewindow?: SubscriptionTimewindow;
latestTsOffset?: number;
}
export class EntityDataSubscription {
@ -95,6 +96,7 @@ export class EntityDataSubscription {
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
private subsTw: SubscriptionTimewindow;
private latestTsOffset: number;
private dataAggregators: Array<DataAggregator>;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | 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<EntityData> = {
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);

5
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<EntityData>,
@ -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();
}

68
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<AlarmData>, alarms: PageData<AlarmData>) {
private alarmsUpdated(updated: Array<AlarmData>, alarms: PageData<AlarmData>) {
this.alarmsLoaded(alarms, 0, 0);
}

1
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">
</tb-timewindow>
<tb-filters-edit [fxShow]="!isEdit && displayFilters()"

1
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -95,6 +95,7 @@
<tb-timewindow *ngIf="widget.hasTimewindow"
#timewindowComponent
aggregation="{{widget.hasAggregation}}"
timezone="true"
[isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">

12
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;

9
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();

27
ui-ngx/src/app/shared/components/time/quick-time-interval.component.html

@ -0,0 +1,27 @@
<!--
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.
-->
<section class="interval-section" fxLayout="row" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>timewindow.interval</mat-label>
<mat-select [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onIntervalChange()">
<mat-option *ngFor="let interval of intervals" [value]="interval">
{{ timeIntervalTranslationMap.get(interval) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</section>

19
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;
}

79
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);
}
}

71
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -21,16 +21,43 @@
<mat-tab-group dynamicHeight [ngClass]="{'tb-headless': historyOnly}"
(selectedIndexChange)="timewindowForm.markAsDirty()" [(selectedIndex)]="timewindow.selectedTab">
<mat-tab label="{{ 'timewindow.realtime' | translate }}">
<div formGroupName="realtime" class="mat-content mat-padding" fxLayout="column">
<tb-timeinterval
[(hideFlag)]="timewindow.hideInterval"
(hideFlagChange)="onHideIntervalChanged()"
[isEdit]="isEdit"
formControlName="timewindowMs"
predefinedName="timewindow.last"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME"
style="padding-top: 8px;"></tb-timeinterval>
</div>
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" style="padding-top: 8px; padding-left: 16px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideInterval"
(ngModelChange)="onHideIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="realtimeType">
<mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
</div>
</section>
</section>
</mat-tab>
<mat-tab label="{{ 'timewindow.history' | translate }}">
<section fxLayout="row">
@ -65,6 +92,17 @@
style="padding-top: 8px;"></tb-datetime-period>
</section>
</mat-radio-button>
<mat-radio-button [value]="historyTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval>
</section>
</mat-radio-button>
</mat-radio-group>
</div>
</section>
@ -95,7 +133,7 @@
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideAggInterval"
(ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column" [fxShow]="isEdit || !timewindow.hideAggInterval">
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
<div class="limit-slider-container"
fxLayout="row" fxLayoutAlign="start center">
<span translate>aggregation.limit</span>
@ -139,6 +177,17 @@
predefinedName="aggregation.group-interval">
</tb-timeinterval>
</div>
<div *ngIf="timezone" class="mat-content mat-padding" fxLayout="row">
<section fxLayout="column" [fxShow]="isEdit">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideTimezone"
(ngModelChange)="onHideTimezoneChanged()"></mat-checkbox>
</section>
<tb-timezone-select fxFlex [fxShow]="isEdit || !timewindow.hideTimezone"
localBrowserTimezonePlaceholderOnEmpty="true"
formControlName="timezone">
</tb-timezone-select>
</div>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button

76
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -20,6 +20,8 @@ import {
AggregationType,
DAY,
HistoryWindowType,
quickTimeIntervalPeriod,
RealtimeWindowType,
Timewindow,
TimewindowType
} from '@shared/models/time/time.models';
@ -36,6 +38,7 @@ export interface TimewindowPanelData {
historyOnly: boolean;
timewindow: Timewindow;
aggregation: boolean;
timezone: boolean;
isEdit: boolean;
}
@ -50,6 +53,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
aggregation = false;
timezone = false;
isEdit = false;
timewindow: Timewindow;
@ -60,6 +65,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
historyTypes = HistoryWindowType;
realtimeTypes = RealtimeWindowType;
timewindowTypes = TimewindowType;
aggregationTypes = AggregationType;
@ -78,6 +85,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.historyOnly = data.historyOnly;
this.timewindow = data.timewindow;
this.aggregation = data.aggregation;
this.timezone = data.timezone;
this.isEdit = data.isEdit;
}
@ -85,10 +93,16 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const hideInterval = this.timewindow.hideInterval || false;
const hideAggregation = this.timewindow.hideAggregation || false;
const hideAggInterval = this.timewindow.hideAggInterval || false;
const hideTimezone = this.timewindow.hideTimezone || false;
this.timewindowForm = this.fb.group({
realtime: this.fb.group(
{
realtimeType: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.realtimeType !== 'undefined'
? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: hideInterval
}),
timewindowMs: [
this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
? this.timewindow.realtime.timewindowMs : null
@ -96,7 +110,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
interval: [
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
? this.timewindow.realtime.interval : null
]
],
quickInterval: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined'
? this.timewindow.realtime.quickInterval : null,
disabled: hideInterval
})
}
),
history: this.fb.group(
@ -119,6 +138,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
value: this.timewindow.history && typeof this.timewindow.history.fixedTimewindow !== 'undefined'
? this.timewindow.history.fixedTimewindow : null,
disabled: hideInterval
}),
quickInterval: this.fb.control({
value: this.timewindow.history && typeof this.timewindow.history.quickInterval !== 'undefined'
? this.timewindow.history.quickInterval : null,
disabled: hideInterval
})
}
),
@ -135,21 +159,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
disabled: hideAggInterval
}, [Validators.min(this.minDatapointsLimit()), Validators.max(this.maxDatapointsLimit())])
}
)
),
timezone: this.fb.control({
value: this.timewindow.timezone !== 'undefined'
? this.timewindow.timezone : null,
disabled: hideTimezone
})
});
}
update() {
const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow.realtime = {
realtimeType: timewindowFormValue.realtime.realtimeType,
timewindowMs: timewindowFormValue.realtime.timewindowMs,
quickInterval: timewindowFormValue.realtime.quickInterval,
interval: timewindowFormValue.realtime.interval
};
this.timewindow.history = {
historyType: timewindowFormValue.history.historyType,
timewindowMs: timewindowFormValue.history.timewindowMs,
interval: timewindowFormValue.history.interval,
fixedTimewindow: timewindowFormValue.history.fixedTimewindow
fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
quickInterval: timewindowFormValue.history.quickInterval,
};
if (this.aggregation) {
this.timewindow.aggregation = {
@ -157,6 +189,9 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
limit: timewindowFormValue.aggregation.limit
};
}
if (this.timezone) {
this.timewindow.timezone = timewindowFormValue.timezone;
}
this.result = this.timewindow;
this.overlayRef.dispose();
}
@ -174,11 +209,23 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
}
minRealtimeAggInterval() {
return this.timeService.minIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value);
return this.timeService.minIntervalLimit(this.currentRealtimeTimewindow());
}
maxRealtimeAggInterval() {
return this.timeService.maxIntervalLimit(this.timewindowForm.get('realtime.timewindowMs').value);
return this.timeService.maxIntervalLimit(this.currentRealtimeTimewindow());
}
currentRealtimeTimewindow(): number {
const timeWindowFormValue = this.timewindowForm.getRawValue();
switch (timeWindowFormValue.realtime.realtimeType) {
case RealtimeWindowType.LAST_INTERVAL:
return timeWindowFormValue.realtime.timewindowMs;
case RealtimeWindowType.INTERVAL:
return quickTimeIntervalPeriod(timeWindowFormValue.realtime.quickInterval);
default:
return DAY;
}
}
minHistoryAggInterval() {
@ -193,6 +240,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
const timewindowFormValue = this.timewindowForm.getRawValue();
if (timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
return timewindowFormValue.history.timewindowMs;
} else if (timewindowFormValue.history.historyType === HistoryWindowType.INTERVAL) {
return quickTimeIntervalPeriod(timewindowFormValue.history.quickInterval);
} else if (timewindowFormValue.history.fixedTimewindow) {
return timewindowFormValue.history.fixedTimewindow.endTimeMs -
timewindowFormValue.history.fixedTimewindow.startTimeMs;
@ -206,10 +255,18 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.timewindowForm.get('history.historyType').disable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').disable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').disable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').disable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').disable({emitEvent: false});
this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false});
this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false});
} else {
this.timewindowForm.get('history.historyType').enable({emitEvent: false});
this.timewindowForm.get('history.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').enable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false});
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
@ -232,4 +289,13 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.timewindowForm.markAsDirty();
}
onHideTimezoneChanged() {
if (this.timewindow.hideTimezone) {
this.timewindowForm.get('timezone').disable({emitEvent: false});
} else {
this.timewindowForm.get('timezone').enable({emitEvent: false});
}
this.timewindowForm.markAsDirty();
}
}

2
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -34,7 +34,7 @@
(click)="openEditMode()"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{innerValue?.displayValue}}
{{innerValue?.displayValue}} <span [fxShow]="innerValue?.displayTimezoneAbbr !== ''">| <span class="timezone-abbr">{{innerValue.displayTimezoneAbbr}}</span></span>
</span>
<button *ngIf="direction === 'right'" [disabled]="timewindowDisabled" mat-icon-button class="tb-mat-32"
type="button"

4
ui-ngx/src/app/shared/components/time/timewindow.component.scss

@ -27,5 +27,9 @@
pointer-events: all;
cursor: pointer;
}
.timezone-abbr {
font-weight: 500;
}
}
}

37
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -31,8 +31,11 @@ import { TranslateService } from '@ngx-translate/core';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
import {
cloneSelectedTimewindow,
getTimezoneInfo,
HistoryWindowType,
initModelFromDefaultTimewindow,
QuickTimeIntervalTranslationMap,
RealtimeWindowType,
Timewindow,
TimewindowType
} from '@shared/models/time/time.models';
@ -49,7 +52,7 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import { WINDOW } from '@core/services/window.service';
import { TimeService } from '@core/services/time.service';
import { TooltipPosition } from '@angular/material/tooltip';
import { deepClone } from '@core/utils';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
// @dynamic
@ -89,6 +92,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
return this.aggregationValue;
}
timezoneValue = false;
@Input()
set timezone(val) {
this.timezoneValue = coerceBooleanProperty(val);
}
get timezone() {
return this.timezoneValue;
}
isToolbarValue = false;
@Input()
@ -169,7 +183,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
});
if (isGtXs) {
config.minWidth = '417px';
config.maxHeight = '440px';
config.maxHeight = '500px';
const panelHeight = 375;
const panelWidth = 417;
const el = this.timewindowPanelOrigin.elementRef.nativeElement;
@ -225,6 +239,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
timewindow: deepClone(this.innerValue),
historyOnly: this.historyOnly,
aggregation: this.aggregation,
timezone: this.timezone,
isEdit: this.isEdit
}
);
@ -272,20 +287,32 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
updateDisplayValue() {
if (this.innerValue.selectedTab === TimewindowType.REALTIME && !this.historyOnly) {
this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ' +
this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
this.innerValue.displayValue = this.translate.instant('timewindow.realtime') + ' - ';
if (this.innerValue.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.realtime.quickInterval));
} else {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
}
} else {
this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);
} else if (this.innerValue.history.historyType === HistoryWindowType.INTERVAL) {
this.innerValue.displayValue += this.translate.instant(QuickTimeIntervalTranslationMap.get(this.innerValue.history.quickInterval));
} else {
const startString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.startTimeMs, 'yyyy-MM-dd HH:mm:ss');
const endString = this.datePipe.transform(this.innerValue.history.fixedTimewindow.endTimeMs, 'yyyy-MM-dd HH:mm:ss');
this.innerValue.displayValue += this.translate.instant('timewindow.period', {startTime: startString, endTime: endString});
}
}
if (isDefinedAndNotNull(this.innerValue.timezone) && this.innerValue.timezone !== '') {
this.innerValue.displayValue += ' ';
this.innerValue.displayTimezoneAbbr = getTimezoneInfo(this.innerValue.timezone).abbr;
} else {
this.innerValue.displayTimezoneAbbr = '';
}
}
hideLabel() {

104
ui-ngx/src/app/shared/components/time/timezone-select.component.ts

@ -16,14 +16,15 @@
import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
import { getDefaultTimezoneInfo, getTimezoneInfo, getTimezones, TimezoneInfo } from '@shared/models/time/time.models';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-timezone-select',
@ -43,10 +44,6 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
defaultTimezoneId: string = null;
timezones$ = getTimezones().pipe(
share()
);
@Input()
set defaultTimezone(timezone: string) {
if (this.defaultTimezoneId !== timezone) {
@ -72,6 +69,15 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
this.userTimezoneByDefaultValue = coerceBooleanProperty(value);
}
private localBrowserTimezonePlaceholderOnEmptyValue: boolean;
get localBrowserTimezonePlaceholderOnEmpty(): boolean {
return this.localBrowserTimezonePlaceholderOnEmptyValue;
}
@Input()
set localBrowserTimezonePlaceholderOnEmpty(value: boolean) {
this.localBrowserTimezonePlaceholderOnEmptyValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ -85,6 +91,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
private dirty = false;
private localBrowserTimezoneInfoPlaceholder: TimezoneInfo;
private timezones: Array<TimezoneInfo>;
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
@ -138,23 +148,24 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
writeValue(value: string | null): void {
this.searchText = '';
getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe(
(foundTimezone) => {
if (foundTimezone !== null) {
this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
if (foundTimezone.id !== value) {
setTimeout(() => {
this.updateView(foundTimezone.id);
}, 0);
} else {
this.modelValue = value;
}
} else {
this.modelValue = null;
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
}
const foundTimezone = getTimezoneInfo(value, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
if (foundTimezone !== null) {
this.selectTimezoneFormGroup.get('timezone').patchValue(foundTimezone, {emitEvent: false});
if (foundTimezone.id !== value) {
setTimeout(() => {
this.updateView(foundTimezone.id);
}, 0);
} else {
this.modelValue = value;
}
} else {
this.modelValue = null;
if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
this.selectTimezoneFormGroup.get('timezone').patchValue(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: false});
} else {
this.selectTimezoneFormGroup.get('timezone').patchValue('', {emitEvent: false});
}
);
}
this.dirty = true;
}
@ -169,15 +180,19 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
if (this.ignoreClosePanel) {
this.ignoreClosePanel = false;
} else {
if (!this.modelValue && (this.defaultTimezoneId || this.userTimezoneByDefaultValue)) {
getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue).subscribe(
(defaultTimezoneInfo) => {
if (defaultTimezoneInfo !== null) {
this.ngZone.run(() => {
this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
});
}
});
if (!this.modelValue) {
if (this.defaultTimezoneId || this.userTimezoneByDefaultValue) {
const defaultTimezoneInfo = getTimezoneInfo(this.defaultTimezoneId, this.defaultTimezoneId, this.userTimezoneByDefaultValue);
if (defaultTimezoneInfo !== null) {
this.ngZone.run(() => {
this.selectTimezoneFormGroup.get('timezone').reset(defaultTimezoneInfo, {emitEvent: true});
});
}
} else if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
this.ngZone.run(() => {
this.selectTimezoneFormGroup.get('timezone').reset(this.getLocalBrowserTimezoneInfoPlaceholder(), {emitEvent: true});
});
}
}
}
}
@ -196,12 +211,10 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
fetchTimezones(searchText?: string): Observable<Array<TimezoneInfo>> {
this.searchText = searchText;
if (searchText && searchText.length) {
return getTimezones().pipe(
map((timezones) => timezones.filter((timezoneInfo) =>
timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())))
);
return of(this.loadTimezones().filter((timezoneInfo) =>
timezoneInfo.name.toLowerCase().includes(searchText.toLowerCase())));
}
return getTimezones();
return of(this.loadTimezones());
}
clear() {
@ -211,4 +224,23 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
}, 0);
}
private loadTimezones(): Array<TimezoneInfo> {
if (!this.timezones) {
this.timezones = [];
if (this.localBrowserTimezonePlaceholderOnEmptyValue) {
this.timezones.push(this.getLocalBrowserTimezoneInfoPlaceholder());
}
this.timezones.push(...getTimezones());
}
return this.timezones;
}
private getLocalBrowserTimezoneInfoPlaceholder(): TimezoneInfo {
if (!this.localBrowserTimezoneInfoPlaceholder) {
this.localBrowserTimezoneInfoPlaceholder = deepClone(getDefaultTimezoneInfo());
this.localBrowserTimezoneInfoPlaceholder.id = null;
this.localBrowserTimezoneInfoPlaceholder.name = this.translate.instant('timezone.browser-time');
}
return this.localBrowserTimezoneInfoPlaceholder;
}
}

101
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -30,6 +30,9 @@ import {
TsValue
} from '@shared/models/query/query.models';
import { PageData } from '@shared/models/page/page-data';
import { alarmFields } from '@shared/models/alarm.models';
import { entityFields } from '@shared/models/entity.models';
import { isUndefined } from '@core/utils';
export enum DataKeyType {
timeseries = 'timeseries',
@ -430,6 +433,44 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
constructor(msg: EntityDataUpdateMsg) {
super(msg);
}
public prepareData(tsOffset: number) {
if (this.data) {
this.processEntityData(this.data.data, tsOffset);
}
if (this.update) {
this.processEntityData(this.update, tsOffset);
}
}
private processEntityData(data: Array<EntityData>, tsOffset: number) {
for (const entityData of data) {
if (entityData.timeseries) {
for (const key of Object.keys(entityData.timeseries)) {
const tsValues = entityData.timeseries[key];
for (const tsValue of tsValues) {
if (tsValue.ts) {
tsValue.ts += tsOffset;
}
}
}
}
if (entityData.latest) {
for (const entityKeyType of Object.keys(entityData.latest)) {
const keyTypeValues = entityData.latest[entityKeyType];
for (const key of Object.keys(keyTypeValues)) {
const tsValue = keyTypeValues[key];
if (tsValue.ts) {
tsValue.ts += tsOffset;
}
if (key === entityFields.createdTime.keyName && tsValue.value) {
tsValue.value = (Number(tsValue.value) + tsOffset) + '';
}
}
}
}
}
}
}
export class AlarmDataUpdate extends DataUpdate<AlarmData> {
@ -441,6 +482,48 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
this.allowedEntities = msg.allowedEntities;
this.totalEntities = msg.totalEntities;
}
public prepareData(tsOffset: number) {
if (this.data) {
this.processAlarmData(this.data.data, tsOffset);
}
if (this.update) {
this.processAlarmData(this.update, tsOffset);
}
}
private processAlarmData(data: Array<AlarmData>, tsOffset: number) {
for (const alarmData of data) {
alarmData.createdTime += tsOffset;
if (alarmData.ackTs) {
alarmData.ackTs += tsOffset;
}
if (alarmData.clearTs) {
alarmData.clearTs += tsOffset;
}
if (alarmData.endTs) {
alarmData.endTs += tsOffset;
}
if (alarmData.latest) {
for (const entityKeyType of Object.keys(alarmData.latest)) {
const keyTypeValues = alarmData.latest[entityKeyType];
for (const key of Object.keys(keyTypeValues)) {
const tsValue = keyTypeValues[key];
if (tsValue.ts) {
tsValue.ts += tsOffset;
}
if (key in [entityFields.createdTime.keyName,
alarmFields.startTime.keyName,
alarmFields.endTime.keyName,
alarmFields.ackTime.keyName,
alarmFields.clearTime.keyName] && tsValue.value) {
tsValue.value = (Number(tsValue.value) + tsOffset) + '';
}
}
}
}
}
}
}
export class EntityCountUpdate extends CmdUpdate {
@ -468,6 +551,8 @@ export class TelemetrySubscriber {
private zone: NgZone;
private tsOffset = undefined;
public subscriptionCommands: Array<WebsocketCmd>;
public data$ = this.dataSubject.asObservable();
@ -522,6 +607,16 @@ export class TelemetrySubscriber {
this.reconnectSubject.complete();
}
public setTsOffset(tsOffset: number): boolean {
if (this.tsOffset !== tsOffset) {
const changed = !isUndefined(this.tsOffset);
this.tsOffset = tsOffset;
return changed;
} else {
return false;
}
}
public onData(message: SubscriptionUpdate) {
const cmdId = message.subscriptionId;
let keys: string[];
@ -545,6 +640,9 @@ export class TelemetrySubscriber {
}
public onEntityData(message: EntityDataUpdate) {
if (this.tsOffset) {
message.prepareData(this.tsOffset);
}
if (this.zone) {
this.zone.run(
() => {
@ -557,6 +655,9 @@ export class TelemetrySubscriber {
}
public onAlarmData(message: AlarmDataUpdate) {
if (this.tsOffset) {
message.prepareData(this.tsOffset);
}
if (this.zone) {
this.zone.run(
() => {

391
ui-ngx/src/app/shared/models/time/time.models.ts

@ -17,9 +17,7 @@
import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined, isUndefined } from '@app/core/utils';
import * as moment_ from 'moment';
import { Observable } from 'rxjs/internal/Observable';
import { from, of } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import * as monentTz from 'moment-timezone';
const moment = moment_;
@ -27,6 +25,7 @@ export const SECOND = 1000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
export const YEAR = DAY * 365;
export enum TimewindowType {
@ -34,14 +33,25 @@ export enum TimewindowType {
HISTORY
}
export enum RealtimeWindowType {
LAST_INTERVAL,
INTERVAL
}
export enum HistoryWindowType {
LAST_INTERVAL,
FIXED
FIXED,
INTERVAL
}
export interface IntervalWindow {
interval?: number;
timewindowMs?: number;
quickInterval?: QuickTimeInterval;
}
export interface RealtimeWindow extends IntervalWindow{
realtimeType?: RealtimeWindowType;
}
export interface FixedWindow {
@ -82,13 +92,16 @@ export interface Aggregation {
export interface Timewindow {
displayValue?: string;
displayTimezoneAbbr?: string;
hideInterval?: boolean;
hideAggregation?: boolean;
hideAggInterval?: boolean;
hideTimezone?: boolean;
selectedTab?: TimewindowType;
realtime?: IntervalWindow;
realtime?: RealtimeWindow;
history?: HistoryWindow;
aggregation?: Aggregation;
timezone?: string;
}
export interface SubscriptionAggregation extends Aggregation {
@ -99,6 +112,9 @@ export interface SubscriptionAggregation extends Aggregation {
export interface SubscriptionTimewindow {
startTs?: number;
quickInterval?: QuickTimeInterval;
timezone?: string;
tsOffset?: number;
realtimeWindowMs?: number;
fixedWindow?: FixedWindow;
aggregation?: SubscriptionAggregation;
@ -108,9 +124,46 @@ export interface WidgetTimewindow {
minTime?: number;
maxTime?: number;
interval?: number;
timezone?: string;
stDiff?: number;
}
export enum QuickTimeInterval {
YESTERDAY = 'YESTERDAY',
DAY_BEFORE_YESTERDAY = 'DAY_BEFORE_YESTERDAY',
THIS_DAY_LAST_WEEK = 'THIS_DAY_LAST_WEEK',
PREVIOUS_WEEK = 'PREVIOUS_WEEK',
PREVIOUS_MONTH = 'PREVIOUS_MONTH',
PREVIOUS_YEAR = 'PREVIOUS_YEAR',
CURRENT_HOUR = 'CURRENT_HOUR',
CURRENT_DAY = 'CURRENT_DAY',
CURRENT_DAY_SO_FAR = 'CURRENT_DAY_SO_FAR',
CURRENT_WEEK = 'CURRENT_WEEK',
CURRENT_WEEK_SO_FAR = 'CURRENT_WEEK_SO_WAR',
CURRENT_MONTH = 'CURRENT_MONTH',
CURRENT_MONTH_SO_FAR = 'CURRENT_MONTH_SO_FAR',
CURRENT_YEAR = 'CURRENT_YEAR',
CURRENT_YEAR_SO_FAR = 'CURRENT_YEAR_SO_FAR'
}
export const QuickTimeIntervalTranslationMap = new Map<QuickTimeInterval, string>([
[QuickTimeInterval.YESTERDAY, 'timeinterval.predefined.yesterday'],
[QuickTimeInterval.DAY_BEFORE_YESTERDAY, 'timeinterval.predefined.day-before-yesterday'],
[QuickTimeInterval.THIS_DAY_LAST_WEEK, 'timeinterval.predefined.this-day-last-week'],
[QuickTimeInterval.PREVIOUS_WEEK, 'timeinterval.predefined.previous-week'],
[QuickTimeInterval.PREVIOUS_MONTH, 'timeinterval.predefined.previous-month'],
[QuickTimeInterval.PREVIOUS_YEAR, 'timeinterval.predefined.previous-year'],
[QuickTimeInterval.CURRENT_HOUR, 'timeinterval.predefined.current-hour'],
[QuickTimeInterval.CURRENT_DAY, 'timeinterval.predefined.current-day'],
[QuickTimeInterval.CURRENT_DAY_SO_FAR, 'timeinterval.predefined.current-day-so-far'],
[QuickTimeInterval.CURRENT_WEEK, 'timeinterval.predefined.current-week'],
[QuickTimeInterval.CURRENT_WEEK_SO_FAR, 'timeinterval.predefined.current-week-so-far'],
[QuickTimeInterval.CURRENT_MONTH, 'timeinterval.predefined.current-month'],
[QuickTimeInterval.CURRENT_MONTH_SO_FAR, 'timeinterval.predefined.current-month-so-far'],
[QuickTimeInterval.CURRENT_YEAR, 'timeinterval.predefined.current-year'],
[QuickTimeInterval.CURRENT_YEAR_SO_FAR, 'timeinterval.predefined.current-year-so-far']
]);
export function historyInterval(timewindowMs: number): Timewindow {
const timewindow: Timewindow = {
selectedTab: TimewindowType.HISTORY,
@ -129,10 +182,13 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
hideInterval: false,
hideAggregation: false,
hideAggInterval: false,
hideTimezone: false,
selectedTab: TimewindowType.REALTIME,
realtime: {
realtimeType: RealtimeWindowType.LAST_INTERVAL,
interval: SECOND,
timewindowMs: MINUTE
timewindowMs: MINUTE,
quickInterval: QuickTimeInterval.CURRENT_DAY
},
history: {
historyType: HistoryWindowType.LAST_INTERVAL,
@ -141,7 +197,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
fixedTimewindow: {
startTimeMs: currentTime - DAY,
endTimeMs: currentTime
}
},
quickInterval: QuickTimeInterval.CURRENT_DAY
},
aggregation: {
type: AggregationType.AVG,
@ -157,6 +214,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
model.hideInterval = value.hideInterval;
model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone;
if (isUndefined(value.selectedTab)) {
if (value.realtime) {
model.selectedTab = TimewindowType.REALTIME;
@ -170,7 +228,20 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
if (isDefined(value.realtime.interval)) {
model.realtime.interval = value.realtime.interval;
}
model.realtime.timewindowMs = value.realtime.timewindowMs;
if (isUndefined(value.realtime.realtimeType)) {
if (isDefined(value.realtime.quickInterval)) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
} else {
model.realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL;
}
} else {
model.realtime.realtimeType = value.realtime.realtimeType;
}
if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
model.realtime.quickInterval = value.realtime.quickInterval;
} else {
model.realtime.timewindowMs = value.realtime.timewindowMs;
}
} else {
if (isDefined(value.history.interval)) {
model.history.interval = value.history.interval;
@ -178,6 +249,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
if (isUndefined(value.history.historyType)) {
if (isDefined(value.history.timewindowMs)) {
model.history.historyType = HistoryWindowType.LAST_INTERVAL;
} else if (isDefined(value.history.quickInterval)) {
model.history.historyType = HistoryWindowType.INTERVAL;
} else {
model.history.historyType = HistoryWindowType.FIXED;
}
@ -186,6 +259,8 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
}
if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) {
model.history.timewindowMs = value.history.timewindowMs;
} else if (model.history.historyType === HistoryWindowType.INTERVAL) {
model.history.quickInterval = value.history.quickInterval;
} else {
model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs;
model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs;
@ -197,6 +272,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
}
model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
}
model.timezone = value.timezone;
}
return model;
}
@ -223,6 +299,7 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
hideInterval: timewindow.hideInterval || false,
hideAggregation: timewindow.hideAggregation || false,
hideAggInterval: timewindow.hideAggInterval || false,
hideTimezone: timewindow.hideTimezone || false,
selectedTab: TimewindowType.HISTORY,
history: {
historyType: HistoryWindowType.FIXED,
@ -235,11 +312,22 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
aggregation: {
type: aggType,
limit
}
},
timezone: timewindow.timezone
};
return historyTimewindow;
}
export function calculateTsOffset(timezone?: string): number {
if (timezone) {
const tz = getTimezone(timezone);
const localOffset = moment().utcOffset();
return (tz.utcOffset() - localOffset) * 60 * 1000;
} else {
return 0;
}
}
export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: number, stateData: boolean,
timeService: TimeService): SubscriptionTimewindow {
const subscriptionTimewindow: SubscriptionTimewindow = {
@ -249,7 +337,9 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
interval: SECOND,
limit: timeService.getMaxDatapointsLimit(),
type: AggregationType.AVG
}
},
timezone: timewindow.timezone,
tsOffset: calculateTsOffset(timewindow.timezone)
};
let aggTimewindow = 0;
if (stateData) {
@ -267,33 +357,66 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
selectedTab = isDefined(timewindow.realtime) ? TimewindowType.REALTIME : TimewindowType.HISTORY;
}
if (selectedTab === TimewindowType.REALTIME) {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
let realtimeType = timewindow.realtime.realtimeType;
if (isUndefined(realtimeType)) {
if (isDefined(timewindow.realtime.quickInterval)) {
realtimeType = RealtimeWindowType.INTERVAL;
} else {
realtimeType = RealtimeWindowType.LAST_INTERVAL;
}
}
if (realtimeType === RealtimeWindowType.INTERVAL) {
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.realtimeWindowMs =
getSubscriptionRealtimeWindowFromTimeInterval(timewindow.realtime.quickInterval, currentDate);
subscriptionTimewindow.quickInterval = timewindow.realtime.quickInterval;
subscriptionTimewindow.startTs = calculateIntervalStartTime(timewindow.realtime.quickInterval, currentDate);
} else {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
}
subscriptionTimewindow.aggregation.interval =
timeService.boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval,
subscriptionTimewindow.aggregation.type);
subscriptionTimewindow.startTs = Date.now() + stDiff - subscriptionTimewindow.realtimeWindowMs;
const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
if (startDiff) {
subscriptionTimewindow.startTs -= startDiff;
aggTimewindow += subscriptionTimewindow.aggregation.interval;
if (realtimeType !== RealtimeWindowType.INTERVAL) {
const startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
if (startDiff) {
subscriptionTimewindow.startTs -= startDiff;
aggTimewindow += subscriptionTimewindow.aggregation.interval;
}
}
} else {
let historyType = timewindow.history.historyType;
if (isUndefined(historyType)) {
historyType = isDefined(timewindow.history.timewindowMs) ? HistoryWindowType.LAST_INTERVAL : HistoryWindowType.FIXED;
if (isDefined(timewindow.history.timewindowMs)) {
historyType = HistoryWindowType.LAST_INTERVAL;
} else if (isDefined(timewindow.history.quickInterval)) {
historyType = HistoryWindowType.INTERVAL;
} else {
historyType = HistoryWindowType.FIXED;
}
}
if (historyType === HistoryWindowType.LAST_INTERVAL) {
const currentTime = Date.now();
const currentDate = getCurrentTime(timewindow.timezone);
const currentTime = currentDate.valueOf();
subscriptionTimewindow.fixedWindow = {
startTimeMs: currentTime - timewindow.history.timewindowMs,
endTimeMs: currentTime
};
aggTimewindow = timewindow.history.timewindowMs;
} else if (historyType === HistoryWindowType.INTERVAL) {
const currentDate = getCurrentTime(timewindow.timezone);
subscriptionTimewindow.fixedWindow = {
startTimeMs: calculateIntervalStartTime(timewindow.history.quickInterval, currentDate),
endTimeMs: calculateIntervalEndTime(timewindow.history.quickInterval, currentDate)
};
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
subscriptionTimewindow.quickInterval = timewindow.history.quickInterval;
} else {
subscriptionTimewindow.fixedWindow = {
startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
startTimeMs: timewindow.history.fixedTimewindow.startTimeMs - subscriptionTimewindow.tsOffset,
endTimeMs: timewindow.history.fixedTimewindow.endTimeMs - subscriptionTimewindow.tsOffset
};
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
}
@ -309,6 +432,127 @@ export function createSubscriptionTimewindow(timewindow: Timewindow, stDiff: num
return subscriptionTimewindow;
}
function getSubscriptionRealtimeWindowFromTimeInterval(interval: QuickTimeInterval, currentDate: moment_.Moment): number {
switch (interval) {
case QuickTimeInterval.CURRENT_HOUR:
return HOUR;
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return DAY;
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return WEEK;
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return currentDate.endOf('month').diff(currentDate.clone().startOf('month'));
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.endOf('year').diff(currentDate.clone().startOf('year'));
}
}
export function calculateIntervalEndTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
switch (interval) {
case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
currentDate.subtract(2, 'days');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.endOf('week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
currentDate.subtract(1, 'months');
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
currentDate.subtract(1, 'years');
return currentDate.endOf('year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
return currentDate.endOf('hour').valueOf();
case QuickTimeInterval.CURRENT_DAY:
return currentDate.endOf('day').valueOf();
case QuickTimeInterval.CURRENT_WEEK:
return currentDate.endOf('week').valueOf();
case QuickTimeInterval.CURRENT_MONTH:
return currentDate.endOf('month').valueOf();
case QuickTimeInterval.CURRENT_YEAR:
return currentDate.endOf('year').valueOf();
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.valueOf();
}
}
export function calculateIntervalStartTime(interval: QuickTimeInterval, currentDate: moment_.Moment = null, tz: string = ''): number {
currentDate = currentDate ? currentDate.clone() : getCurrentTime(tz);
switch (interval) {
case QuickTimeInterval.YESTERDAY:
currentDate.subtract(1, 'days');
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
currentDate.subtract(2, 'days');
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.PREVIOUS_WEEK:
currentDate.subtract(1, 'weeks');
return currentDate.startOf('week').valueOf();
case QuickTimeInterval.PREVIOUS_MONTH:
currentDate.subtract(1, 'months');
return currentDate.startOf('month').valueOf();
case QuickTimeInterval.PREVIOUS_YEAR:
currentDate.subtract(1, 'years');
return currentDate.startOf('year').valueOf();
case QuickTimeInterval.CURRENT_HOUR:
return currentDate.startOf('hour').valueOf();
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return currentDate.startOf('day').valueOf();
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return currentDate.startOf('week').valueOf();
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return currentDate.startOf('month').valueOf();
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return currentDate.startOf('year').valueOf();
}
}
export function quickTimeIntervalPeriod(interval: QuickTimeInterval): number {
switch (interval) {
case QuickTimeInterval.CURRENT_HOUR:
return HOUR;
case QuickTimeInterval.YESTERDAY:
case QuickTimeInterval.DAY_BEFORE_YESTERDAY:
case QuickTimeInterval.THIS_DAY_LAST_WEEK:
case QuickTimeInterval.CURRENT_DAY:
case QuickTimeInterval.CURRENT_DAY_SO_FAR:
return DAY;
case QuickTimeInterval.PREVIOUS_WEEK:
case QuickTimeInterval.CURRENT_WEEK:
case QuickTimeInterval.CURRENT_WEEK_SO_FAR:
return WEEK;
case QuickTimeInterval.PREVIOUS_MONTH:
case QuickTimeInterval.CURRENT_MONTH:
case QuickTimeInterval.CURRENT_MONTH_SO_FAR:
return DAY * 30;
case QuickTimeInterval.PREVIOUS_YEAR:
case QuickTimeInterval.CURRENT_YEAR:
case QuickTimeInterval.CURRENT_YEAR_SO_FAR:
return YEAR;
}
}
export function createTimewindowForComparison(subscriptionTimewindow: SubscriptionTimewindow,
timeUnit: moment_.unitOfTime.DurationConstructor): SubscriptionTimewindow {
const timewindowForComparison: SubscriptionTimewindow = {
@ -339,6 +583,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
cloned.hideInterval = timewindow.hideInterval || false;
cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || false;
if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab;
if (timewindow.selectedTab === TimewindowType.REALTIME) {
@ -348,6 +593,7 @@ export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
}
}
cloned.aggregation = deepClone(timewindow.aggregation);
cloned.timezone = timewindow.timezone;
return cloned;
}
@ -358,6 +604,8 @@ export function cloneSelectedHistoryTimewindow(historyWindow: HistoryWindow): Hi
cloned.interval = historyWindow.interval;
if (historyWindow.historyType === HistoryWindowType.LAST_INTERVAL) {
cloned.timewindowMs = historyWindow.timewindowMs;
} else if (historyWindow.historyType === HistoryWindowType.INTERVAL) {
cloned.quickInterval = historyWindow.quickInterval;
} else if (historyWindow.historyType === HistoryWindowType.FIXED) {
cloned.fixedTimewindow = deepClone(historyWindow.fixedTimewindow);
}
@ -375,7 +623,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{
name: 'timeinterval.seconds-interval',
translateParams: {seconds: 1},
value: 1 * SECOND
value: SECOND
},
{
name: 'timeinterval.seconds-interval',
@ -400,7 +648,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{
name: 'timeinterval.minutes-interval',
translateParams: {minutes: 1},
value: 1 * MINUTE
value: MINUTE
},
{
name: 'timeinterval.minutes-interval',
@ -430,7 +678,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{
name: 'timeinterval.hours-interval',
translateParams: {hours: 1},
value: 1 * HOUR
value: HOUR
},
{
name: 'timeinterval.hours-interval',
@ -455,7 +703,7 @@ export const defaultTimeIntervals = new Array<TimeInterval>(
{
name: 'timeinterval.days-interval',
translateParams: {days: 1},
value: 1 * DAY
value: DAY
},
{
name: 'timeinterval.days-interval',
@ -490,65 +738,62 @@ export interface TimezoneInfo {
name: string;
offset: string;
nOffset: number;
abbr: string;
}
let timezones: TimezoneInfo[] = null;
let defaultTimezone: string = null;
export function getTimezones(): Observable<TimezoneInfo[]> {
if (timezones) {
return of(timezones);
} else {
return from(import('moment-timezone')).pipe(
map((monentTz) => {
return monentTz.tz.names().map((zoneName) => {
const tz = monentTz.tz(zoneName);
return {
id: zoneName,
name: zoneName.replace(/_/g, ' '),
offset: `UTC${tz.format('Z')}`,
nOffset: tz.utcOffset()
};
});
}),
tap((zones) => {
timezones = zones;
})
);
export function getTimezones(): TimezoneInfo[] {
if (!timezones) {
timezones = monentTz.tz.names().map((zoneName) => {
const tz = monentTz.tz(zoneName);
return {
id: zoneName,
name: zoneName.replace(/_/g, ' '),
offset: `UTC${tz.format('Z')}`,
nOffset: tz.utcOffset(),
abbr: tz.zoneAbbr()
};
});
}
return timezones;
}
export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): Observable<TimezoneInfo> {
return getTimezones().pipe(
mergeMap((timezoneList) => {
let foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId);
if (!foundTimezone) {
if (userTimezoneByDefault) {
return getDefaultTimezone().pipe(
map((userTimezone) => {
return timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone);
})
);
} else if (defaultTimezoneId) {
foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
}
}
return of(foundTimezone);
})
);
export function getTimezoneInfo(timezoneId: string, defaultTimezoneId?: string, userTimezoneByDefault?: boolean): TimezoneInfo {
const timezoneList = getTimezones();
let foundTimezone = timezoneId ? timezoneList.find(timezoneInfo => timezoneInfo.id === timezoneId) : null;
if (!foundTimezone) {
if (userTimezoneByDefault) {
const userTimezone = getDefaultTimezone();
foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === userTimezone);
} else if (defaultTimezoneId) {
foundTimezone = timezoneList.find(timezoneInfo => timezoneInfo.id === defaultTimezoneId);
}
}
return foundTimezone;
}
export function getDefaultTimezone(): Observable<string> {
if (defaultTimezone) {
return of(defaultTimezone);
export function getDefaultTimezoneInfo(): TimezoneInfo {
const userTimezone = getDefaultTimezone();
return getTimezoneInfo(userTimezone);
}
export function getDefaultTimezone(): string {
if (!defaultTimezone) {
defaultTimezone = monentTz.tz.guess();
}
return defaultTimezone;
}
export function getCurrentTime(tz?: string): moment_.Moment {
if (tz) {
return moment().tz(tz);
} else {
return from(import('moment-timezone')).pipe(
map((monentTz) => {
return monentTz.tz.guess();
}),
tap((zone) => {
defaultTimezone = zone;
})
);
return moment();
}
}
export function getTimezone(tz: string): moment_.Moment {
return moment.tz(tz);
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -140,6 +140,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
@NgModule({
providers: [
@ -175,6 +176,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
TimewindowComponent,
TimewindowPanelComponent,
TimeintervalComponent,
QuickTimeIntervalComponent,
DashboardSelectComponent,
DashboardSelectPanelComponent,
DatetimePeriodComponent,
@ -302,6 +304,7 @@ import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
TimewindowComponent,
TimewindowPanelComponent,
TimeintervalComponent,
QuickTimeIntervalComponent,
DashboardSelectComponent,
DatetimePeriodComponent,
DatetimeComponent,

25
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1988,7 +1988,8 @@
"timezone": "Timezone",
"select-timezone": "Select timezone",
"no-timezones-matching": "No timezones matching '{{timezone}}' were found.",
"timezone-required": "Timezone is required."
"timezone-required": "Timezone is required.",
"browser-time": "Browser Time"
},
"queue": {
"select_name": "Select queue name",
@ -2118,7 +2119,24 @@
"hours": "Hours",
"minutes": "Minutes",
"seconds": "Seconds",
"advanced": "Advanced"
"advanced": "Advanced",
"predefined": {
"yesterday": "Yesterday",
"day-before-yesterday": "Day before yesterday",
"this-day-last-week": "This day last week",
"previous-week": "Previous week",
"previous-month": "Previous month",
"previous-year": "Previous year",
"current-hour": "Current hour",
"current-day": "Current day",
"current-day-so-far": "Current day so far",
"current-week": "Current week",
"current-week-so-far": "Current week so far",
"current-month": "Current month",
"current-month-so-far": "Current month so far",
"current-year": "Current year",
"current-year-so-far": "Current year so far"
}
},
"timeunit": {
"seconds": "Seconds",
@ -2139,7 +2157,8 @@
"date-range": "Date range",
"last": "Last",
"time-period": "Time period",
"hide": "Hide"
"hide": "Hide",
"interval": "Interval"
},
"user": {
"user": "User",

Loading…
Cancel
Save