UI: WS Alarm Data

This commit is contained in:
Igor Kulikov 2020-07-06 14:42:36 +03:00
parent 15e302e06c
commit dee4625fa9
26 changed files with 452 additions and 458 deletions

View File

@ -16,9 +16,9 @@
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStatusFilter\": {\n \"title\": \"Enable alarm status filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStatusFilter\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable alarm filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableFilter\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStatusFilter\":true,\"enableStickyAction\":false},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}"
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}"
}
}
]

View File

@ -293,7 +293,7 @@ export class AliasController implements IAliasController {
}
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource> {
return this.resolveDatasource(alarmSource, true).pipe(
return this.resolveDatasource(alarmSource).pipe(
map((datasource) => {
if (datasource.type === DatasourceType.function) {
let name: string;

View File

@ -29,11 +29,9 @@ import {
} from '@shared/models/widget.models';
import { TimeService } from '../services/time.service';
import { DeviceService } from '../http/device.service';
import { AlarmService } from '../http/alarm.service';
import { UtilsService } from '@core/services/utils.service';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { EntityAliases } from '@shared/models/alias.models';
@ -41,11 +39,13 @@ import { EntityInfo } from '@app/shared/models/entity.models';
import { IDashboardComponent } from '@home/models/dashboard-component.models';
import * as moment_ from 'moment';
import {
AlarmData, AlarmDataPageLink,
AlarmData,
AlarmDataPageLink,
EntityData,
EntityDataPageLink,
EntityFilter,
Filter, FilterInfo,
Filter,
FilterInfo,
Filters,
KeyFilter
} from '@shared/models/query/query.models';
@ -220,10 +220,6 @@ export interface WidgetSubscriptionOptions {
type?: widgetType;
stateData?: boolean;
alarmSource?: Datasource;
/* alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;
alarmsMaxCountLoad?: number;
alarmsFetchSize?: number; */
datasources?: Array<Datasource>;
hasDataPageLink?: boolean;
singleEntity?: boolean;
@ -273,8 +269,6 @@ export interface IWidgetSubscription {
alarms?: PageData<AlarmData>;
alarmSource?: Datasource;
/* alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number; */
targetDeviceAliasIds?: Array<string>;
targetDeviceIds?: Array<string>;

View File

@ -104,30 +104,10 @@ export class WidgetSubscription implements IWidgetSubscription {
comparisonTimeWindow: WidgetTimewindow;
timewindowForComparison: SubscriptionTimewindow;
// alarms: Array<AlarmInfo>;
alarms: PageData<AlarmData>;
alarmSource: Datasource;
/* private alarmSearchStatusValue: AlarmSearchStatus;
set alarmSearchStatus(value: AlarmSearchStatus) {
if (this.alarmSearchStatusValue !== value) {
this.alarmSearchStatusValue = value;
this.onAlarmSearchStatusChanged();
}
}
get alarmSearchStatus(): AlarmSearchStatus {
return this.alarmSearchStatusValue;
}*/
alarmDataListener: AlarmDataListener;
/* alarmsPollingInterval: number;
alarmsMaxCountLoad: number;
alarmsFetchSize: number;
alarmSourceListener: AlarmSourceListener;*/
loadingData: boolean;
targetDeviceAliasIds?: Array<string>;
@ -186,17 +166,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) ?
options.alarmSearchStatus : AlarmSearchStatus.ANY;
this.alarmsPollingInterval = isDefined(options.alarmsPollingInterval) ?
options.alarmsPollingInterval : 5000;
this.alarmsMaxCountLoad = isDefined(options.alarmsMaxCountLoad) ?
options.alarmsMaxCountLoad : 0;
this.alarmsFetchSize = isDefined(options.alarmsFetchSize) ?
options.alarmsFetchSize : 100;
this.alarmSourceListener = null;*/
this.alarmDataListener = null;
// this.alarms = [];
this.alarms = emptyPageData();
this.originalTimewindow = null;
this.timeWindow = {};
@ -834,9 +804,6 @@ export class WidgetSubscription implements IWidgetSubscription {
}
if (this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
this.alarmDataListener = {
subscriptionTimewindow: this.subscriptionTimewindow,
@ -850,9 +817,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.ctx.alarmDataService.subscribeForAlarms(this.alarmDataListener, pageLink, keyFilters);
let forceUpdate = false;
if (this.alarmSource.unresolvedStateEntity ||
(this.alarmSource.type === DatasourceType.entity && !this.alarmSource.entityId)
) {
if (this.alarmSource.unresolvedStateEntity) {
forceUpdate = true;
}
if (forceUpdate) {
@ -892,41 +857,6 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
/* private alarmsSubscribe() {
this.notifyDataLoading();
if (this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
}
this.alarmSourceListener = {
subscriptionTimewindow: this.subscriptionTimewindow,
alarmSource: this.alarmSource,
alarmSearchStatus: this.alarmSearchStatus,
alarmsPollingInterval: this.alarmsPollingInterval,
alarmsMaxCountLoad: this.alarmsMaxCountLoad,
alarmsFetchSize: this.alarmsFetchSize,
alarmsUpdated: alarms => this.alarmsUpdated(alarms)
};
this.alarms = emptyPageData();
this.ctx.alarmDataService.subscribeForAlarms(this.alarmDataListener);
let forceUpdate = false;
if (this.alarmSource.unresolvedStateEntity ||
(this.alarmSource.type === DatasourceType.entity && !this.alarmSource.entityId)
) {
forceUpdate = true;
}
if (forceUpdate) {
this.notifyDataLoaded();
this.onDataUpdated();
}
} */
unsubscribe() {
if (this.type !== widgetType.rpc) {
if (this.type === widgetType.alarm) {

View File

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { EMPTY, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
@ -26,55 +26,15 @@ import {
AlarmQuery,
AlarmSearchStatus,
AlarmSeverity,
AlarmStatus,
simulatedAlarm
AlarmStatus
} from '@shared/models/alarm.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Datasource, DatasourceType } from '@shared/models/widget.models';
import { SubscriptionTimewindow } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { TimePageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { concatMap, expand, map, toArray } from 'rxjs/operators';
import { isDefined } from '@core/utils';
import Timeout = NodeJS.Timeout;
interface AlarmSourceListenerQuery {
entityType: EntityType;
entityId: string;
alarmSearchStatus: AlarmSearchStatus;
alarmStatus: AlarmStatus;
alarmsMaxCountLoad: number;
alarmsFetchSize: number;
fetchOriginator?: boolean;
limit?: number;
interval?: number;
startTime?: number;
endTime?: number;
onAlarms?: (alarms: Array<AlarmInfo>) => void;
}
export interface AlarmSourceListener {
id?: string;
subscriptionTimewindow: SubscriptionTimewindow;
alarmSource: Datasource;
alarmsPollingInterval: number;
alarmSearchStatus: AlarmSearchStatus;
alarmsMaxCountLoad: number;
alarmsFetchSize: number;
alarmsUpdated: (alarms: Array<AlarmInfo>) => void;
lastUpdateTs?: number;
alarmsQuery?: AlarmSourceListenerQuery;
pollTimer?: Timeout;
}
@Injectable({
providedIn: 'root'
})
export class AlarmService {
private alarmSourceListeners: {[id: string]: AlarmSourceListener} = {};
constructor(
private http: HttpClient,
private utils: UtilsService
@ -122,128 +82,4 @@ export class AlarmService {
defaultHttpOptionsFromConfig(config));
}
public subscribeForAlarms(alarmSourceListener: AlarmSourceListener): void {
alarmSourceListener.id = this.utils.guid();
this.alarmSourceListeners[alarmSourceListener.id] = alarmSourceListener;
const alarmSource = alarmSourceListener.alarmSource;
if (alarmSource.type === DatasourceType.function) {
setTimeout(() => {
alarmSourceListener.alarmsUpdated([simulatedAlarm]);
}, 0);
} else if (alarmSource.entityType && alarmSource.entityId) {
const pollingInterval = alarmSourceListener.alarmsPollingInterval;
alarmSourceListener.alarmsQuery = {
entityType: alarmSource.entityType,
entityId: alarmSource.entityId,
alarmSearchStatus: alarmSourceListener.alarmSearchStatus,
alarmStatus: null,
alarmsMaxCountLoad: alarmSourceListener.alarmsMaxCountLoad,
alarmsFetchSize: alarmSourceListener.alarmsFetchSize
};
const originatorKeys = alarmSource.dataKeys.filter(dataKey => dataKey.name.toLocaleLowerCase().includes('originator'));
if (originatorKeys.length) {
alarmSourceListener.alarmsQuery.fetchOriginator = true;
}
const subscriptionTimewindow = alarmSourceListener.subscriptionTimewindow;
if (subscriptionTimewindow.realtimeWindowMs) {
alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.startTs;
} else {
alarmSourceListener.alarmsQuery.startTime = subscriptionTimewindow.fixedWindow.startTimeMs;
alarmSourceListener.alarmsQuery.endTime = subscriptionTimewindow.fixedWindow.endTimeMs;
}
alarmSourceListener.alarmsQuery.onAlarms = (alarms) => {
if (subscriptionTimewindow.realtimeWindowMs) {
const now = Date.now();
if (alarmSourceListener.lastUpdateTs) {
const interval = now - alarmSourceListener.lastUpdateTs;
alarmSourceListener.alarmsQuery.startTime += interval;
}
alarmSourceListener.lastUpdateTs = now;
}
alarmSourceListener.alarmsUpdated(alarms);
};
this.onPollAlarms(alarmSourceListener.alarmsQuery);
alarmSourceListener.pollTimer = setInterval(this.onPollAlarms.bind(this), pollingInterval, alarmSourceListener.alarmsQuery);
}
}
public unsubscribeFromAlarms(alarmSourceListener: AlarmSourceListener): void {
if (alarmSourceListener && alarmSourceListener.id) {
if (alarmSourceListener.pollTimer) {
clearInterval(alarmSourceListener.pollTimer);
alarmSourceListener.pollTimer = null;
}
delete this.alarmSourceListeners[alarmSourceListener.id];
}
}
private onPollAlarms(alarmsQuery: AlarmSourceListenerQuery): void {
this.getAlarmsByAlarmSourceQuery(alarmsQuery).subscribe((alarms) => {
alarmsQuery.onAlarms(alarms);
});
}
private getAlarmsByAlarmSourceQuery(alarmsQuery: AlarmSourceListenerQuery): Observable<Array<AlarmInfo>> {
const time = Date.now();
let pageLink: TimePageLink;
const sortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC};
if (alarmsQuery.limit) {
pageLink = new TimePageLink(alarmsQuery.limit, 0,
null,
sortOrder);
} else if (alarmsQuery.interval) {
pageLink = new TimePageLink(alarmsQuery.alarmsFetchSize || 100, 0,
null,
sortOrder, time - alarmsQuery.interval);
} else if (alarmsQuery.startTime) {
pageLink = new TimePageLink(alarmsQuery.alarmsFetchSize || 100, 0,
null,
sortOrder, Math.round(alarmsQuery.startTime));
if (alarmsQuery.endTime) {
pageLink.endTime = Math.round(alarmsQuery.endTime);
}
}
let leftToLoad;
if (isDefined(alarmsQuery.alarmsMaxCountLoad) && alarmsQuery.alarmsMaxCountLoad !== 0) {
leftToLoad = alarmsQuery.alarmsMaxCountLoad;
if (leftToLoad < pageLink.pageSize) {
pageLink.pageSize = leftToLoad;
}
}
return this.fetchAlarms(alarmsQuery, pageLink, leftToLoad);
}
private fetchAlarms(query: AlarmSourceListenerQuery,
pageLink: TimePageLink, leftToLoad?: number): Observable<Array<AlarmInfo>> {
const alarmQuery = new AlarmQuery(
{id: query.entityId, entityType: query.entityType},
pageLink,
query.alarmSearchStatus,
query.alarmStatus,
query.fetchOriginator,
null);
return this.getAlarms(alarmQuery, {ignoreLoading: true, ignoreErrors: true}).pipe(
expand((data) => {
let continueLoad = data.hasNext && !query.limit;
if (continueLoad && isDefined(leftToLoad)) {
leftToLoad -= data.data.length;
if (leftToLoad === 0) {
continueLoad = false;
} else if (leftToLoad < alarmQuery.pageLink.pageSize) {
alarmQuery.pageLink.pageSize = leftToLoad;
}
}
if (continueLoad) {
alarmQuery.offset = data.data[data.data.length-1].id.id;
return this.getAlarms(alarmQuery, {ignoreLoading: true});
} else {
return EMPTY;
}
}),
map((data) => data.data),
concatMap((data) => data),
toArray(),
map((data) => data.sort((a, b) => alarmQuery.pageLink.sort(a, b))),
);
}
}

View File

@ -61,9 +61,10 @@ import {
EntityKey,
EntityKeyType,
FilterPredicateType,
singleEntityDataPageLink, StringFilterPredicate,
singleEntityDataPageLink,
StringOperation
} from '@shared/models/query/query.models';
import { alarmFields } from '@shared/models/alarm.models';
@Injectable({
providedIn: 'root'
@ -622,10 +623,18 @@ export class EntityService {
return query ? entityFieldKeys.filter((entityField) => entityField.toLowerCase().indexOf(query) === 0) : entityFieldKeys;
}
private getAlarmKeys(searchText: string): Array<string> {
const alarmKeys: string[] = Object.keys(alarmFields);
const query = searchText.toLowerCase();
return query ? alarmKeys.filter((alarmField) => alarmField.toLowerCase().indexOf(query) === 0) : alarmKeys;
}
public getEntityKeys(entityId: EntityId, query: string, type: DataKeyType,
config?: RequestConfig): Observable<Array<string>> {
if (type === DataKeyType.entityField) {
return of(this.getEntityFieldKeys(entityId.entityType as EntityType, query));
} else if (type === DataKeyType.alarm) {
return of(this.getAlarmKeys(query));
}
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/keys/`;
if (type === DataKeyType.timeseries) {

View File

@ -32,6 +32,7 @@
<tb-data-key-config #dataKeyConfig
[dataKeySettingsSchema]="data.dataKeySettingsSchema"
[entityAliasId]="data.entityAliasId"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"
formControlName="dataKey">
</tb-data-key-config>

View File

@ -30,6 +30,7 @@ export interface DataKeyConfigDialogData {
dataKey: DataKey;
dataKeySettingsSchema: any;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;
}

View File

@ -72,7 +72,7 @@
formControlName="funcBody">
</tb-js-func>
</section>
<section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute">
<section fxLayout="column" *ngIf="(modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute) && showPostProcessing">
<mat-checkbox formControlName="usePostProcessing">
{{ 'datakey.use-data-post-processing-func' | translate }}
</mat-checkbox>

View File

@ -71,6 +71,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
dataKeySettingsSchema: any;
@Input()
showPostProcessing = true;
@ViewChild('keyInput') keyInput: ElementRef;
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;

View File

@ -31,7 +31,12 @@
</div>
<div class="tb-chip-labels">
<div class="tb-chip-label">
<span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm">
<span *ngIf="datasourceType !== datasourceTypes.function">
<span *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-mat-20">notifications</mat-icon>
</span>
<span *ngIf="key.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above">
@ -86,7 +91,12 @@
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;">
<span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm">
<span *ngIf="datasourceType !== datasourceTypes.function">
<span *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-mat-16">notifications</mat-icon>
</span>
<span *ngIf="key.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above">
@ -118,11 +128,17 @@
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="datasourceType === datasourceTypes.function || widgetType === widgetTypes.alarm; else createEntityKey">
<span *ngIf="datasourceType === datasourceTypes.function; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>
<span>{{'entity.create-new-key' | translate }} </span>
<span *ngIf="widgetType == widgetTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">
<mat-icon (click)="createKey(searchText, dataKeyTypes.alarm)"
class="tb-mat-16">notifications</mat-icon>
</span>
<span *ngIf="widgetType == widgetTypes.latest"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above">

View File

@ -218,11 +218,6 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
}
private updateParams() {
if (this.widgetType === widgetType.alarm) {
this.dataKeyType = DataKeyType.alarm;
this.placeholder = this.translate.instant('datakey.alarm');
this.requiredText = this.translate.instant('datakey.alarm-fields-required');
} else {
if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function;
this.placeholder = this.translate.instant('datakey.function-types');
@ -231,6 +226,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.widgetType === widgetType.latest) {
this.dataKeyType = null;
this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required');
} else if (this.widgetType === widgetType.alarm) {
this.dataKeyType = null;
this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required');
} else {
this.dataKeyType = DataKeyType.timeseries;
this.requiredText = this.translate.instant('datakey.timeseries-required');
@ -238,7 +236,6 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
this.placeholder = '';
}
}
}
private reset() {
if (this.widgetType === widgetType.alarm) {
@ -387,6 +384,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dataKey: deepClone(key),
dataKeySettingsSchema: this.datakeySettingsSchema,
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks
}
}).afterClosed().subscribe((updatedDataKey) => {
@ -411,16 +409,19 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.latestSearchTextResult === null || this.searchText !== searchText) {
this.searchText = searchText;
let fetchObservable: Observable<Array<DataKey>> = null;
if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) {
if (this.datasourceType === DatasourceType.function) {
const dataKeyFilter = this.createDataKeyFilter(this.searchText);
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList.filter(dataKeyFilter));
} else {
if (this.entityAliasId) {
const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest) {
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute);
dataKeyTypes.push(DataKeyType.entityField);
if (this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.alarm);
}
}
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes);
} else {

View File

@ -0,0 +1,66 @@
<!--
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.
-->
<form fxLayout="column" class="mat-content mat-padding" [formGroup]="alarmFilterFormGroup" (ngSubmit)="update()">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" multiple
placeholder="{{ !alarmFilterFormGroup.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-select formControlName="alarmSeverityList" multiple
placeholder="{{ !alarmFilterFormGroup.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-list #alarmTypeChipList formControlName="alarmTypeList">
<mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="{{ !alarmFilterFormGroup.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
</mat-form-field>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="submit"
mat-raised-button
color="primary"
[disabled]="alarmFilterFormGroup.invalid || !alarmFilterFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
<button type="button"
mat-button
(click)="cancel()"
style="margin-right: 20px;">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

View File

@ -0,0 +1,119 @@
///
/// 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 { Component, Inject, InjectionToken } from '@angular/core';
import {
AlarmSearchStatus,
alarmSearchStatusTranslations,
AlarmSeverity,
alarmSeverityTranslations
} from '@shared/models/alarm.models';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { OverlayRef } from '@angular/cdk/overlay';
export const ALARM_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmFilterPanelData');
export interface AlarmFilterPanelData {
statusList: AlarmSearchStatus[];
severityList: AlarmSeverity[];
typeList: string[];
}
@Component({
selector: 'tb-alarm-filter-panel',
templateUrl: './alarm-filter-panel.component.html',
styleUrls: ['./alarm-filter-panel.component.scss']
})
export class AlarmFilterPanelComponent {
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
alarmFilterFormGroup: FormGroup;
result: AlarmFilterPanelData;
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE,
AlarmSearchStatus.CLEARED,
AlarmSearchStatus.ACK,
AlarmSearchStatus.UNACK];
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
constructor(@Inject(ALARM_FILTER_PANEL_DATA)
public data: AlarmFilterPanelData,
public overlayRef: OverlayRef,
private fb: FormBuilder) {
this.alarmFilterFormGroup = this.fb.group(
{
alarmStatusList: [this.data.statusList],
alarmSeverityList: [this.data.severityList],
alarmTypeList: [this.data.typeList]
}
);
}
public alarmTypeList(): string[] {
return this.alarmFilterFormGroup.get('alarmTypeList').value;
}
public removeAlarmType(type: string): void {
const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value;
const index = types.indexOf(type);
if (index >= 0) {
types.splice(index, 1);
this.alarmFilterFormGroup.get('alarmTypeList').setValue(types);
this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty();
}
}
public addAlarmType(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
const types: string[] = this.alarmFilterFormGroup.get('alarmTypeList').value;
if ((value || '').trim()) {
types.push(value.trim());
this.alarmFilterFormGroup.get('alarmTypeList').setValue(types);
this.alarmFilterFormGroup.get('alarmTypeList').markAsDirty();
}
if (input) {
input.value = '';
}
}
update() {
this.result = {
statusList: this.alarmFilterFormGroup.get('alarmStatusList').value,
severityList: this.alarmFilterFormGroup.get('alarmSeverityList').value,
typeList: this.alarmFilterFormGroup.get('alarmTypeList').value
};
this.overlayRef.dispose();
}
cancel() {
this.overlayRef.dispose();
}
}

View File

@ -1,25 +0,0 @@
<!--
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.
-->
<div fxLayout="column" class="mat-content mat-padding">
<label class="tb-title" translate>alarm.alarm-status-filter</label>
<mat-radio-group [(ngModel)]="subscription.alarmSearchStatus" fxLayout="column" fxLayoutGap="16px">
<mat-radio-button *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus" color="primary">
{{ alarmSearchStatusTranslationMap.get(alarmSearchStatusEnum[searchStatus]) | translate }}
</mat-radio-button>
</mat-radio-group>
</div>

View File

@ -1,43 +0,0 @@
///
/// 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 { Component, Inject, InjectionToken } from '@angular/core';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models';
export const ALARM_STATUS_FILTER_PANEL_DATA = new InjectionToken<any>('AlarmStatusFilterPanelData');
export interface AlarmStatusFilterPanelData {
subscription: IWidgetSubscription;
}
@Component({
selector: 'tb-alarm-status-filter-panel',
templateUrl: './alarm-status-filter-panel.component.html',
styleUrls: ['./alarm-status-filter-panel.component.scss']
})
export class AlarmStatusFilterPanelComponent {
subscription: IWidgetSubscription;
alarmSearchStatuses = Object.keys(AlarmSearchStatus);
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
alarmSearchStatusEnum = AlarmSearchStatus;
constructor(@Inject(ALARM_STATUS_FILTER_PANEL_DATA) public data: AlarmStatusFilterPanelData) {
this.subscription = this.data.subscription;
}
}

View File

@ -73,17 +73,13 @@ import {
import {
AlarmDataInfo,
alarmFields,
AlarmSearchStatus,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common';
import {
ALARM_STATUS_FILTER_PANEL_DATA,
AlarmStatusFilterPanelComponent,
AlarmStatusFilterPanelData
} from '@home/components/widget/lib/alarm-status-filter-panel.component';
import {
AlarmDetailsDialogComponent,
AlarmDetailsDialogData
@ -101,11 +97,18 @@ import {
KeyFilter
} from '@app/shared/models/query/query.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
ALARM_FILTER_PANEL_DATA,
AlarmFilterPanelComponent,
AlarmFilterPanelData
} from '@home/components/widget/lib/alarm-filter-panel.component';
import { entityFields } from '@shared/models/entity.models';
interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string;
enableSelection: boolean;
enableStatusFilter: boolean;
enableStatusFilter?: boolean;
enableFilter: boolean;
enableStickyAction: boolean;
displayDetails: boolean;
allowAcknowledgment: boolean;
@ -178,11 +181,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
};
private statusFilterAction: WidgetAction = {
name: 'alarm.alarm-status-filter',
private alarmFilterAction: WidgetAction = {
name: 'alarm.alarm-filter',
show: true,
onAction: ($event) => {
this.editAlarmStatusFilter($event);
this.editAlarmFilter($event);
},
icon: 'filter_list'
};
@ -255,7 +258,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.statusFilterAction, this.columnDisplayAction];
this.ctx.widgetActions = [this.searchAction, this.alarmFilterAction, this.columnDisplayAction];
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true;
@ -312,7 +315,15 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.statusFilterAction.show = isDefined(this.settings.enableStatusFilter) ? this.settings.enableStatusFilter : true;
let enableFilter;
if (isDefined(this.settings.enableFilter)) {
enableFilter = this.settings.enableFilter;
} else if (isDefined(this.settings.enableStatusFilter)) {
enableFilter = this.settings.enableStatusFilter;
} else {
enableFilter = true;
}
this.alarmFilterAction.show = enableFilter;
const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
@ -321,11 +332,17 @@ 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 = [];
this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms)
? this.widgetConfig.searchPropagatedAlarms : true;
let alarmStatusList: AlarmSearchStatus[] = [];
if (isDefined(this.widgetConfig.alarmStatusList) && this.widgetConfig.alarmStatusList.length) {
alarmStatusList = this.widgetConfig.alarmStatusList;
} else if (isDefined(this.widgetConfig.alarmSearchStatus) && this.widgetConfig.alarmSearchStatus !== AlarmSearchStatus.ANY) {
alarmStatusList = [this.widgetConfig.alarmSearchStatus];
}
this.pageLink.statusList = alarmStatusList;
this.pageLink.severityList = isDefined(this.widgetConfig.alarmSeverityList) ? this.widgetConfig.alarmSeverityList : [];
this.pageLink.typeList = isDefined(this.widgetConfig.alarmTypeList) ? this.widgetConfig.alarmTypeList : [];
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
@ -440,7 +457,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.ctx.detectChanges();
}
private editAlarmStatusFilter($event: Event) {
private editAlarmFilter($event: Event) {
if ($event) {
$event.stopPropagation();
}
@ -462,14 +479,25 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
overlayRef.dispose();
});
const injectionTokens = new WeakMap<any, any>([
[ALARM_STATUS_FILTER_PANEL_DATA, {
subscription: this.subscription,
} as AlarmStatusFilterPanelData],
[ALARM_FILTER_PANEL_DATA, {
statusList: this.pageLink.statusList,
severityList: this.pageLink.severityList,
typeList: this.pageLink.typeList
} as AlarmFilterPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
overlayRef.attach(new ComponentPortal(AlarmStatusFilterPanelComponent,
const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result) {
const result = componentRef.instance.result;
this.pageLink.statusList = result.statusList;
this.pageLink.severityList = result.severityList;
this.pageLink.typeList = result.typeList;
this.updateData();
}
});
this.ctx.detectChanges();
}
@ -555,7 +583,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
content = '' + value;
}
} else {
content = this.defaultContent(key, value);
content = this.defaultContent(key, contentInfo, value);
}
return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
} else {
@ -750,7 +778,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
}
}
private defaultContent(key: EntityColumn, value: any): any {
private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
@ -765,9 +793,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} else {
return value;
}
} else {
return value;
}
const entityField = entityFields[key.name];
if (entityField) {
if (entityField.time) {
return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss');
}
}
const decimals = (contentInfo.decimals || contentInfo.decimals === 0) ? contentInfo.decimals : this.ctx.widgetConfig.decimals;
const units = contentInfo.units || this.ctx.widgetConfig.units;
return this.ctx.utils.formatValue(value, decimals, units, true);
} else {
return '';
}
@ -827,13 +862,20 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
}
loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) {
this.dataLoading = true;
this.clear();
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.subscription.subscribeForAlarms(pageLink, keyFilters);
}
private clear() {
if (this.selection.hasValue()) {
this.selection.clear();
this.onSelectionModeChanged(false);
}
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.subscription.subscribeForAlarms(pageLink, keyFilters);
this.alarmsSubject.next([]);
this.pageDataSubject.next(emptyPageData<AlarmDataInfo>());
}
updateAlarms() {

View File

@ -86,7 +86,6 @@ import {
} from '@shared/models/query/query.models';
import { sortItems } from '@shared/models/page/page-link';
import { entityFields } from '@shared/models/entity.models';
import { alarmFields } from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common';
interface EntitiesTableWidgetSettings extends TableWidgetSettings {
@ -596,11 +595,17 @@ class EntityDatasource implements DataSource<EntityData> {
loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) {
this.dataLoading = true;
this.clear();
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.subscription.subscribeForPaginatedData(0, pageLink, keyFilters);
}
private clear() {
this.entitiesSubject.next([]);
this.pageDataSubject.next(emptyPageData<EntityData>());
}
dataUpdated() {
const datasourcesPageData = this.subscription.datasourcePages[0];
const dataPageData = this.subscription.dataPages[0];

View File

@ -20,7 +20,7 @@ import { SharedModule } from '@app/shared/shared.module';
import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component';
import { AlarmStatusFilterPanelComponent } from '@home/components/widget/lib/alarm-status-filter-panel.component';
import { AlarmFilterPanelComponent } from '@home/components/widget/lib/alarm-filter-panel.component';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-widget.component';
@ -40,7 +40,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
declarations:
[
DisplayColumnsPanelComponent,
AlarmStatusFilterPanelComponent,
AlarmFilterPanelComponent,
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent,

View File

@ -42,58 +42,45 @@
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center"
fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.alarm-status</mat-label>
<mat-select formControlName="alarmSearchStatus">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="alarmStatusList" multiple
placeholder="{{ !dataSettings.get('alarmStatusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ ('alarm.search-status.' + searchStatus) | translate }}
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.polling-interval</mat-label>
<input matInput required
formControlName="alarmsPollingInterval"
type="number"
step="1"/>
<mat-error *ngIf="dataSettings.get('alarmsPollingInterval').hasError('required')">
{{ 'alarm.polling-interval-required' | translate }}
</mat-error>
<mat-error *ngIf="dataSettings.get('alarmsPollingInterval').hasError('min')">
{{ 'alarm.min-polling-interval-message' | translate }}
</mat-error>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-select formControlName="alarmSeverityList" multiple
placeholder="{{ !dataSettings.get('alarmSeverityList').value?.length ? ('alarm.any-severity' | translate) : '' }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start center"
fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.max-count-load</mat-label>
<input matInput required
formControlName="alarmsMaxCountLoad"
type="number"
min="0"
step="1">
<mat-error *ngIf="dataSettings.get('alarmsMaxCountLoad').hasError('required')">
{{ 'alarm.max-count-load-required' | translate }}
</mat-error>
<mat-error *ngIf="dataSettings.get('alarmsMaxCountLoad').hasError('min')">
{{ 'alarm.max-count-load-error-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>alarm.fetch-size</mat-label>
<input matInput required
formControlName="alarmsFetchSize"
type="number"
min="10"
step="1">
<mat-error *ngIf="dataSettings.get('alarmsFetchSize').hasError('required')">
{{ 'alarm.fetch-size-required' | translate }}
</mat-error>
<mat-error *ngIf="dataSettings.get('alarmsFetchSize').hasError('min')">
{{ 'alarm.fetch-size-error-min' | translate }}
</mat-error>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-list #alarmTypeChipList formControlName="alarmTypeList">
<mat-chip *ngFor="let type of alarmTypeList()" [selectable]="true"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="{{ !dataSettings.get('alarmTypeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-list>
</mat-form-field>
<mat-checkbox fxFlex formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-checkbox>
</div>
</div>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&

View File

@ -41,7 +41,13 @@ import {
} from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined, isObject } from '@app/core/utils';
import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models';
import {
alarmFields,
AlarmSearchStatus,
alarmSearchStatusTranslations,
AlarmSeverity,
alarmSeverityTranslations
} from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service';
@ -63,6 +69,8 @@ import { DashboardState } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models';
import { Filter, Filters } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -92,11 +100,23 @@ const defaultSettingsForm = [
})
export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
widgetTypes = widgetType;
entityTypes = EntityType;
alarmSearchStatuses = Object.keys(AlarmSearchStatus);
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE,
AlarmSearchStatus.CLEARED,
AlarmSearchStatus.ACK,
AlarmSearchStatus.UNACK];
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
@Input()
forceExpandDatasources: boolean;
@ -293,13 +313,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
});
if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmSearchStatus', this.fb.control(null));
this.dataSettings.addControl('alarmsPollingInterval', this.fb.control(null,
[Validators.required, Validators.min(1)]));
this.dataSettings.addControl('alarmsMaxCountLoad', this.fb.control(null,
[Validators.required, Validators.min(0)]));
this.dataSettings.addControl('alarmsFetchSize', this.fb.control(null,
[Validators.required, Validators.min(10)]));
this.dataSettings.addControl('alarmStatusList', this.fb.control(null));
this.dataSettings.addControl('alarmSeverityList', this.fb.control(null));
this.dataSettings.addControl('alarmTypeList', this.fb.control(null));
this.dataSettings.addControl('searchPropagatedAlarms', this.fb.control(null));
}
}
if (this.modelValue.isDataEnabled) {
@ -445,21 +462,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
targetDeviceAliasId
}, {emitEvent: false});
} else if (this.widgetType === widgetType.alarm) {
let alarmStatusList: AlarmSearchStatus[] = [];
if (isDefined(config.alarmStatusList) && config.alarmStatusList.length) {
alarmStatusList = config.alarmStatusList;
} else if (isDefined(config.alarmSearchStatus) && config.alarmSearchStatus !== AlarmSearchStatus.ANY) {
alarmStatusList = [config.alarmSearchStatus];
}
this.dataSettings.patchValue(
{ alarmSearchStatus: isDefined(config.alarmSearchStatus) ?
config.alarmSearchStatus : AlarmSearchStatus.ANY }, {emitEvent: false}
{ alarmStatusList }, {emitEvent: false}
);
this.dataSettings.patchValue(
{ alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ?
config.alarmsPollingInterval : 5}, {emitEvent: false}
{ alarmSeverityList: isDefined(config.alarmSeverityList) ? config.alarmSeverityList : [] }, {emitEvent: false}
);
this.dataSettings.patchValue(
{ alarmsMaxCountLoad: isDefined(config.alarmsMaxCountLoad) ?
config.alarmsMaxCountLoad : 0}, {emitEvent: false}
{ alarmTypeList: isDefined(config.alarmTypeList) ? config.alarmTypeList : [] }, {emitEvent: false}
);
this.dataSettings.patchValue(
{ alarmsFetchSize: isDefined(config.alarmsFetchSize) ?
config.alarmsFetchSize : 100}, {emitEvent: false}
{ searchPropagatedAlarms: isDefined(config.searchPropagatedAlarms) ?
config.searchPropagatedAlarms : true }, {emitEvent: false}
);
this.alarmSourceSettings.patchValue(
config.alarmSource, {emitEvent: false}
@ -604,6 +624,37 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
public alarmTypeList(): string[] {
return this.dataSettings.get('alarmTypeList').value;
}
public removeAlarmType(type: string): void {
const types: string[] = this.dataSettings.get('alarmTypeList').value;
const index = types.indexOf(type);
if (index >= 0) {
types.splice(index, 1);
this.dataSettings.get('alarmTypeList').setValue(types);
this.dataSettings.get('alarmTypeList').markAsDirty();
}
}
public addAlarmType(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
const types: string[] = this.dataSettings.get('alarmTypeList').value;
if ((value || '').trim()) {
types.push(value.trim());
this.dataSettings.get('alarmTypeList').setValue(types);
this.dataSettings.get('alarmTypeList').markAsDirty();
}
if (input) {
input.value = '';
}
}
public displayAdvanced(): boolean {
return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema;
}

View File

@ -902,14 +902,6 @@ 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) ?
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;*/
} else {
options.datasources = deepClone(this.widget.config.datasources);
}

View File

@ -33,7 +33,7 @@ export enum EntityKeyType {
SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE',
TIME_SERIES = 'TIME_SERIES',
ENTITY_FIELD = 'ENTITY_FIELD',
ALARM_FIELD = 'ENTITY_FIELD'
ALARM_FIELD = 'ALARM_FIELD'
}
export const entityKeyTypeTranslationMap = new Map<EntityKeyType, string>(

View File

@ -19,7 +19,7 @@ import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import * as moment_ from 'moment';
@ -378,10 +378,10 @@ export interface WidgetConfig {
actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
settings?: any;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;
alarmsMaxCountLoad?: number;
alarmsFetchSize?: number;
alarmStatusList?: AlarmSearchStatus[];
alarmSeverityList?: AlarmSeverity[];
alarmTypeList?: string[];
searchPropagatedAlarms?: boolean;
datasources?: Array<Datasource>;
targetDeviceAliasIds?: Array<string>;
[key: string]: any;

View File

@ -128,6 +128,8 @@
"no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required",
"alarm-status": "Alarm status",
"alarm-status-list": "Alarm status list",
"any-status": "Any status",
"search-status": {
"ANY": "Any",
"ACTIVE": "Active",
@ -154,6 +156,8 @@
"end-time": "End time",
"ack-time": "Acknowledged time",
"clear-time": "Cleared time",
"alarm-severity-list": "Alarm severity list",
"any-severity": "Any severity",
"severity-critical": "Critical",
"severity-major": "Major",
"severity-minor": "Minor",
@ -176,12 +180,16 @@
"clear-alarm-title": "Clear Alarm",
"clear-alarm-text": "Are you sure you want to clear Alarm?",
"alarm-status-filter": "Alarm Status Filter",
"alarm-filter": "Alarm Filter",
"max-count-load": "Maximum number of alarms to load (0 - unlimited)",
"max-count-load-required": "Maximum number of alarms to load is required.",
"max-count-load-error-min": "Minimum value is 0.",
"fetch-size": "Fetch size",
"fetch-size-required": "Fetch size is required.",
"fetch-size-error-min": "Minimum value is 10."
"fetch-size-error-min": "Minimum value is 10.",
"alarm-type-list": "Alarm type list",
"any-type": "Any type",
"search-propagated-alarms": "Search propagated alarms"
},
"alias": {
"add": "Add alias",
@ -616,6 +624,7 @@
"alarm": "Alarm fields",
"timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
"alarm-fields-timeseries-or-attributes-required": "Alarm fields or entity timeseries/attributes are required.",
"maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
"alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types",