Browse Source

UI: WS Alarm Data

pull/3068/head
Igor Kulikov 6 years ago
parent
commit
dee4625fa9
  1. 6
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  2. 2
      ui-ngx/src/app/core/api/alias-controller.ts
  3. 14
      ui-ngx/src/app/core/api/widget-api.models.ts
  4. 72
      ui-ngx/src/app/core/api/widget-subscription.ts
  5. 168
      ui-ngx/src/app/core/http/alarm.service.ts
  6. 11
      ui-ngx/src/app/core/http/entity.service.ts
  7. 1
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html
  8. 1
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  10. 3
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  11. 22
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  12. 57
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  13. 66
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html
  14. 0
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss
  15. 119
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts
  16. 25
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.html
  17. 43
      ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.ts
  18. 98
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  19. 7
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  20. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  21. 73
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  22. 85
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  23. 8
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  24. 2
      ui-ngx/src/app/shared/models/query/query.models.ts
  25. 10
      ui-ngx/src/app/shared/models/widget.models.ts
  26. 11
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

@ -16,10 +16,10 @@
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>", "templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "", "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", "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}", "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}"
} }
} }
] ]
} }

2
ui-ngx/src/app/core/api/alias-controller.ts

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

14
ui-ngx/src/app/core/api/widget-api.models.ts

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

72
ui-ngx/src/app/core/api/widget-subscription.ts

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

168
ui-ngx/src/app/core/http/alarm.service.ts

@ -16,7 +16,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { EMPTY, Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
@ -26,55 +26,15 @@ import {
AlarmQuery, AlarmQuery,
AlarmSearchStatus, AlarmSearchStatus,
AlarmSeverity, AlarmSeverity,
AlarmStatus, AlarmStatus
simulatedAlarm
} from '@shared/models/alarm.models'; } 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 { 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AlarmService { export class AlarmService {
private alarmSourceListeners: {[id: string]: AlarmSourceListener} = {};
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private utils: UtilsService private utils: UtilsService
@ -122,128 +82,4 @@ export class AlarmService {
defaultHttpOptionsFromConfig(config)); 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))),
);
}
} }

11
ui-ngx/src/app/core/http/entity.service.ts

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

1
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html

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

1
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts

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

2
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html

@ -72,7 +72,7 @@
formControlName="funcBody"> formControlName="funcBody">
</tb-js-func> </tb-js-func>
</section> </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"> <mat-checkbox formControlName="usePostProcessing">
{{ 'datakey.use-data-post-processing-func' | translate }} {{ 'datakey.use-data-post-processing-func' | translate }}
</mat-checkbox> </mat-checkbox>

3
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts

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

22
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

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

57
ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts

