Commit 8f4944fe170ed29b1415f071fdbd52351bcdd95b
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> </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 { } | ... | ... |
... | ... | @@ -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); | ... | ... |
... | ... | @@ -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; | ... | ... |