diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 47a79ca2f4..00b2b811fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -256,7 +256,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } else { entitiesSortOrder = sortOrder; } - EntityDataPageLink edpl = new EntityDataPageLink(0, maxEntitiesPerAlarmSubscription, null, entitiesSortOrder); + EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); EntityDataQuery edq = new EntityDataQuery(adq.getEntityFilter(), edpl, adq.getEntityFields(), adq.getLatestValues(), adq.getKeyFilters()); PageData entitiesData = entityService.findEntityDataByQuery(ctx.getTenantId(), ctx.getCustomerId(), edq); List entities = entitiesData.getData(); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java index eac0082db9..fd2a52dc02 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java @@ -35,6 +35,11 @@ public class AlarmDataUpdate extends DataUpdate { super(cmdId, null, null, errorCode, errorMsg); } + @Override + public DataUpdateType getDataUpdateType() { + return DataUpdateType.ALARM_DATA; + } + @JsonCreator public AlarmDataUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("data") PageData data, diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java index d35d2bcd53..81dc4ef902 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java @@ -40,4 +40,5 @@ public abstract class DataUpdate { this(cmdId, null, null, errorCode, errorMsg); } + public abstract DataUpdateType getDataUpdateType(); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java new file mode 100644 index 0000000000..d43ab5ab99 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2020 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. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +public enum DataUpdateType { + ENTITY_DATA, + ALARM_DATA +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java index 6f878c42fe..28cf6c8719 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java @@ -33,6 +33,11 @@ public class EntityDataUpdate extends DataUpdate { super(cmdId, null, null, errorCode, errorMsg); } + @Override + public DataUpdateType getDataUpdateType() { + return DataUpdateType.ENTITY_DATA; + } + @JsonCreator public EntityDataUpdate(@JsonProperty("cmdId") int cmdId, @JsonProperty("data") PageData data, diff --git a/ui-ngx/src/app/core/api/alarm-data-subscription.ts b/ui-ngx/src/app/core/api/alarm-data-subscription.ts new file mode 100644 index 0000000000..9b05b680f8 --- /dev/null +++ b/ui-ngx/src/app/core/api/alarm-data-subscription.ts @@ -0,0 +1,176 @@ +/// +/// Copyright © 2016-2020 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 { + AlarmDataCmd, + DataKeyType, + TelemetryService, + TelemetrySubscriber +} from '@shared/models/telemetry/telemetry.models'; +import { DatasourceType } from '@shared/models/widget.models'; +import { + AlarmData, + AlarmDataPageLink, + EntityFilter, + EntityKey, + EntityKeyType, + KeyFilter +} from '@shared/models/query/query.models'; +import { SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { AlarmDataListener } from '@core/api/alarm-data.service'; +import { UtilsService } from '@core/services/utils.service'; +import { PageData } from '@shared/models/page/page-data'; +import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils'; +import { simulatedAlarm } from '@shared/models/alarm.models'; + +export interface AlarmSubscriptionDataKey { + name: string; + type: DataKeyType; +} + +export interface AlarmDataSubscriptionOptions { + datasourceType: DatasourceType; + dataKeys: Array; + entityFilter?: EntityFilter; + pageLink?: AlarmDataPageLink; + keyFilters?: Array; + additionalKeyFilters?: Array; + subscriptionTimewindow?: SubscriptionTimewindow; +} + +export class AlarmDataSubscription { + + private datasourceType: DatasourceType = this.alarmDataSubscriptionOptions.datasourceType; + + private history: boolean; + private realtime: boolean; + + private subscriber: TelemetrySubscriber; + private alarmDataCommand: AlarmDataCmd; + + private pageData: PageData; + private alarmIdToDataIndex: {[id: string]: number}; + + private subsTw: SubscriptionTimewindow; + + constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions, + private listener: AlarmDataListener, + private telemetryService: TelemetryService, + private utils: UtilsService) { + } + + public unsubscribe() { + if (this.datasourceType === DatasourceType.entity) { + if (this.subscriber) { + this.subscriber.unsubscribe(); + this.subscriber = null; + } + } + } + + public subscribe() { + this.subsTw = this.alarmDataSubscriptionOptions.subscriptionTimewindow; + this.history = this.alarmDataSubscriptionOptions.subscriptionTimewindow && + isObject(this.alarmDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); + this.realtime = this.alarmDataSubscriptionOptions.subscriptionTimewindow && + isDefinedAndNotNull(this.alarmDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + if (this.datasourceType === DatasourceType.entity) { + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.alarmDataCommand = new AlarmDataCmd(); + + const entityFields: Array = + this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) + ); + + const attrFields = this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( + dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) + ); + const tsFields = this.alarmDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.timeseries).map( + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); + const latestValues = attrFields.concat(tsFields); + + let keyFilters = this.alarmDataSubscriptionOptions.keyFilters; + if (this.alarmDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.alarmDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.alarmDataSubscriptionOptions.additionalKeyFilters; + } + } + this.alarmDataCommand.query = { + entityFilter: this.alarmDataSubscriptionOptions.entityFilter, + pageLink: deepClone(this.alarmDataSubscriptionOptions.pageLink), + keyFilters, + entityFields, + latestValues + }; + if (this.history) { + this.alarmDataCommand.query.pageLink.startTs = this.subsTw.fixedWindow.startTimeMs; + this.alarmDataCommand.query.pageLink.endTs = this.subsTw.fixedWindow.endTimeMs; + } else { + this.alarmDataCommand.query.pageLink.timeWindow = this.subsTw.realtimeWindowMs; + } + + this.subscriber.subscriptionCommands.push(this.alarmDataCommand); + + this.subscriber.alarmData$.subscribe((alarmDataUpdate) => { + if (alarmDataUpdate.data) { + this.onPageData(alarmDataUpdate.data); + } else if (alarmDataUpdate.update) { + this.onDataUpdate(alarmDataUpdate.update); + } + }); + + this.subscriber.subscribe(); + + } else if (this.datasourceType === DatasourceType.function) { + const pageData: PageData = { + data: [{...simulatedAlarm, entityId: '1', latest: {}}], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + } + } + + private resetData() { + this.alarmIdToDataIndex = {}; + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + const alarmData = this.pageData.data[dataIndex]; + this.alarmIdToDataIndex[alarmData.id.id] = dataIndex; + } + } + + private onPageData(pageData: PageData) { + this.pageData = pageData; + this.resetData(); + this.listener.alarmsLoaded(pageData, this.alarmDataSubscriptionOptions.pageLink); + } + + private onDataUpdate(update: Array) { + for (const alarmData of update) { + const dataIndex = this.alarmIdToDataIndex[alarmData.id.id]; + if (isDefined(dataIndex) && dataIndex >= 0) { + this.pageData.data[dataIndex] = alarmData; + } + } + this.listener.alarmsUpdated(update, this.pageData); + } + +} diff --git a/ui-ngx/src/app/core/api/alarm-data.service.ts b/ui-ngx/src/app/core/api/alarm-data.service.ts new file mode 100644 index 0000000000..304b068094 --- /dev/null +++ b/ui-ngx/src/app/core/api/alarm-data.service.ts @@ -0,0 +1,94 @@ +/// +/// Copyright © 2016-2020 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 { SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { Datasource, DatasourceType } from '@shared/models/widget.models'; +import { PageData } from '@shared/models/page/page-data'; +import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models'; +import { Injectable } from '@angular/core'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { UtilsService } from '@core/services/utils.service'; +import { + AlarmDataSubscription, + AlarmDataSubscriptionOptions, + AlarmSubscriptionDataKey +} from '@core/api/alarm-data-subscription'; +import { deepClone } from '@core/utils'; + +export interface AlarmDataListener { + subscriptionTimewindow?: SubscriptionTimewindow; + alarmSource: Datasource; + alarmsLoaded: (pageData: PageData, pageLink: AlarmDataPageLink) => void; + alarmsUpdated: (update: Array, pageData: PageData) => void; + subscription?: AlarmDataSubscription; +} + +@Injectable({ + providedIn: 'root' +}) +export class AlarmDataService { + + constructor(private telemetryService: TelemetryWebsocketService, + private utils: UtilsService) {} + + + public subscribeForAlarms(listener: AlarmDataListener, + pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[]) { + const alarmSource = listener.alarmSource; + if (alarmSource.type === DatasourceType.entity && (!alarmSource.entityFilter || !pageLink)) { + return; + } + listener.subscription = this.createSubscription(listener, + pageLink, alarmSource.keyFilters, keyFilters); + return listener.subscription.subscribe(); + } + + public stopSubscription(listener: AlarmDataListener) { + if (listener.subscription) { + listener.subscription.unsubscribe(); + } + } + + private createSubscription(listener: AlarmDataListener, + pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[], + additionalKeyFilters: KeyFilter[]): AlarmDataSubscription { + const alarmSource = listener.alarmSource; + const alarmSubscriptionDataKeys: Array = []; + alarmSource.dataKeys.forEach((dataKey) => { + const alarmSubscriptionDataKey: AlarmSubscriptionDataKey = { + name: dataKey.name, + type: dataKey.type + }; + alarmSubscriptionDataKeys.push(alarmSubscriptionDataKey); + }); + const alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions = { + datasourceType: alarmSource.type, + dataKeys: alarmSubscriptionDataKeys, + subscriptionTimewindow: deepClone(listener.subscriptionTimewindow) + }; + if (alarmDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + alarmDataSubscriptionOptions.entityFilter = alarmSource.entityFilter; + alarmDataSubscriptionOptions.pageLink = pageLink; + alarmDataSubscriptionOptions.keyFilters = keyFilters; + alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + } + return new AlarmDataSubscription(alarmDataSubscriptionOptions, + listener, this.telemetryService, this.utils); + } + +} diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 6f60bc839e..f6bb4067ed 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -41,6 +41,7 @@ import { EntityInfo } from '@app/shared/models/entity.models'; import { IDashboardComponent } from '@home/models/dashboard-component.models'; import * as moment_ from 'moment'; import { + AlarmData, AlarmDataPageLink, EntityData, EntityDataPageLink, EntityFilter, @@ -51,6 +52,7 @@ import { import { EntityDataService } from '@core/api/entity-data.service'; import { PageData } from '@shared/models/page/page-data'; import { TranslateService } from '@ngx-translate/core'; +import { AlarmDataService } from '@core/api/alarm-data.service'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -184,9 +186,9 @@ export class WidgetSubscriptionContext { timeService: TimeService; deviceService: DeviceService; - alarmService: AlarmService; translate: TranslateService; entityDataService: EntityDataService; + alarmDataService: AlarmDataService; utils: UtilsService; raf: RafService; widgetUtils: IWidgetUtils; @@ -218,10 +220,10 @@ export interface WidgetSubscriptionOptions { type?: widgetType; stateData?: boolean; alarmSource?: Datasource; - alarmSearchStatus?: AlarmSearchStatus; +/* alarmSearchStatus?: AlarmSearchStatus; alarmsPollingInterval?: number; alarmsMaxCountLoad?: number; - alarmsFetchSize?: number; + alarmsFetchSize?: number; */ datasources?: Array; hasDataPageLink?: boolean; singleEntity?: boolean; @@ -269,10 +271,10 @@ export interface IWidgetSubscription { timeWindow?: WidgetTimewindow; comparisonTimeWindow?: WidgetTimewindow; - alarms?: Array; + alarms?: PageData; alarmSource?: Datasource; - alarmSearchStatus?: AlarmSearchStatus; - alarmsPollingInterval?: number; + /* alarmSearchStatus?: AlarmSearchStatus; + alarmsPollingInterval?: number; */ targetDeviceAliasIds?: Array; targetDeviceIds?: Array; @@ -309,6 +311,9 @@ export interface IWidgetSubscription { pageLink: EntityDataPageLink, keyFilters: KeyFilter[]): Observable; + subscribeForAlarms(pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[]): void; + isDataResolved(): boolean; destroy(): void; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index bfed43f3f5..5b2a2d983b 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -47,21 +47,23 @@ import { import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; import { CancelAnimationFrame } from '@core/services/raf.service'; import { EntityType } from '@shared/models/entity-type.models'; -import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models'; import { createLabelFromDatasource, deepClone, isDefined, isEqual } from '@core/utils'; -import { AlarmSourceListener } from '@core/http/alarm.service'; import { EntityId } from '@app/shared/models/id/entity-id'; import * as moment_ from 'moment'; -import { PageData } from '@shared/models/page/page-data'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { EntityDataListener } from '@core/api/entity-data.service'; import { + AlarmData, + AlarmDataPageLink, EntityData, EntityDataPageLink, entityDataToEntityInfo, + EntityKeyType, KeyFilter, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; import { map } from 'rxjs/operators'; +import { AlarmDataListener } from '@core/api/alarm-data.service'; const moment = moment_; @@ -102,10 +104,11 @@ export class WidgetSubscription implements IWidgetSubscription { comparisonTimeWindow: WidgetTimewindow; timewindowForComparison: SubscriptionTimewindow; - alarms: Array; + // alarms: Array; + alarms: PageData; alarmSource: Datasource; - private alarmSearchStatusValue: AlarmSearchStatus; + /* private alarmSearchStatusValue: AlarmSearchStatus; set alarmSearchStatus(value: AlarmSearchStatus) { if (this.alarmSearchStatusValue !== value) { @@ -116,12 +119,14 @@ export class WidgetSubscription implements IWidgetSubscription { get alarmSearchStatus(): AlarmSearchStatus { return this.alarmSearchStatusValue; - } + }*/ + + alarmDataListener: AlarmDataListener; - alarmsPollingInterval: number; +/* alarmsPollingInterval: number; alarmsMaxCountLoad: number; alarmsFetchSize: number; - alarmSourceListener: AlarmSourceListener; + alarmSourceListener: AlarmSourceListener;*/ loadingData: boolean; @@ -181,7 +186,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {}); this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {}); this.alarmSource = options.alarmSource; - this.alarmSearchStatusValue = isDefined(options.alarmSearchStatus) ? + /*this.alarmSearchStatusValue = isDefined(options.alarmSearchStatus) ? options.alarmSearchStatus : AlarmSearchStatus.ANY; this.alarmsPollingInterval = isDefined(options.alarmsPollingInterval) ? options.alarmsPollingInterval : 5000; @@ -189,8 +194,10 @@ export class WidgetSubscription implements IWidgetSubscription { options.alarmsMaxCountLoad : 0; this.alarmsFetchSize = isDefined(options.alarmsFetchSize) ? options.alarmsFetchSize : 100; - this.alarmSourceListener = null; - this.alarms = []; + this.alarmSourceListener = null;*/ + this.alarmDataListener = null; + // this.alarms = []; + this.alarms = emptyPageData(); this.originalTimewindow = null; this.timeWindow = {}; this.useDashboardTimewindow = options.useDashboardTimewindow; @@ -290,7 +297,7 @@ export class WidgetSubscription implements IWidgetSubscription { if (this.targetDeviceId) { this.rpcEnabled = true; } else { - this.rpcEnabled = this.ctx.utils.widgetEditMode ? true : false; + this.rpcEnabled = this.ctx.utils.widgetEditMode; } this.hasResolvedData = this.rpcEnabled; this.callbacks.rpcStateChanged(this); @@ -317,7 +324,7 @@ export class WidgetSubscription implements IWidgetSubscription { if (this.targetDeviceId) { this.rpcEnabled = true; } else { - this.rpcEnabled = this.ctx.utils.widgetEditMode ? true : false; + this.rpcEnabled = this.ctx.utils.widgetEditMode; } this.hasResolvedData = true; this.callbacks.rpcStateChanged(this); @@ -356,6 +363,7 @@ export class WidgetSubscription implements IWidgetSubscription { } private configureAlarmsData() { + this.notifyDataLoaded(); } private initDataSubscription(): Observable { @@ -482,13 +490,17 @@ export class WidgetSubscription implements IWidgetSubscription { entityName = this.targetDeviceName; } } else if (this.type === widgetType.alarm) { - if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) { - entityId = { - entityType: this.alarmSource.entityType, - id: this.alarmSource.entityId - }; - entityName = this.alarmSource.entityName; - entityLabel = this.alarmSource.entityLabel; + if (this.alarms && this.alarms.data.length) { + const data = this.alarms.data[0]; + entityId = data.originator; + entityName = data.originatorName; + if (data.latest && data.latest[EntityKeyType.ENTITY_FIELD]) { + const entityFields = data.latest[EntityKeyType.ENTITY_FIELD]; + const labelValue = entityFields.label; + if (labelValue) { + entityLabel = labelValue.value; + } + } } } else { for (const datasource of this.datasources) { @@ -522,7 +534,6 @@ export class WidgetSubscription implements IWidgetSubscription { } else { return this.checkSubscriptions(aliasIds); } - return false; } onFiltersChanged(filterIds: Array): boolean { @@ -573,12 +584,6 @@ export class WidgetSubscription implements IWidgetSubscription { return false; } - private onAlarmSearchStatusChanged() { - if (this.type === widgetType.alarm) { - this.update(); - } - } - updateDataVisibility(index: number): void { if (this.displayLegend) { const hidden = this.legendData.keys[index].dataKey.hidden; @@ -752,11 +757,12 @@ export class WidgetSubscription implements IWidgetSubscription { } update() { - if (this.type === widgetType.rpc || this.type === widgetType.alarm) { - this.unsubscribe(); - this.subscribe(); - } else { - this.dataSubscribe(); + if (this.type !== widgetType.rpc) { + if (this.type === widgetType.alarm) { + this.updateAlarmDataSubscription(); + } else { + this.dataSubscribe(); + } } } @@ -821,13 +827,41 @@ export class WidgetSubscription implements IWidgetSubscription { } } - private doSubscribe() { - if (this.type === widgetType.rpc) { - return; + subscribeForAlarms(pageLink: AlarmDataPageLink, + keyFilters: KeyFilter[]) { + if (this.alarmDataListener) { + this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); } - if (this.type === widgetType.alarm) { - this.alarmsSubscribe(); - } else { + if (this.timeWindowConfig) { + this.updateRealtimeSubscription(); + if (this.subscriptionTimewindow.fixedWindow) { + this.onDataUpdated(); + } + } + this.alarmDataListener = { + subscriptionTimewindow: this.subscriptionTimewindow, + alarmSource: this.alarmSource, + alarmsLoaded: this.alarmsLoaded.bind(this), + alarmsUpdated: this.alarmsUpdated.bind(this) + }; + + this.alarms = emptyPageData(); + + this.ctx.alarmDataService.subscribeForAlarms(this.alarmDataListener, pageLink, keyFilters); + + let forceUpdate = false; + if (this.alarmSource.unresolvedStateEntity || + (this.alarmSource.type === DatasourceType.entity && !this.alarmSource.entityId) + ) { + forceUpdate = true; + } + if (forceUpdate) { + this.onDataUpdated(); + } + } + + private doSubscribe() { + if (this.type !== widgetType.rpc && this.type !== widgetType.alarm) { this.dataSubscribe(); } } @@ -858,7 +892,7 @@ export class WidgetSubscription implements IWidgetSubscription { } } - private alarmsSubscribe() { + /* private alarmsSubscribe() { this.notifyDataLoading(); if (this.timeWindowConfig) { this.updateRealtimeSubscription(); @@ -875,9 +909,10 @@ export class WidgetSubscription implements IWidgetSubscription { alarmsFetchSize: this.alarmsFetchSize, alarmsUpdated: alarms => this.alarmsUpdated(alarms) }; - this.alarms = null; - this.ctx.alarmService.subscribeForAlarms(this.alarmSourceListener); + this.alarms = emptyPageData(); + + this.ctx.alarmDataService.subscribeForAlarms(this.alarmDataListener); let forceUpdate = false; if (this.alarmSource.unresolvedStateEntity || @@ -889,7 +924,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.notifyDataLoaded(); this.onDataUpdated(); } - } + } */ unsubscribe() { @@ -910,33 +945,62 @@ export class WidgetSubscription implements IWidgetSubscription { } private alarmsUnsubscribe() { - if (this.alarmSourceListener) { - this.ctx.alarmService.unsubscribeFromAlarms(this.alarmSourceListener); - this.alarmSourceListener = null; + if (this.alarmDataListener) { + this.ctx.alarmDataService.stopSubscription(this.alarmDataListener); + this.alarmDataListener = null; } } private checkRpcTarget(aliasIds: Array): boolean { - if (aliasIds.indexOf(this.targetDeviceAliasId) > -1) { - return true; - } else { - return false; - } + return aliasIds.indexOf(this.targetDeviceAliasId) > -1; } private checkAlarmSource(aliasIds: Array): boolean { if (this.options.alarmSource && this.options.alarmSource.entityAliasId) { - return aliasIds.indexOf(this.options.alarmSource.entityAliasId) > -1; - } else { - return false; + if (aliasIds.indexOf(this.options.alarmSource.entityAliasId) > -1) { + this.updateAlarmSubscription(); + } } + return false; } private checkAlarmSourceFilters(filterIds: Array): boolean { if (this.options.alarmSource && this.options.alarmSource.filterId) { - return filterIds.indexOf(this.options.alarmSource.filterId) > -1; + if (filterIds.indexOf(this.options.alarmSource.filterId) > -1) { + this.updateAlarmSubscription(); + } + } + return false; + } + + private updateAlarmSubscription() { + this.alarmSource = this.options.alarmSource; + if (!this.ctx.aliasController) { + this.hasResolvedData = true; + this.configureAlarmsData(); + this.updateAlarmDataSubscription(); } else { - return false; + this.ctx.aliasController.resolveAlarmSource(this.alarmSource).subscribe( + (alarmSource) => { + this.alarmSource = alarmSource; + if (alarmSource) { + this.hasResolvedData = true; + } + this.configureAlarmsData(); + this.updateAlarmDataSubscription(); + }, + () => { + this.notifyDataLoaded(); + } + ); + } + } + + private updateAlarmDataSubscription() { + if (this.alarmDataListener) { + const pageLink = this.alarmDataListener.subscription.alarmDataSubscriptionOptions.pageLink; + const keyFilters = this.alarmDataListener.subscription.alarmDataSubscriptionOptions.additionalKeyFilters; + this.subscribeForAlarms(pageLink, keyFilters); } } @@ -999,7 +1063,7 @@ export class WidgetSubscription implements IWidgetSubscription { } ); }, - (err) => { + () => { this.notifyDataLoaded(); } ); @@ -1031,11 +1095,6 @@ export class WidgetSubscription implements IWidgetSubscription { } } - private notifyDataLoading() { - this.loadingData = true; - this.callbacks.dataLoading(this); - } - private notifyDataLoaded() { this.loadingData = false; this.callbacks.dataLoading(this); @@ -1100,23 +1159,21 @@ export class WidgetSubscription implements IWidgetSubscription { const datasources = pageData.data.map((entityData, index) => this.entityDataToDatasource(datasource, entityData, index) ); - const datasourcesPage: PageData = { + this.datasourcePages[datasourceIndex] = { data: datasources, hasNext: pageData.hasNext, totalElements: pageData.totalElements, totalPages: pageData.totalPages }; - this.datasourcePages[datasourceIndex] = datasourcesPage; const datasourceData = datasources.map((datasourceElement, index) => this.entityDataToDatasourceData(datasourceElement, data[index]) ); - const datasourceDataPage: PageData> = { + this.dataPages[datasourceIndex] = { data: datasourceData, hasNext: pageData.hasNext, totalElements: pageData.totalElements, totalPages: pageData.totalPages }; - this.dataPages[datasourceIndex] = datasourceDataPage; if (datasource.type === DatasourceType.entity && pageData.hasNext && pageLink.pageSize > 1) { if (this.warnOnPageDataOverflow) { @@ -1215,8 +1272,8 @@ export class WidgetSubscription implements IWidgetSubscription { private entityDataToDatasourceData(datasource: Datasource, data: Array): Array { return datasource.dataKeys.map((dataKey, keyIndex) => { - dataKey.hidden = dataKey.settings.hideDataByDefault ? true : false; - dataKey.inLegend = dataKey.settings.removeFromLegend ? false : true; + dataKey.hidden = !!dataKey.settings.hideDataByDefault; + dataKey.inLegend = !dataKey.settings.removeFromLegend; if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) { dataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel); } else { @@ -1242,7 +1299,7 @@ export class WidgetSubscription implements IWidgetSubscription { const newDatasource = deepClone(configDatasource); const entityInfo = entityDataToEntityInfo(entityData); updateDatasourceFromEntityInfo(newDatasource, entityInfo); - newDatasource.generated = index > 0 ? true : false; + newDatasource.generated = index > 0; return newDatasource; } @@ -1285,16 +1342,16 @@ export class WidgetSubscription implements IWidgetSubscription { } } - private alarmsUpdated(alarms: Array) { - this.notifyDataLoaded(); - const updated = !this.alarms || !isEqual(this.alarms, alarms); + private alarmsLoaded(alarms: PageData) { this.alarms = alarms; if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) { this.updateTimewindow(); } - if (updated) { - this.onDataUpdated(); - } + this.onDataUpdated(); + } + + private alarmsUpdated(_updated: Array, alarms: PageData) { + this.alarmsLoaded(alarms); } private updateLegend(dataIndex: number, data: DataSet, detectChanges: boolean) { diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts index 026b6a7a2e..4d94556fff 100644 --- a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts +++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts @@ -16,8 +16,10 @@ import { Inject, Injectable, NgZone } from '@angular/core'; import { + AlarmDataCmd, AlarmDataUnsubscribeCmd, + AlarmDataUpdate, AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, - GetHistoryCmd, isEntityDataUpdateMsg, + GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg, SubscriptionCmd, SubscriptionUpdate, SubscriptionUpdateMsg, @@ -107,6 +109,8 @@ export class TelemetryWebsocketService implements TelemetryService { this.cmdsWrapper.historyCmds.push(subscriptionCommand); } else if (subscriptionCommand instanceof EntityDataCmd) { this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); + } else if (subscriptionCommand instanceof AlarmDataCmd) { + this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand); } } ); @@ -142,6 +146,10 @@ export class TelemetryWebsocketService implements TelemetryService { const entityDataUnsubscribeCmd = new EntityDataUnsubscribeCmd(); entityDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; this.cmdsWrapper.entityDataUnsubscribeCmds.push(entityDataUnsubscribeCmd); + } else if (subscriptionCommand instanceof AlarmDataCmd) { + const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd(); + alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; + this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd); } const cmdId = subscriptionCommand.cmdId; if (cmdId) { @@ -281,6 +289,11 @@ export class TelemetryWebsocketService implements TelemetryService { if (subscriber) { subscriber.onEntityData(new EntityDataUpdate(message)); } + } else if (isAlarmDataUpdateMsg(message)) { + subscriber = this.subscribersMap.get(message.cmdId); + if (subscriber) { + subscriber.onAlarmData(new AlarmDataUpdate(message)); + } } else if (message.subscriptionId) { subscriber = this.subscribersMap.get(message.subscriptionId); if (subscriber) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 4a66ba8d43..ed0950979b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -62,7 +62,7 @@
+ matSort [matSortActive]="sortOrderProperty" [matSortDirection]="pageLinkSortDirection()" matSortDisableClear>
- alarm.no-alarms-prompt + {{ 'common.loading' | translate }}
= []; @@ -189,9 +200,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private dialogService: DialogService, private alarmService: AlarmService) { super(store); - - const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); - this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); + this.pageLink = { + page: 0, + pageSize: this.defaultPageSize, + textSearch: null + }; } ngOnInit(): void { @@ -232,11 +245,15 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public onDataUpdated() { this.ngZone.run(() => { - this.alarmsDatasource.updateAlarms(this.subscription.alarms); + this.alarmsDatasource.updateAlarms(); this.ctx.detectChanges(); }); } + public pageLinkSortDirection(): SortDirection { + return entityDataPageLinkSortDirection(this.pageLink); + } + private initializeConfig() { this.ctx.widgetActions = [this.searchAction, this.statusFilterAction, this.columnDisplayAction]; @@ -304,6 +321,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; + // TODO: search status, severity, types, searchPropagatedAlarms from widget config to pageLink + this.pageLink.searchPropagatedAlarms = false; // true for old widget configs + this.pageLink.severityList = []; + this.pageLink.statusList = []; + this.pageLink.typeList = []; + const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); cssParser.testMode = false; @@ -319,9 +342,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.displayedColumns.push('select'); } + const latestDataKeys: Array = []; + if (this.alarmSource) { this.alarmSource.dataKeys.forEach((alarmDataKey) => { const dataKey: EntityColumn = deepClone(alarmDataKey) as EntityColumn; + dataKey.entityKey = dataKeyToEntityKey(alarmDataKey); dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label); dataKey.def = 'def' + this.columns.length; const keySettings: TableWidgetDataKeySettings = dataKey.settings; @@ -330,19 +356,28 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); this.columnWidth[dataKey.def] = getColumnWidth(keySettings); this.columns.push(dataKey); + + if (dataKey.type !== DataKeyType.alarm) { + latestDataKeys.push(dataKey); + } }); this.displayedColumns.push(...this.columns.map(column => column.def)); } if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { this.defaultSortOrder = this.settings.defaultSortOrder; } - this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder); - this.sortOrderProperty = toAlarmColumnDef(this.pageLink.sortOrder.property, this.columns); + this.pageLink.sortOrder = entityDataSortOrderFromString(this.defaultSortOrder, this.columns); + let sortColumn: EntityColumn; + if (this.pageLink.sortOrder) { + sortColumn = findColumnByEntityKey(this.pageLink.sortOrder.key, this.columns); + } + this.sortOrderProperty = sortColumn ? sortColumn.def : null; if (this.actionCellDescriptors.length) { this.displayedColumns.push('actions'); } - this.alarmsDatasource = new AlarmsDatasource(); + + this.alarmsDatasource = new AlarmsDatasource(this.subscription, latestDataKeys); if (this.enableSelection) { this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => { const hideTitlePanel = selectionMode || this.textSearchMode; @@ -467,9 +502,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } else { this.pageLink.page = 0; } - this.pageLink.sortOrder.property = fromAlarmColumnDef(this.sort.active, this.columns); - this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; - this.alarmsDatasource.loadAlarms(this.pageLink); + this.pageLink.sortOrder = { + key: findEntityKeyByColumnDef(this.sort.active, this.columns), + direction: Direction[this.sort.direction.toUpperCase()] + }; + const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns); + const keyFilters: KeyFilter[] = null; // TODO: + this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters); this.ctx.detectChanges(); } @@ -482,7 +521,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return widthStyle(columnWidth); } - public cellStyle(alarm: AlarmInfo, key: EntityColumn): any { + public cellStyle(alarm: AlarmDataInfo, key: EntityColumn): any { let style: any = {}; if (alarm && key) { const styleInfo = this.stylesInfo[key.def]; @@ -504,7 +543,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return style; } - public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml { + public cellContent(alarm: AlarmDataInfo, key: EntityColumn): SafeHtml { if (alarm && key) { const contentInfo = this.contentsInfo[key.def]; const value = getAlarmValue(alarm, key); @@ -524,7 +563,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } - public onRowClick($event: Event, alarm: AlarmInfo) { + public onRowClick($event: Event, alarm: AlarmDataInfo) { if ($event) { $event.stopPropagation(); } @@ -541,7 +580,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } - public onActionButtonClick($event: Event, alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor) { + public onActionButtonClick($event: Event, alarm: AlarmDataInfo, actionDescriptor: AlarmWidgetActionDescriptor) { if (actionDescriptor.details) { this.openAlarmDetails($event, alarm); } else if (actionDescriptor.acknowledge) { @@ -562,7 +601,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } - public actionEnabled(alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor): boolean { + public actionEnabled(alarm: AlarmDataInfo, actionDescriptor: AlarmWidgetActionDescriptor): boolean { if (actionDescriptor.acknowledge) { return (alarm.status === AlarmStatus.ACTIVE_UNACK || alarm.status === AlarmStatus.CLEARED_UNACK); @@ -573,7 +612,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return true; } - private openAlarmDetails($event: Event, alarm: AlarmInfo) { + private openAlarmDetails($event: Event, alarm: AlarmDataInfo) { if ($event) { $event.stopPropagation(); } @@ -599,7 +638,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } - private ackAlarm($event: Event, alarm: AlarmInfo) { + private ackAlarm($event: Event, alarm: AlarmDataInfo) { if ($event) { $event.stopPropagation(); } @@ -626,12 +665,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, $event.stopPropagation(); } if (this.alarmsDatasource.selection.hasValue()) { - const alarms = this.alarmsDatasource.selection.selected.filter( - (alarm) => alarm.id.id !== NULL_UUID + const alarmIds = this.alarmsDatasource.selection.selected.filter( + (alarmId) => alarmId !== NULL_UUID ); - if (alarms.length) { - const title = this.translate.instant('alarm.aknowledge-alarms-title', {count: alarms.length}); - const content = this.translate.instant('alarm.aknowledge-alarms-text', {count: alarms.length}); + if (alarmIds.length) { + const title = this.translate.instant('alarm.aknowledge-alarms-title', {count: alarmIds.length}); + const content = this.translate.instant('alarm.aknowledge-alarms-text', {count: alarmIds.length}); this.dialogService.confirm( title, content, @@ -641,8 +680,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (res) { if (res) { const tasks: Observable[] = []; - for (const alarm of alarms) { - tasks.push(this.alarmService.ackAlarm(alarm.id.id)); + for (const alarmId of alarmIds) { + tasks.push(this.alarmService.ackAlarm(alarmId)); } forkJoin(tasks).subscribe(() => { this.alarmsDatasource.clearSelection(); @@ -655,7 +694,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } - private clearAlarm($event: Event, alarm: AlarmInfo) { + private clearAlarm($event: Event, alarm: AlarmDataInfo) { if ($event) { $event.stopPropagation(); } @@ -682,12 +721,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, $event.stopPropagation(); } if (this.alarmsDatasource.selection.hasValue()) { - const alarms = this.alarmsDatasource.selection.selected.filter( - (alarm) => alarm.id.id !== NULL_UUID + const alarmIds = this.alarmsDatasource.selection.selected.filter( + (alarmId) => alarmId !== NULL_UUID ); - if (alarms.length) { - const title = this.translate.instant('alarm.clear-alarms-title', {count: alarms.length}); - const content = this.translate.instant('alarm.clear-alarms-text', {count: alarms.length}); + if (alarmIds.length) { + const title = this.translate.instant('alarm.clear-alarms-title', {count: alarmIds.length}); + const content = this.translate.instant('alarm.clear-alarms-text', {count: alarmIds.length}); this.dialogService.confirm( title, content, @@ -697,8 +736,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (res) { if (res) { const tasks: Observable[] = []; - for (const alarm of alarms) { - tasks.push(this.alarmService.clearAlarm(alarm.id.id)); + for (const alarmId of alarmIds) { + tasks.push(this.alarmService.clearAlarm(alarmId)); } forkJoin(tasks).subscribe(() => { this.alarmsDatasource.clearSelection(); @@ -756,27 +795,29 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } -class AlarmsDatasource implements DataSource { +class AlarmsDatasource implements DataSource { - private alarmsSubject = new BehaviorSubject([]); - private pageDataSubject = new BehaviorSubject>(emptyPageData()); + private alarmsSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); - public selection = new SelectionModel(true, [], false); + public selection = new SelectionModel(true, [], false); private selectionModeChanged = new EventEmitter(); - public selectionModeChanged$ = this.selectionModeChanged.asObservable(); + public selectionModeChanged$ = this.selectionModeChanged.asObservable(); + + private currentAlarm: AlarmDataInfo = null; - private allAlarms: Array = []; - private allAlarmsSubject = new BehaviorSubject([]); - private allAlarms$: Observable> = this.allAlarmsSubject.asObservable(); + public dataLoading = true; - private currentAlarm: AlarmInfo = null; + private appliedPageLink: AlarmDataPageLink; + private appliedSortOrderLabel: string; - constructor() { + constructor(private subscription: IWidgetSubscription, + private dataKeys: Array) { } - connect(collectionViewer: CollectionViewer): Observable> { + connect(collectionViewer: CollectionViewer): Observable> { return this.alarmsSubject.asObservable(); } @@ -785,51 +826,63 @@ class AlarmsDatasource implements DataSource { this.pageDataSubject.complete(); } - loadAlarms(pageLink: PageLink) { + loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) { if (this.selection.hasValue()) { this.selection.clear(); this.onSelectionModeChanged(false); } - this.fetchAlarms(pageLink).pipe( - catchError(() => of(emptyPageData())), - ).subscribe( - (pageData) => { - this.alarmsSubject.next(pageData.data); - this.pageDataSubject.next(pageData); - } - ); + this.appliedPageLink = pageLink; + this.appliedSortOrderLabel = sortOrderLabel; + this.subscription.subscribeForAlarms(pageLink, keyFilters); } - updateAlarms(alarms: AlarmInfo[]) { - alarms.forEach((newAlarm) => { - const existingAlarmIndex = this.allAlarms.findIndex(alarm => alarm.id.id === newAlarm.id.id); - if (existingAlarmIndex > -1) { - Object.assign(this.allAlarms[existingAlarmIndex], newAlarm); - } else { - this.allAlarms.push(newAlarm); - } + updateAlarms() { + const subscriptionAlarms = this.subscription.alarms; + let alarms = new Array(); + subscriptionAlarms.data.forEach((alarmData) => { + alarms.push(this.alarmDataToInfo(alarmData)); }); - for (let i = this.allAlarms.length - 1; i >= 0; i--) { - const oldAlarm = this.allAlarms[i]; - const newAlarmIndex = alarms.findIndex(alarm => alarm.id.id === oldAlarm.id.id); - if (newAlarmIndex === -1) { - this.allAlarms.splice(i, 1); - } + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); } if (this.selection.hasValue()) { - const toRemove: AlarmInfo[] = []; - this.selection.selected.forEach((selectedAlarm) => { - const existingAlarm = this.allAlarms.find(alarm => alarm.id.id === selectedAlarm.id.id); - if (!existingAlarm) { - toRemove.push(selectedAlarm); - } - }); + const alarmIds = alarms.map((alarm) => alarm.id.id); + const toRemove = this.selection.selected.filter(alarmId => alarmIds.indexOf(alarmId) === -1); this.selection.deselect(...toRemove); if (this.selection.isEmpty()) { this.onSelectionModeChanged(false); } } - this.allAlarmsSubject.next(this.allAlarms); + const alarmsPageData: PageData = { + data: alarms, + totalPages: subscriptionAlarms.totalPages, + totalElements: subscriptionAlarms.totalElements, + hasNext: subscriptionAlarms.hasNext + }; + this.alarmsSubject.next(alarms); + this.pageDataSubject.next(alarmsPageData); + this.dataLoading = false; + } + + private alarmDataToInfo(alarmData: AlarmData): AlarmDataInfo { + const alarm: AlarmDataInfo = deepClone(alarmData); + delete alarm.latest; + const latest = alarmData.latest; + this.dataKeys.forEach((dataKey, index) => { + const type = dataKeyTypeToEntityKeyType(dataKey.type); + let value = ''; + if (type) { + if (latest && latest[type]) { + const tsVal = latest[type][dataKey.name]; + if (tsVal) { + value = tsVal.value; + } + } + } + alarm[dataKey.label] = value; + }); + return alarm; } isAllSelected(): Observable { @@ -851,16 +904,16 @@ class AlarmsDatasource implements DataSource { ); } - toggleSelection(alarm: AlarmInfo) { + toggleSelection(alarm: AlarmDataInfo) { const hasValue = this.selection.hasValue(); - this.selection.toggle(alarm); + this.selection.toggle(alarm.id.id); if (hasValue !== this.selection.hasValue()) { this.onSelectionModeChanged(this.selection.hasValue()); } } - isSelected(alarm: AlarmInfo): boolean { - return this.selection.isSelected(alarm); + isSelected(alarm: AlarmDataInfo): boolean { + return this.selection.isSelected(alarm.id.id); } clearSelection() { @@ -881,7 +934,7 @@ class AlarmsDatasource implements DataSource { } } else { alarms.forEach(row => { - this.selection.select(row); + this.selection.select(row.id.id); }); if (numSelected === 0) { this.onSelectionModeChanged(true); @@ -892,7 +945,7 @@ class AlarmsDatasource implements DataSource { ).subscribe(); } - public toggleCurrentAlarm(alarm: AlarmInfo): boolean { + public toggleCurrentAlarm(alarm: AlarmDataInfo): boolean { if (this.currentAlarm !== alarm) { this.currentAlarm = alarm; return true; @@ -901,7 +954,7 @@ class AlarmsDatasource implements DataSource { } } - public isCurrentAlarm(alarm: AlarmInfo): boolean { + public isCurrentAlarm(alarm: AlarmDataInfo): boolean { return (this.currentAlarm && alarm && this.currentAlarm.id && alarm.id) && (this.currentAlarm.id.id === alarm.id.id); } @@ -909,10 +962,4 @@ class AlarmsDatasource implements DataSource { private onSelectionModeChanged(selectionMode: boolean) { this.selectionModeChanged.emit(selectionMode); } - - private fetchAlarms(pageLink: PageLink): Observable> { - return this.allAlarms$.pipe( - map((data) => pageLink.filterData(data)) - ); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 083852a077..3b5c40e157 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -77,6 +77,7 @@ import { DisplayColumnsPanelData } from '@home/components/widget/lib/display-columns-panel.component'; import { + dataKeyToEntityKey, Direction, EntityDataPageLink, entityDataPageLinkSortDirection, @@ -205,7 +206,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public onDataUpdated() { this.ngZone.run(() => { - this.entityDatasource.dataUpdated(); // .updateEntitiesData(this.subscription.data); + this.entityDatasource.dataUpdated(); this.ctx.detectChanges(); }); } @@ -339,19 +340,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (datasource && datasource.dataKeys) { datasource.dataKeys.forEach((entityDataKey) => { const dataKey: EntityColumn = deepClone(entityDataKey) as EntityColumn; - dataKey.entityKey = { - key: dataKey.name, - type: null - }; + dataKey.entityKey = dataKeyToEntityKey(entityDataKey); if (dataKey.type === DataKeyType.function) { dataKey.name = dataKey.label; - dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD; - } else if (dataKey.type === DataKeyType.entityField) { - dataKey.entityKey.type = EntityKeyType.ENTITY_FIELD; - } else if (dataKey.type === DataKeyType.attribute) { - dataKey.entityKey.type = EntityKeyType.ATTRIBUTE; - } else if (dataKey.type === DataKeyType.timeseries) { - dataKey.entityKey.type = EntityKeyType.TIME_SERIES; } dataKeys.push(dataKey); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 77acf524df..dd295da76d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -17,9 +17,10 @@ import { EntityId } from '@shared/models/id/entity-id'; import { DataKey, WidgetConfig } from '@shared/models/widget.models'; import { getDescendantProp, isDefined } from '@core/utils'; -import { alarmFields, AlarmInfo } from '@shared/models/alarm.models'; +import { AlarmDataInfo, alarmFields } from '@shared/models/alarm.models'; import * as tinycolor_ from 'tinycolor2'; import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; const tinycolor = tinycolor_; @@ -120,7 +121,15 @@ export function findColumn(searchProperty: string, searchValue: string, columns: } export function findColumnByLabel(label: string, columns: EntityColumn[]): EntityColumn { - return findColumn('label', label, columns); + let column: EntityColumn; + const alarmColumns = columns.filter(c => c.type === DataKeyType.alarm); + if (alarmColumns.length) { + column = findColumn('name', label, alarmColumns); + } + if (!column) { + column = findColumn('label', label, columns); + } + return column; } export function findColumnByDef(def: string, columns: EntityColumn[]): EntityColumn { @@ -160,12 +169,12 @@ export function getEntityValue(entity: any, key: DataKey): any { return getDescendantProp(entity, key.label); } -export function getAlarmValue(alarm: AlarmInfo, key: EntityColumn) { +export function getAlarmValue(alarm: AlarmDataInfo, key: EntityColumn) { const alarmField = alarmFields[key.name]; if (alarmField) { return getDescendantProp(alarm, alarmField.value); } else { - return getDescendantProp(alarm, key.name); + return getDescendantProp(alarm, key.label); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 2cb4647a43..e19a0925d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -94,6 +94,7 @@ import { ResizeObserver } from '@juggle/resize-observer'; import { EntityDataService } from '@core/api/entity-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationType } from '@core/notification/notification.models'; +import { AlarmDataService } from '@core/api/alarm-data.service'; @Component({ selector: 'tb-widget', @@ -167,9 +168,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private timeService: TimeService, private deviceService: DeviceService, private entityService: EntityService, - private alarmService: AlarmService, private dashboardService: DashboardService, private entityDataService: EntityDataService, + private alarmDataService: AlarmDataService, private translate: TranslateService, private utils: UtilsService, private raf: RafService, @@ -300,9 +301,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); this.subscriptionContext.timeService = this.timeService; this.subscriptionContext.deviceService = this.deviceService; - this.subscriptionContext.alarmService = this.alarmService; this.subscriptionContext.translate = this.translate; this.subscriptionContext.entityDataService = this.entityDataService; + this.subscriptionContext.alarmDataService = this.alarmDataService; this.subscriptionContext.utils = this.utils; this.subscriptionContext.raf = this.raf; this.subscriptionContext.widgetUtils = this.widgetContext.utils; @@ -901,14 +902,14 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI }; if (this.widget.type === widgetType.alarm) { options.alarmSource = deepClone(this.widget.config.alarmSource); - options.alarmSearchStatus = isDefined(this.widget.config.alarmSearchStatus) ? + /*options.alarmSearchStatus = isDefined(this.widget.config.alarmSearchStatus) ? this.widget.config.alarmSearchStatus : AlarmSearchStatus.ANY; options.alarmsPollingInterval = isDefined(this.widget.config.alarmsPollingInterval) ? this.widget.config.alarmsPollingInterval * 1000 : 5000; options.alarmsMaxCountLoad = isDefined(this.widget.config.alarmsMaxCountLoad) ? this.widget.config.alarmsMaxCountLoad : 0; options.alarmsFetchSize = isDefined(this.widget.config.alarmsFetchSize) ? - this.widget.config.alarmsFetchSize : 100; + this.widget.config.alarmsFetchSize : 100;*/ } else { options.datasources = deepClone(this.widget.config.datasources); } diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 6ac8f9f2df..8f0741206d 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -103,6 +103,10 @@ export interface AlarmInfo extends Alarm { originatorName: string; } +export interface AlarmDataInfo extends AlarmInfo { + [key: string]: any; +} + export const simulatedAlarm: AlarmInfo = { id: new AlarmId(NULL_UUID), tenantId: new TenantId(NULL_UUID), diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index f45c79967e..e1f78b1e29 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -20,10 +20,11 @@ import { SortDirection } from '@angular/material/sort'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { EntityInfo } from '@shared/models/entity.models'; import { EntityType } from '@shared/models/entity-type.models'; -import { Datasource, DatasourceType } from '@shared/models/widget.models'; +import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models'; import { PageData } from '@shared/models/page/page-data'; import { isDefined, isEqual } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; +import { AlarmInfo, AlarmSearchStatus, AlarmSeverity } from '../alarm.models'; export enum EntityKeyType { ATTRIBUTE = 'ATTRIBUTE', @@ -31,7 +32,8 @@ export enum EntityKeyType { SHARED_ATTRIBUTE = 'SHARED_ATTRIBUTE', SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE', TIME_SERIES = 'TIME_SERIES', - ENTITY_FIELD = 'ENTITY_FIELD' + ENTITY_FIELD = 'ENTITY_FIELD', + ALARM_FIELD = 'ENTITY_FIELD' } export const entityKeyTypeTranslationMap = new Map( @@ -53,6 +55,23 @@ export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKe return DataKeyType.timeseries; case EntityKeyType.ENTITY_FIELD: return DataKeyType.entityField; + case EntityKeyType.ALARM_FIELD: + return DataKeyType.alarm; + } +} + +export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyType { + switch (dataKeyType) { + case DataKeyType.timeseries: + return EntityKeyType.TIME_SERIES; + case DataKeyType.attribute: + return EntityKeyType.ATTRIBUTE; + case DataKeyType.function: + return EntityKeyType.ENTITY_FIELD; + case DataKeyType.alarm: + return EntityKeyType.ALARM_FIELD; + case DataKeyType.entityField: + return EntityKeyType.ENTITY_FIELD; } } @@ -61,6 +80,14 @@ export interface EntityKey { key: string; } +export function dataKeyToEntityKey(dataKey: DataKey): EntityKey { + const entityKey: EntityKey = { + key: dataKey.name, + type: dataKeyTypeToEntityKeyType(dataKey.type) + }; + return entityKey; +} + export enum EntityKeyValueType { STRING = 'STRING', NUMERIC = 'NUMERIC', @@ -479,6 +506,16 @@ export interface EntityDataPageLink { dynamic?: boolean; } +export interface AlarmDataPageLink extends EntityDataPageLink { + startTs?: number; + endTs?: number; + timeWindow?: number; + typeList?: Array; + statusList?: Array; + severityList?: Array; + searchPropagatedAlarms?: boolean; +} + export function entityDataPageLinkSortDirection(pageLink: EntityDataPageLink): SortDirection { if (pageLink.sortOrder) { return (pageLink.sortOrder.direction + '').toLowerCase() as SortDirection; @@ -508,13 +545,19 @@ export interface EntityCountQuery { entityFilter: EntityFilter; } -export interface EntityDataQuery extends EntityCountQuery { - pageLink: EntityDataPageLink; +export interface AbstractDataQuery extends EntityCountQuery { + pageLink: T; entityFields?: Array; latestValues?: Array; keyFilters?: Array; } +export interface EntityDataQuery extends AbstractDataQuery { +} + +export interface AlarmDataQuery extends AbstractDataQuery { +} + export interface TsValue { ts: number; value: string; @@ -526,6 +569,11 @@ export interface EntityData { timeseries: {[key: string]: Array}; } +export interface AlarmData extends AlarmInfo { + entityId: string; + latest: {[entityKeyType: string]: {[key: string]: TsValue}}; +} + export function entityPageDataChanged(prevPageData: PageData, nextPageData: PageData): boolean { const prevIds = prevPageData.data.map((entityData) => entityData.entityId.id); const nextIds = nextPageData.data.map((entityData) => entityData.entityId.id); diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 562a4d7eb4..b92f9244db 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -21,7 +21,7 @@ import { Observable, ReplaySubject, Subject } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { map } from 'rxjs/operators'; import { NgZone } from '@angular/core'; -import { EntityData, EntityDataQuery, EntityKey } from '@shared/models/query/query.models'; +import { AlarmData, AlarmDataQuery, EntityData, EntityDataQuery, EntityKey } from '@shared/models/query/query.models'; import { PageData } from '@shared/models/page/page-data'; export enum DataKeyType { @@ -165,16 +165,31 @@ export class EntityDataCmd implements WebsocketCmd { } } +export class AlarmDataCmd implements WebsocketCmd { + cmdId: number; + query?: AlarmDataQuery; + + public isEmpty(): boolean { + return !this.query; + } +} + export class EntityDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } +export class AlarmDataUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + export class TelemetryPluginCmdsWrapper { attrSubCmds: Array; tsSubCmds: Array; historyCmds: Array; entityDataCmds: Array; entityDataUnsubscribeCmds: Array; + alarmDataCmds: Array; + alarmDataUnsubscribeCmds: Array; constructor() { this.attrSubCmds = []; @@ -182,6 +197,8 @@ export class TelemetryPluginCmdsWrapper { this.historyCmds = []; this.entityDataCmds = []; this.entityDataUnsubscribeCmds = []; + this.alarmDataCmds = []; + this.alarmDataUnsubscribeCmds = []; } public hasCommands(): boolean { @@ -189,7 +206,9 @@ export class TelemetryPluginCmdsWrapper { this.historyCmds.length > 0 || this.attrSubCmds.length > 0 || this.entityDataCmds.length > 0 || - this.entityDataUnsubscribeCmds.length > 0; + this.entityDataUnsubscribeCmds.length > 0 || + this.alarmDataCmds.length > 0 || + this.alarmDataUnsubscribeCmds.length > 0; } public clear() { @@ -198,6 +217,8 @@ export class TelemetryPluginCmdsWrapper { this.historyCmds.length = 0; this.entityDataCmds.length = 0; this.entityDataUnsubscribeCmds.length = 0; + this.alarmDataCmds.length = 0; + this.alarmDataUnsubscribeCmds.length = 0; } public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { @@ -212,6 +233,10 @@ export class TelemetryPluginCmdsWrapper { preparedWrapper.entityDataCmds = this.popCmds(this.entityDataCmds, leftCount); leftCount -= preparedWrapper.entityDataCmds.length; preparedWrapper.entityDataUnsubscribeCmds = this.popCmds(this.entityDataUnsubscribeCmds, leftCount); + leftCount -= preparedWrapper.entityDataUnsubscribeCmds.length; + preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); + leftCount -= preparedWrapper.alarmDataCmds.length; + preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); return preparedWrapper; } @@ -239,18 +264,38 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { errorMsg: string; } -export interface EntityDataUpdateMsg { +export enum DataUpdateType { + ENTITY_DATA = 'ENTITY_DATA', + ALARM_DATA = 'ALARM_DATA' +} + +export interface DataUpdateMsg { cmdId: number; - data?: PageData; - update?: Array; + data?: PageData; + update?: Array; errorCode: number; errorMsg: string; + dataUpdateType: DataUpdateType; +} + +export interface EntityDataUpdateMsg extends DataUpdateMsg { + dataUpdateType: DataUpdateType.ENTITY_DATA; } -export type WebsocketDataMsg = EntityDataUpdateMsg | SubscriptionUpdateMsg; +export interface AlarmDataUpdateMsg extends DataUpdateMsg { + dataUpdateType: DataUpdateType.ALARM_DATA; +} + +export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | SubscriptionUpdateMsg; export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg { - return (message as EntityDataUpdateMsg).cmdId !== undefined; + const updateMsg = (message as DataUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA; +} + +export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { + const updateMsg = (message as DataUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA; } export class SubscriptionUpdate implements SubscriptionUpdateMsg { @@ -302,19 +347,33 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg { } } -export class EntityDataUpdate implements EntityDataUpdateMsg { +export class DataUpdate implements DataUpdateMsg { cmdId: number; errorCode: number; errorMsg: string; - data?: PageData; - update?: Array; + data?: PageData; + update?: Array; + dataUpdateType: DataUpdateType; - constructor(msg: EntityDataUpdateMsg) { + constructor(msg: DataUpdateMsg) { this.cmdId = msg.cmdId; this.errorCode = msg.errorCode; this.errorMsg = msg.errorMsg; this.data = msg.data; this.update = msg.update; + this.dataUpdateType = msg.dataUpdateType; + } +} + +export class EntityDataUpdate extends DataUpdate { + constructor(msg: EntityDataUpdateMsg) { + super(msg); + } +} + +export class AlarmDataUpdate extends DataUpdate { + constructor(msg: AlarmDataUpdateMsg) { + super(msg); } } @@ -328,6 +387,7 @@ export class TelemetrySubscriber { private dataSubject = new ReplaySubject(1); private entityDataSubject = new ReplaySubject(1); + private alarmDataSubject = new ReplaySubject(1); private reconnectSubject = new Subject(); private zone: NgZone; @@ -336,6 +396,7 @@ export class TelemetrySubscriber { public data$ = this.dataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable(); + public alarmData$ = this.alarmDataSubject.asObservable(); public reconnect$ = this.reconnectSubject.asObservable(); public static createEntityAttributesSubscription(telemetryService: TelemetryService, @@ -379,6 +440,7 @@ export class TelemetrySubscriber { public complete() { this.dataSubject.complete(); this.entityDataSubject.complete(); + this.alarmDataSubject.complete(); this.reconnectSubject.complete(); } @@ -416,6 +478,18 @@ export class TelemetrySubscriber { } } + public onAlarmData(message: AlarmDataUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.alarmDataSubject.next(message); + } + ); + } else { + this.alarmDataSubject.next(message); + } + } + public onReconnected() { this.reconnectSubject.next(); }