Commit 8f4944fe170ed29b1415f071fdbd52351bcdd95b

Authored by Igor Kulikov
1 parent 68954f8d

Material table improvements. Alarm widget implementation.

Showing 34 changed files with 1444 additions and 303 deletions
@@ -13,10 +13,10 @@ @@ -13,10 +13,10 @@
13 "sizeX": 10.5, 13 "sizeX": 10.5,
14 "sizeY": 6.5, 14 "sizeY": 6.5,
15 "resources": [], 15 "resources": [],
16 - "templateHtml": "<tb-alarms-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-alarms-table-widget>", 16 + "templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
17 "templateCss": "", 17 "templateCss": "",
18 - "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\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",  
19 - "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 \"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 \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", 18 + "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",
  19 + "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 \"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 \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
20 "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}", 20 "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}",
21 "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\"},\"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}" 21 "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\"},\"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}"
22 } 22 }
@@ -19,11 +19,15 @@ import { Component, OnInit } from '@angular/core'; @@ -19,11 +19,15 @@ import { Component, OnInit } from '@angular/core';
19 import { environment as env } from '@env/environment'; 19 import { environment as env } from '@env/environment';
20 20
21 import { TranslateService } from '@ngx-translate/core'; 21 import { TranslateService } from '@ngx-translate/core';
22 -import { Store } from '@ngrx/store'; 22 +import { select, Store } from '@ngrx/store';
23 import { AppState } from './core/core.state'; 23 import { AppState } from './core/core.state';
24 import { LocalStorageService } from './core/local-storage/local-storage.service'; 24 import { LocalStorageService } from './core/local-storage/local-storage.service';
25 import { DomSanitizer } from '@angular/platform-browser'; 25 import { DomSanitizer } from '@angular/platform-browser';
26 import { MatIconRegistry } from '@angular/material'; 26 import { MatIconRegistry } from '@angular/material';
  27 +import { combineLatest } from 'rxjs';
  28 +import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
  29 +import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
  30 +import { AuthService } from '@core/auth/auth.service';
27 31
28 @Component({ 32 @Component({
29 selector: 'tb-root', 33 selector: 'tb-root',
@@ -36,7 +40,8 @@ export class AppComponent implements OnInit { @@ -36,7 +40,8 @@ export class AppComponent implements OnInit {
36 private storageService: LocalStorageService, 40 private storageService: LocalStorageService,
37 private translate: TranslateService, 41 private translate: TranslateService,
38 private matIconRegistry: MatIconRegistry, 42 private matIconRegistry: MatIconRegistry,
39 - private domSanitizer: DomSanitizer) { 43 + private domSanitizer: DomSanitizer,
  44 + private authService: AuthService) {
40 45
41 console.log(`ThingsBoard Version: ${env.tbVersion}`); 46 console.log(`ThingsBoard Version: ${env.tbVersion}`);
42 47
@@ -56,6 +61,7 @@ export class AppComponent implements OnInit { @@ -56,6 +61,7 @@ export class AppComponent implements OnInit {
56 this.storageService.testLocalStorage(); 61 this.storageService.testLocalStorage();
57 62
58 this.setupTranslate(); 63 this.setupTranslate();
  64 + this.setupAuth();
59 } 65 }
60 66
61 setupTranslate() { 67 setupTranslate() {
@@ -69,6 +75,21 @@ export class AppComponent implements OnInit { @@ -69,6 +75,21 @@ export class AppComponent implements OnInit {
69 this.translate.setDefaultLang(env.defaultLang); 75 this.translate.setDefaultLang(env.defaultLang);
70 } 76 }
71 77
  78 + setupAuth() {
  79 + combineLatest([
  80 + this.store.pipe(select(selectIsAuthenticated)),
  81 + this.store.pipe(select(selectIsUserLoaded))]
  82 + ).pipe(
  83 + map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),
  84 + distinctUntilChanged(),
  85 + filter((data) => data.isUserLoaded ),
  86 + skip(1),
  87 + ).subscribe((data) => {
  88 + this.authService.gotoDefaultPlace(data.isAuthenticated);
  89 + });
  90 + this.authService.reloadUser();
  91 + }
  92 +
72 ngOnInit() { 93 ngOnInit() {
73 } 94 }
74 95
@@ -30,7 +30,7 @@ import { AlarmService } from '../http/alarm.service'; @@ -30,7 +30,7 @@ import { AlarmService } from '../http/alarm.service';
30 import { UtilsService } from '@core/services/utils.service'; 30 import { UtilsService } from '@core/services/utils.service';
31 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; 31 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
32 import { EntityType } from '@shared/models/entity-type.models'; 32 import { EntityType } from '@shared/models/entity-type.models';
33 -import { AlarmSearchStatus } from '@shared/models/alarm.models'; 33 +import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
34 import { HttpErrorResponse } from '@angular/common/http'; 34 import { HttpErrorResponse } from '@angular/common/http';
35 import { DatasourceService } from '@core/api/datasource.service'; 35 import { DatasourceService } from '@core/api/datasource.service';
36 import { RafService } from '@core/services/raf.service'; 36 import { RafService } from '@core/services/raf.service';
@@ -226,6 +226,7 @@ export interface IWidgetSubscription { @@ -226,6 +226,7 @@ export interface IWidgetSubscription {
226 timeWindowConfig?: Timewindow; 226 timeWindowConfig?: Timewindow;
227 timeWindow?: WidgetTimewindow; 227 timeWindow?: WidgetTimewindow;
228 228
  229 + alarms?: Array<AlarmInfo>;
229 alarmSource?: Datasource; 230 alarmSource?: Datasource;
230 alarmSearchStatus?: AlarmSearchStatus; 231 alarmSearchStatus?: AlarmSearchStatus;
231 alarmsPollingInterval?: number; 232 alarmsPollingInterval?: number;
@@ -62,18 +62,6 @@ export class AuthService { @@ -62,18 +62,6 @@ export class AuthService {
62 private adminService: AdminService, 62 private adminService: AdminService,
63 private translate: TranslateService 63 private translate: TranslateService
64 ) { 64 ) {
65 - combineLatest(  
66 - this.store.pipe(select(selectIsAuthenticated)),  
67 - this.store.pipe(select(selectIsUserLoaded))  
68 - ).pipe(  
69 - map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),  
70 - distinctUntilChanged(),  
71 - filter((data) => data.isUserLoaded ),  
72 - skip(1),  
73 - ).subscribe((data) => {  
74 - this.gotoDefaultPlace(data.isAuthenticated);  
75 - });  
76 - this.reloadUser();  
77 } 65 }
78 66
79 redirectUrl: string; 67 redirectUrl: string;
@@ -398,3 +398,7 @@ export function snakeCase(name: string, separator: string): string { @@ -398,3 +398,7 @@ export function snakeCase(name: string, separator: string): string {
398 return (pos ? separator : '') + letter.toLowerCase(); 398 return (pos ? separator : '') + letter.toLowerCase();
399 }); 399 });
400 } 400 }
  401 +
  402 +export function getDescendantProp(obj: any, path: string): any {
  403 + return path.split('.').reduce((acc, part) => acc && acc[part], obj)
  404 +}
@@ -84,19 +84,19 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink> @@ -84,19 +84,19 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
84 this.columns.push( 84 this.columns.push(
85 new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px')); 85 new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px'));
86 this.columns.push( 86 this.columns.push(
87 - new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '100%', 87 + new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%',
88 (entity) => entity.originatorName, entity => ({}), false)); 88 (entity) => entity.originatorName, entity => ({}), false));
89 this.columns.push( 89 this.columns.push(
90 - new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '100%')); 90 + new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%'));
91 this.columns.push( 91 this.columns.push(
92 - new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '100%', 92 + new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%',
93 (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)), 93 (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)),
94 entity => ({ 94 entity => ({
95 fontWeight: 'bold', 95 fontWeight: 'bold',
96 color: alarmSeverityColors.get(entity.severity) 96 color: alarmSeverityColors.get(entity.severity)
97 }))); 97 })));
98 this.columns.push( 98 this.columns.push(
99 - new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '100%', 99 + new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
100 (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status)))); 100 (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status))));
101 101
102 this.cellActionDescriptors.push( 102 this.cellActionDescriptors.push(
@@ -83,22 +83,22 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin @@ -83,22 +83,22 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
83 83
84 if (this.auditLogMode !== AuditLogMode.ENTITY) { 84 if (this.auditLogMode !== AuditLogMode.ENTITY) {
85 this.columns.push( 85 this.columns.push(
86 - new EntityTableColumn<AuditLog>('entityType', 'audit-log.entity-type', '100%', 86 + new EntityTableColumn<AuditLog>('entityType', 'audit-log.entity-type', '20%',
87 (entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)), 87 (entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)),
88 - new EntityTableColumn<AuditLog>('entityName', 'audit-log.entity-name'), 88 + new EntityTableColumn<AuditLog>('entityName', 'audit-log.entity-name', '20%'),
89 ); 89 );
90 } 90 }
91 91
92 if (this.auditLogMode !== AuditLogMode.USER) { 92 if (this.auditLogMode !== AuditLogMode.USER) {
93 this.columns.push( 93 this.columns.push(
94 - new EntityTableColumn<AuditLog>('userName', 'audit-log.user') 94 + new EntityTableColumn<AuditLog>('userName', 'audit-log.user', '33%')
95 ); 95 );
96 } 96 }
97 97
98 this.columns.push( 98 this.columns.push(
99 - new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '100%', 99 + new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '33%',
100 (entity) => translate.instant(actionTypeTranslations.get(entity.actionType))), 100 (entity) => translate.instant(actionTypeTranslations.get(entity.actionType))),
101 - new EntityTableColumn<AuditLog>('actionStatus', 'audit-log.status', '100%', 101 + new EntityTableColumn<AuditLog>('actionStatus', 'audit-log.status', '33%',
102 (entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus))) 102 (entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus)))
103 ); 103 );
104 104
@@ -153,8 +153,10 @@ @@ -153,8 +153,10 @@
153 </mat-cell> 153 </mat-cell>
154 </ng-container> 154 </ng-container>
155 <ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;"> 155 <ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;">
156 - <mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>  
157 - <mat-cell *matCellDef="let entity; let row = index" 156 + <mat-header-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}"
  157 + *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
  158 + <mat-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}"
  159 + *matCellDef="let entity; let row = index"