@ -16,26 +16,26 @@
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
SkipSelf, SkipSelf,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
FormGroupDirective, FormGroupDirective,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
NgForm, NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators';
@ -56,8 +56,8 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigDialogData DataKeyConfigDialogData
} from '@home/components/widget/data-key-config-dialog.component'; } from '@home/components/widget/data-key-config-dialog.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
@ -218,11 +218,6 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
} }
private updateParams() { 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) { if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function; this.dataKeyType = DataKeyType.function;
this.placeholder = this.translate.instant('datakey.function-types'); this.placeholder = this.translate.instant('datakey.function-types');
@ -231,13 +226,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.widgetType === widgetType.latest) { if (this.widgetType === widgetType.latest) {
this.dataKeyType = null; this.dataKeyType = null;
this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); 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 { } else {
this.dataKeyType = DataKeyType.timeseries; this.dataKeyType = DataKeyType.timeseries;
this.requiredText = this.translate.instant('datakey.timeseries-required'); this.requiredText = this.translate.instant('datakey.timeseries-required');
} }
this.placeholder = ''; this.placeholder = '';
} }
}
} }
private reset() { private reset() {
@ -387,6 +384,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dataKey: deepClone(key), dataKey: deepClone(key),
dataKeySettingsSchema: this.datakeySettingsSchema, dataKeySettingsSchema: this.datakeySettingsSchema,
entityAliasId: this.entityAliasId, entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks callbacks: this.callbacks
} }
}).afterClosed().subscribe((updatedDataKey) => { }).afterClosed().subscribe((updatedDataKey) => {
@ -411,16 +409,19 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.latestSearchTextResult === null || this.searchText !== searchText) { if (this.latestSearchTextResult === null || this.searchText !== searchText) {
this.searchText = searchText; this.searchText = searchText;
let fetchObservable: Observable<Array<DataKey>> = null; 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 dataKeyFilter = this.createDataKeyFilter(this.searchText);
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList.filter(dataKeyFilter)); fetchObservable = of(targetKeysList.filter(dataKeyFilter));
} else { } else {
if (this.entityAliasId) { if (this.entityAliasId) {
const dataKeyTypes = [DataKeyType.timeseries]; 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.attribute);
dataKeyTypes.push(DataKeyType.entityField); dataKeyTypes.push(DataKeyType.entityField);
if (this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.alarm);
}
} }
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes); fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes);
} else { } else {

66
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.html

@ -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>

0
ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss

119
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.ts

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

25
ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.html

@ -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>

43
ui-ngx/src/app/modules/home/components/widget/lib/alarm-status-filter-panel.component.ts

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

98
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -73,17 +73,13 @@ import {
import { import {
AlarmDataInfo, AlarmDataInfo,
alarmFields, alarmFields,
AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
AlarmStatus, AlarmStatus,
alarmStatusTranslations alarmStatusTranslations
} from '@shared/models/alarm.models'; } from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import {
ALARM_STATUS_FILTER_PANEL_DATA,
AlarmStatusFilterPanelComponent,
AlarmStatusFilterPanelData
} from '@home/components/widget/lib/alarm-status-filter-panel.component';
import { import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AlarmDetailsDialogData AlarmDetailsDialogData
@ -101,11 +97,18 @@ import {
KeyFilter KeyFilter
} from '@app/shared/models/query/query.models'; } from '@app/shared/models/query/query.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.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 { interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string; alarmsTitle: string;
enableSelection: boolean; enableSelection: boolean;
enableStatusFilter: boolean; enableStatusFilter?: boolean;
enableFilter: boolean;
enableStickyAction: boolean; enableStickyAction: boolean;
displayDetails: boolean; displayDetails: boolean;
allowAcknowledgment: boolean; allowAcknowledgment: boolean;
@ -178,11 +181,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
}; };
private statusFilterAction: WidgetAction = { private alarmFilterAction: WidgetAction = {
name: 'alarm.alarm-status-filter', name: 'alarm.alarm-filter',
show: true, show: true,
onAction: ($event) => { onAction: ($event) => {
this.editAlarmStatusFilter($event); this.editAlarmFilter($event);
}, },
icon: 'filter_list' icon: 'filter_list'
}; };
@ -255,7 +258,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
private initializeConfig() { 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.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : 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.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; 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; const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { 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.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
// TODO: search status, severity, types, searchPropagatedAlarms from widget config to pageLink this.pageLink.searchPropagatedAlarms = isDefined(this.widgetConfig.searchPropagatedAlarms)
this.pageLink.searchPropagatedAlarms = false; // true for old widget configs ? this.widgetConfig.searchPropagatedAlarms : true;
this.pageLink.severityList = []; let alarmStatusList: AlarmSearchStatus[] = [];
this.pageLink.statusList = []; if (isDefined(this.widgetConfig.alarmStatusList) && this.widgetConfig.alarmStatusList.length) {
this.pageLink.typeList = []; 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 cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs(); const cssParser = new cssjs();
@ -440,7 +457,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.ctx.detectChanges(); this.ctx.detectChanges();
} }
private editAlarmStatusFilter($event: Event) { private editAlarmFilter($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -462,14 +479,25 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
overlayRef.dispose(); overlayRef.dispose();
}); });
const injectionTokens = new WeakMap<any, any>([ const injectionTokens = new WeakMap<any, any>([
[ALARM_STATUS_FILTER_PANEL_DATA, { [ALARM_FILTER_PANEL_DATA, {
subscription: this.subscription, statusList: this.pageLink.statusList,
} as AlarmStatusFilterPanelData], severityList: this.pageLink.severityList,
typeList: this.pageLink.typeList
} as AlarmFilterPanelData],
[OverlayRef, overlayRef] [OverlayRef, overlayRef]
]); ]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
overlayRef.attach(new ComponentPortal(AlarmStatusFilterPanelComponent, const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent,
this.viewContainerRef, injector)); 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(); this.ctx.detectChanges();
} }
@ -555,7 +583,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
content = '' + value; content = '' + value;
} }
} else { } else {
content = this.defaultContent(key, value); content = this.defaultContent(key, contentInfo, value);
} }
return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : ''; return isDefined(content) ? this.domSanitizer.bypassSecurityTrustHtml(content) : '';
} else { } 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)) { if (isDefined(value)) {
const alarmField = alarmFields[key.name]; const alarmField = alarmFields[key.name];
if (alarmField) { if (alarmField) {
@ -765,9 +793,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} else { } else {
return value; 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 { } else {
return ''; return '';
} }
@ -827,13 +862,20 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
} }
loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) { 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()) { if (this.selection.hasValue()) {
this.selection.clear(); this.selection.clear();
this.onSelectionModeChanged(false); this.onSelectionModeChanged(false);
} }
this.appliedPageLink = pageLink; this.alarmsSubject.next([]);
this.appliedSortOrderLabel = sortOrderLabel; this.pageDataSubject.next(emptyPageData<AlarmDataInfo>());
this.subscription.subscribeForAlarms(pageLink, keyFilters);
} }
updateAlarms() { updateAlarms() {

7
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

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

4
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -20,7 +20,7 @@ import { SharedModule } from '@app/shared/shared.module';
import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component'; import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component'; import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.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 { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component';
import { EntitiesHierarchyWidgetComponent } from '@home/components/widget/lib/entities-hierarchy-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: declarations:
[ [
DisplayColumnsPanelComponent, DisplayColumnsPanelComponent,
AlarmStatusFilterPanelComponent, AlarmFilterPanelComponent,
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent, AlarmsTableWidgetComponent,
TimeseriesTableWidgetComponent, TimeseriesTableWidgetComponent,

73
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

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

85
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -41,7 +41,13 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined, isObject } from '@app/core/utils'; 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 { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service'; 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 { entityFields } from '@shared/models/entity.models';
import { Filter, Filters } from '@shared/models/query/query.models'; import { Filter, Filters } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; 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 = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -92,11 +100,23 @@ const defaultSettingsForm = [
}) })
export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
widgetTypes = widgetType; widgetTypes = widgetType;
entityTypes = EntityType; 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() @Input()
forceExpandDatasources: boolean; forceExpandDatasources: boolean;
@ -293,13 +313,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} }
}); });
if (this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('alarmSearchStatus', this.fb.control(null)); this.dataSettings.addControl('alarmStatusList', this.fb.control(null));
this.dataSettings.addControl('alarmsPollingInterval', this.fb.control(null, this.dataSettings.addControl('alarmSeverityList', this.fb.control(null));
[Validators.required, Validators.min(1)])); this.dataSettings.addControl('alarmTypeList', this.fb.control(null));
this.dataSettings.addControl('alarmsMaxCountLoad', this.fb.control(null, this.dataSettings.addControl('searchPropagatedAlarms', this.fb.control(null));
[Validators.required, Validators.min(0)]));
this.dataSettings.addControl('alarmsFetchSize', this.fb.control(null,
[Validators.required, Validators.min(10)]));
} }
} }
if (this.modelValue.isDataEnabled) { if (this.modelValue.isDataEnabled) {
@ -445,21 +462,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
targetDeviceAliasId targetDeviceAliasId
}, {emitEvent: false}); }, {emitEvent: false});
} else if (this.widgetType === widgetType.alarm) { } 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( this.dataSettings.patchValue(
{ alarmSearchStatus: isDefined(config.alarmSearchStatus) ? { alarmStatusList }, {emitEvent: false}
config.alarmSearchStatus : AlarmSearchStatus.ANY }, {emitEvent: false}
); );
this.dataSettings.patchValue( this.dataSettings.patchValue(
{ alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ? { alarmSeverityList: isDefined(config.alarmSeverityList) ? config.alarmSeverityList : [] }, {emitEvent: false}
config.alarmsPollingInterval : 5}, {emitEvent: false}
); );
this.dataSettings.patchValue( this.dataSettings.patchValue(
{ alarmsMaxCountLoad: isDefined(config.alarmsMaxCountLoad) ? { alarmTypeList: isDefined(config.alarmTypeList) ? config.alarmTypeList : [] }, {emitEvent: false}
config.alarmsMaxCountLoad : 0}, {emitEvent: false}
); );
this.dataSettings.patchValue( this.dataSettings.patchValue(
{ alarmsFetchSize: isDefined(config.alarmsFetchSize) ? { searchPropagatedAlarms: isDefined(config.searchPropagatedAlarms) ?
config.alarmsFetchSize : 100}, {emitEvent: false} config.searchPropagatedAlarms : true }, {emitEvent: false}
); );
this.alarmSourceSettings.patchValue( this.alarmSourceSettings.patchValue(
config.alarmSource, {emitEvent: false} 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 { public displayAdvanced(): boolean {
return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema;
} }

8
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -902,14 +902,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}; };
if (this.widget.type === widgetType.alarm) { if (this.widget.type === widgetType.alarm) {
options.alarmSource = deepClone(this.widget.config.alarmSource); 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 { } else {
options.datasources = deepClone(this.widget.config.datasources); options.datasources = deepClone(this.widget.config.datasources);
} }

2
ui-ngx/src/app/shared/models/query/query.models.ts

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

10
ui-ngx/src/app/shared/models/widget.models.ts

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

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

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

Loading…
Cancel
Save