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 13 "sizeX": 10.5,
14 14 "sizeY": 6.5,
15 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 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 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 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 19 import { environment as env } from '@env/environment';
20 20
21 21 import { TranslateService } from '@ngx-translate/core';
22   -import { Store } from '@ngrx/store';
  22 +import { select, Store } from '@ngrx/store';
23 23 import { AppState } from './core/core.state';
24 24 import { LocalStorageService } from './core/local-storage/local-storage.service';
25 25 import { DomSanitizer } from '@angular/platform-browser';
26 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 32 @Component({
29 33 selector: 'tb-root',
... ... @@ -36,7 +40,8 @@ export class AppComponent implements OnInit {
36 40 private storageService: LocalStorageService,
37 41 private translate: TranslateService,
38 42 private matIconRegistry: MatIconRegistry,
39   - private domSanitizer: DomSanitizer) {
  43 + private domSanitizer: DomSanitizer,
  44 + private authService: AuthService) {
40 45
41 46 console.log(`ThingsBoard Version: ${env.tbVersion}`);
42 47
... ... @@ -56,6 +61,7 @@ export class AppComponent implements OnInit {
56 61 this.storageService.testLocalStorage();
57 62
58 63 this.setupTranslate();
  64 + this.setupAuth();
59 65 }
60 66
61 67 setupTranslate() {
... ... @@ -69,6 +75,21 @@ export class AppComponent implements OnInit {
69 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 93 ngOnInit() {
73 94 }
74 95
... ...
... ... @@ -30,7 +30,7 @@ import { AlarmService } from '../http/alarm.service';
30 30 import { UtilsService } from '@core/services/utils.service';
31 31 import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
32 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 34 import { HttpErrorResponse } from '@angular/common/http';
35 35 import { DatasourceService } from '@core/api/datasource.service';
36 36 import { RafService } from '@core/services/raf.service';
... ... @@ -226,6 +226,7 @@ export interface IWidgetSubscription {
226 226 timeWindowConfig?: Timewindow;
227 227 timeWindow?: WidgetTimewindow;
228 228
  229 + alarms?: Array<AlarmInfo>;
229 230 alarmSource?: Datasource;
230 231 alarmSearchStatus?: AlarmSearchStatus;
231 232 alarmsPollingInterval?: number;
... ...
... ... @@ -62,18 +62,6 @@ export class AuthService {
62 62 private adminService: AdminService,
63 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 67 redirectUrl: string;
... ...
... ... @@ -398,3 +398,7 @@ export function snakeCase(name: string, separator: string): string {
398 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 84 this.columns.push(
85 85 new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px'));
86 86 this.columns.push(
87   - new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '100%',
  87 + new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%',
88 88 (entity) => entity.originatorName, entity => ({}), false));
89 89 this.columns.push(
90   - new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '100%'));
  90 + new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%'));
91 91 this.columns.push(
92   - new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '100%',
  92 + new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%',
93 93 (entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)),
94 94 entity => ({
95 95 fontWeight: 'bold',
96 96 color: alarmSeverityColors.get(entity.severity)
97 97 })));
98 98 this.columns.push(
99   - new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '100%',
  99 + new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
100 100 (entity) => this.translate.instant(alarmStatusTranslations.get(entity.status))));
101 101
102 102 this.cellActionDescriptors.push(
... ...
... ... @@ -83,22 +83,22 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
83 83
84 84 if (this.auditLogMode !== AuditLogMode.ENTITY) {
85 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 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 92 if (this.auditLogMode !== AuditLogMode.USER) {
93 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 98 this.columns.push(
99   - new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '100%',
  99 + new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '33%',
100 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 102 (entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus)))
103 103 );
104 104
... ...
... ... @@ -153,8 +153,10 @@
153 153 </mat-cell>
154 154 </ng-container>
155 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 160 [matTooltip]="cellTooltip(entity, column, row)"
159 161 matTooltipPosition="above"
160 162 [innerHTML]="cellContent(entity, column, row)"
... ... @@ -176,10 +178,10 @@
176 178 </mat-cell>
177 179 </ng-container>
178 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 182 {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
181 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 185 <div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
184 186 <button mat-button mat-icon-button [disabled]="isLoading$ | async"
185 187 [fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors"
... ... @@ -190,7 +192,7 @@
190 192 {{actionDescriptor.icon}}</mat-icon>
191 193 </button>
192 194 </div>
193   - <div fxHide fxShow.lt-lg>
  195 + <div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length">
194 196 <button mat-button mat-icon-button
195 197 (click)="$event.stopPropagation()"
196 198 [matMenuTriggerFor]="cellActionsMenu">
... ...
... ... @@ -398,9 +398,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
398 398 let res = this.headerCellStyleCache[index];
399 399 if (!res) {
400 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 402 } else {
403   - res = {maxWidth: column.maxWidth};
  403 + res = {width: column.width};
404 404 }
405 405 this.headerCellStyleCache[index] = res;
406 406 }
... ... @@ -445,9 +445,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
445 445 let res = this.cellStyleCache[index];
446 446 if (!res) {
447 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 449 } else {
450   - res = {maxWidth: column.maxWidth};
  450 + res = {width: column.width};
451 451 }
452 452 this.cellStyleCache[index] = res;
453 453 }
... ...
... ... @@ -109,8 +109,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
109 109 updateColumns(updateTableColumns: boolean = false): void {
110 110 this.columns = [];
111 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 114 (entity) => entity.body.server, entity => ({}), false));
115 115 switch (this.eventType) {
116 116 case EventType.ERROR:
... ... @@ -146,20 +146,21 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
146 146 break;
147 147 case EventType.STATS:
148 148 this.columns.push(
149   - new EntityTableColumn<Event>('messagesProcessed', 'event.messages-processed', '100%',
  149 + new EntityTableColumn<Event>('messagesProcessed', 'event.messages-processed', '50%',
150 150 (entity) => entity.body.messagesProcessed + '',
151   - entity => ({justifyContent: 'flex-end'}),
  151 + () => ({}),
152 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 155 (entity) => entity.body.errorsOccurred + '',
156   - entity => ({justifyContent: 'flex-end'}),
  156 + () => ({}),
157 157 false,
158   - key => ({justifyContent: 'flex-end'}))
  158 + () => ({}), () => undefined, true)
159 159 );
160 160 break;
161 161 case DebugEventType.DEBUG_RULE_NODE:
162 162 case DebugEventType.DEBUG_RULE_CHAIN:
  163 + this.columns[0].width = '100px';
163 164 this.columns.push(
164 165 new EntityTableColumn<Event>('type', 'event.type', '40px',
165 166 (entity) => entity.body.type, entity => ({
... ... @@ -173,24 +174,18 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
173 174 }), false, key => ({
174 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 178 (entity) => entity.body.msgId, entity => ({
178 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 181 }), false, key => ({
184 182 padding: '0 12px 0 0'
185 183 }),
186 184 entity => entity.body.msgId),
187   - new EntityTableColumn<Event>('msgType', 'event.message-type', '100%',
  185 + new EntityTableColumn<Event>('msgType', 'event.message-type', '100px',
188 186 (entity) => entity.body.msgType, entity => ({
189 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 189 }), false, key => ({
195 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 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 19 <div fxFlex fxLayout="column" class="tb-absolute-fill">
20 20 <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
21 21 <div class="mat-toolbar-tools">
... ... @@ -39,8 +39,8 @@
39 39 </mat-toolbar>
40 40 <div fxFlex class="table-container">
41 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 44 <mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
45 45 <mat-cell *matCellDef="let entity;"
46 46 [innerHTML]="cellContent(entity, column)"
... ...
... ... @@ -16,24 +16,4 @@
16 16 :host {
17 17 width: 100%;
18 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 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 27 import { PageComponent } from '@shared/components/page.component';
19 28 import { Store } from '@ngrx/store';
20 29 import { AppState } from '@core/core.state';
... ... @@ -31,7 +40,6 @@ import { IWidgetSubscription } from '@core/api/widget-api.models';
31 40 import { UtilsService } from '@core/services/utils.service';
32 41 import { TranslateService } from '@ngx-translate/core';
33 42 import { deepClone, isDefined, isNumber } from '@core/utils';
34   -import * as tinycolor_ from 'tinycolor2';
35 43 import cssjs from '@core/css/css';
36 44 import { PageLink } from '@shared/models/page/page-link';
37 45 import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
... ... @@ -49,10 +57,18 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
49 57 import {
50 58 CellContentInfo,
51 59 CellStyleInfo,
  60 + constructTableCssString,
52 61 DisplayColumn,
53 62 EntityColumn,
54 63 EntityData,
55   - getEntityValue
  64 + fromEntityColumnDef,
  65 + getCellContentInfo,
  66 + getCellStyleInfo,
  67 + getColumnWidth,
  68 + getEntityValue,
  69 + TableWidgetDataKeySettings,
  70 + TableWidgetSettings,
  71 + toEntityColumnDef
56 72 } from '@home/components/widget/lib/table-widget.models';
57 73 import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
58 74 import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
... ... @@ -62,32 +78,20 @@ import {
62 78 DisplayColumnsPanelData
63 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 82 entitiesTitle: string;
69   - enableSearch: boolean;
70   - enableSelectColumnDisplay: boolean;
71 83 displayEntityName: boolean;
72 84 entityNameColumnTitle: string;
73 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 91 @Component({
88 92 selector: 'tb-entities-table-widget',
89 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 96 export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
93 97
... ... @@ -101,6 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
101 105 public displayPagination = true;
102 106 public pageSizeOptions;
103 107 public pageLink: PageLink;
  108 + public sortOrderProperty: string;
104 109 public textSearchMode = false;
105 110 public columns: Array<EntityColumn> = [];
106 111 public displayedColumns: string[] = [];
... ... @@ -138,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
138 143
139 144 constructor(protected store: Store<AppState>,
140 145 private elementRef: ElementRef,
  146 + private ngZone: NgZone,
141 147 private overlay: Overlay,
142 148 private viewContainerRef: ViewContainerRef,
143 149 private utils: UtilsService,
... ... @@ -185,13 +191,17 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
185 191 }
186 192
187 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 200 private initializeConfig() {
193 201 this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
194 202
  203 + this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
  204 +
195 205 let entitiesTitle: string;
196 206
197 207 if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
... ... @@ -212,78 +222,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
212 222 this.defaultPageSize = pageSize;
213 223 }
214 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 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 228 const cssParser = new cssjs();
288 229 cssParser.testMode = false;
289 230 const namespace = 'entities-table-' + this.utils.hashCode(cssString);
... ... @@ -294,8 +235,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
294 235
295 236 private updateDatasources() {
296 237
297   - this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
298   -
299 238 const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true;
300 239 let entityNameColumnTitle: string;
301 240 if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) {
... ... @@ -306,11 +245,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
306 245 const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true;
307 246
308 247 if (displayEntityName) {
309   - this.displayedColumns.push('entityName');
310 248 this.columns.push(
311 249 {
312 250 name: 'entityName',
313 251 label: 'entityName',
  252 + def: 'entityName',
314 253 title: entityNameColumnTitle
315 254 } as EntityColumn
316 255 );
... ... @@ -320,14 +259,14 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
320 259 this.stylesInfo['entityName'] = {
321 260 useCellStyleFunction: false
322 261 };
323   - this.columnWidth['entityName'] = '100px';
  262 + this.columnWidth['entityName'] = '0px';
324 263 }
325 264 if (displayEntityType) {
326   - this.displayedColumns.push('entityType');
327 265 this.columns.push(
328 266 {
329 267 name: 'entityType',
330 268 label: 'entityType',
  269 + def: 'entityType',
331 270 title: this.translate.instant('entity.entity-type'),
332 271 } as EntityColumn
333 272 );
... ... @@ -337,7 +276,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
337 276 this.stylesInfo['entityType'] = {
338 277 useCellStyleFunction: false
339 278 };
340   - this.columnWidth['entityType'] = '100px';
  279 + this.columnWidth['entityType'] = '0px';
341 280 }
342 281
343 282 const dataKeys: Array<DataKey> = [];
... ... @@ -353,55 +292,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
353 292 dataKeys.push(dataKey);
354 293
355 294 dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label);
  295 + dataKey.def = 'def' + this.columns.length;
356 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 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 314 if (this.actionCellDescriptors.length) {
407 315 this.displayedColumns.push('actions');
... ... @@ -435,8 +343,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
435 343 const columns: DisplayColumn[] = this.columns.map(column => {
436 344 return {
437 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 352 [DISPLAY_COLUMNS_PANEL_DATA, {
445 353 columns,
446 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 356 this.displayedColumns.push('actions');
449 357 }
450 358 } as DisplayColumnsPanelData],
... ... @@ -485,24 +393,27 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
485 393 } else {
486 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 397 this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
490 398 this.entityDatasource.loadEntities(this.pageLink);
491 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 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 413 public cellStyle(entity: EntityData, key: EntityColumn): any {
503 414 let style: any = {};
504 415 if (entity && key) {
505   - const styleInfo = this.stylesInfo[key.label];
  416 + const styleInfo = this.stylesInfo[key.def];
506 417 const value = getEntityValue(entity, key);
507 418 if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
508 419 try {
... ... @@ -514,20 +425,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
514 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 430 style.width = columnWidth;
528   - } else {
529   - style.minWidth = "auto";
530   - style.width = "auto";
531 431 }
532 432 return style;
533 433 }
... ... @@ -535,7 +435,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
535 435 public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
536 436 let strContent = '';
537 437 if (entity && key) {
538   - const contentInfo = this.contentsInfo[key.label];
  438 + const contentInfo = this.contentsInfo[key.def];
539 439 const value = getEntityValue(entity, key);
540 440 if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
541 441 if (isDefined(value)) {
... ...
... ... @@ -15,7 +15,28 @@
15 15 ///
16 16
17 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 41 export interface EntityData {
21 42 id: EntityId;
... ... @@ -25,12 +46,13 @@ export interface EntityData {
25 46 }
26 47
27 48 export interface EntityColumn extends DataKey {
  49 + def: string;
28 50 title: string;
29 51 }
30 52
31 53 export interface DisplayColumn {
32 54 title: string;
33   - label: string;
  55 + def: string;
34 56 display: boolean;
35 57 }
36 58
... ... @@ -46,10 +68,186 @@ export interface CellStyleInfo {
46 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 96 export function getEntityValue(entity: any, key: DataKey): any {
50 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 21 import { LegendComponent } from '@home/components/widget/legend.component';
22 22 import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
23 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 26 @NgModule({
26 27 entryComponents: [
... ... @@ -29,14 +30,16 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa
29 30 declarations:
30 31 [
31 32 DisplayColumnsPanelComponent,
32   - EntitiesTableWidgetComponent
  33 + EntitiesTableWidgetComponent,
  34 + AlarmsTableWidgetComponent
33 35 ],
34 36 imports: [
35 37 CommonModule,
36 38 SharedModule
37 39 ],
38 40 exports: [
39   - EntitiesTableWidgetComponent
  41 + EntitiesTableWidgetComponent,
  42 + AlarmsTableWidgetComponent
40 43 ]
41 44 })
42 45 export class WidgetComponentsModule { }
... ...
... ... @@ -365,6 +365,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
365 365 this.handleWidgetException(e);
366 366 }
367 367 }
  368 + this.widgetContext.destroyed = true;
368 369 this.destroyDynamicWidgetComponent();
369 370 }
370 371
... ...
... ... @@ -75,7 +75,7 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
75 75 constructor(public type: EntityTableColumnType,
76 76 public key: string,
77 77 public title: string,
78   - public maxWidth: string = '100%',
  78 + public width: string = '0px',
79 79 public sortable: boolean = true) {
80 80 }
81 81 }
... ... @@ -83,13 +83,14 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
83 83 export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTableColumn<T> {
84 84 constructor(public key: string,
85 85 public title: string,
86   - public maxWidth: string = '100%',
  86 + public width: string = '0px',
87 87 public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
88 88 public cellStyleFunction: CellStyleFunction<T> = () => ({}),
89 89 public sortable: boolean = true,
90 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 98 constructor(public key: string,
98 99 public title: string,
99 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 107 constructor(key: string,
107 108 title: string,
108 109 datePipe: DatePipe,
109   - maxWidth: string = '100%',
  110 + width: string = '0px',
110 111 dateFormat: string = 'yyyy-MM-dd HH:mm:ss',
111 112 cellStyleFunction: CellStyleFunction<T> = () => ({})) {
112 113 super(key,
113 114 title,
114   - maxWidth,
  115 + width,
115 116 (entity, property) => datePipe.transform(entity[property], dateFormat),
116 117 cellStyleFunction);
117 118 }
... ...
... ... @@ -114,19 +114,24 @@ export class WidgetContext {
114 114 private _changeDetector: ChangeDetectorRef;
115 115
116 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 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 133 inited = false;
  134 + destroyed = false;
130 135
131 136 subscriptions: {[id: string]: IWidgetSubscription} = {};
132 137 defaultSubscription: IWidgetSubscription = null;
... ...
... ... @@ -146,12 +146,12 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
146 146 configureColumns(assetScope: string): Array<EntityTableColumn<AssetInfo>> {
147 147 const columns: Array<EntityTableColumn<AssetInfo>> = [
148 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 152 if (assetScope === 'tenant') {
153 153 columns.push(
154   - new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer'),
  154 + new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer', '33%'),
155 155 new EntityTableColumn<AssetInfo>('customerIsPublic', 'asset.public', '60px',
156 156 entity => {
157 157 return checkBoxCell(entity.customerIsPublic);
... ...
... ... @@ -55,10 +55,10 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
55 55
56 56 this.config.columns.push(
57 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 64 this.config.cellActionDescriptors.push(
... ...
... ... @@ -143,12 +143,12 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
143 143 configureColumns(dashboardScope: string): Array<EntityTableColumn<DashboardInfo>> {
144 144 const columns: Array<EntityTableColumn<DashboardInfo>> = [
145 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 148 if (dashboardScope === 'tenant') {
149 149 columns.push(
150 150 new EntityTableColumn<DashboardInfo>('customersTitle', 'dashboard.assignedToCustomers',
151   - '100%', entity => {
  151 + '50%', entity => {
152 152 return getDashboardAssignedCustomersText(entity);
153 153 }, () => ({}), false),
154 154 new EntityTableColumn<DashboardInfo>('dashboardIsPublic', 'dashboard.public', '60px',
... ...
... ... @@ -150,13 +150,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
150 150 configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
151 151 const columns: Array<EntityTableColumn<DeviceInfo>> = [
152 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 157 if (deviceScope === 'tenant') {
158 158 columns.push(
159   - new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer'),
  159 + new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
160 160 new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
161 161 entity => {
162 162 return checkBoxCell(entity.customerIsPublic);
... ...
... ... @@ -147,12 +147,12 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
147 147 configureColumns(entityViewScope: string): Array<EntityTableColumn<EntityViewInfo>> {
148 148 const columns: Array<EntityTableColumn<EntityViewInfo>> = [
149 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 153 if (entityViewScope === 'tenant') {
154 154 columns.push(
155   - new EntityTableColumn<EntityViewInfo>('customerTitle', 'customer.customer'),
  155 + new EntityTableColumn<EntityViewInfo>('customerTitle', 'customer.customer', '33%'),
156 156 new EntityTableColumn<EntityViewInfo>('customerIsPublic', 'entity-view.public', '60px',
157 157 entity => {
158 158 return checkBoxCell(entity.customerIsPublic);
... ...
... ... @@ -55,10 +55,10 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten
55 55
56 56 this.config.columns.push(
57 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 64 this.config.cellActionDescriptors.push(
... ...
... ... @@ -90,9 +90,9 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>
90 90
91 91 this.config.columns.push(
92 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 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 58
59 59 this.config.columns.push(
60 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 62 new EntityTableColumn<WidgetsBundle>('tenantId', 'widgets-bundle.system', '60px',
63 63 entity => {
64 64 return checkBoxCell(entity.tenantId.id === NULL_UUID);
... ...
... ... @@ -23,6 +23,7 @@
23 23 .tb-breadcrumb {
24 24 font-size: 18px !important;
25 25 font-weight: 400 !important;
  26 + overflow: hidden;
26 27
27 28 h1,
28 29 a,
... ...
... ... @@ -106,6 +106,7 @@ export interface AlarmInfo extends Alarm {
106 106 }
107 107
108 108 export const simulatedAlarm: AlarmInfo = {
  109 + id: new AlarmId(NULL_UUID),
109 110 tenantId: new TenantId(NULL_UUID),
110 111 createdTime: new Date().getTime(),
111 112 startTs: new Date().getTime(),
... ...
... ... @@ -16,6 +16,7 @@
16 16
17 17 import { Direction, SortOrder } from '@shared/models/page/sort-order';
18 18 import { emptyPageData, PageData } from '@shared/models/page/page-data';
  19 +import { getDescendantProp } from '@core/utils';
19 20
20 21 export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean;
21 22
... ... @@ -69,8 +70,8 @@ export class PageLink {
69 70 public sort(item1: any, item2: any): number {
70 71 if (this.sortOrder) {
71 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 75 let result = 0;
75 76 if (item1Value !== item2Value) {
76 77 if (typeof item1Value === 'number' && typeof item2Value === 'number') {
... ...
... ... @@ -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 485 mat-toolbar.mat-table-toolbar {
476 486 background: #fff;
477 487 padding: 0 24px;
... ... @@ -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 497 button.mat-icon-button {
488 498 mat-icon {
489 499 color: rgba(0, 0, 0, .54);
... ... @@ -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 531 .mat-row {
510 532 transition: background-color .2s;
511 533 &:hover:not(.tb-current-entity) {
512   - background-color: rgba(221, 221, 221, 0.3);
  534 + background-color: #f4f4f4;
513 535 }
514 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 563 }
542 564 }
543 565
  566 + .mat-cell,
544 567 .mat-header-cell {
545   - white-space: nowrap;
546   - }
547   -
548   - .mat-cell, .mat-header-cell {
549 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 580 &:last-child {
551 581 padding: 0 12px 0 0;
552 582 }
... ... @@ -561,8 +591,28 @@ mat-label {
561 591 text-overflow: ellipsis;
562 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 625 height: 20px;
576 626 }
577 627
  628 + .mat-sort-header-sorted .mat-sort-header-arrow {
  629 + opacity: 1 !important;
  630 + }
  631 +
578 632 .mat-toolbar-tools {
579 633 font-size: 20px;
580 634 letter-spacing: .005em;
... ...