158 [matTooltip]="cellTooltip(entity, column, row)" 160 [matTooltip]="cellTooltip(entity, column, row)"
159 matTooltipPosition="above" 161 matTooltipPosition="above"
160 [innerHTML]="cellContent(entity, column, row)" 162 [innerHTML]="cellContent(entity, column, row)"
@@ -176,10 +178,10 @@ @@ -176,10 +178,10 @@
176 </mat-cell> 178 </mat-cell>
177 </ng-container> 179 </ng-container>
178 <ng-container matColumnDef="actions" stickyEnd> 180 <ng-container matColumnDef="actions" stickyEnd>
179 - <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px', maxWidth: (cellActionDescriptors.length * 40) + 'px' }"> 181 + <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ width: (cellActionDescriptors.length * 40) + 'px' }">
180 {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} 182 {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
181 </mat-header-cell> 183 </mat-header-cell>
182 - <mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px', maxWidth: (cellActionDescriptors.length * 40) + 'px' }"> 184 + <mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ width: (cellActionDescriptors.length * 40) + 'px' }">
183 <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> 185 <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
184 <button mat-button mat-icon-button [disabled]="isLoading$ | async" 186 <button mat-button mat-icon-button [disabled]="isLoading$ | async"
185 [fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors" 187 [fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors"
@@ -190,7 +192,7 @@ @@ -190,7 +192,7 @@
190 {{actionDescriptor.icon}}</mat-icon> 192 {{actionDescriptor.icon}}</mat-icon>
191 </button> 193 </button>
192 </div> 194 </div>
193 - <div fxHide fxShow.lt-lg> 195 + <div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length">
194 <button mat-button mat-icon-button 196 <button mat-button mat-icon-button
195 (click)="$event.stopPropagation()" 197 (click)="$event.stopPropagation()"
196 [matMenuTriggerFor]="cellActionsMenu"> 198 [matMenuTriggerFor]="cellActionsMenu">
@@ -398,9 +398,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -398,9 +398,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
398 let res = this.headerCellStyleCache[index]; 398 let res = this.headerCellStyleCache[index];
399 if (!res) { 399 if (!res) {
400 if (column instanceof EntityTableColumn) { 400 if (column instanceof EntityTableColumn) {
401 - res = {...column.headerCellStyleFunction(column.key), ...{maxWidth: column.maxWidth}}; 401 + res = {...column.headerCellStyleFunction(column.key), ...{minWidth: column.width, maxWidth: column.width, width: column.width}};
402 } else { 402 } else {
403 - res = {maxWidth: column.maxWidth}; 403 + res = {width: column.width};
404 } 404 }
405 this.headerCellStyleCache[index] = res; 405 this.headerCellStyleCache[index] = res;
406 } 406 }
@@ -445,9 +445,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -445,9 +445,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
445 let res = this.cellStyleCache[index]; 445 let res = this.cellStyleCache[index];
446 if (!res) { 446 if (!res) {
447 if (column instanceof EntityTableColumn) { 447 if (column instanceof EntityTableColumn) {
448 - res = {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}}; 448 + res = {...column.cellStyleFunction(entity, column.key), ...{minWidth: column.width, maxWidth: column.width, width: column.width}};
449 } else { 449 } else {
450 - res = {maxWidth: column.maxWidth}; 450 + res = {width: column.width};
451 } 451 }
452 this.cellStyleCache[index] = res; 452 this.cellStyleCache[index] = res;
453 } 453 }
@@ -109,8 +109,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> { @@ -109,8 +109,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
109 updateColumns(updateTableColumns: boolean = false): void { 109 updateColumns(updateTableColumns: boolean = false): void {
110 this.columns = []; 110 this.columns = [];
111 this.columns.push( 111 this.columns.push(
112 - new DateEntityTableColumn<Event>('createdTime', 'event.event-time', this.datePipe, '150px'),  
113 - new EntityTableColumn<Event>('server', 'event.server', '150px', 112 + new DateEntityTableColumn<Event>('createdTime', 'event.event-time', this.datePipe, '120px'),
  113 + new EntityTableColumn<Event>('server', 'event.server', '100px',
114 (entity) => entity.body.server, entity => ({}), false)); 114 (entity) => entity.body.server, entity => ({}), false));
115 switch (this.eventType) { 115 switch (this.eventType) {
116 case EventType.ERROR: 116 case EventType.ERROR:
@@ -146,20 +146,21 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> { @@ -146,20 +146,21 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
146 break; 146 break;
147 case EventType.STATS: 147 case EventType.STATS:
148 this.columns.push( 148 this.columns.push(
149 - new EntityTableColumn<Event>('messagesProcessed', 'event.messages-processed', '100%', 149 + new EntityTableColumn<Event>('messagesProcessed', 'event.messages-processed', '50%',
150 (entity) => entity.body.messagesProcessed + '', 150 (entity) => entity.body.messagesProcessed + '',
151 - entity => ({justifyContent: 'flex-end'}), 151 + () => ({}),
152 false, 152 false,
153 - key => ({justifyContent: 'flex-end'})),  
154 - new EntityTableColumn<Event>('errorsOccurred', 'event.errors-occurred', '100%', 153 + () => ({}), () => undefined, true),
  154 + new EntityTableColumn<Event>('errorsOccurred', 'event.errors-occurred', '50%',
155 (entity) => entity.body.errorsOccurred + '', 155 (entity) => entity.body.errorsOccurred + '',
156 - entity => ({justifyContent: 'flex-end'}), 156 + () => ({}),
157 false, 157 false,
158 - key => ({justifyContent: 'flex-end'})) 158 + () => ({}), () => undefined, true)
159 ); 159 );
160 break; 160 break;
161 case DebugEventType.DEBUG_RULE_NODE: 161 case DebugEventType.DEBUG_RULE_NODE:
162 case DebugEventType.DEBUG_RULE_CHAIN: 162 case DebugEventType.DEBUG_RULE_CHAIN:
  163 + this.columns[0].width = '100px';
163 this.columns.push( 164 this.columns.push(
164 new EntityTableColumn<Event>('type', 'event.type', '40px', 165 new EntityTableColumn<Event>('type', 'event.type', '40px',
165 (entity) => entity.body.type, entity => ({ 166 (entity) => entity.body.type, entity => ({
@@ -173,24 +174,18 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> { @@ -173,24 +174,18 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
173 }), false, key => ({ 174 }), false, key => ({
174 padding: '0 12px 0 0' 175 padding: '0 12px 0 0'
175 })), 176 })),
176 - new EntityTableColumn<Event>('msgId', 'event.message-id', '100%', 177 + new EntityTableColumn<Event>('msgId', 'event.message-id', '100px',
177 (entity) => entity.body.msgId, entity => ({ 178 (entity) => entity.body.msgId, entity => ({
178 whiteSpace: 'nowrap', 179 whiteSpace: 'nowrap',
179 - padding: '0 12px 0 0',  
180 - textOverflow: 'ellipsis',  
181 - display: 'inline-block',  
182 - lineHeight: '48px', 180 + padding: '0 12px 0 0'
183 }), false, key => ({ 181 }), false, key => ({
184 padding: '0 12px 0 0' 182 padding: '0 12px 0 0'
185 }), 183 }),
186 entity => entity.body.msgId), 184 entity => entity.body.msgId),
187 - new EntityTableColumn<Event>('msgType', 'event.message-type', '100%', 185 + new EntityTableColumn<Event>('msgType', 'event.message-type', '100px',
188 (entity) => entity.body.msgType, entity => ({ 186 (entity) => entity.body.msgType, entity => ({
189 whiteSpace: 'nowrap', 187 whiteSpace: 'nowrap',
190 - padding: '0 12px 0 0',  
191 - textOverflow: 'ellipsis',  
192 - display: 'inline-block',  
193 - lineHeight: '48px', 188 + padding: '0 12px 0 0'
194 }), false, key => ({ 189 }), false, key => ({
195 padding: '0 12px 0 0' 190 padding: '0 12px 0 0'
196 }), 191 }),
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-table-widget tb-absolute-fill">
  19 + <div fxFlex fxLayout="column" class="tb-absolute-fill">
  20 + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && alarmsDatasource.selection.isEmpty()">
  21 + <div class="mat-toolbar-tools">
  22 + <button mat-button mat-icon-button
  23 + matTooltip="{{ 'action.search' | translate }}"
  24 + matTooltipPosition="above">
  25 + <mat-icon>search</mat-icon>
  26 + </button>
  27 + <mat-form-field fxFlex>
  28 + <mat-label>&nbsp;</mat-label>
  29 + <input #searchInput matInput
  30 + [(ngModel)]="pageLink.textSearch"
  31 + placeholder="{{ 'alarm.search' | translate }}"/>
  32 + </mat-form-field>
  33 + <button mat-button mat-icon-button (click)="exitFilterMode()"
  34 + matTooltip="{{ 'action.close' | translate }}"
  35 + matTooltipPosition="above">
  36 + <mat-icon>close</mat-icon>
  37 + </button>
  38 + </div>
  39 + </mat-toolbar>
  40 + <mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!alarmsDatasource.selection.isEmpty()">
  41 + <div class="mat-toolbar-tools">
  42 + <span>
  43 + {{ translate.get('alarm.selected-alarms',
  44 + {count: alarmsDatasource.selection.selected.length}) | async }}
  45 + </span>
  46 + <span fxFlex></span>
  47 + <button *ngIf="allowAcknowledgment"
  48 + mat-button mat-icon-button [disabled]="isLoading$ | async"
  49 + matTooltip="{{ 'alarm.acknowledge' | translate }}"
  50 + matTooltipPosition="above"
  51 + (click)="ackAlarms($event)">
  52 + <mat-icon>done</mat-icon>
  53 + </button>
  54 + <button mat-button mat-icon-button
  55 + [disabled]="isLoading$ | async"
  56 + matTooltip="{{ 'alarm.clear' | translate }}"
  57 + matTooltipPosition="above"
  58 + (click)="clearAlarms($event)">
  59 + <mat-icon>clear</mat-icon>
  60 + </button>
  61 + </div>
  62 + </mat-toolbar>
  63 + <div fxFlex class="table-container">
  64 + <mat-table [dataSource]="alarmsDatasource"
  65 + matSort [matSortActive]="sortOrderProperty" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
  66 + <ng-container matColumnDef="select" sticky>
  67 + <mat-header-cell *matHeaderCellDef style="width: 30px;">
  68 + <mat-checkbox (change)="$event ? alarmsDatasource.masterToggle() : null"
  69 + [checked]="alarmsDatasource.selection.hasValue() && (alarmsDatasource.isAllSelected() | async)"
  70 + [indeterminate]="alarmsDatasource.selection.hasValue() && !(alarmsDatasource.isAllSelected() | async)">
  71 + </mat-checkbox>
  72 + </mat-header-cell>
  73 + <mat-cell *matCellDef="let alarm" style="width: 30px;">
  74 + <mat-checkbox (click)="$event.stopPropagation();"
  75 + (change)="$event ? alarmsDatasource.toggleSelection(alarm) : null"
  76 + [checked]="alarmsDatasource.isSelected(alarm)">
  77 + </mat-checkbox>
  78 + </mat-cell>
  79 + </ng-container>
  80 + <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
  81 + <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
  82 + <mat-cell *matCellDef="let alarm;"
  83 + [innerHTML]="cellContent(alarm, column)"
  84 + [ngStyle]="cellStyle(alarm, column)">
  85 + </mat-cell>
  86 + </ng-container>
  87 + <ng-container matColumnDef="actions" stickyEnd>
  88 + <mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ width: (actionCellDescriptors.length * 36) + 'px' }">
  89 + </mat-header-cell>
  90 + <mat-cell *matCellDef="let alarm" [ngStyle.gt-md]="{ width: (actionCellDescriptors.length * 36) + 'px' }">
  91 + <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
  92 + <button mat-button mat-icon-button [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)"
  93 + *ngFor="let actionDescriptor of actionCellDescriptors"
  94 + matTooltip="{{ actionDescriptor.displayName }}"
  95 + matTooltipPosition="above"
  96 + (click)="onActionButtonClick($event, alarm, actionDescriptor)">
  97 + <mat-icon>{{actionDescriptor.icon}}</mat-icon>
  98 + </button>
  99 + </div>
  100 + <div fxHide fxShow.lt-lg>
  101 + <button mat-button mat-icon-button
  102 + (click)="$event.stopPropagation(); ctx.detectChanges();"
  103 + [matMenuTriggerFor]="cellActionsMenu">
  104 + <mat-icon class="material-icons">more_vert</mat-icon>
  105 + </button>
  106 + <mat-menu #cellActionsMenu="matMenu" xPosition="before">
  107 + <button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors"
  108 + [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)"
  109 + (click)="onActionButtonClick($event, alarm, actionDescriptor)">
  110 + <mat-icon>{{actionDescriptor.icon}}</mat-icon>
  111 + <span>{{ actionDescriptor.displayName }}</span>
  112 + </button>
  113 + </mat-menu>
  114 + </div>
  115 + </mat-cell>
  116 + </ng-container>
  117 + <mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
  118 + <mat-row [ngClass]="{'mat-row-select': enableSelection,
  119 + 'mat-selected': alarmsDatasource.isSelected(alarm),
  120 + 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}"
  121 + *matRowDef="let alarm; columns: displayedColumns;"
  122 + (click)="onRowClick($event, alarm)"></mat-row>
  123 + </mat-table>
  124 + <span [fxShow]="alarmsDatasource.isEmpty() | async"
  125 + fxLayoutAlign="center center"
  126 + class="no-data-found" translate>alarm.no-alarms-prompt</span>
  127 + </div>
  128 + <mat-divider *ngIf="displayPagination"></mat-divider>
  129 + <mat-paginator *ngIf="displayPagination"
  130 + [length]="alarmsDatasource.total() | async"
  131 + [pageIndex]="pageLink.page"
  132 + [pageSize]="pageLink.pageSize"
  133 + [pageSizeOptions]="pageSizeOptions"></mat-paginator>
  134 + </div>
  135 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + width: 100%;
  18 + height: 100%;
  19 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + ElementRef,
  21 + EventEmitter,
  22 + Input,
  23 + NgZone,
  24 + OnInit,
  25 + ViewChild,
  26 + ViewContainerRef
  27 +} from '@angular/core';
  28 +import { PageComponent } from '@shared/components/page.component';
  29 +import { Store } from '@ngrx/store';
  30 +import { AppState } from '@core/core.state';
  31 +import { WidgetAction, WidgetContext } from '@home/models/widget-component.models';
  32 +import { Datasource, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models';
  33 +import { IWidgetSubscription } from '@core/api/widget-api.models';
  34 +import { UtilsService } from '@core/services/utils.service';
  35 +import { TranslateService } from '@ngx-translate/core';
  36 +import { deepClone, isDefined, isNumber } from '@core/utils';
  37 +import cssjs from '@core/css/css';
  38 +import { PageLink } from '@shared/models/page/page-link';
  39 +import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
  40 +import { DataSource } from '@angular/cdk/typings/collections';
  41 +import { CollectionViewer, SelectionModel } from '@angular/cdk/collections';
  42 +import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
  43 +import { emptyPageData, PageData } from '@shared/models/page/page-data';
  44 +import { entityTypeTranslations } from '@shared/models/entity-type.models';
  45 +import { catchError, debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators';
  46 +import { MatPaginator } from '@angular/material/paginator';
  47 +import { MatSort } from '@angular/material/sort';
  48 +import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
  49 +import {
  50 + CellContentInfo,
  51 + CellStyleInfo,
  52 + constructTableCssString,
  53 + DisplayColumn,
  54 + EntityColumn,
  55 + fromAlarmColumnDef,
  56 + getAlarmValue,
  57 + getCellContentInfo,
  58 + getCellStyleInfo,
  59 + getColumnWidth,
  60 + TableWidgetDataKeySettings,
  61 + TableWidgetSettings,
  62 + toAlarmColumnDef
  63 +} from '@home/components/widget/lib/table-widget.models';
  64 +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
  65 +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
  66 +import {
  67 + DISPLAY_COLUMNS_PANEL_DATA,
  68 + DisplayColumnsPanelComponent,
  69 + DisplayColumnsPanelData
  70 +} from '@home/components/widget/lib/display-columns-panel.component';
  71 +import {
  72 + alarmFields,
  73 + AlarmInfo,
  74 + alarmSeverityColors,
  75 + alarmSeverityTranslations,
  76 + AlarmStatus,
  77 + alarmStatusTranslations
  78 +} from '@shared/models/alarm.models';
  79 +import { DatePipe } from '@angular/common';
  80 +
  81 +interface AlarmsTableWidgetSettings extends TableWidgetSettings {
  82 + alarmsTitle: string;
  83 + enableSelection: boolean;
  84 + enableStatusFilter: boolean;
  85 + displayDetails: boolean;
  86 + allowAcknowledgment: boolean;
  87 + allowClear: boolean;
  88 +}
  89 +
  90 +interface AlarmsTableDataKeySettings extends TableWidgetDataKeySettings {
  91 +}
  92 +
  93 +interface AlarmWidgetActionDescriptor extends WidgetActionDescriptor {
  94 + details?: boolean;
  95 + acknowledge?: boolean;
  96 + clear?: boolean;
  97 +}
  98 +
  99 +@Component({
  100 + selector: 'tb-alarms-table-widget',
  101 + templateUrl: './alarms-table-widget.component.html',
  102 + styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss']
  103 +})
  104 +export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
  105 +
  106 + @Input()
  107 + ctx: WidgetContext;
  108 +
  109 + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
  110 + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
  111 + @ViewChild(MatSort, {static: false}) sort: MatSort;
  112 +
  113 + public enableSelection = true;
  114 + public displayPagination = true;
  115 + public pageSizeOptions;
  116 + public pageLink: PageLink;
  117 + public sortOrderProperty: string;
  118 + public textSearchMode = false;
  119 + public columns: Array<EntityColumn> = [];
  120 + public displayedColumns: string[] = [];
  121 + public actionCellDescriptors: AlarmWidgetActionDescriptor[] = [];
  122 + public alarmsDatasource: AlarmsDatasource;
  123 +
  124 + private settings: AlarmsTableWidgetSettings;
  125 + private widgetConfig: WidgetConfig;
  126 + private subscription: IWidgetSubscription;
  127 + private alarmSource: Datasource;
  128 +
  129 + private displayDetails = true;
  130 + private allowAcknowledgment = true;
  131 + private allowClear = true;
  132 +
  133 + private defaultPageSize = 10;
  134 + private defaultSortOrder = '-' + alarmFields.createdTime.value;
  135 +
  136 + private contentsInfo: {[key: string]: CellContentInfo} = {};
  137 + private stylesInfo: {[key: string]: CellStyleInfo} = {};
  138 + private columnWidth: {[key: string]: string} = {};
  139 +
  140 + private searchAction: WidgetAction = {
  141 + name: 'action.search',
  142 + show: true,
  143 + icon: 'search',
  144 + onAction: () => {
  145 + this.enterFilterMode();
  146 + }
  147 + };
  148 +
  149 + private columnDisplayAction: WidgetAction = {
  150 + name: 'entity.columns-to-display',
  151 + show: true,
  152 + icon: 'view_column',
  153 + onAction: ($event) => {
  154 + this.editColumnsToDisplay($event);
  155 + }
  156 + };
  157 +
  158 + private statusFilterAction: WidgetAction = {
  159 + name: 'alarm.alarm-status-filter',
  160 + show: true,
  161 + onAction: ($event) => {
  162 + this.editAlarmStatusFilter($event);
  163 + },
  164 + icon: 'filter_list'
  165 + };
  166 +
  167 + constructor(protected store: Store<AppState>,
  168 + private elementRef: ElementRef,
  169 + private ngZone: NgZone,
  170 + private overlay: Overlay,
  171 + private viewContainerRef: ViewContainerRef,
  172 + private utils: UtilsService,
  173 + public translate: TranslateService,
  174 + private domSanitizer: DomSanitizer,
  175 + private datePipe: DatePipe) {
  176 + super(store);
  177 +
  178 + const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
  179 + this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
  180 + }
  181 +
  182 + ngOnInit(): void {
  183 + this.ctx.$scope.alarmsTableWidget = this;
  184 + this.settings = this.ctx.settings;
  185 + this.widgetConfig = this.ctx.widgetConfig;
  186 + this.subscription = this.ctx.defaultSubscription;
  187 + this.alarmSource = this.subscription.alarmSource;
  188 + this.initializeConfig();
  189 + this.updateAlarmSource();
  190 + this.ctx.updateWidgetParams();
  191 + }
  192 +
  193 + ngAfterViewInit(): void {
  194 + fromEvent(this.searchInputField.nativeElement, 'keyup')
  195 + .pipe(
  196 + debounceTime(150),
  197 + distinctUntilChanged(),
  198 + tap(() => {
  199 + if (this.displayPagination) {
  200 + this.paginator.pageIndex = 0;
  201 + }
  202 + this.updateData();
  203 + })
  204 + )
  205 + .subscribe();
  206 +
  207 + if (this.displayPagination) {
  208 + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
  209 + }
  210 + (this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange)
  211 + .pipe(
  212 + tap(() => this.updateData())
  213 + )
  214 + .subscribe();
  215 + this.updateData();
  216 + }
  217 +
  218 + public onDataUpdated() {
  219 + this.ngZone.run(() => {
  220 + this.alarmsDatasource.updateAlarms(this.subscription.alarms);
  221 + this.ctx.detectChanges();
  222 + });
  223 + }
  224 +
  225 + private initializeConfig() {
  226 + this.ctx.widgetActions = [this.searchAction, this.statusFilterAction, this.columnDisplayAction];
  227 +
  228 + this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
  229 + this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true;
  230 + this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true;
  231 +
  232 + if (this.displayDetails) {
  233 + this.actionCellDescriptors.push(
  234 + {
  235 + displayName: this.translate.instant('alarm.details'),
  236 + icon: 'more_horiz',
  237 + details: true
  238 + } as AlarmWidgetActionDescriptor
  239 + );
  240 + }
  241 +
  242 + if (this.allowAcknowledgment) {
  243 + this.actionCellDescriptors.push(
  244 + {
  245 + displayName: this.translate.instant('alarm.acknowledge'),
  246 + icon: 'done',
  247 + acknowledge: true
  248 + } as AlarmWidgetActionDescriptor
  249 + );
  250 + }
  251 +
  252 + if (this.allowClear) {
  253 + this.actionCellDescriptors.push(
  254 + {
  255 + displayName: this.translate.instant('alarm.clear'),
  256 + icon: 'clear',
  257 + clear: true
  258 + } as AlarmWidgetActionDescriptor
  259 + );
  260 + }
  261 +
  262 + this.actionCellDescriptors = this.actionCellDescriptors.concat(this.ctx.actionsApi.getActionDescriptors('actionCellButton'));
  263 +
  264 + let alarmsTitle: string;
  265 +
  266 + if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) {
  267 + alarmsTitle = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle);
  268 + } else {
  269 + alarmsTitle = this.translate.instant('alarm.alarms');
  270 + }
  271 +
  272 + this.ctx.widgetTitle = this.utils.createLabelFromDatasource(this.alarmSource, alarmsTitle);
  273 +
  274 + this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
  275 + if (!this.allowAcknowledgment && !this.allowClear) {
  276 + this.enableSelection = false;
  277 + }
  278 +
  279 + this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
  280 + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
  281 + this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
  282 + this.statusFilterAction.show = isDefined(this.settings.enableStatusFilter) ? this.settings.enableStatusFilter : true;
  283 +
  284 + const pageSize = this.settings.defaultPageSize;
  285 + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
  286 + this.defaultPageSize = pageSize;
  287 + }
  288 + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3];
  289 + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
  290 +
  291 + const cssString = constructTableCssString(this.widgetConfig);
  292 + const cssParser = new cssjs();
  293 + cssParser.testMode = false;
  294 + const namespace = 'alarms-table-' + this.utils.hashCode(cssString);
  295 + cssParser.cssPreviewNamespace = namespace;
  296 + cssParser.createStyleElement(namespace, cssString);
  297 + $(this.elementRef.nativeElement).addClass(namespace);
  298 + }
  299 +
  300 + private updateAlarmSource() {
  301 +
  302 + if (this.enableSelection) {
  303 + this.displayedColumns.push('select');
  304 + }
  305 +
  306 + if (this.alarmSource) {
  307 + this.alarmSource.dataKeys.forEach((_dataKey) => {
  308 + const dataKey: EntityColumn = deepClone(_dataKey) as EntityColumn;
  309 + dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label);
  310 + dataKey.def = 'def' + this.columns.length;
  311 + const keySettings: AlarmsTableDataKeySettings = dataKey.settings;
  312 +
  313 + this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings);
  314 + this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx');
  315 + this.columnWidth[dataKey.def] = getColumnWidth(keySettings);
  316 + this.columns.push(dataKey);
  317 + });
  318 + this.displayedColumns.push(...this.columns.map(column => column.def));
  319 + }
  320 + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
  321 + this.defaultSortOrder = this.settings.defaultSortOrder;
  322 + }
  323 + this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
  324 + this.sortOrderProperty = toAlarmColumnDef(this.pageLink.sortOrder.property, this.columns);
  325 +
  326 + if (this.actionCellDescriptors.length) {
  327 + this.displayedColumns.push('actions');
  328 + }
  329 + this.alarmsDatasource = new AlarmsDatasource();
  330 + if (this.enableSelection) {
  331 + this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => {
  332 + const hideTitlePanel = selectionMode || this.textSearchMode;
  333 + if (this.ctx.hideTitlePanel !== hideTitlePanel) {
  334 + this.ctx.hideTitlePanel = hideTitlePanel;
  335 + this.ctx.detectChanges(true);
  336 + } else {
  337 + this.ctx.detectChanges();
  338 + }
  339 + });
  340 + }
  341 + }
  342 +
  343 + private editColumnsToDisplay($event: Event) {
  344 + if ($event) {
  345 + $event.stopPropagation();
  346 + }
  347 + const target = $event.target || $event.srcElement || $event.currentTarget;
  348 + const config = new OverlayConfig();
  349 + config.backdropClass = 'cdk-overlay-transparent-backdrop';
  350 + config.hasBackdrop = true;
  351 + const connectedPosition: ConnectedPosition = {
  352 + originX: 'end',
  353 + originY: 'bottom',
  354 + overlayX: 'end',
  355 + overlayY: 'top'
  356 + };
  357 + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
  358 + .withPositions([connectedPosition]);
  359 +
  360 + const overlayRef = this.overlay.create(config);
  361 + overlayRef.backdropClick().subscribe(() => {
  362 + overlayRef.dispose();
  363 + });
  364 +
  365 + const columns: DisplayColumn[] = this.columns.map(column => {
  366 + return {
  367 + title: column.title,
  368 + def: column.def,
  369 + display: this.displayedColumns.indexOf(column.def) > -1
  370 + }
  371 + });
  372 +
  373 + const injectionTokens = new WeakMap<any, any>([
  374 + [DISPLAY_COLUMNS_PANEL_DATA, {
  375 + columns,
  376 + columnsUpdated: (newColumns) => {
  377 + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
  378 + if (this.enableSelection) {
  379 + this.displayedColumns.unshift('select');
  380 + }
  381 + this.displayedColumns.push('actions');
  382 + }
  383 + } as DisplayColumnsPanelData],
  384 + [OverlayRef, overlayRef]
  385 + ]);
  386 + const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
  387 + overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
  388 + this.viewContainerRef, injector));
  389 + this.ctx.detectChanges();
  390 + }
  391 +
  392 + private editAlarmStatusFilter($event: Event) {
  393 + // TODO:
  394 + }
  395 +
  396 + private enterFilterMode() {
  397 + this.textSearchMode = true;
  398 + this.pageLink.textSearch = '';
  399 + this.ctx.hideTitlePanel = true;
  400 + this.ctx.detectChanges(true);
  401 + setTimeout(() => {
  402 + this.searchInputField.nativeElement.focus();
  403 + this.searchInputField.nativeElement.setSelectionRange(0, 0);
  404 + }, 10);
  405 + }
  406 +
  407 + exitFilterMode() {
  408 + this.textSearchMode = false;
  409 + this.pageLink.textSearch = null;
  410 + if (this.displayPagination) {
  411 + this.paginator.pageIndex = 0;
  412 + }
  413 + this.updateData();
  414 + this.ctx.hideTitlePanel = false;
  415 + this.ctx.detectChanges(true);
  416 + }
  417 +
  418 + private updateData() {
  419 + if (this.displayPagination) {
  420 + this.pageLink.page = this.paginator.pageIndex;
  421 + this.pageLink.pageSize = this.paginator.pageSize;
  422 + } else {
  423 + this.pageLink.page = 0;
  424 + }
  425 + this.pageLink.sortOrder.property = fromAlarmColumnDef(this.sort.active, this.columns);
  426 + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
  427 + this.alarmsDatasource.loadAlarms(this.pageLink);
  428 + this.ctx.detectChanges();
  429 + }
  430 +
  431 + public trackByColumnDef(index, column: EntityColumn) {
  432 + return column.def;
  433 + }
  434 +
  435 + public headerStyle(key: EntityColumn): any {
  436 + const columnWidth = this.columnWidth[key.def];
  437 + return {
  438 + width: columnWidth
  439 + }
  440 + }
  441 +
  442 + public cellStyle(alarm: AlarmInfo, key: EntityColumn): any {
  443 + let style: any = {};
  444 + if (alarm && key) {
  445 + const styleInfo = this.stylesInfo[key.def];
  446 + const value = getAlarmValue(alarm, key);
  447 + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
  448 + try {
  449 + style = styleInfo.cellStyleFunction(value);
  450 + } catch (e) {
  451 + style = {};
  452 + }
  453 + } else {
  454 + style = this.defaultStyle(key, value);
  455 + }
  456 + }
  457 + if (!style.width) {
  458 + const columnWidth = this.columnWidth[key.def];
  459 + style.width = columnWidth;
  460 + }
  461 + return style;
  462 + }
  463 +
  464 + public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml {
  465 + let strContent = '';
  466 + if (alarm && key) {
  467 + const contentInfo = this.contentsInfo[key.def];
  468 + const value = getAlarmValue(alarm, key);
  469 + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
  470 + if (isDefined(value)) {
  471 + strContent = '' + value;
  472 + }
  473 + var content = strContent;
  474 + try {
  475 + content = contentInfo.cellContentFunction(value, alarm, this.ctx);
  476 + } catch (e) {
  477 + content = strContent;
  478 + }
  479 + } else {
  480 + content = this.defaultContent(key, value);
  481 + }
  482 + return this.domSanitizer.bypassSecurityTrustHtml(content);
  483 + } else {
  484 + return strContent;
  485 + }
  486 + }
  487 +
  488 + public onRowClick($event: Event, alarm: AlarmInfo) {
  489 + if ($event) {
  490 + $event.stopPropagation();
  491 + }
  492 + this.alarmsDatasource.toggleCurrentAlarm(alarm);
  493 + const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick');
  494 + if (descriptors.length) {
  495 + let entityId;
  496 + let entityName;
  497 + if (alarm && alarm.originator) {
  498 + entityId = alarm.originator;
  499 + entityName = alarm.originatorName;
  500 + }
  501 + this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm});
  502 + }
  503 + }
  504 +
  505 + public onActionButtonClick($event: Event, alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor) {
  506 + if (actionDescriptor.details) {
  507 + this.openAlarmDetails($event, alarm);
  508 + } else if (actionDescriptor.acknowledge) {
  509 + this.ackAlarm($event, alarm);
  510 + } else if (actionDescriptor.clear) {
  511 + this.clearAlarm($event, alarm);
  512 + } else {
  513 + if ($event) {
  514 + $event.stopPropagation();
  515 + }
  516 + let entityId;
  517 + let entityName;
  518 + if (alarm && alarm.originator) {
  519 + entityId = alarm.originator;
  520 + entityName = alarm.originatorName;
  521 + }
  522 + this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm});
  523 + }
  524 + }
  525 +
  526 + public actionEnabled(alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor): boolean {
  527 + if (actionDescriptor.acknowledge) {
  528 + return (alarm.status === AlarmStatus.ACTIVE_UNACK ||
  529 + alarm.status === AlarmStatus.CLEARED_UNACK);
  530 + } else if (actionDescriptor.clear) {
  531 + return (alarm.status === AlarmStatus.ACTIVE_ACK ||
  532 + alarm.status === AlarmStatus.ACTIVE_UNACK);
  533 + }
  534 + return true;
  535 + }
  536 +
  537 + private openAlarmDetails($event: Event, alarm: AlarmInfo) {
  538 + if ($event) {
  539 + $event.stopPropagation();
  540 + }
  541 + // TODO:
  542 + }
  543 +
  544 + private ackAlarm($event: Event, alarm: AlarmInfo) {
  545 + if ($event) {
  546 + $event.stopPropagation();
  547 + }
  548 + // TODO:
  549 + }
  550 +
  551 + public ackAlarms($event: Event) {
  552 + if ($event) {
  553 + $event.stopPropagation();
  554 + }
  555 + // TODO:
  556 + }
  557 +
  558 + private clearAlarm($event: Event, alarm: AlarmInfo) {
  559 + if ($event) {
  560 + $event.stopPropagation();
  561 + }
  562 + // TODO:
  563 + }
  564 +
  565 + public clearAlarms($event: Event) {
  566 + if ($event) {
  567 + $event.stopPropagation();
  568 + }
  569 + // TODO:
  570 + }
  571 +
  572 + private defaultContent(key: EntityColumn, value: any): any {
  573 + if (isDefined(value)) {
  574 + const alarmField = alarmFields[key.name];
  575 + if (alarmField) {
  576 + if (alarmField.time) {
  577 + return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss');
  578 + } else if (alarmField.value === alarmFields.severity.value) {
  579 + return this.translate.instant(alarmSeverityTranslations.get(value));
  580 + } else if (alarmField.value === alarmFields.status.value) {
  581 + return this.translate.instant(alarmStatusTranslations.get(value));
  582 + } else if (alarmField.value === alarmFields.originatorType.value) {
  583 + return this.translate.instant(entityTypeTranslations.get(value).type);
  584 + }
  585 + else {
  586 + return value;
  587 + }
  588 + } else {
  589 + return value;
  590 + }
  591 + } else {
  592 + return '';
  593 + }
  594 + }
  595 +
  596 + private defaultStyle(key: EntityColumn, value: any): any {
  597 + if (isDefined(value)) {
  598 + const alarmField = alarmFields[key.name];
  599 + if (alarmField) {
  600 + if (alarmField.value == alarmFields.severity.value) {
  601 + return {
  602 + fontWeight: 'bold',
  603 + color: alarmSeverityColors.get(value)
  604 + };
  605 + } else {
  606 + return {};
  607 + }
  608 + } else {
  609 + return {};
  610 + }
  611 + } else {
  612 + return {};
  613 + }
  614 + }
  615 +
  616 +}
  617 +
  618 +class AlarmsDatasource implements DataSource<AlarmInfo> {
  619 +
  620 + private alarmsSubject = new BehaviorSubject<AlarmInfo[]>([]);
  621 + private pageDataSubject = new BehaviorSubject<PageData<AlarmInfo>>(emptyPageData<AlarmInfo>());
  622 +
  623 + public selection = new SelectionModel<AlarmInfo>(true, [], false);
  624 +
  625 + private selectionModeChanged = new EventEmitter<boolean>();
  626 +
  627 + public selectionModeChanged$ = this.selectionModeChanged.asObservable();
  628 +
  629 + private allAlarms: Array<AlarmInfo> = [];
  630 + private allAlarmsSubject = new BehaviorSubject<AlarmInfo[]>([]);
  631 + private allAlarms$: Observable<Array<AlarmInfo>> = this.allAlarmsSubject.asObservable();
  632 +
  633 + private currentAlarm: AlarmInfo = null;
  634 +
  635 + constructor() {
  636 + }
  637 +
  638 + connect(collectionViewer: CollectionViewer): Observable<AlarmInfo[] | ReadonlyArray<AlarmInfo>> {
  639 + return this.alarmsSubject.asObservable();
  640 + }
  641 +
  642 + disconnect(collectionViewer: CollectionViewer): void {
  643 + this.alarmsSubject.complete();
  644 + this.pageDataSubject.complete();
  645 + }
  646 +
  647 + loadAlarms(pageLink: PageLink) {
  648 + if (this.selection.hasValue()) {
  649 + this.selection.clear();
  650 + this.onSelectionModeChanged(false);
  651 + }
  652 + this.fetchAlarms(pageLink).pipe(
  653 + catchError(() => of(emptyPageData<AlarmInfo>())),
  654 + ).subscribe(
  655 + (pageData) => {
  656 + this.alarmsSubject.next(pageData.data);
  657 + this.pageDataSubject.next(pageData);
  658 + }
  659 + );
  660 + }
  661 +
  662 + updateAlarms(alarms: AlarmInfo[]) {
  663 + alarms.forEach((newAlarm) => {
  664 + const existingAlarmIndex = this.allAlarms.findIndex(alarm => alarm.id.id === newAlarm.id.id);
  665 + if (existingAlarmIndex > -1) {
  666 + Object.assign(this.allAlarms[existingAlarmIndex], newAlarm);
  667 + } else {
  668 + this.allAlarms.push(newAlarm);
  669 + }
  670 + });
  671 + for (let i = this.allAlarms.length - 1; i >= 0; i--) {
  672 + const oldAlarm = this.allAlarms[i];
  673 + const newAlarmIndex = alarms.findIndex(alarm => alarm.id.id === oldAlarm.id.id);
  674 + if (newAlarmIndex === -1) {
  675 + this.allAlarms.splice(i, 1);
  676 + }
  677 + }
  678 + if (this.selection.hasValue()) {
  679 + const toRemove: AlarmInfo[] = [];
  680 + this.selection.selected.forEach((selectedAlarm) => {
  681 + const existingAlarm = this.allAlarms.find(alarm => alarm.id.id === selectedAlarm.id.id);
  682 + if (!existingAlarm) {
  683 + toRemove.push(selectedAlarm);
  684 + }
  685 + });
  686 + this.selection.deselect(...toRemove);
  687 + if (this.selection.isEmpty()) {
  688 + this.onSelectionModeChanged(false);
  689 + }
  690 + }
  691 + this.allAlarmsSubject.next(this.allAlarms);
  692 + }
  693 +
  694 + isAllSelected(): Observable<boolean> {
  695 + const numSelected = this.selection.selected.length;
  696 + return this.alarmsSubject.pipe(
  697 + map((alarms) => numSelected === alarms.length)
  698 + );
  699 + }
  700 +
  701 + isEmpty(): Observable<boolean> {
  702 + return this.alarmsSubject.pipe(
  703 + map((alarms) => !alarms.length)
  704 + );
  705 + }
  706 +
  707 + total(): Observable<number> {
  708 + return this.pageDataSubject.pipe(
  709 + map((pageData) => pageData.totalElements)
  710 + );
  711 + }
  712 +
  713 + toggleSelection(alarm: AlarmInfo) {
  714 + const hasValue = this.selection.hasValue();
  715 + this.selection.toggle(alarm);
  716 + if (hasValue !== this.selection.hasValue()) {
  717 + this.onSelectionModeChanged(this.selection.hasValue());
  718 + }
  719 + }
  720 +
  721 + isSelected(alarm: AlarmInfo): boolean {
  722 + return this.selection.isSelected(alarm);
  723 + }
  724 +
  725 + masterToggle() {
  726 + this.alarmsSubject.pipe(
  727 + tap((alarms) => {
  728 + const numSelected = this.selection.selected.length;
  729 + if (numSelected === alarms.length) {
  730 + this.selection.clear();
  731 + if (numSelected > 0) {
  732 + this.onSelectionModeChanged(false);
  733 + }
  734 + } else {
  735 + alarms.forEach(row => {
  736 + this.selection.select(row);
  737 + });
  738 + if (numSelected === 0) {
  739 + this.onSelectionModeChanged(true);
  740 + }
  741 + }
  742 + }),
  743 + take(1)
  744 + ).subscribe();
  745 + }
  746 +
  747 + public toggleCurrentAlarm(alarm: AlarmInfo): boolean {
  748 + if (this.currentAlarm !== alarm) {
  749 + this.currentAlarm = alarm;
  750 + return true;
  751 + } else {
  752 + return false;
  753 + }
  754 + }
  755 +
  756 + public isCurrentAlarm(alarm: AlarmInfo): boolean {
  757 + return (this.currentAlarm && alarm && this.currentAlarm.id && alarm.id) &&
  758 + (this.currentAlarm.id.id === alarm.id.id);
  759 + }
  760 +
  761 + private onSelectionModeChanged(selectionMode: boolean) {
  762 + this.selectionModeChanged.emit(selectionMode);
  763 + }
  764 +
  765 + private fetchAlarms(pageLink: PageLink): Observable<PageData<AlarmInfo>> {
  766 + return this.allAlarms$.pipe(
  767 + map((data) => pageLink.filterData(data))
  768 + );
  769 + }
  770 +}
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-entity-table tb-absolute-fill"> 18 +<div class="tb-table-widget tb-absolute-fill">
19 <div fxFlex fxLayout="column" class="tb-absolute-fill"> 19 <div fxFlex fxLayout="column" class="tb-absolute-fill">
20 <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> 20 <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
21 <div class="mat-toolbar-tools"> 21 <div class="mat-toolbar-tools">
@@ -39,8 +39,8 @@ @@ -39,8 +39,8 @@
39 </mat-toolbar> 39 </mat-toolbar>
40 <div fxFlex class="table-container"> 40 <div fxFlex class="table-container">
41 <mat-table [dataSource]="entityDatasource" 41 <mat-table [dataSource]="entityDatasource"
42 - matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>  
43 - <ng-container [matColumnDef]="column.label" *ngFor="let column of columns; trackBy: trackByColumnLabel;"> 42 + matSort [matSortActive]="sortOrderProperty" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
  43 + <ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
44 <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell> 44 <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
45 <mat-cell *matCellDef="let entity;" 45 <mat-cell *matCellDef="let entity;"
46 [innerHTML]="cellContent(entity, column)" 46 [innerHTML]="cellContent(entity, column)"
@@ -16,24 +16,4 @@ @@ -16,24 +16,4 @@
16 :host { 16 :host {
17 width: 100%; 17 width: 100%;
18 height: 100%; 18 height: 100%;
19 - .tb-entity-table {  
20 - .mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar {  
21 - background: transparent;  
22 - }  
23 - mat-toolbar {  
24 - height: 39px;  
25 - max-height: 39px;  
26 - .mat-toolbar-tools {  
27 - height: 39px;  
28 - max-height: 39px;  
29 - }  
30 - }  
31 - .table-container {  
32 - overflow: auto;  
33 - }  
34 - }  
35 -}  
36 -  
37 -:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {  
38 - opacity: 1 !important;  
39 } 19 }
@@ -14,7 +14,16 @@ @@ -14,7 +14,16 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; 17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + ElementRef,
  21 + Input,
  22 + NgZone,
  23 + OnInit,
  24 + ViewChild,
  25 + ViewContainerRef
  26 +} from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 27 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 28 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 29 import { AppState } from '@core/core.state';
@@ -31,7 +40,6 @@ import { IWidgetSubscription } from '@core/api/widget-api.models'; @@ -31,7 +40,6 @@ import { IWidgetSubscription } from '@core/api/widget-api.models';
31 import { UtilsService } from '@core/services/utils.service'; 40 import { UtilsService } from '@core/services/utils.service';
32 import { TranslateService } from '@ngx-translate/core'; 41 import { TranslateService } from '@ngx-translate/core';
33 import { deepClone, isDefined, isNumber } from '@core/utils'; 42 import { deepClone, isDefined, isNumber } from '@core/utils';
34 -import * as tinycolor_ from 'tinycolor2';  
35 import cssjs from '@core/css/css'; 43 import cssjs from '@core/css/css';
36 import { PageLink } from '@shared/models/page/page-link'; 44 import { PageLink } from '@shared/models/page/page-link';
37 import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; 45 import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@@ -49,10 +57,18 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -49,10 +57,18 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
49 import { 57 import {
50 CellContentInfo, 58 CellContentInfo,
51 CellStyleInfo, 59 CellStyleInfo,
  60 + constructTableCssString,
52 DisplayColumn, 61 DisplayColumn,
53 EntityColumn, 62 EntityColumn,
54 EntityData, 63 EntityData,
55 - getEntityValue 64 + fromEntityColumnDef,
  65 + getCellContentInfo,
  66 + getCellStyleInfo,
  67 + getColumnWidth,
  68 + getEntityValue,
  69 + TableWidgetDataKeySettings,
  70 + TableWidgetSettings,
  71 + toEntityColumnDef
56 } from '@home/components/widget/lib/table-widget.models'; 72 } from '@home/components/widget/lib/table-widget.models';
57 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; 73 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
58 import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; 74 import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@@ -62,32 +78,20 @@ import { @@ -62,32 +78,20 @@ import {
62 DisplayColumnsPanelData 78 DisplayColumnsPanelData
63 } from '@home/components/widget/lib/display-columns-panel.component'; 79 } from '@home/components/widget/lib/display-columns-panel.component';
64 80
65 -const tinycolor = tinycolor_;  
66 -  
67 -interface EntitiesTableWidgetSettings { 81 +interface EntitiesTableWidgetSettings extends TableWidgetSettings {
68 entitiesTitle: string; 82 entitiesTitle: string;
69 - enableSearch: boolean;  
70 - enableSelectColumnDisplay: boolean;  
71 displayEntityName: boolean; 83 displayEntityName: boolean;
72 entityNameColumnTitle: string; 84 entityNameColumnTitle: string;
73 displayEntityType: boolean; 85 displayEntityType: boolean;
74 - displayPagination: boolean;  
75 - defaultPageSize: number;  
76 - defaultSortOrder: string;  
77 } 86 }
78 87
79 -interface EntitiesTableDataKeySettings {  
80 - columnWidth: string;  
81 - useCellStyleFunction: boolean;  
82 - cellStyleFunction: string;  
83 - useCellContentFunction: boolean;  
84 - cellContentFunction: string; 88 +interface EntitiesTableDataKeySettings extends TableWidgetDataKeySettings {
85 } 89 }
86 90
87 @Component({ 91 @Component({
88 selector: 'tb-entities-table-widget', 92 selector: 'tb-entities-table-widget',
89 templateUrl: './entities-table-widget.component.html', 93 templateUrl: './entities-table-widget.component.html',
90 - styleUrls: ['./entities-table-widget.component.scss'] 94 + styleUrls: ['./entities-table-widget.component.scss', './table-widget.scss']
91 }) 95 })
92 export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { 96 export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
93 97
@@ -101,6 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -101,6 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
101 public displayPagination = true; 105 public displayPagination = true;
102 public pageSizeOptions; 106 public pageSizeOptions;
103 public pageLink: PageLink; 107 public pageLink: PageLink;
  108 + public sortOrderProperty: string;
104 public textSearchMode = false; 109 public textSearchMode = false;
105 public columns: Array<EntityColumn> = []; 110 public columns: Array<EntityColumn> = [];
106 public displayedColumns: string[] = []; 111 public displayedColumns: string[] = [];
@@ -138,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -138,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
138 143
139 constructor(protected store: Store<AppState>, 144 constructor(protected store: Store<AppState>,
140 private elementRef: ElementRef, 145 private elementRef: ElementRef,
  146 + private ngZone: NgZone,
141 private overlay: Overlay, 147 private overlay: Overlay,
142 private viewContainerRef: ViewContainerRef, 148 private viewContainerRef: ViewContainerRef,
143 private utils: UtilsService, 149 private utils: UtilsService,
@@ -185,13 +191,17 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -185,13 +191,17 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
185 } 191 }
186 192
187 public onDataUpdated() { 193 public onDataUpdated() {
188 - this.entityDatasource.updateEntitiesData(this.subscription.data);  
189 - this.ctx.detectChanges(); 194 + this.ngZone.run(() => {
  195 + this.entityDatasource.updateEntitiesData(this.subscription.data);
  196 + this.ctx.detectChanges();
  197 + });
190 } 198 }
191 199
192 private initializeConfig() { 200 private initializeConfig() {
193 this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; 201 this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
194 202
  203 + this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
  204 +
195 let entitiesTitle: string; 205 let entitiesTitle: string;
196 206
197 if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) { 207 if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
@@ -212,78 +222,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -212,78 +222,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
212 this.defaultPageSize = pageSize; 222 this.defaultPageSize = pageSize;
213 } 223 }
214 this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3]; 224 this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3];
215 -  
216 - if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {  
217 - this.defaultSortOrder = this.settings.defaultSortOrder;  
218 - }  
219 -  
220 this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY; 225 this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
221 - this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);  
222 -  
223 - const origColor = this.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';  
224 - const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)';  
225 - const defaultColor = tinycolor(origColor);  
226 - const mdDark = defaultColor.setAlpha(0.87).toRgbString();  
227 - const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();  
228 - const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();  
229 - const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();  
230 -  
231 - const cssString =  
232 - '.mat-input-element::placeholder {\n' +  
233 - ' color: ' + mdDarkSecondary + ';\n'+  
234 - '}\n' +  
235 - '.mat-input-element::-moz-placeholder {\n' +  
236 - ' color: ' + mdDarkSecondary + ';\n'+  
237 - '}\n' +  
238 - '.mat-input-element::-webkit-input-placeholder {\n' +  
239 - ' color: ' + mdDarkSecondary + ';\n'+  
240 - '}\n' +  
241 - '.mat-input-element:-ms-input-placeholder {\n' +  
242 - ' color: ' + mdDarkSecondary + ';\n'+  
243 - '}\n' +  
244 - 'mat-toolbar.mat-table-toolbar {\n'+  
245 - 'color: ' + mdDark + ';\n'+  
246 - '}\n'+  
247 - 'mat-toolbar.mat-table-toolbar button.mat-icon-button mat-icon {\n'+  
248 - 'color: ' + mdDarkSecondary + ';\n'+  
249 - '}\n'+  
250 - '.mat-table .mat-header-row {\n'+  
251 - 'background-color: ' + origBackgroundColor + ';\n'+  
252 - '}\n'+  
253 - '.mat-table .mat-header-cell {\n'+  
254 - 'color: ' + mdDarkSecondary + ';\n'+  
255 - '}\n'+  
256 - '.mat-table .mat-header-cell .mat-sort-header-arrow {\n'+  
257 - 'color: ' + mdDarkDisabled + ';\n'+  
258 - '}\n'+  
259 - '.mat-table .mat-row, .mat-table .mat-header-row {\n'+  
260 - 'border-bottom-color: '+mdDarkDivider+';\n'+  
261 - '}\n'+  
262 - '.mat-table .mat-row:not(.tb-current-entity):not(:hover) .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n'+  
263 - 'background-color: ' + origBackgroundColor + ';\n'+  
264 - '}\n'+  
265 - '.mat-table .mat-cell {\n'+  
266 - 'color: ' + mdDark + ';\n'+  
267 - '}\n'+  
268 - '.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+  
269 - 'color: ' + mdDarkSecondary + ';\n'+  
270 - '}\n'+  
271 - '.mat-divider {\n'+  
272 - 'border-top-color: ' + mdDarkDivider + ';\n'+  
273 - '}\n'+  
274 - '.mat-paginator {\n'+  
275 - 'color: ' + mdDarkSecondary + ';\n'+  
276 - '}\n'+  
277 - '.mat-paginator button.mat-icon-button {\n'+  
278 - 'color: ' + mdDarkSecondary + ';\n'+  
279 - '}\n'+  
280 - '.mat-paginator button.mat-icon-button[disabled][disabled] {\n'+  
281 - 'color: ' + mdDarkDisabled + ';\n'+  
282 - '}\n'+  
283 - '.mat-paginator .mat-select-value {\n'+  
284 - 'color: ' + mdDarkSecondary + ';\n'+  
285 - '}';  
286 226
  227 + const cssString = constructTableCssString(this.widgetConfig);
287 const cssParser = new cssjs(); 228 const cssParser = new cssjs();
288 cssParser.testMode = false; 229 cssParser.testMode = false;
289 const namespace = 'entities-table-' + this.utils.hashCode(cssString); 230 const namespace = 'entities-table-' + this.utils.hashCode(cssString);
@@ -294,8 +235,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -294,8 +235,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
294 235
295 private updateDatasources() { 236 private updateDatasources() {
296 237
297 - this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');  
298 -  
299 const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true; 238 const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true;
300 let entityNameColumnTitle: string; 239 let entityNameColumnTitle: string;
301 if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) { 240 if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) {
@@ -306,11 +245,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -306,11 +245,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
306 const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true; 245 const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true;
307 246
308 if (displayEntityName) { 247 if (displayEntityName) {
309 - this.displayedColumns.push('entityName');  
310 this.columns.push( 248 this.columns.push(
311 { 249 {
312 name: 'entityName', 250 name: 'entityName',
313 label: 'entityName', 251 label: 'entityName',
  252 + def: 'entityName',
314 title: entityNameColumnTitle 253 title: entityNameColumnTitle
315 } as EntityColumn 254 } as EntityColumn
316 ); 255 );
@@ -320,14 +259,14 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -320,14 +259,14 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
320 this.stylesInfo['entityName'] = { 259 this.stylesInfo['entityName'] = {
321 useCellStyleFunction: false 260 useCellStyleFunction: false
322 }; 261 };
323 - this.columnWidth['entityName'] = '100px'; 262 + this.columnWidth['entityName'] = '0px';
324 } 263 }
325 if (displayEntityType) { 264 if (displayEntityType) {
326 - this.displayedColumns.push('entityType');  
327 this.columns.push( 265 this.columns.push(
328 { 266 {
329 name: 'entityType', 267 name: 'entityType',
330 label: 'entityType', 268 label: 'entityType',
  269 + def: 'entityType',
331 title: this.translate.instant('entity.entity-type'), 270 title: this.translate.instant('entity.entity-type'),
332 } as EntityColumn 271 } as EntityColumn
333 ); 272 );
@@ -337,7 +276,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -337,7 +276,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
337 this.stylesInfo['entityType'] = { 276 this.stylesInfo['entityType'] = {
338 useCellStyleFunction: false 277 useCellStyleFunction: false
339 }; 278 };
340 - this.columnWidth['entityType'] = '100px'; 279 + this.columnWidth['entityType'] = '0px';
341 } 280 }
342 281
343 const dataKeys: Array<DataKey> = []; 282 const dataKeys: Array<DataKey> = [];
@@ -353,55 +292,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -353,55 +292,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
353 dataKeys.push(dataKey); 292 dataKeys.push(dataKey);
354 293
355 dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label); 294 dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label);
  295 + dataKey.def = 'def' + this.columns.length;
356 const keySettings: EntitiesTableDataKeySettings = dataKey.settings; 296 const keySettings: EntitiesTableDataKeySettings = dataKey.settings;
357 297
358 - let cellStyleFunction: Function = null;  
359 - let useCellStyleFunction = false;  
360 -  
361 - if (keySettings.useCellStyleFunction === true) {  
362 - if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {  
363 - try {  
364 - cellStyleFunction = new Function('value', keySettings.cellStyleFunction);  
365 - useCellStyleFunction = true;  
366 - } catch (e) {  
367 - cellStyleFunction = null;  
368 - useCellStyleFunction = false;  
369 - }  
370 - }  
371 - }  
372 - this.stylesInfo[dataKey.label] = {  
373 - useCellStyleFunction,  
374 - cellStyleFunction  
375 - };  
376 -  
377 - let cellContentFunction: Function = null;  
378 - let useCellContentFunction = false;  
379 -  
380 - if (keySettings.useCellContentFunction === true) {  
381 - if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {  
382 - try {  
383 - cellContentFunction = new Function('value, entity, ctx', keySettings.cellContentFunction);  
384 - useCellContentFunction = true;  
385 - } catch (e) {  
386 - cellContentFunction = null;  
387 - useCellContentFunction = false;  
388 - }  
389 - }  
390 - }  
391 -  
392 - this.contentsInfo[dataKey.label] = {  
393 - useCellContentFunction,  
394 - cellContentFunction,  
395 - units: dataKey.units,  
396 - decimals: dataKey.decimals  
397 - };  
398 -  
399 - const columnWidth = isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';  
400 - this.columnWidth[dataKey.label] = columnWidth;  
401 - this.displayedColumns.push(dataKey.label); 298 + this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings);
  299 + this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, entity, ctx');
  300 + this.contentsInfo[dataKey.def].units = dataKey.units;
  301 + this.contentsInfo[dataKey.def].decimals = dataKey.decimals;
  302 + this.columnWidth[dataKey.def] = getColumnWidth(keySettings);
402 this.columns.push(dataKey); 303 this.columns.push(dataKey);
403 }); 304 });
  305 + this.displayedColumns.push(...this.columns.map(column => column.def));
  306 + }
  307 +
  308 + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
  309 + this.defaultSortOrder = this.settings.defaultSortOrder;
404 } 310 }
  311 + this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
  312 + this.sortOrderProperty = toEntityColumnDef(this.pageLink.sortOrder.property, this.columns);
405 313
406 if (this.actionCellDescriptors.length) { 314 if (this.actionCellDescriptors.length) {
407 this.displayedColumns.push('actions'); 315 this.displayedColumns.push('actions');
@@ -435,8 +343,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -435,8 +343,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
435 const columns: DisplayColumn[] = this.columns.map(column => { 343 const columns: DisplayColumn[] = this.columns.map(column => {
436 return { 344 return {
437 title: column.title, 345 title: column.title,
438 - label: column.label,  
439 - display: this.displayedColumns.indexOf(column.label) > -1 346 + def: column.def,
  347 + display: this.displayedColumns.indexOf(column.def) > -1
440 } 348 }
441 }); 349 });
442 350
@@ -444,7 +352,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -444,7 +352,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
444 [DISPLAY_COLUMNS_PANEL_DATA, { 352 [DISPLAY_COLUMNS_PANEL_DATA, {
445 columns, 353 columns,
446 columnsUpdated: (newColumns) => { 354 columnsUpdated: (newColumns) => {
447 - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.label); 355 + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
448 this.displayedColumns.push('actions'); 356 this.displayedColumns.push('actions');
449 } 357 }
450 } as DisplayColumnsPanelData], 358 } as DisplayColumnsPanelData],
@@ -485,24 +393,27 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -485,24 +393,27 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
485 } else { 393 } else {
486 this.pageLink.page = 0; 394 this.pageLink.page = 0;
487 } 395 }
488 - this.pageLink.sortOrder.property = this.sort.active; 396 + this.pageLink.sortOrder.property = fromEntityColumnDef(this.sort.active, this.columns);
489 this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; 397 this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
490 this.entityDatasource.loadEntities(this.pageLink); 398 this.entityDatasource.loadEntities(this.pageLink);
491 this.ctx.detectChanges(); 399 this.ctx.detectChanges();
492 } 400 }
493 401
494 - public trackByColumnLabel(index, column: EntityColumn) {  
495 - return column.label; 402 + public trackByColumnDef(index, column: EntityColumn) {
  403 + return column.def;
496 } 404 }
497 405
498 public headerStyle(key: EntityColumn): any { 406 public headerStyle(key: EntityColumn): any {
499 - return this.widthStyle(key); 407 + const columnWidth = this.columnWidth[key.def];
  408 + return {
  409 + width: columnWidth
  410 + }
500 } 411 }
501 412
502 public cellStyle(entity: EntityData, key: EntityColumn): any { 413 public cellStyle(entity: EntityData, key: EntityColumn): any {
503 let style: any = {}; 414 let style: any = {};
504 if (entity && key) { 415 if (entity && key) {
505 - const styleInfo = this.stylesInfo[key.label]; 416 + const styleInfo = this.stylesInfo[key.def];
506 const value = getEntityValue(entity, key); 417 const value = getEntityValue(entity, key);
507 if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { 418 if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
508 try { 419 try {
@@ -514,20 +425,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -514,20 +425,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
514 style = this.defaultStyle(key, value); 425 style = this.defaultStyle(key, value);
515 } 426 }
516 } 427 }
517 - const widthStyle = this.widthStyle(key);  
518 - style = {...style, ...widthStyle};  
519 - return style;  
520 - }  
521 -  
522 - private widthStyle(key: EntityColumn): any {  
523 - let style: any = {};  
524 - const columnWidth = this.columnWidth[key.label];  
525 - if (columnWidth !== "0px") {  
526 - style.minWidth = columnWidth; 428 + if (!style.width) {
  429 + const columnWidth = this.columnWidth[key.def];
527 style.width = columnWidth; 430 style.width = columnWidth;
528 - } else {  
529 - style.minWidth = "auto";  
530 - style.width = "auto";  
531 } 431 }
532 return style; 432 return style;
533 } 433 }
@@ -535,7 +435,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @@ -535,7 +435,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
535 public cellContent(entity: EntityData, key: EntityColumn): SafeHtml { 435 public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
536 let strContent = ''; 436 let strContent = '';
537 if (entity && key) { 437 if (entity && key) {
538 - const contentInfo = this.contentsInfo[key.label]; 438 + const contentInfo = this.contentsInfo[key.def];
539 const value = getEntityValue(entity, key); 439 const value = getEntityValue(entity, key);
540 if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { 440 if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
541 if (isDefined(value)) { 441 if (isDefined(value)) {
@@ -15,7 +15,28 @@ @@ -15,7 +15,28 @@
15 /// 15 ///
16 16
17 import { EntityId } from '@shared/models/id/entity-id'; 17 import { EntityId } from '@shared/models/id/entity-id';
18 -import { DataKey } from '@shared/models/widget.models'; 18 +import { DataKey, WidgetConfig } from '@shared/models/widget.models';
  19 +import { getDescendantProp, isDefined } from '@core/utils';
  20 +import { alarmFields, AlarmInfo } from '@shared/models/alarm.models';
  21 +import * as tinycolor_ from 'tinycolor2';
  22 +
  23 +const tinycolor = tinycolor_;
  24 +
  25 +export interface TableWidgetSettings {
  26 + enableSearch: boolean;
  27 + enableSelectColumnDisplay: boolean;
  28 + displayPagination: boolean;
  29 + defaultPageSize: number;
  30 + defaultSortOrder: string;
  31 +}
  32 +
  33 +export interface TableWidgetDataKeySettings {
  34 + columnWidth: string;
  35 + useCellStyleFunction: boolean;
  36 + cellStyleFunction: string;
  37 + useCellContentFunction: boolean;
  38 + cellContentFunction: string;
  39 +}
19 40
20 export interface EntityData { 41 export interface EntityData {
21 id: EntityId; 42 id: EntityId;
@@ -25,12 +46,13 @@ export interface EntityData { @@ -25,12 +46,13 @@ export interface EntityData {
25 } 46 }
26 47
27 export interface EntityColumn extends DataKey { 48 export interface EntityColumn extends DataKey {
  49 + def: string;
28 title: string; 50 title: string;
29 } 51 }
30 52
31 export interface DisplayColumn { 53 export interface DisplayColumn {
32 title: string; 54 title: string;
33 - label: string; 55 + def: string;
34 display: boolean; 56 display: boolean;
35 } 57 }
36 58
@@ -46,10 +68,186 @@ export interface CellStyleInfo { @@ -46,10 +68,186 @@ export interface CellStyleInfo {
46 cellStyleFunction?: Function; 68 cellStyleFunction?: Function;
47 } 69 }
48 70
  71 +export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string {
  72 + let res = searchValue;
  73 + const column = columns.find(column => column[searchProperty] === searchValue);
  74 + if (column) {
  75 + res = column[columnProperty];
  76 + }
  77 + return res;
  78 +}
  79 +
  80 +export function toEntityColumnDef(label: string, columns: EntityColumn[]): string {
  81 + return findColumnProperty('label', label, 'def', columns);
  82 +}
  83 +
  84 +export function fromEntityColumnDef(def: string, columns: EntityColumn[]): string {
  85 + return findColumnProperty('def', def, 'label', columns);
  86 +}
  87 +
  88 +export function toAlarmColumnDef(name: string, columns: EntityColumn[]): string {
  89 + return findColumnProperty('name', name, 'def', columns);
  90 +}
  91 +
  92 +export function fromAlarmColumnDef(def: string, columns: EntityColumn[]): string {
  93 + return findColumnProperty('def', def, 'name', columns);
  94 +}
  95 +
49 export function getEntityValue(entity: any, key: DataKey): any { 96 export function getEntityValue(entity: any, key: DataKey): any {
50 return getDescendantProp(entity, key.label); 97 return getDescendantProp(entity, key.label);
51 } 98 }
52 99
53 -export function getDescendantProp(obj: any, path: string): any {  
54 - return path.split('.').reduce((acc, part) => acc && acc[part], obj) 100 +export function getAlarmValue(alarm: AlarmInfo, key: EntityColumn) {
  101 + const alarmField = alarmFields[key.name];
  102 + if (alarmField) {
  103 + return getDescendantProp(alarm, alarmField.value);
  104 + } else {
  105 + return getDescendantProp(alarm, key.name);
  106 + }
  107 +}
  108 +
  109 +export function getCellStyleInfo(keySettings: TableWidgetDataKeySettings): CellStyleInfo {
  110 + let cellStyleFunction: Function = null;
  111 + let useCellStyleFunction = false;
  112 +
  113 + if (keySettings.useCellStyleFunction === true) {
  114 + if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
  115 + try {
  116 + cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
  117 + useCellStyleFunction = true;
  118 + } catch (e) {
  119 + cellStyleFunction = null;
  120 + useCellStyleFunction = false;
  121 + }
  122 + }
  123 + }
  124 + return {
  125 + useCellStyleFunction,
  126 + cellStyleFunction
  127 + };
  128 +}
  129 +
  130 +export function getCellContentInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellContentInfo {
  131 + let cellContentFunction: Function = null;
  132 + let useCellContentFunction = false;
  133 +
  134 + if (keySettings.useCellContentFunction === true) {
  135 + if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
  136 + try {
  137 + cellContentFunction = new Function(...args, keySettings.cellContentFunction);
  138 + useCellContentFunction = true;
  139 + } catch (e) {
  140 + cellContentFunction = null;
  141 + useCellContentFunction = false;
  142 + }
  143 + }
  144 + }
  145 + return {
  146 + cellContentFunction,
  147 + useCellContentFunction
  148 + };
  149 +}
  150 +
  151 +export function getColumnWidth(keySettings: TableWidgetDataKeySettings): string {
  152 + return isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
  153 +}
  154 +
  155 +export function constructTableCssString(widgetConfig: WidgetConfig): string {
  156 + const origColor = widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
  157 + const origBackgroundColor = widgetConfig.backgroundColor || 'rgb(255, 255, 255)';
  158 + const currentEntityColor = 'rgba(221, 221, 221, 0.65)';
  159 + const currentEntityStickyColor = tinycolor.mix(origBackgroundColor,
  160 + tinycolor(currentEntityColor).setAlpha(1), 65).toRgbString();
  161 + const selectedColor = 'rgba(221, 221, 221, 0.5)';
  162 + const selectedStickyColor = tinycolor.mix(origBackgroundColor,
  163 + tinycolor(selectedColor).setAlpha(1), 50).toRgbString();
  164 + const hoverColor = 'rgba(221, 221, 221, 0.3)';
  165 + const hoverStickyColor = tinycolor.mix(origBackgroundColor,
  166 + tinycolor(hoverColor).setAlpha(1), 30).toRgbString();
  167 + const defaultColor = tinycolor(origColor);
  168 + const mdDark = defaultColor.setAlpha(0.87).toRgbString();
  169 + const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
  170 + const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
  171 + const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
  172 +
  173 + const cssString =
  174 + '.mat-input-element::placeholder {\n' +
  175 + ' color: ' + mdDarkSecondary + ';\n'+
  176 + '}\n' +
  177 + '.mat-input-element::-moz-placeholder {\n' +
  178 + ' color: ' + mdDarkSecondary + ';\n'+
  179 + '}\n' +
  180 + '.mat-input-element::-webkit-input-placeholder {\n' +
  181 + ' color: ' + mdDarkSecondary + ';\n'+
  182 + '}\n' +
  183 + '.mat-input-element:-ms-input-placeholder {\n' +
  184 + ' color: ' + mdDarkSecondary + ';\n'+
  185 + '}\n' +
  186 + 'mat-toolbar.mat-table-toolbar {\n'+
  187 + 'color: ' + mdDark + ';\n'+
  188 + '}\n'+
  189 + 'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+
  190 + 'color: ' + mdDarkSecondary + ';\n'+
  191 + '}\n'+
  192 + '.mat-table .mat-header-row {\n'+
  193 + 'background-color: ' + origBackgroundColor + ';\n'+
  194 + '}\n'+
  195 + '.mat-table .mat-header-cell {\n'+
  196 + 'color: ' + mdDarkSecondary + ';\n'+
  197 + '}\n'+
  198 + '.mat-table .mat-header-cell .mat-sort-header-arrow {\n'+
  199 + 'color: ' + mdDarkDisabled + ';\n'+
  200 + '}\n'+
  201 + '.mat-table .mat-cell, .mat-table .mat-header-cell {\n'+
  202 + 'border-bottom-color: '+mdDarkDivider+';\n'+
  203 + '}\n'+
  204 + '.mat-table .mat-cell .mat-checkbox-frame, .mat-table .mat-header-cell .mat-checkbox-frame {\n'+
  205 + 'border-color: '+mdDarkSecondary+';\n'+
  206 + '}\n'+
  207 + '.mat-table .mat-row .mat-cell.mat-table-sticky {\n'+
  208 + 'transition: background-color .2s;\n'+
  209 + '}\n'+
  210 + '.mat-table .mat-row.tb-current-entity {\n'+
  211 + 'background-color: ' + currentEntityColor + ';\n'+
  212 + '}\n'+
  213 + '.mat-table .mat-row.tb-current-entity .mat-cell.mat-table-sticky {\n'+
  214 + 'background-color: ' + currentEntityStickyColor + ';\n'+
  215 + '}\n'+
  216 + '.mat-table .mat-row:hover:not(.tb-current-entity) {\n'+
  217 + 'background-color: ' + hoverColor + ';\n'+
  218 + '}\n'+
  219 + '.mat-table .mat-row:hover:not(.tb-current-entity) .mat-cell.mat-table-sticky {\n'+
  220 + 'background-color: ' + hoverStickyColor + ';\n'+
  221 + '}\n'+
  222 + '.mat-table .mat-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n'+
  223 + 'background-color: ' + selectedColor + ';\n'+
  224 + '}\n'+
  225 + '.mat-table .mat-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-cell.mat-table-sticky {\n'+
  226 + 'background-color: ' + selectedStickyColor + ';\n'+
  227 + '}\n'+
  228 + '.mat-table .mat-row .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n'+
  229 + 'background-color: ' + origBackgroundColor + ';\n'+
  230 + '}\n'+
  231 + '.mat-table .mat-cell {\n'+
  232 + 'color: ' + mdDark + ';\n'+
  233 + '}\n'+
  234 + '.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+
  235 + 'color: ' + mdDarkSecondary + ';\n'+
  236 + '}\n'+
  237 + '.mat-divider {\n'+
  238 + 'border-top-color: ' + mdDarkDivider + ';\n'+
  239 + '}\n'+
  240 + '.mat-paginator {\n'+
  241 + 'color: ' + mdDarkSecondary + ';\n'+
  242 + '}\n'+
  243 + '.mat-paginator button.mat-icon-button {\n'+
  244 + 'color: ' + mdDarkSecondary + ';\n'+
  245 + '}\n'+
  246 + '.mat-paginator button.mat-icon-button[disabled][disabled] {\n'+
  247 + 'color: ' + mdDarkDisabled + ';\n'+
  248 + '}\n'+
  249 + '.mat-paginator .mat-select-value {\n'+
  250 + 'color: ' + mdDarkSecondary + ';\n'+
  251 + '}';
  252 + return cssString;
55 } 253 }
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-table-widget {
  18 + .mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar:not([color="primary"]) {
  19 + background: transparent;
  20 + }
  21 + mat-toolbar {
  22 + height: 39px;
  23 + max-height: 39px;
  24 + .mat-toolbar-tools {
  25 + height: 39px;
  26 + max-height: 39px;
  27 + }
  28 + }
  29 + .table-container {
  30 + overflow: auto;
  31 + }
  32 +
  33 + .mat-row:not(.mat-row-select), .mat-header-row:not(.mat-row-select) {
  34 + mat-cell:nth-child(n+2):nth-last-child(n+2), mat-footer-cell:nth-child(n+2):nth-last-child(n+2), mat-header-cell:nth-child(n+2):nth-last-child(n+2) {
  35 + padding: 0px 5px;
  36 + }
  37 + }
  38 +
  39 + .mat-row.mat-row-select, .mat-header-row.mat-row-select {
  40 + mat-cell:nth-child(2), mat-footer-cell:nth-child(2), mat-header-cell:nth-child(2) {
  41 + padding: 0px 5px;
  42 + }
  43 + mat-cell:nth-child(n+3):nth-last-child(n+2), mat-footer-cell:nth-child(n+3):nth-last-child(n+2), mat-header-cell:nth-child(n+3):nth-last-child(n+2) {
  44 + padding: 0px 5px;
  45 + }
  46 + }
  47 + }
  48 +}
  49 +
  50 +:host-context(.tb-has-timewindow) {
  51 + .tb-table-widget {
  52 + mat-toolbar {
  53 + height: 65px;
  54 + max-height: 65px;
  55 + .mat-toolbar-tools {
  56 + height: 65px;
  57 + max-height: 65px;
  58 + }
  59 + }
  60 + }
  61 +}
@@ -21,6 +21,7 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail @@ -21,6 +21,7 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
21 import { LegendComponent } from '@home/components/widget/legend.component'; 21 import { LegendComponent } from '@home/components/widget/legend.component';
22 import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component'; 22 import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
23 import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component'; 23 import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
  24 +import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component';
24 25
25 @NgModule({ 26 @NgModule({
26 entryComponents: [ 27 entryComponents: [
@@ -29,14 +30,16 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa @@ -29,14 +30,16 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa
29 declarations: 30 declarations:
30 [ 31 [
31 DisplayColumnsPanelComponent, 32 DisplayColumnsPanelComponent,
32 - EntitiesTableWidgetComponent 33 + EntitiesTableWidgetComponent,
  34 + AlarmsTableWidgetComponent
33 ], 35 ],
34 imports: [ 36 imports: [
35 CommonModule, 37 CommonModule,
36 SharedModule 38 SharedModule
37 ], 39 ],
38 exports: [ 40 exports: [
39 - EntitiesTableWidgetComponent 41 + EntitiesTableWidgetComponent,
  42 + AlarmsTableWidgetComponent
40 ] 43 ]
41 }) 44 })
42 export class WidgetComponentsModule { } 45 export class WidgetComponentsModule { }
@@ -365,6 +365,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -365,6 +365,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
365 this.handleWidgetException(e); 365 this.handleWidgetException(e);
366 } 366 }
367 } 367 }
  368 + this.widgetContext.destroyed = true;
368 this.destroyDynamicWidgetComponent(); 369 this.destroyDynamicWidgetComponent();
369 } 370 }
370 371
@@ -75,7 +75,7 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> { @@ -75,7 +75,7 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
75 constructor(public type: EntityTableColumnType, 75 constructor(public type: EntityTableColumnType,
76 public key: string, 76 public key: string,
77 public title: string, 77 public title: string,
78 - public maxWidth: string = '100%', 78 + public width: string = '0px',
79 public sortable: boolean = true) { 79 public sortable: boolean = true) {
80 } 80 }
81 } 81 }
@@ -83,13 +83,14 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> { @@ -83,13 +83,14 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
83 export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTableColumn<T> { 83 export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTableColumn<T> {
84 constructor(public key: string, 84 constructor(public key: string,
85 public title: string, 85 public title: string,
86 - public maxWidth: string = '100%', 86 + public width: string = '0px',
87 public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property], 87 public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
88 public cellStyleFunction: CellStyleFunction<T> = () => ({}), 88 public cellStyleFunction: CellStyleFunction<T> = () => ({}),
89 public sortable: boolean = true, 89 public sortable: boolean = true,
90 public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}), 90 public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}),
91 - public cellTooltipFunction: CellTooltipFunction<T> = () => undefined) {  
92 - super('content', key, title, maxWidth, sortable); 91 + public cellTooltipFunction: CellTooltipFunction<T> = () => undefined,
  92 + public isNumberColumn: boolean = false) {
  93 + super('content', key, title, width, sortable);
93 } 94 }
94 } 95 }
95 96
@@ -97,8 +98,8 @@ export class EntityActionTableColumn<T extends BaseData<HasId>> extends BaseEnti @@ -97,8 +98,8 @@ export class EntityActionTableColumn<T extends BaseData<HasId>> extends BaseEnti
97 constructor(public key: string, 98 constructor(public key: string,
98 public title: string, 99 public title: string,
99 public actionDescriptor: CellActionDescriptor<T>, 100 public actionDescriptor: CellActionDescriptor<T>,
100 - public maxWidth: string = '100%') {  
101 - super('action', key, title, maxWidth, false); 101 + public width: string = '0px') {
  102 + super('action', key, title, width, false);
102 } 103 }
103 } 104 }
104 105
@@ -106,12 +107,12 @@ export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTabl @@ -106,12 +107,12 @@ export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTabl
106 constructor(key: string, 107 constructor(key: string,
107 title: string, 108 title: string,
108 datePipe: DatePipe, 109 datePipe: DatePipe,
109 - maxWidth: string = '100%', 110 + width: string = '0px',
110 dateFormat: string = 'yyyy-MM-dd HH:mm:ss', 111 dateFormat: string = 'yyyy-MM-dd HH:mm:ss',
111 cellStyleFunction: CellStyleFunction<T> = () => ({})) { 112 cellStyleFunction: CellStyleFunction<T> = () => ({})) {
112 super(key, 113 super(key,
113 title, 114 title,
114 - maxWidth, 115 + width,
115 (entity, property) => datePipe.transform(entity[property], dateFormat), 116 (entity, property) => datePipe.transform(entity[property], dateFormat),
116 cellStyleFunction); 117 cellStyleFunction);
117 } 118 }
@@ -114,19 +114,24 @@ export class WidgetContext { @@ -114,19 +114,24 @@ export class WidgetContext {
114 private _changeDetector: ChangeDetectorRef; 114 private _changeDetector: ChangeDetectorRef;
115 115
116 detectChanges(updateWidgetParams: boolean = false) { 116 detectChanges(updateWidgetParams: boolean = false) {
117 - if (updateWidgetParams) {  
118 - this.dashboardWidget.updateWidgetParams(); 117 + if (!this.destroyed) {
  118 + if (updateWidgetParams) {
  119 + this.dashboardWidget.updateWidgetParams();
  120 + }
  121 + this._changeDetector.detectChanges();
119 } 122 }
120 - this._changeDetector.detectChanges();  
121 } 123 }
122 124
123 updateWidgetParams() { 125 updateWidgetParams() {
124 - setTimeout(() => {  
125 - this.dashboardWidget.updateWidgetParams();  
126 - }, 0); 126 + if (!this.destroyed) {
  127 + setTimeout(() => {
  128 + this.dashboardWidget.updateWidgetParams();
  129 + }, 0);
  130 + }
127 } 131 }
128 132
129 inited = false; 133 inited = false;
  134 + destroyed = false;
130 135
131 subscriptions: {[id: string]: IWidgetSubscription} = {}; 136 subscriptions: {[id: string]: IWidgetSubscription} = {};
132 defaultSubscription: IWidgetSubscription = null; 137 defaultSubscription: IWidgetSubscription = null;
@@ -146,12 +146,12 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse @@ -146,12 +146,12 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
146 configureColumns(assetScope: string): Array<EntityTableColumn<AssetInfo>> { 146 configureColumns(assetScope: string): Array<EntityTableColumn<AssetInfo>> {
147 const columns: Array<EntityTableColumn<AssetInfo>> = [ 147 const columns: Array<EntityTableColumn<AssetInfo>> = [
148 new DateEntityTableColumn<AssetInfo>('createdTime', 'asset.created-time', this.datePipe, '150px'), 148 new DateEntityTableColumn<AssetInfo>('createdTime', 'asset.created-time', this.datePipe, '150px'),
149 - new EntityTableColumn<AssetInfo>('name', 'asset.name'),  
150 - new EntityTableColumn<AssetInfo>('type', 'asset.asset-type'), 149 + new EntityTableColumn<AssetInfo>('name', 'asset.name', '33%'),
  150 + new EntityTableColumn<AssetInfo>('type', 'asset.asset-type', '33%'),
151 ]; 151 ];
152 if (assetScope === 'tenant') { 152 if (assetScope === 'tenant') {
153 columns.push( 153 columns.push(
154 - new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer'), 154 + new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer', '33%'),
155 new EntityTableColumn<AssetInfo>('customerIsPublic', 'asset.public', '60px', 155 new EntityTableColumn<AssetInfo>('customerIsPublic', 'asset.public', '60px',
156 entity => { 156 entity => {
157 return checkBoxCell(entity.customerIsPublic); 157 return checkBoxCell(entity.customerIsPublic);
@@ -55,10 +55,10 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C @@ -55,10 +55,10 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
55 55
56 this.config.columns.push( 56 this.config.columns.push(
57 new DateEntityTableColumn<Customer>('createdTime', 'customer.created-time', this.datePipe, '150px'), 57 new DateEntityTableColumn<Customer>('createdTime', 'customer.created-time', this.datePipe, '150px'),
58 - new EntityTableColumn<Customer>('title', 'customer.title'),  
59 - new EntityTableColumn<Customer>('email', 'contact.email'),  
60 - new EntityTableColumn<Customer>('country', 'contact.country'),  
61 - new EntityTableColumn<Customer>('city', 'contact.city') 58 + new EntityTableColumn<Customer>('title', 'customer.title', '25%'),
  59 + new EntityTableColumn<Customer>('email', 'contact.email', '25%'),
  60 + new EntityTableColumn<Customer>('country', 'contact.country', '25%'),
  61 + new EntityTableColumn<Customer>('city', 'contact.city', '25%')
62 ); 62 );
63 63
64 this.config.cellActionDescriptors.push( 64 this.config.cellActionDescriptors.push(
@@ -143,12 +143,12 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< @@ -143,12 +143,12 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
143 configureColumns(dashboardScope: string): Array<EntityTableColumn<DashboardInfo>> { 143 configureColumns(dashboardScope: string): Array<EntityTableColumn<DashboardInfo>> {
144 const columns: Array<EntityTableColumn<DashboardInfo>> = [ 144 const columns: Array<EntityTableColumn<DashboardInfo>> = [
145 new DateEntityTableColumn<DashboardInfo>('createdTime', 'dashboard.created-time', this.datePipe, '150px'), 145 new DateEntityTableColumn<DashboardInfo>('createdTime', 'dashboard.created-time', this.datePipe, '150px'),
146 - new EntityTableColumn<DashboardInfo>('title', 'dashboard.title') 146 + new EntityTableColumn<DashboardInfo>('title', 'dashboard.title', '50%')
147 ]; 147 ];
148 if (dashboardScope === 'tenant') { 148 if (dashboardScope === 'tenant') {
149 columns.push( 149 columns.push(
150 new EntityTableColumn<DashboardInfo>('customersTitle', 'dashboard.assignedToCustomers', 150 new EntityTableColumn<DashboardInfo>('customersTitle', 'dashboard.assignedToCustomers',
151 - '100%', entity => { 151 + '50%', entity => {
152 return getDashboardAssignedCustomersText(entity); 152 return getDashboardAssignedCustomersText(entity);
153 }, () => ({}), false), 153 }, () => ({}), false),
154 new EntityTableColumn<DashboardInfo>('dashboardIsPublic', 'dashboard.public', '60px', 154 new EntityTableColumn<DashboardInfo>('dashboardIsPublic', 'dashboard.public', '60px',
@@ -150,13 +150,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev @@ -150,13 +150,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
150 configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> { 150 configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
151 const columns: Array<EntityTableColumn<DeviceInfo>> = [ 151 const columns: Array<EntityTableColumn<DeviceInfo>> = [
152 new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'), 152 new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'),
153 - new EntityTableColumn<DeviceInfo>('name', 'device.name'),  
154 - new EntityTableColumn<DeviceInfo>('type', 'device.device-type'),  
155 - new EntityTableColumn<DeviceInfo>('label', 'device.label') 153 + new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'),
  154 + new EntityTableColumn<DeviceInfo>('type', 'device.device-type', '25%'),
  155 + new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%')
156 ]; 156 ];
157 if (deviceScope === 'tenant') { 157 if (deviceScope === 'tenant') {
158 columns.push( 158 columns.push(
159 - new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer'), 159 + new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
160 new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px', 160 new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
161 entity => { 161 entity => {
162 return checkBoxCell(entity.customerIsPublic); 162 return checkBoxCell(entity.customerIsPublic);
@@ -147,12 +147,12 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig @@ -147,12 +147,12 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
147 configureColumns(entityViewScope: string): Array<EntityTableColumn<EntityViewInfo>> { 147 configureColumns(entityViewScope: string): Array<EntityTableColumn<EntityViewInfo>> {
148 const columns: Array<EntityTableColumn<EntityViewInfo>> = [ 148 const columns: Array<EntityTableColumn<EntityViewInfo>> = [
149 new DateEntityTableColumn<EntityViewInfo>('createdTime', 'entity-view.created-time', this.datePipe, '150px'), 149 new DateEntityTableColumn<EntityViewInfo>('createdTime', 'entity-view.created-time', this.datePipe, '150px'),
150 - new EntityTableColumn<EntityViewInfo>('name', 'entity-view.name'),  
151 - new EntityTableColumn<EntityViewInfo>('type', 'entity-view.entity-view-type'), 150 + new EntityTableColumn<EntityViewInfo>('name', 'entity-view.name', '33%'),
  151 + new EntityTableColumn<EntityViewInfo>('type', 'entity-view.entity-view-type', '33%'),
152 ]; 152 ];
153 if (entityViewScope === 'tenant') { 153 if (entityViewScope === 'tenant') {
154 columns.push( 154 columns.push(
155 - new EntityTableColumn<EntityViewInfo>('customerTitle', 'customer.customer'), 155 + new EntityTableColumn<EntityViewInfo>('customerTitle', 'customer.customer', '33%'),
156 new EntityTableColumn<EntityViewInfo>('customerIsPublic', 'entity-view.public', '60px', 156 new EntityTableColumn<EntityViewInfo>('customerIsPublic', 'entity-view.public', '60px',
157 entity => { 157 entity => {
158 return checkBoxCell(entity.customerIsPublic); 158 return checkBoxCell(entity.customerIsPublic);
@@ -55,10 +55,10 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten @@ -55,10 +55,10 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten
55 55
56 this.config.columns.push( 56 this.config.columns.push(
57 new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'), 57 new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'),
58 - new EntityTableColumn<Tenant>('title', 'tenant.title'),  
59 - new EntityTableColumn<Tenant>('email', 'contact.email'),  
60 - new EntityTableColumn<Tenant>('country', 'contact.country'),  
61 - new EntityTableColumn<Tenant>('city', 'contact.city') 58 + new EntityTableColumn<Tenant>('title', 'tenant.title', '25%'),
  59 + new EntityTableColumn<Tenant>('email', 'contact.email', '25%'),
  60 + new EntityTableColumn<Tenant>('country', 'contact.country', '25%'),
  61 + new EntityTableColumn<Tenant>('city', 'contact.city', '25%')
62 ); 62 );
63 63
64 this.config.cellActionDescriptors.push( 64 this.config.cellActionDescriptors.push(
@@ -90,9 +90,9 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User> @@ -90,9 +90,9 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>
90 90
91 this.config.columns.push( 91 this.config.columns.push(
92 new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'), 92 new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'),
93 - new EntityTableColumn<User>('firstName', 'user.first-name'),  
94 - new EntityTableColumn<User>('lastName', 'user.last-name'),  
95 - new EntityTableColumn<User>('email', 'user.email') 93 + new EntityTableColumn<User>('firstName', 'user.first-name', '33%'),
  94 + new EntityTableColumn<User>('lastName', 'user.last-name', '33%'),
  95 + new EntityTableColumn<User>('email', 'user.email', '33%')
96 ); 96 );
97 97
98 this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id; 98 this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id;
@@ -58,7 +58,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon @@ -58,7 +58,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
58 58
59 this.config.columns.push( 59 this.config.columns.push(
60 new DateEntityTableColumn<WidgetsBundle>('createdTime', 'widgets-bundle.created-time', this.datePipe, '150px'), 60 new DateEntityTableColumn<WidgetsBundle>('createdTime', 'widgets-bundle.created-time', this.datePipe, '150px'),
61 - new EntityTableColumn<WidgetsBundle>('title', 'widgets-bundle.title'), 61 + new EntityTableColumn<WidgetsBundle>('title', 'widgets-bundle.title', '100%'),
62 new EntityTableColumn<WidgetsBundle>('tenantId', 'widgets-bundle.system', '60px', 62 new EntityTableColumn<WidgetsBundle>('tenantId', 'widgets-bundle.system', '60px',
63 entity => { 63 entity => {
64 return checkBoxCell(entity.tenantId.id === NULL_UUID); 64 return checkBoxCell(entity.tenantId.id === NULL_UUID);
@@ -23,6 +23,7 @@ @@ -23,6 +23,7 @@
23 .tb-breadcrumb { 23 .tb-breadcrumb {
24 font-size: 18px !important; 24 font-size: 18px !important;
25 font-weight: 400 !important; 25 font-weight: 400 !important;
  26 + overflow: hidden;
26 27
27 h1, 28 h1,
28 a, 29 a,
@@ -106,6 +106,7 @@ export interface AlarmInfo extends Alarm { @@ -106,6 +106,7 @@ export interface AlarmInfo extends Alarm {
106 } 106 }
107 107
108 export const simulatedAlarm: AlarmInfo = { 108 export const simulatedAlarm: AlarmInfo = {
  109 + id: new AlarmId(NULL_UUID),
109 tenantId: new TenantId(NULL_UUID), 110 tenantId: new TenantId(NULL_UUID),
110 createdTime: new Date().getTime(), 111 createdTime: new Date().getTime(),
111 startTs: new Date().getTime(), 112 startTs: new Date().getTime(),
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 import { Direction, SortOrder } from '@shared/models/page/sort-order'; 17 import { Direction, SortOrder } from '@shared/models/page/sort-order';
18 import { emptyPageData, PageData } from '@shared/models/page/page-data'; 18 import { emptyPageData, PageData } from '@shared/models/page/page-data';
  19 +import { getDescendantProp } from '@core/utils';
19 20
20 export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean; 21 export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean;
21 22
@@ -69,8 +70,8 @@ export class PageLink { @@ -69,8 +70,8 @@ export class PageLink {
69 public sort(item1: any, item2: any): number { 70 public sort(item1: any, item2: any): number {
70 if (this.sortOrder) { 71 if (this.sortOrder) {
71 const property = this.sortOrder.property; 72 const property = this.sortOrder.property;
72 - const item1Value = item1[property];  
73 - const item2Value = item2[property]; 73 + const item1Value = getDescendantProp(item1, property);
  74 + const item2Value = getDescendantProp(item2, property);
74 let result = 0; 75 let result = 0;
75 if (item1Value !== item2Value) { 76 if (item1Value !== item2Value) {
76 if (typeof item1Value === 'number' && typeof item2Value === 'number') { 77 if (typeof item1Value === 'number' && typeof item2Value === 'number') {
@@ -472,6 +472,16 @@ mat-label { @@ -472,6 +472,16 @@ mat-label {
472 } 472 }
473 } 473 }
474 474
  475 + // Material table
  476 +
  477 + mat-toolbar.mat-primary {
  478 + button.mat-icon-button {
  479 + mat-icon {
  480 + color: white;
  481 + }
  482 + }
  483 + }
  484 +
475 mat-toolbar.mat-table-toolbar { 485 mat-toolbar.mat-table-toolbar {
476 background: #fff; 486 background: #fff;
477 padding: 0 24px; 487 padding: 0 24px;
@@ -483,7 +493,7 @@ mat-label { @@ -483,7 +493,7 @@ mat-label {
483 } 493 }
484 } 494 }
485 495
486 - mat-toolbar.mat-table-toolbar, .mat-cell { 496 + mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell {
487 button.mat-icon-button { 497 button.mat-icon-button {
488 mat-icon { 498 mat-icon {
489 color: rgba(0, 0, 0, .54); 499 color: rgba(0, 0, 0, .54);
@@ -491,28 +501,40 @@ mat-label { @@ -491,28 +501,40 @@ mat-label {
491 } 501 }
492 } 502 }
493 503
494 - .mat-cell {  
495 - mat-icon {  
496 - color: rgba(0, 0, 0, .54);  
497 - } 504 + .mat-table {
  505 + width: 100%;
  506 + max-width: 100%;
  507 + margin-bottom: 1rem;
  508 + display: table;
  509 + border-collapse: separate;
  510 + margin: 0px;
498 } 511 }
499 512
500 - mat-toolbar.mat-primary {  
501 - button.mat-icon-button {  
502 - mat-icon {  
503 - color: white; 513 + .mat-row,
  514 + .mat-header-row {
  515 + display: table-row;
  516 + }
  517 +
  518 +
  519 + .mat-header-row.mat-table-sticky {
  520 + .mat-header-cell {
  521 + position: sticky;
  522 + top: 0;
  523 + z-index: 10;
  524 + background: inherit;
  525 + &.mat-table-sticky {
  526 + z-index: 11 !important;
504 } 527 }
505 } 528 }
506 } 529 }
507 530
508 -  
509 .mat-row { 531 .mat-row {
510 transition: background-color .2s; 532 transition: background-color .2s;
511 &:hover:not(.tb-current-entity) { 533 &:hover:not(.tb-current-entity) {
512 - background-color: rgba(221, 221, 221, 0.3); 534 + background-color: #f4f4f4;
513 } 535 }
514 &.tb-current-entity { 536 &.tb-current-entity {
515 - background-color: rgba(221, 221, 221, 0.65); 537 + background-color: #e9e9e9;
516 } 538 }
517 } 539 }
518 540
@@ -541,12 +563,20 @@ mat-label { @@ -541,12 +563,20 @@ mat-label {
541 } 563 }
542 } 564 }
543 565
  566 + .mat-cell,
544 .mat-header-cell { 567 .mat-header-cell {
545 - white-space: nowrap;  
546 - }  
547 -  
548 - .mat-cell, .mat-header-cell {  
549 min-width: 40px; 568 min-width: 40px;
  569 + word-wrap: initial;
  570 + display: table-cell;
  571 + line-break: unset;
  572 + width: 0px;
  573 + overflow: hidden;
  574 + vertical-align: middle;
  575 + border-width: 0;
  576 + border-bottom-width: 1px;
  577 + border-bottom-color: rgba(0, 0, 0, 0.12);
  578 + border-style: solid;
  579 + text-overflow: ellipsis;
550 &:last-child { 580 &:last-child {
551 padding: 0 12px 0 0; 581 padding: 0 12px 0 0;
552 } 582 }
@@ -561,8 +591,28 @@ mat-label { @@ -561,8 +591,28 @@ mat-label {
561 text-overflow: ellipsis; 591 text-overflow: ellipsis;
562 white-space: nowrap; 592 white-space: nowrap;
563 } 593 }
564 - &.mat-table-sticky {  
565 - background: transparent; 594 + }
  595 +
  596 + .mat-header-cell {
  597 + white-space: nowrap;
  598 + button.mat-sort-header-button {
  599 + text-overflow: ellipsis;
  600 + overflow: hidden;
  601 + white-space: nowrap;
  602 + }
  603 + &.mat-number-cell {
  604 + .mat-sort-header-container {
  605 + justify-content: flex-end;
  606 + }
  607 + }
  608 + }
  609 +
  610 + .mat-cell {
  611 + &.mat-number-cell {
  612 + text-align: end;
  613 + }
  614 + mat-icon {
  615 + color: rgba(0, 0, 0, .54);
566 } 616 }
567 } 617 }
568 618
@@ -575,6 +625,10 @@ mat-label { @@ -575,6 +625,10 @@ mat-label {
575 height: 20px; 625 height: 20px;
576 } 626 }
577 627
  628 + .mat-sort-header-sorted .mat-sort-header-arrow {
  629 + opacity: 1 !important;
  630 + }
  631 +
578 .mat-toolbar-tools { 632 .mat-toolbar-tools {
579 font-size: 20px; 633 font-size: 20px;
580 letter-spacing: .005em; 634 letter-spacing: .005em;