Browse Source

Material table improvements. Alarm widget implementation.

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
8f4944fe17
  1. 6
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  2. 25
      ui-ngx/src/app/app.component.ts
  3. 3
      ui-ngx/src/app/core/api/widget-api.models.ts
  4. 12
      ui-ngx/src/app/core/auth/auth.service.ts
  5. 4
      ui-ngx/src/app/core/utils.ts
  6. 8
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  7. 10
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts
  8. 12
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  9. 8
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  10. 31
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  11. 135
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  12. 19
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss
  13. 770
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  14. 6
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  15. 20
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss
  16. 224
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  17. 206
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  18. 61
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss
  19. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  20. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  21. 17
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  22. 17
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  23. 6
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  24. 8
      ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
  25. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  26. 8
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  27. 6
      ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts
  28. 8
      ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts
  29. 6
      ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts
  30. 2
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts
  31. 1
      ui-ngx/src/app/shared/components/breadcrumb.component.scss
  32. 1
      ui-ngx/src/app/shared/models/alarm.models.ts
  33. 5
      ui-ngx/src/app/shared/models/page/page-link.ts
  34. 90
      ui-ngx/src/styles.scss

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

@ -13,10 +13,10 @@
"sizeX": 10.5,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-alarms-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-alarms-table-widget>",
"templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
"templateCss": "",
"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",
"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}",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStatusFilter\": {\n \"title\": \"Enable alarm status filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"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}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"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}"
}

25
ui-ngx/src/app/app.component.ts

@ -19,11 +19,15 @@ import { Component, OnInit } from '@angular/core';
import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { select, Store } from '@ngrx/store';
import { AppState } from './core/core.state';
import { LocalStorageService } from './core/local-storage/local-storage.service';
import { DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material';
import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
@Component({
selector: 'tb-root',
@ -36,7 +40,8 @@ export class AppComponent implements OnInit {
private storageService: LocalStorageService,
private translate: TranslateService,
private matIconRegistry: MatIconRegistry,
private domSanitizer: DomSanitizer) {
private domSanitizer: DomSanitizer,
private authService: AuthService) {
console.log(`ThingsBoard Version: ${env.tbVersion}`);
@ -56,6 +61,7 @@ export class AppComponent implements OnInit {
this.storageService.testLocalStorage();
this.setupTranslate();
this.setupAuth();
}
setupTranslate() {
@ -69,6 +75,21 @@ export class AppComponent implements OnInit {
this.translate.setDefaultLang(env.defaultLang);
}
setupAuth() {
combineLatest([
this.store.pipe(select(selectIsAuthenticated)),
this.store.pipe(select(selectIsUserLoaded))]
).pipe(
map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),
distinctUntilChanged(),
filter((data) => data.isUserLoaded ),
skip(1),
).subscribe((data) => {
this.authService.gotoDefaultPlace(data.isAuthenticated);
});
this.authService.reloadUser();
}
ngOnInit() {
}

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

@ -30,7 +30,7 @@ import { AlarmService } from '../http/alarm.service';
import { UtilsService } from '@core/services/utils.service';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { AlarmInfo, AlarmSearchStatus } from '@shared/models/alarm.models';
import { HttpErrorResponse } from '@angular/common/http';
import { DatasourceService } from '@core/api/datasource.service';
import { RafService } from '@core/services/raf.service';
@ -226,6 +226,7 @@ export interface IWidgetSubscription {
timeWindowConfig?: Timewindow;
timeWindow?: WidgetTimewindow;
alarms?: Array<AlarmInfo>;
alarmSource?: Datasource;
alarmSearchStatus?: AlarmSearchStatus;
alarmsPollingInterval?: number;

12
ui-ngx/src/app/core/auth/auth.service.ts

@ -62,18 +62,6 @@ export class AuthService {
private adminService: AdminService,
private translate: TranslateService
) {
combineLatest(
this.store.pipe(select(selectIsAuthenticated)),
this.store.pipe(select(selectIsUserLoaded))
).pipe(
map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})),
distinctUntilChanged(),
filter((data) => data.isUserLoaded ),
skip(1),
).subscribe((data) => {
this.gotoDefaultPlace(data.isAuthenticated);
});
this.reloadUser();
}
redirectUrl: string;

4
ui-ngx/src/app/core/utils.ts

@ -398,3 +398,7 @@ export function snakeCase(name: string, separator: string): string {
return (pos ? separator : '') + letter.toLowerCase();
});
}
export function getDescendantProp(obj: any, path: string): any {
return path.split('.').reduce((acc, part) => acc && acc[part], obj)
}

8
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -84,19 +84,19 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.columns.push(
new DateEntityTableColumn<AlarmInfo>('createdTime', 'alarm.created-time', this.datePipe, '150px'));
this.columns.push(
new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '100%',
new EntityTableColumn<AlarmInfo>('originatorName', 'alarm.originator', '25%',
(entity) => entity.originatorName, entity => ({}), false));
this.columns.push(
new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '100%'));
new EntityTableColumn<AlarmInfo>('type', 'alarm.type', '25%'));
this.columns.push(
new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '100%',
new EntityTableColumn<AlarmInfo>('severity', 'alarm.severity', '25%',
(entity) => this.translate.instant(alarmSeverityTranslations.get(entity.severity)),
entity => ({
fontWeight: 'bold',
color: alarmSeverityColors.get(entity.severity)
})));
this.columns.push(
new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '100%',
new EntityTableColumn<AlarmInfo>('status', 'alarm.status', '25%',
(entity) => this.translate.instant(alarmStatusTranslations.get(entity.status))));
this.cellActionDescriptors.push(

10
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts

@ -83,22 +83,22 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
if (this.auditLogMode !== AuditLogMode.ENTITY) {
this.columns.push(
new EntityTableColumn<AuditLog>('entityType', 'audit-log.entity-type', '100%',
new EntityTableColumn<AuditLog>('entityType', 'audit-log.entity-type', '20%',
(entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)),
new EntityTableColumn<AuditLog>('entityName', 'audit-log.entity-name'),
new EntityTableColumn<AuditLog>('entityName', 'audit-log.entity-name', '20%'),
);
}
if (this.auditLogMode !== AuditLogMode.USER) {
this.columns.push(
new EntityTableColumn<AuditLog>('userName', 'audit-log.user')
new EntityTableColumn<AuditLog>('userName', 'audit-log.user', '33%')
);
}
this.columns.push(
new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '100%',
new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '33%',
(entity) => translate.instant(actionTypeTranslations.get(entity.actionType))),
new EntityTableColumn<AuditLog>('actionStatus', 'audit-log.status', '100%',
new EntityTableColumn<AuditLog>('actionStatus', 'audit-log.status', '33%',
(entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus)))
);

12
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -153,8 +153,10 @@
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;">
<mat-header-cell *matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell *matCellDef="let entity; let row = index"
<mat-header-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}"
*matHeaderCellDef [ngStyle]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
<mat-cell [ngClass]="{'mat-number-cell': column.isNumberColumn}"
*matCellDef="let entity; let row = index"
[matTooltip]="cellTooltip(entity, column, row)"
matTooltipPosition="above"
[innerHTML]="cellContent(entity, column, row)"
@ -176,10 +178,10 @@
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px', maxWidth: (cellActionDescriptors.length * 40) + 'px' }">
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ width: (cellActionDescriptors.length * 40) + 'px' }">
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
</mat-header-cell>
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 40) + 'px', maxWidth: (cellActionDescriptors.length * 40) + 'px' }">
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ width: (cellActionDescriptors.length * 40) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
[fxShow]="actionDescriptor.isEnabled(entity)" *ngFor="let actionDescriptor of cellActionDescriptors"
@ -190,7 +192,7 @@
{{actionDescriptor.icon}}</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length">
<button mat-button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">

8
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -398,9 +398,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
let res = this.headerCellStyleCache[index];
if (!res) {
if (column instanceof EntityTableColumn) {
res = {...column.headerCellStyleFunction(column.key), ...{maxWidth: column.maxWidth}};
res = {...column.headerCellStyleFunction(column.key), ...{minWidth: column.width, maxWidth: column.width, width: column.width}};
} else {
res = {maxWidth: column.maxWidth};
res = {width: column.width};
}
this.headerCellStyleCache[index] = res;
}
@ -445,9 +445,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
let res = this.cellStyleCache[index];
if (!res) {
if (column instanceof EntityTableColumn) {
res = {...column.cellStyleFunction(entity, column.key), ...{maxWidth: column.maxWidth}};
res = {...column.cellStyleFunction(entity, column.key), ...{minWidth: column.width, maxWidth: column.width, width: column.width}};
} else {
res = {maxWidth: column.maxWidth};
res = {width: column.width};
}
this.cellStyleCache[index] = res;
}

31
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -109,8 +109,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
updateColumns(updateTableColumns: boolean = false): void {
this.columns = [];
this.columns.push(
new DateEntityTableColumn<Event>('createdTime', 'event.event-time', this.datePipe, '150px'),
new EntityTableColumn<Event>('server', 'event.server', '150px',
new DateEntityTableColumn<Event>('createdTime', 'event.event-time', this.datePipe, '120px'),
new EntityTableColumn<Event>('server', 'event.server', '100px',
(entity) => entity.body.server, entity => ({}), false));
switch (this.eventType) {
case EventType.ERROR:
@ -146,20 +146,21 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
break;
case EventType.STATS:
this.columns.push(
new EntityTableColumn<Event>('messagesProcessed', 'event.messages-processed', '100%',
new EntityTableColumn<Event>('messagesProcessed', 'event.messages-processed', '50%',
(entity) => entity.body.messagesProcessed + '',
entity => ({justifyContent: 'flex-end'}),
() => ({}),
false,
key => ({justifyContent: 'flex-end'})),
new EntityTableColumn<Event>('errorsOccurred', 'event.errors-occurred', '100%',
() => ({}), () => undefined, true),
new EntityTableColumn<Event>('errorsOccurred', 'event.errors-occurred', '50%',
(entity) => entity.body.errorsOccurred + '',
entity => ({justifyContent: 'flex-end'}),
() => ({}),
false,
key => ({justifyContent: 'flex-end'}))
() => ({}), () => undefined, true)
);
break;
case DebugEventType.DEBUG_RULE_NODE:
case DebugEventType.DEBUG_RULE_CHAIN:
this.columns[0].width = '100px';
this.columns.push(
new EntityTableColumn<Event>('type', 'event.type', '40px',
(entity) => entity.body.type, entity => ({
@ -173,24 +174,18 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
}), false, key => ({
padding: '0 12px 0 0'
})),
new EntityTableColumn<Event>('msgId', 'event.message-id', '100%',
new EntityTableColumn<Event>('msgId', 'event.message-id', '100px',
(entity) => entity.body.msgId, entity => ({
whiteSpace: 'nowrap',
padding: '0 12px 0 0',
textOverflow: 'ellipsis',
display: 'inline-block',
lineHeight: '48px',
padding: '0 12px 0 0'
}), false, key => ({
padding: '0 12px 0 0'
}),
entity => entity.body.msgId),
new EntityTableColumn<Event>('msgType', 'event.message-type', '100%',
new EntityTableColumn<Event>('msgType', 'event.message-type', '100px',
(entity) => entity.body.msgType, entity => ({
whiteSpace: 'nowrap',
padding: '0 12px 0 0',
textOverflow: 'ellipsis',
display: 'inline-block',
lineHeight: '48px',
padding: '0 12px 0 0'
}), false, key => ({
padding: '0 12px 0 0'
}),

135
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html

@ -0,0 +1,135 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-table-widget tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-absolute-fill">
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode && alarmsDatasource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<button mat-button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[(ngModel)]="pageLink.textSearch"
placeholder="{{ 'alarm.search' | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-table-toolbar" color="primary" [fxShow]="!alarmsDatasource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span>
{{ translate.get('alarm.selected-alarms',
{count: alarmsDatasource.selection.selected.length}) | async }}
</span>
<span fxFlex></span>
<button *ngIf="allowAcknowledgment"
mat-button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'alarm.acknowledge' | translate }}"
matTooltipPosition="above"
(click)="ackAlarms($event)">
<mat-icon>done</mat-icon>
</button>
<button mat-button mat-icon-button
[disabled]="isLoading$ | async"
matTooltip="{{ 'alarm.clear' | translate }}"
matTooltipPosition="above"
(click)="clearAlarms($event)">
<mat-icon>clear</mat-icon>
</button>
</div>
</mat-toolbar>
<div fxFlex class="table-container">
<mat-table [dataSource]="alarmsDatasource"
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 30px;">
<mat-checkbox (change)="$event ? alarmsDatasource.masterToggle() : null"
[checked]="alarmsDatasource.selection.hasValue() && (alarmsDatasource.isAllSelected() | async)"
[indeterminate]="alarmsDatasource.selection.hasValue() && !(alarmsDatasource.isAllSelected() | async)">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let alarm" style="width: 30px;">
<mat-checkbox (click)="$event.stopPropagation();"
(change)="$event ? alarmsDatasource.toggleSelection(alarm) : null"
[checked]="alarmsDatasource.isSelected(alarm)">
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let alarm;"
[innerHTML]="cellContent(alarm, column)"
[ngStyle]="cellStyle(alarm, column)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ width: (actionCellDescriptors.length * 36) + 'px' }">
</mat-header-cell>
<mat-cell *matCellDef="let alarm" [ngStyle.gt-md]="{ width: (actionCellDescriptors.length * 36) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)"
*ngFor="let actionDescriptor of actionCellDescriptors"
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, alarm, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg>
<button mat-button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of actionCellDescriptors"
[disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)"
(click)="onActionButtonClick($event, alarm, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<span>{{ actionDescriptor.displayName }}</span>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': enableSelection,
'mat-selected': alarmsDatasource.isSelected(alarm),
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}"
*matRowDef="let alarm; columns: displayedColumns;"
(click)="onRowClick($event, alarm)"></mat-row>
</mat-table>
<span [fxShow]="alarmsDatasource.isEmpty() | async"
fxLayoutAlign="center center"
class="no-data-found" translate>alarm.no-alarms-prompt</span>
</div>
<mat-divider *ngIf="displayPagination"></mat-divider>
<mat-paginator *ngIf="displayPagination"
[length]="alarmsDatasource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"></mat-paginator>
</div>
</div>

19
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
}

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

@ -0,0 +1,770 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
NgZone,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetAction, WidgetContext } from '@home/models/widget-component.models';
import { Datasource, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, isDefined, isNumber } from '@core/utils';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
import { DataSource } from '@angular/cdk/typings/collections';
import { CollectionViewer, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { catchError, debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import {
CellContentInfo,
CellStyleInfo,
constructTableCssString,
DisplayColumn,
EntityColumn,
fromAlarmColumnDef,
getAlarmValue,
getCellContentInfo,
getCellStyleInfo,
getColumnWidth,
TableWidgetDataKeySettings,
TableWidgetSettings,
toAlarmColumnDef
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import {
DISPLAY_COLUMNS_PANEL_DATA,
DisplayColumnsPanelComponent,
DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component';
import {
alarmFields,
AlarmInfo,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common';
interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string;
enableSelection: boolean;
enableStatusFilter: boolean;
displayDetails: boolean;
allowAcknowledgment: boolean;
allowClear: boolean;
}
interface AlarmsTableDataKeySettings extends TableWidgetDataKeySettings {
}
interface AlarmWidgetActionDescriptor extends WidgetActionDescriptor {
details?: boolean;
acknowledge?: boolean;
clear?: boolean;
}
@Component({
selector: 'tb-alarms-table-widget',
templateUrl: './alarms-table-widget.component.html',
styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss']
})
export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
@Input()
ctx: WidgetContext;
@ViewChild('searchInput', {static: false}) searchInputField: ElementRef;
@ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
@ViewChild(MatSort, {static: false}) sort: MatSort;
public enableSelection = true;
public displayPagination = true;
public pageSizeOptions;
public pageLink: PageLink;
public sortOrderProperty: string;
public textSearchMode = false;
public columns: Array<EntityColumn> = [];
public displayedColumns: string[] = [];
public actionCellDescriptors: AlarmWidgetActionDescriptor[] = [];
public alarmsDatasource: AlarmsDatasource;
private settings: AlarmsTableWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private alarmSource: Datasource;
private displayDetails = true;
private allowAcknowledgment = true;
private allowClear = true;
private defaultPageSize = 10;
private defaultSortOrder = '-' + alarmFields.createdTime.value;
private contentsInfo: {[key: string]: CellContentInfo} = {};
private stylesInfo: {[key: string]: CellStyleInfo} = {};
private columnWidth: {[key: string]: string} = {};
private searchAction: WidgetAction = {
name: 'action.search',
show: true,
icon: 'search',
onAction: () => {
this.enterFilterMode();
}
};
private columnDisplayAction: WidgetAction = {
name: 'entity.columns-to-display',
show: true,
icon: 'view_column',
onAction: ($event) => {
this.editColumnsToDisplay($event);
}
};
private statusFilterAction: WidgetAction = {
name: 'alarm.alarm-status-filter',
show: true,
onAction: ($event) => {
this.editAlarmStatusFilter($event);
},
icon: 'filter_list'
};
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private ngZone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
public translate: TranslateService,
private domSanitizer: DomSanitizer,
private datePipe: DatePipe) {
super(store);
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder);
}
ngOnInit(): void {
this.ctx.$scope.alarmsTableWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.alarmSource = this.subscription.alarmSource;
this.initializeConfig();
this.updateAlarmSource();
this.ctx.updateWidgetParams();
}
ngAfterViewInit(): void {
fromEvent(this.searchInputField.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData();
})
)
.subscribe();
if (this.displayPagination) {
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
}
(this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange)
.pipe(
tap(() => this.updateData())
)
.subscribe();
this.updateData();
}
public onDataUpdated() {
this.ngZone.run(() => {
this.alarmsDatasource.updateAlarms(this.subscription.alarms);
this.ctx.detectChanges();
});
}
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.statusFilterAction, this.columnDisplayAction];
this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true;
this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true;
this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true;
if (this.displayDetails) {
this.actionCellDescriptors.push(
{
displayName: this.translate.instant('alarm.details'),
icon: 'more_horiz',
details: true
} as AlarmWidgetActionDescriptor
);
}
if (this.allowAcknowledgment) {
this.actionCellDescriptors.push(
{
displayName: this.translate.instant('alarm.acknowledge'),
icon: 'done',
acknowledge: true
} as AlarmWidgetActionDescriptor
);
}
if (this.allowClear) {
this.actionCellDescriptors.push(
{
displayName: this.translate.instant('alarm.clear'),
icon: 'clear',
clear: true
} as AlarmWidgetActionDescriptor
);
}
this.actionCellDescriptors = this.actionCellDescriptors.concat(this.ctx.actionsApi.getActionDescriptors('actionCellButton'));
let alarmsTitle: string;
if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) {
alarmsTitle = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle);
} else {
alarmsTitle = this.translate.instant('alarm.alarms');
}
this.ctx.widgetTitle = this.utils.createLabelFromDatasource(this.alarmSource, alarmsTitle);
this.enableSelection = isDefined(this.settings.enableSelection) ? this.settings.enableSelection : true;
if (!this.allowAcknowledgment && !this.allowClear) {
this.enableSelection = false;
}
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.statusFilterAction.show = isDefined(this.settings.enableStatusFilter) ? this.settings.enableStatusFilter : true;
const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {
this.defaultPageSize = pageSize;
}
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3];
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'alarms-table-' + this.utils.hashCode(cssString);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, cssString);
$(this.elementRef.nativeElement).addClass(namespace);
}
private updateAlarmSource() {
if (this.enableSelection) {
this.displayedColumns.push('select');
}
if (this.alarmSource) {
this.alarmSource.dataKeys.forEach((_dataKey) => {
const dataKey: EntityColumn = deepClone(_dataKey) as EntityColumn;
dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label);
dataKey.def = 'def' + this.columns.length;
const keySettings: AlarmsTableDataKeySettings = dataKey.settings;
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings);
this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx');
this.columnWidth[dataKey.def] = getColumnWidth(keySettings);
this.columns.push(dataKey);
});
this.displayedColumns.push(...this.columns.map(column => column.def));
}
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
this.sortOrderProperty = toAlarmColumnDef(this.pageLink.sortOrder.property, this.columns);
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
}
this.alarmsDatasource = new AlarmsDatasource();
if (this.enableSelection) {
this.alarmsDatasource.selectionModeChanged$.subscribe((selectionMode) => {
const hideTitlePanel = selectionMode || this.textSearchMode;
if (this.ctx.hideTitlePanel !== hideTitlePanel) {
this.ctx.hideTitlePanel = hideTitlePanel;
this.ctx.detectChanges(true);
} else {
this.ctx.detectChanges();
}
});
}
}
private editColumnsToDisplay($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.srcElement || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const columns: DisplayColumn[] = this.columns.map(column => {
return {
title: column.title,
def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1
}
});
const injectionTokens = new WeakMap<any, any>([
[DISPLAY_COLUMNS_PANEL_DATA, {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
if (this.enableSelection) {
this.displayedColumns.unshift('select');
}
this.displayedColumns.push('actions');
}
} as DisplayColumnsPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();
}
private editAlarmStatusFilter($event: Event) {
// TODO:
}
private enterFilterMode() {
this.textSearchMode = true;
this.pageLink.textSearch = '';
this.ctx.hideTitlePanel = true;
this.ctx.detectChanges(true);
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode() {
this.textSearchMode = false;
this.pageLink.textSearch = null;
if (this.displayPagination) {
this.paginator.pageIndex = 0;
}
this.updateData();
this.ctx.hideTitlePanel = false;
this.ctx.detectChanges(true);
}
private updateData() {
if (this.displayPagination) {
this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize;
} else {
this.pageLink.page = 0;
}
this.pageLink.sortOrder.property = fromAlarmColumnDef(this.sort.active, this.columns);
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.alarmsDatasource.loadAlarms(this.pageLink);
this.ctx.detectChanges();
}
public trackByColumnDef(index, column: EntityColumn) {
return column.def;
}
public headerStyle(key: EntityColumn): any {
const columnWidth = this.columnWidth[key.def];
return {
width: columnWidth
}
}
public cellStyle(alarm: AlarmInfo, key: EntityColumn): any {
let style: any = {};
if (alarm && key) {
const styleInfo = this.stylesInfo[key.def];
const value = getAlarmValue(alarm, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
style = styleInfo.cellStyleFunction(value);
} catch (e) {
style = {};
}
} else {
style = this.defaultStyle(key, value);
}
}
if (!style.width) {
const columnWidth = this.columnWidth[key.def];
style.width = columnWidth;
}
return style;
}
public cellContent(alarm: AlarmInfo, key: EntityColumn): SafeHtml {
let strContent = '';
if (alarm && key) {
const contentInfo = this.contentsInfo[key.def];
const value = getAlarmValue(alarm, key);
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
if (isDefined(value)) {
strContent = '' + value;
}
var content = strContent;
try {
content = contentInfo.cellContentFunction(value, alarm, this.ctx);
} catch (e) {
content = strContent;
}
} else {
content = this.defaultContent(key, value);
}
return this.domSanitizer.bypassSecurityTrustHtml(content);
} else {
return strContent;
}
}
public onRowClick($event: Event, alarm: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
this.alarmsDatasource.toggleCurrentAlarm(alarm);
const descriptors = this.ctx.actionsApi.getActionDescriptors('rowClick');
if (descriptors.length) {
let entityId;
let entityName;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
}
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, {alarm});
}
}
public onActionButtonClick($event: Event, alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor) {
if (actionDescriptor.details) {
this.openAlarmDetails($event, alarm);
} else if (actionDescriptor.acknowledge) {
this.ackAlarm($event, alarm);
} else if (actionDescriptor.clear) {
this.clearAlarm($event, alarm);
} else {
if ($event) {
$event.stopPropagation();
}
let entityId;
let entityName;
if (alarm && alarm.originator) {
entityId = alarm.originator;
entityName = alarm.originatorName;
}
this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, {alarm});
}
}
public actionEnabled(alarm: AlarmInfo, actionDescriptor: AlarmWidgetActionDescriptor): boolean {
if (actionDescriptor.acknowledge) {
return (alarm.status === AlarmStatus.ACTIVE_UNACK ||
alarm.status === AlarmStatus.CLEARED_UNACK);
} else if (actionDescriptor.clear) {
return (alarm.status === AlarmStatus.ACTIVE_ACK ||
alarm.status === AlarmStatus.ACTIVE_UNACK);
}
return true;
}
private openAlarmDetails($event: Event, alarm: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
private ackAlarm($event: Event, alarm: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
public ackAlarms($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
private clearAlarm($event: Event, alarm: AlarmInfo) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
public clearAlarms($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
}
private defaultContent(key: EntityColumn, value: any): any {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
if (alarmField.time) {
return this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss');
} else if (alarmField.value === alarmFields.severity.value) {
return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField.value === alarmFields.status.value) {
return this.translate.instant(alarmStatusTranslations.get(value));
} else if (alarmField.value === alarmFields.originatorType.value) {
return this.translate.instant(entityTypeTranslations.get(value).type);
}
else {
return value;
}
} else {
return value;
}
} else {
return '';
}
}
private defaultStyle(key: EntityColumn, value: any): any {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
if (alarmField.value == alarmFields.severity.value) {
return {
fontWeight: 'bold',
color: alarmSeverityColors.get(value)
};
} else {
return {};
}
} else {
return {};
}
} else {
return {};
}
}
}
class AlarmsDatasource implements DataSource<AlarmInfo> {
private alarmsSubject = new BehaviorSubject<AlarmInfo[]>([]);
private pageDataSubject = new BehaviorSubject<PageData<AlarmInfo>>(emptyPageData<AlarmInfo>());
public selection = new SelectionModel<AlarmInfo>(true, [], false);
private selectionModeChanged = new EventEmitter<boolean>();
public selectionModeChanged$ = this.selectionModeChanged.asObservable();
private allAlarms: Array<AlarmInfo> = [];
private allAlarmsSubject = new BehaviorSubject<AlarmInfo[]>([]);
private allAlarms$: Observable<Array<AlarmInfo>> = this.allAlarmsSubject.asObservable();
private currentAlarm: AlarmInfo = null;
constructor() {
}
connect(collectionViewer: CollectionViewer): Observable<AlarmInfo[] | ReadonlyArray<AlarmInfo>> {
return this.alarmsSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.alarmsSubject.complete();
this.pageDataSubject.complete();
}
loadAlarms(pageLink: PageLink) {
if (this.selection.hasValue()) {
this.selection.clear();
this.onSelectionModeChanged(false);
}
this.fetchAlarms(pageLink).pipe(
catchError(() => of(emptyPageData<AlarmInfo>())),
).subscribe(
(pageData) => {
this.alarmsSubject.next(pageData.data);
this.pageDataSubject.next(pageData);
}
);
}
updateAlarms(alarms: AlarmInfo[]) {
alarms.forEach((newAlarm) => {
const existingAlarmIndex = this.allAlarms.findIndex(alarm => alarm.id.id === newAlarm.id.id);
if (existingAlarmIndex > -1) {
Object.assign(this.allAlarms[existingAlarmIndex], newAlarm);
} else {
this.allAlarms.push(newAlarm);
}
});
for (let i = this.allAlarms.length - 1; i >= 0; i--) {
const oldAlarm = this.allAlarms[i];
const newAlarmIndex = alarms.findIndex(alarm => alarm.id.id === oldAlarm.id.id);
if (newAlarmIndex === -1) {
this.allAlarms.splice(i, 1);
}
}
if (this.selection.hasValue()) {
const toRemove: AlarmInfo[] = [];
this.selection.selected.forEach((selectedAlarm) => {
const existingAlarm = this.allAlarms.find(alarm => alarm.id.id === selectedAlarm.id.id);
if (!existingAlarm) {
toRemove.push(selectedAlarm);
}
});
this.selection.deselect(...toRemove);
if (this.selection.isEmpty()) {
this.onSelectionModeChanged(false);
}
}
this.allAlarmsSubject.next(this.allAlarms);
}
isAllSelected(): Observable<boolean> {
const numSelected = this.selection.selected.length;
return this.alarmsSubject.pipe(
map((alarms) => numSelected === alarms.length)
);
}
isEmpty(): Observable<boolean> {
return this.alarmsSubject.pipe(
map((alarms) => !alarms.length)
);
}
total(): Observable<number> {
return this.pageDataSubject.pipe(
map((pageData) => pageData.totalElements)
);
}
toggleSelection(alarm: AlarmInfo) {
const hasValue = this.selection.hasValue();
this.selection.toggle(alarm);
if (hasValue !== this.selection.hasValue()) {
this.onSelectionModeChanged(this.selection.hasValue());
}
}
isSelected(alarm: AlarmInfo): boolean {
return this.selection.isSelected(alarm);
}
masterToggle() {
this.alarmsSubject.pipe(
tap((alarms) => {
const numSelected = this.selection.selected.length;
if (numSelected === alarms.length) {
this.selection.clear();
if (numSelected > 0) {
this.onSelectionModeChanged(false);
}
} else {
alarms.forEach(row => {
this.selection.select(row);
});
if (numSelected === 0) {
this.onSelectionModeChanged(true);
}
}
}),
take(1)
).subscribe();
}
public toggleCurrentAlarm(alarm: AlarmInfo): boolean {
if (this.currentAlarm !== alarm) {
this.currentAlarm = alarm;
return true;
} else {
return false;
}
}
public isCurrentAlarm(alarm: AlarmInfo): boolean {
return (this.currentAlarm && alarm && this.currentAlarm.id && alarm.id) &&
(this.currentAlarm.id.id === alarm.id.id);
}
private onSelectionModeChanged(selectionMode: boolean) {
this.selectionModeChanged.emit(selectionMode);
}
private fetchAlarms(pageLink: PageLink): Observable<PageData<AlarmInfo>> {
return this.allAlarms$.pipe(
map((data) => pageLink.filterData(data))
);
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-entity-table tb-absolute-fill">
<div class="tb-table-widget tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-absolute-fill">
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
@ -39,8 +39,8 @@
</mat-toolbar>
<div fxFlex class="table-container">
<mat-table [dataSource]="entityDatasource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container [matColumnDef]="column.label" *ngFor="let column of columns; trackBy: trackByColumnLabel;">
matSort [matSortActive]="sortOrderProperty" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear>
<ng-container [matColumnDef]="column.def" *ngFor="let column of columns; trackBy: trackByColumnDef;">
<mat-header-cell [ngStyle]="headerStyle(column)" *matHeaderCellDef mat-sort-header> {{ column.title }} </mat-header-cell>
<mat-cell *matCellDef="let entity;"
[innerHTML]="cellContent(entity, column)"

20
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss

@ -16,24 +16,4 @@
:host {
width: 100%;
height: 100%;
.tb-entity-table {
.mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar {
background: transparent;
}
mat-toolbar {
height: 39px;
max-height: 39px;
.mat-toolbar-tools {
height: 39px;
max-height: 39px;
}
}
.table-container {
overflow: auto;
}
}
}
:host ::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}

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

@ -14,7 +14,16 @@
/// limitations under the License.
///
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
AfterViewInit,
Component,
ElementRef,
Input,
NgZone,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -31,7 +40,6 @@ import { IWidgetSubscription } from '@core/api/widget-api.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, isDefined, isNumber } from '@core/utils';
import * as tinycolor_ from 'tinycolor2';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -49,10 +57,18 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import {
CellContentInfo,
CellStyleInfo,
constructTableCssString,
DisplayColumn,
EntityColumn,
EntityData,
getEntityValue
fromEntityColumnDef,
getCellContentInfo,
getCellStyleInfo,
getColumnWidth,
getEntityValue,
TableWidgetDataKeySettings,
TableWidgetSettings,
toEntityColumnDef
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@ -62,32 +78,20 @@ import {
DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component';
const tinycolor = tinycolor_;
interface EntitiesTableWidgetSettings {
interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string;
enableSearch: boolean;
enableSelectColumnDisplay: boolean;
displayEntityName: boolean;
entityNameColumnTitle: string;
displayEntityType: boolean;
displayPagination: boolean;
defaultPageSize: number;
defaultSortOrder: string;
}
interface EntitiesTableDataKeySettings {
columnWidth: string;
useCellStyleFunction: boolean;
cellStyleFunction: string;
useCellContentFunction: boolean;
cellContentFunction: string;
interface EntitiesTableDataKeySettings extends TableWidgetDataKeySettings {
}
@Component({
selector: 'tb-entities-table-widget',
templateUrl: './entities-table-widget.component.html',
styleUrls: ['./entities-table-widget.component.scss']
styleUrls: ['./entities-table-widget.component.scss', './table-widget.scss']
})
export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit {
@ -101,6 +105,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayPagination = true;
public pageSizeOptions;
public pageLink: PageLink;
public sortOrderProperty: string;
public textSearchMode = false;
public columns: Array<EntityColumn> = [];
public displayedColumns: string[] = [];
@ -138,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
constructor(protected store: Store<AppState>,
private elementRef: ElementRef,
private ngZone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
@ -185,13 +191,17 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
}
public onDataUpdated() {
this.entityDatasource.updateEntitiesData(this.subscription.data);
this.ctx.detectChanges();
this.ngZone.run(() => {
this.entityDatasource.updateEntitiesData(this.subscription.data);
this.ctx.detectChanges();
});
}
private initializeConfig() {
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
let entitiesTitle: string;
if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
@ -212,78 +222,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.defaultPageSize = pageSize;
}
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize*2, this.defaultPageSize*3];
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
const origColor = this.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)';
const defaultColor = tinycolor(origColor);
const mdDark = defaultColor.setAlpha(0.87).toRgbString();
const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
const cssString =
'.mat-input-element::placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element::-moz-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element::-webkit-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element:-ms-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'mat-toolbar.mat-table-toolbar {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'mat-toolbar.mat-table-toolbar button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-header-row {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
'.mat-table .mat-header-cell {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-header-cell .mat-sort-header-arrow {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-table .mat-row, .mat-table .mat-header-row {\n'+
'border-bottom-color: '+mdDarkDivider+';\n'+
'}\n'+
'.mat-table .mat-row:not(.tb-current-entity):not(:hover) .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
'.mat-table .mat-cell {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-divider {\n'+
'border-top-color: ' + mdDarkDivider + ';\n'+
'}\n'+
'.mat-paginator {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-paginator button.mat-icon-button {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-paginator button.mat-icon-button[disabled][disabled] {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-paginator .mat-select-value {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}';
const cssString = constructTableCssString(this.widgetConfig);
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'entities-table-' + this.utils.hashCode(cssString);
@ -294,8 +235,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
private updateDatasources() {
this.actionCellDescriptors = this.ctx.actionsApi.getActionDescriptors('actionCellButton');
const displayEntityName = isDefined(this.settings.displayEntityName) ? this.settings.displayEntityName : true;
let entityNameColumnTitle: string;
if (this.settings.entityNameColumnTitle && this.settings.entityNameColumnTitle.length) {
@ -306,11 +245,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
const displayEntityType = isDefined(this.settings.displayEntityType) ? this.settings.displayEntityType : true;
if (displayEntityName) {
this.displayedColumns.push('entityName');
this.columns.push(
{
name: 'entityName',
label: 'entityName',
def: 'entityName',
title: entityNameColumnTitle
} as EntityColumn
);
@ -320,14 +259,14 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.stylesInfo['entityName'] = {
useCellStyleFunction: false
};
this.columnWidth['entityName'] = '100px';
this.columnWidth['entityName'] = '0px';
}
if (displayEntityType) {
this.displayedColumns.push('entityType');
this.columns.push(
{
name: 'entityType',
label: 'entityType',
def: 'entityType',
title: this.translate.instant('entity.entity-type'),
} as EntityColumn
);
@ -337,7 +276,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.stylesInfo['entityType'] = {
useCellStyleFunction: false
};
this.columnWidth['entityType'] = '100px';
this.columnWidth['entityType'] = '0px';
}
const dataKeys: Array<DataKey> = [];
@ -353,55 +292,24 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
dataKeys.push(dataKey);
dataKey.title = this.utils.customTranslation(dataKey.label, dataKey.label);
dataKey.def = 'def' + this.columns.length;
const keySettings: EntitiesTableDataKeySettings = dataKey.settings;
let cellStyleFunction: Function = null;
let useCellStyleFunction = false;
if (keySettings.useCellStyleFunction === true) {
if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
try {
cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
useCellStyleFunction = true;
} catch (e) {
cellStyleFunction = null;
useCellStyleFunction = false;
}
}
}
this.stylesInfo[dataKey.label] = {
useCellStyleFunction,
cellStyleFunction
};
let cellContentFunction: Function = null;
let useCellContentFunction = false;
if (keySettings.useCellContentFunction === true) {
if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
try {
cellContentFunction = new Function('value, entity, ctx', keySettings.cellContentFunction);
useCellContentFunction = true;
} catch (e) {
cellContentFunction = null;
useCellContentFunction = false;
}
}
}
this.contentsInfo[dataKey.label] = {
useCellContentFunction,
cellContentFunction,
units: dataKey.units,
decimals: dataKey.decimals
};
const columnWidth = isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
this.columnWidth[dataKey.label] = columnWidth;
this.displayedColumns.push(dataKey.label);
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings);
this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, entity, ctx');
this.contentsInfo[dataKey.def].units = dataKey.units;
this.contentsInfo[dataKey.def].decimals = dataKey.decimals;
this.columnWidth[dataKey.def] = getColumnWidth(keySettings);
this.columns.push(dataKey);
});
this.displayedColumns.push(...this.columns.map(column => column.def));
}
if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) {
this.defaultSortOrder = this.settings.defaultSortOrder;
}
this.pageLink.sortOrder = sortOrderFromString(this.defaultSortOrder);
this.sortOrderProperty = toEntityColumnDef(this.pageLink.sortOrder.property, this.columns);
if (this.actionCellDescriptors.length) {
this.displayedColumns.push('actions');
@ -435,8 +343,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
const columns: DisplayColumn[] = this.columns.map(column => {
return {
title: column.title,
label: column.label,
display: this.displayedColumns.indexOf(column.label) > -1
def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1
}
});
@ -444,7 +352,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
[DISPLAY_COLUMNS_PANEL_DATA, {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.label);
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
this.displayedColumns.push('actions');
}
} as DisplayColumnsPanelData],
@ -485,24 +393,27 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
} else {
this.pageLink.page = 0;
}
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.property = fromEntityColumnDef(this.sort.active, this.columns);
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.entityDatasource.loadEntities(this.pageLink);
this.ctx.detectChanges();
}
public trackByColumnLabel(index, column: EntityColumn) {
return column.label;
public trackByColumnDef(index, column: EntityColumn) {
return column.def;
}
public headerStyle(key: EntityColumn): any {
return this.widthStyle(key);
const columnWidth = this.columnWidth[key.def];
return {
width: columnWidth
}
}
public cellStyle(entity: EntityData, key: EntityColumn): any {
let style: any = {};
if (entity && key) {
const styleInfo = this.stylesInfo[key.label];
const styleInfo = this.stylesInfo[key.def];
const value = getEntityValue(entity, key);
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
@ -514,20 +425,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
style = this.defaultStyle(key, value);
}
}
const widthStyle = this.widthStyle(key);
style = {...style, ...widthStyle};
return style;
}
private widthStyle(key: EntityColumn): any {
let style: any = {};
const columnWidth = this.columnWidth[key.label];
if (columnWidth !== "0px") {
style.minWidth = columnWidth;
if (!style.width) {
const columnWidth = this.columnWidth[key.def];
style.width = columnWidth;
} else {
style.minWidth = "auto";
style.width = "auto";
}
return style;
}
@ -535,7 +435,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public cellContent(entity: EntityData, key: EntityColumn): SafeHtml {
let strContent = '';
if (entity && key) {
const contentInfo = this.contentsInfo[key.label];
const contentInfo = this.contentsInfo[key.def];
const value = getEntityValue(entity, key);
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
if (isDefined(value)) {

206
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -15,7 +15,28 @@
///
import { EntityId } from '@shared/models/id/entity-id';
import { DataKey } from '@shared/models/widget.models';
import { DataKey, WidgetConfig } from '@shared/models/widget.models';
import { getDescendantProp, isDefined } from '@core/utils';
import { alarmFields, AlarmInfo } from '@shared/models/alarm.models';
import * as tinycolor_ from 'tinycolor2';
const tinycolor = tinycolor_;
export interface TableWidgetSettings {
enableSearch: boolean;
enableSelectColumnDisplay: boolean;
displayPagination: boolean;
defaultPageSize: number;
defaultSortOrder: string;
}
export interface TableWidgetDataKeySettings {
columnWidth: string;
useCellStyleFunction: boolean;
cellStyleFunction: string;
useCellContentFunction: boolean;
cellContentFunction: string;
}
export interface EntityData {
id: EntityId;
@ -25,12 +46,13 @@ export interface EntityData {
}
export interface EntityColumn extends DataKey {
def: string;
title: string;
}
export interface DisplayColumn {
title: string;
label: string;
def: string;
display: boolean;
}
@ -46,10 +68,186 @@ export interface CellStyleInfo {
cellStyleFunction?: Function;
}
export function findColumnProperty(searchProperty: string, searchValue: string, columnProperty: string, columns: EntityColumn[]): string {
let res = searchValue;
const column = columns.find(column => column[searchProperty] === searchValue);
if (column) {
res = column[columnProperty];
}
return res;
}
export function toEntityColumnDef(label: string, columns: EntityColumn[]): string {
return findColumnProperty('label', label, 'def', columns);
}
export function fromEntityColumnDef(def: string, columns: EntityColumn[]): string {
return findColumnProperty('def', def, 'label', columns);
}
export function toAlarmColumnDef(name: string, columns: EntityColumn[]): string {
return findColumnProperty('name', name, 'def', columns);
}
export function fromAlarmColumnDef(def: string, columns: EntityColumn[]): string {
return findColumnProperty('def', def, 'name', columns);
}
export function getEntityValue(entity: any, key: DataKey): any {
return getDescendantProp(entity, key.label);
}
export function getDescendantProp(obj: any, path: string): any {
return path.split('.').reduce((acc, part) => acc && acc[part], obj)
export function getAlarmValue(alarm: AlarmInfo, key: EntityColumn) {
const alarmField = alarmFields[key.name];
if (alarmField) {
return getDescendantProp(alarm, alarmField.value);
} else {
return getDescendantProp(alarm, key.name);
}
}
export function getCellStyleInfo(keySettings: TableWidgetDataKeySettings): CellStyleInfo {
let cellStyleFunction: Function = null;
let useCellStyleFunction = false;
if (keySettings.useCellStyleFunction === true) {
if (isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
try {
cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
useCellStyleFunction = true;
} catch (e) {
cellStyleFunction = null;
useCellStyleFunction = false;
}
}
}
return {
useCellStyleFunction,
cellStyleFunction
};
}
export function getCellContentInfo(keySettings: TableWidgetDataKeySettings, ...args: string[]): CellContentInfo {
let cellContentFunction: Function = null;
let useCellContentFunction = false;
if (keySettings.useCellContentFunction === true) {
if (isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
try {
cellContentFunction = new Function(...args, keySettings.cellContentFunction);
useCellContentFunction = true;
} catch (e) {
cellContentFunction = null;
useCellContentFunction = false;
}
}
}
return {
cellContentFunction,
useCellContentFunction
};
}
export function getColumnWidth(keySettings: TableWidgetDataKeySettings): string {
return isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
}
export function constructTableCssString(widgetConfig: WidgetConfig): string {
const origColor = widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
const origBackgroundColor = widgetConfig.backgroundColor || 'rgb(255, 255, 255)';
const currentEntityColor = 'rgba(221, 221, 221, 0.65)';
const currentEntityStickyColor = tinycolor.mix(origBackgroundColor,
tinycolor(currentEntityColor).setAlpha(1), 65).toRgbString();
const selectedColor = 'rgba(221, 221, 221, 0.5)';
const selectedStickyColor = tinycolor.mix(origBackgroundColor,
tinycolor(selectedColor).setAlpha(1), 50).toRgbString();
const hoverColor = 'rgba(221, 221, 221, 0.3)';
const hoverStickyColor = tinycolor.mix(origBackgroundColor,
tinycolor(hoverColor).setAlpha(1), 30).toRgbString();
const defaultColor = tinycolor(origColor);
const mdDark = defaultColor.setAlpha(0.87).toRgbString();
const mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
const mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
const mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
const cssString =
'.mat-input-element::placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element::-moz-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element::-webkit-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'.mat-input-element:-ms-input-placeholder {\n' +
' color: ' + mdDarkSecondary + ';\n'+
'}\n' +
'mat-toolbar.mat-table-toolbar {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'mat-toolbar.mat-table-toolbar:not([color="primary"]) button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-header-row {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
'.mat-table .mat-header-cell {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-table .mat-header-cell .mat-sort-header-arrow {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-table .mat-cell, .mat-table .mat-header-cell {\n'+
'border-bottom-color: '+mdDarkDivider+';\n'+
'}\n'+
'.mat-table .mat-cell .mat-checkbox-frame, .mat-table .mat-header-cell .mat-checkbox-frame {\n'+
'border-color: '+mdDarkSecondary+';\n'+
'}\n'+
'.mat-table .mat-row .mat-cell.mat-table-sticky {\n'+
'transition: background-color .2s;\n'+
'}\n'+
'.mat-table .mat-row.tb-current-entity {\n'+
'background-color: ' + currentEntityColor + ';\n'+
'}\n'+
'.mat-table .mat-row.tb-current-entity .mat-cell.mat-table-sticky {\n'+
'background-color: ' + currentEntityStickyColor + ';\n'+
'}\n'+
'.mat-table .mat-row:hover:not(.tb-current-entity) {\n'+
'background-color: ' + hoverColor + ';\n'+
'}\n'+
'.mat-table .mat-row:hover:not(.tb-current-entity) .mat-cell.mat-table-sticky {\n'+
'background-color: ' + hoverStickyColor + ';\n'+
'}\n'+
'.mat-table .mat-row.mat-row-select.mat-selected:not(.tb-current-entity) {\n'+
'background-color: ' + selectedColor + ';\n'+
'}\n'+
'.mat-table .mat-row.mat-row-select.mat-selected:not(.tb-current-entity) .mat-cell.mat-table-sticky {\n'+
'background-color: ' + selectedStickyColor + ';\n'+
'}\n'+
'.mat-table .mat-row .mat-cell.mat-table-sticky, .mat-table .mat-header-cell.mat-table-sticky {\n'+
'background-color: ' + origBackgroundColor + ';\n'+
'}\n'+
'.mat-table .mat-cell {\n'+
'color: ' + mdDark + ';\n'+
'}\n'+
'.mat-table .mat-cell button.mat-icon-button mat-icon {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-divider {\n'+
'border-top-color: ' + mdDarkDivider + ';\n'+
'}\n'+
'.mat-paginator {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-paginator button.mat-icon-button {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}\n'+
'.mat-paginator button.mat-icon-button[disabled][disabled] {\n'+
'color: ' + mdDarkDisabled + ';\n'+
'}\n'+
'.mat-paginator .mat-select-value {\n'+
'color: ' + mdDarkSecondary + ';\n'+
'}';
return cssString;
}

61
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.scss

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-table-widget {
.mat-table, .mat-paginator, mat-toolbar.mat-table-toolbar:not([color="primary"]) {
background: transparent;
}
mat-toolbar {
height: 39px;
max-height: 39px;
.mat-toolbar-tools {
height: 39px;
max-height: 39px;
}
}
.table-container {
overflow: auto;
}
.mat-row:not(.mat-row-select), .mat-header-row:not(.mat-row-select) {
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) {
padding: 0px 5px;
}
}
.mat-row.mat-row-select, .mat-header-row.mat-row-select {
mat-cell:nth-child(2), mat-footer-cell:nth-child(2), mat-header-cell:nth-child(2) {
padding: 0px 5px;
}
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) {
padding: 0px 5px;
}
}
}
}
:host-context(.tb-has-timewindow) {
.tb-table-widget {
mat-toolbar {
height: 65px;
max-height: 65px;
.mat-toolbar-tools {
height: 65px;
max-height: 65px;
}
}
}
}

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

@ -21,6 +21,7 @@ import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-detail
import { LegendComponent } from '@home/components/widget/legend.component';
import { EntitiesTableWidgetComponent } from '@home/components/widget/lib/entities-table-widget.component';
import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/display-columns-panel.component';
import { AlarmsTableWidgetComponent } from '@home/components/widget/lib/alarms-table-widget.component';
@NgModule({
entryComponents: [
@ -29,14 +30,16 @@ import { DisplayColumnsPanelComponent } from '@home/components/widget/lib/displa
declarations:
[
DisplayColumnsPanelComponent,
EntitiesTableWidgetComponent
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent
],
imports: [
CommonModule,
SharedModule
],
exports: [
EntitiesTableWidgetComponent
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent
]
})
export class WidgetComponentsModule { }

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

@ -365,6 +365,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.handleWidgetException(e);
}
}
this.widgetContext.destroyed = true;
this.destroyDynamicWidgetComponent();
}

17
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -75,7 +75,7 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
constructor(public type: EntityTableColumnType,
public key: string,
public title: string,
public maxWidth: string = '100%',
public width: string = '0px',
public sortable: boolean = true) {
}
}
@ -83,13 +83,14 @@ export class BaseEntityTableColumn<T extends BaseData<HasId>> {
export class EntityTableColumn<T extends BaseData<HasId>> extends BaseEntityTableColumn<T> {
constructor(public key: string,
public title: string,
public maxWidth: string = '100%',
public width: string = '0px',
public cellContentFunction: CellContentFunction<T> = (entity, property) => entity[property],
public cellStyleFunction: CellStyleFunction<T> = () => ({}),
public sortable: boolean = true,
public headerCellStyleFunction: HeaderCellStyleFunction<T> = () => ({}),
public cellTooltipFunction: CellTooltipFunction<T> = () => undefined) {
super('content', key, title, maxWidth, sortable);
public cellTooltipFunction: CellTooltipFunction<T> = () => undefined,
public isNumberColumn: boolean = false) {
super('content', key, title, width, sortable);
}
}
@ -97,8 +98,8 @@ export class EntityActionTableColumn<T extends BaseData<HasId>> extends BaseEnti
constructor(public key: string,
public title: string,
public actionDescriptor: CellActionDescriptor<T>,
public maxWidth: string = '100%') {
super('action', key, title, maxWidth, false);
public width: string = '0px') {
super('action', key, title, width, false);
}
}
@ -106,12 +107,12 @@ export class DateEntityTableColumn<T extends BaseData<HasId>> extends EntityTabl
constructor(key: string,
title: string,
datePipe: DatePipe,
maxWidth: string = '100%',
width: string = '0px',
dateFormat: string = 'yyyy-MM-dd HH:mm:ss',
cellStyleFunction: CellStyleFunction<T> = () => ({})) {
super(key,
title,
maxWidth,
width,
(entity, property) => datePipe.transform(entity[property], dateFormat),
cellStyleFunction);
}

17
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -114,19 +114,24 @@ export class WidgetContext {
private _changeDetector: ChangeDetectorRef;
detectChanges(updateWidgetParams: boolean = false) {
if (updateWidgetParams) {
this.dashboardWidget.updateWidgetParams();
if (!this.destroyed) {
if (updateWidgetParams) {
this.dashboardWidget.updateWidgetParams();
}
this._changeDetector.detectChanges();
}
this._changeDetector.detectChanges();
}
updateWidgetParams() {
setTimeout(() => {
this.dashboardWidget.updateWidgetParams();
}, 0);
if (!this.destroyed) {
setTimeout(() => {
this.dashboardWidget.updateWidgetParams();
}, 0);
}
}
inited = false;
destroyed = false;
subscriptions: {[id: string]: IWidgetSubscription} = {};
defaultSubscription: IWidgetSubscription = null;

6
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -146,12 +146,12 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
configureColumns(assetScope: string): Array<EntityTableColumn<AssetInfo>> {
const columns: Array<EntityTableColumn<AssetInfo>> = [
new DateEntityTableColumn<AssetInfo>('createdTime', 'asset.created-time', this.datePipe, '150px'),
new EntityTableColumn<AssetInfo>('name', 'asset.name'),
new EntityTableColumn<AssetInfo>('type', 'asset.asset-type'),
new EntityTableColumn<AssetInfo>('name', 'asset.name', '33%'),
new EntityTableColumn<AssetInfo>('type', 'asset.asset-type', '33%'),
];
if (assetScope === 'tenant') {
columns.push(
new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer'),
new EntityTableColumn<AssetInfo>('customerTitle', 'customer.customer', '33%'),
new EntityTableColumn<AssetInfo>('customerIsPublic', 'asset.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);

8
ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts

@ -55,10 +55,10 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C
this.config.columns.push(
new DateEntityTableColumn<Customer>('createdTime', 'customer.created-time', this.datePipe, '150px'),
new EntityTableColumn<Customer>('title', 'customer.title'),
new EntityTableColumn<Customer>('email', 'contact.email'),
new EntityTableColumn<Customer>('country', 'contact.country'),
new EntityTableColumn<Customer>('city', 'contact.city')
new EntityTableColumn<Customer>('title', 'customer.title', '25%'),
new EntityTableColumn<Customer>('email', 'contact.email', '25%'),
new EntityTableColumn<Customer>('country', 'contact.country', '25%'),
new EntityTableColumn<Customer>('city', 'contact.city', '25%')
);
this.config.cellActionDescriptors.push(

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -143,12 +143,12 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
configureColumns(dashboardScope: string): Array<EntityTableColumn<DashboardInfo>> {
const columns: Array<EntityTableColumn<DashboardInfo>> = [
new DateEntityTableColumn<DashboardInfo>('createdTime', 'dashboard.created-time', this.datePipe, '150px'),
new EntityTableColumn<DashboardInfo>('title', 'dashboard.title')
new EntityTableColumn<DashboardInfo>('title', 'dashboard.title', '50%')
];
if (dashboardScope === 'tenant') {
columns.push(
new EntityTableColumn<DashboardInfo>('customersTitle', 'dashboard.assignedToCustomers',
'100%', entity => {
'50%', entity => {
return getDashboardAssignedCustomersText(entity);
}, () => ({}), false),
new EntityTableColumn<DashboardInfo>('dashboardIsPublic', 'dashboard.public', '60px',

8
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -150,13 +150,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
configureColumns(deviceScope: string): Array<EntityTableColumn<DeviceInfo>> {
const columns: Array<EntityTableColumn<DeviceInfo>> = [
new DateEntityTableColumn<DeviceInfo>('createdTime', 'device.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceInfo>('name', 'device.name'),
new EntityTableColumn<DeviceInfo>('type', 'device.device-type'),
new EntityTableColumn<DeviceInfo>('label', 'device.label')
new EntityTableColumn<DeviceInfo>('name', 'device.name', '25%'),
new EntityTableColumn<DeviceInfo>('type', 'device.device-type', '25%'),
new EntityTableColumn<DeviceInfo>('label', 'device.label', '25%')
];
if (deviceScope === 'tenant') {
columns.push(
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer'),
new EntityTableColumn<DeviceInfo>('customerTitle', 'customer.customer', '25%'),
new EntityTableColumn<DeviceInfo>('customerIsPublic', 'device.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);

6
ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts

@ -147,12 +147,12 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
configureColumns(entityViewScope: string): Array<EntityTableColumn<EntityViewInfo>> {
const columns: Array<EntityTableColumn<EntityViewInfo>> = [
new DateEntityTableColumn<EntityViewInfo>('createdTime', 'entity-view.created-time', this.datePipe, '150px'),
new EntityTableColumn<EntityViewInfo>('name', 'entity-view.name'),
new EntityTableColumn<EntityViewInfo>('type', 'entity-view.entity-view-type'),
new EntityTableColumn<EntityViewInfo>('name', 'entity-view.name', '33%'),
new EntityTableColumn<EntityViewInfo>('type', 'entity-view.entity-view-type', '33%'),
];
if (entityViewScope === 'tenant') {
columns.push(
new EntityTableColumn<EntityViewInfo>('customerTitle', 'customer.customer'),
new EntityTableColumn<EntityViewInfo>('customerTitle', 'customer.customer', '33%'),
new EntityTableColumn<EntityViewInfo>('customerIsPublic', 'entity-view.public', '60px',
entity => {
return checkBoxCell(entity.customerIsPublic);

8
ui-ngx/src/app/modules/home/pages/tenant/tenants-table-config.resolver.ts

@ -55,10 +55,10 @@ export class TenantsTableConfigResolver implements Resolve<EntityTableConfig<Ten
this.config.columns.push(
new DateEntityTableColumn<Tenant>('createdTime', 'tenant.created-time', this.datePipe, '150px'),
new EntityTableColumn<Tenant>('title', 'tenant.title'),
new EntityTableColumn<Tenant>('email', 'contact.email'),
new EntityTableColumn<Tenant>('country', 'contact.country'),
new EntityTableColumn<Tenant>('city', 'contact.city')
new EntityTableColumn<Tenant>('title', 'tenant.title', '25%'),
new EntityTableColumn<Tenant>('email', 'contact.email', '25%'),
new EntityTableColumn<Tenant>('country', 'contact.country', '25%'),
new EntityTableColumn<Tenant>('city', 'contact.city', '25%')
);
this.config.cellActionDescriptors.push(

6
ui-ngx/src/app/modules/home/pages/user/users-table-config.resolver.ts

@ -90,9 +90,9 @@ export class UsersTableConfigResolver implements Resolve<EntityTableConfig<User>
this.config.columns.push(
new DateEntityTableColumn<User>('createdTime', 'user.created-time', this.datePipe, '150px'),
new EntityTableColumn<User>('firstName', 'user.first-name'),
new EntityTableColumn<User>('lastName', 'user.last-name'),
new EntityTableColumn<User>('email', 'user.email')
new EntityTableColumn<User>('firstName', 'user.first-name', '33%'),
new EntityTableColumn<User>('lastName', 'user.last-name', '33%'),
new EntityTableColumn<User>('email', 'user.email', '33%')
);
this.config.deleteEnabled = user => user && user.id && user.id.id !== this.authUser.id.id;

2
ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts

@ -58,7 +58,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
this.config.columns.push(
new DateEntityTableColumn<WidgetsBundle>('createdTime', 'widgets-bundle.created-time', this.datePipe, '150px'),
new EntityTableColumn<WidgetsBundle>('title', 'widgets-bundle.title'),
new EntityTableColumn<WidgetsBundle>('title', 'widgets-bundle.title', '100%'),
new EntityTableColumn<WidgetsBundle>('tenantId', 'widgets-bundle.system', '60px',
entity => {
return checkBoxCell(entity.tenantId.id === NULL_UUID);

1
ui-ngx/src/app/shared/components/breadcrumb.component.scss

@ -23,6 +23,7 @@
.tb-breadcrumb {
font-size: 18px !important;
font-weight: 400 !important;
overflow: hidden;
h1,
a,

1
ui-ngx/src/app/shared/models/alarm.models.ts

@ -106,6 +106,7 @@ export interface AlarmInfo extends Alarm {
}
export const simulatedAlarm: AlarmInfo = {
id: new AlarmId(NULL_UUID),
tenantId: new TenantId(NULL_UUID),
createdTime: new Date().getTime(),
startTs: new Date().getTime(),

5
ui-ngx/src/app/shared/models/page/page-link.ts

@ -16,6 +16,7 @@
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { getDescendantProp } from '@core/utils';
export type PageLinkSearchFunction<T> = (entity: T, textSearch: string) => boolean;
@ -69,8 +70,8 @@ export class PageLink {
public sort(item1: any, item2: any): number {
if (this.sortOrder) {
const property = this.sortOrder.property;
const item1Value = item1[property];
const item2Value = item2[property];
const item1Value = getDescendantProp(item1, property);
const item2Value = getDescendantProp(item2, property);
let result = 0;
if (item1Value !== item2Value) {
if (typeof item1Value === 'number' && typeof item2Value === 'number') {

90
ui-ngx/src/styles.scss

@ -472,6 +472,16 @@ mat-label {
}
}
// Material table
mat-toolbar.mat-primary {
button.mat-icon-button {
mat-icon {
color: white;
}
}
}
mat-toolbar.mat-table-toolbar {
background: #fff;
padding: 0 24px;
@ -483,7 +493,7 @@ mat-label {
}
}
mat-toolbar.mat-table-toolbar, .mat-cell {
mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell {
button.mat-icon-button {
mat-icon {
color: rgba(0, 0, 0, .54);
@ -491,28 +501,40 @@ mat-label {
}
}
.mat-cell {
mat-icon {
color: rgba(0, 0, 0, .54);
}
.mat-table {
width: 100%;
max-width: 100%;
margin-bottom: 1rem;
display: table;
border-collapse: separate;
margin: 0px;
}
mat-toolbar.mat-primary {
button.mat-icon-button {
mat-icon {
color: white;
.mat-row,
.mat-header-row {
display: table-row;
}
.mat-header-row.mat-table-sticky {
.mat-header-cell {
position: sticky;
top: 0;
z-index: 10;
background: inherit;
&.mat-table-sticky {
z-index: 11 !important;
}
}
}
.mat-row {
transition: background-color .2s;
&:hover:not(.tb-current-entity) {
background-color: rgba(221, 221, 221, 0.3);
background-color: #f4f4f4;
}
&.tb-current-entity {
background-color: rgba(221, 221, 221, 0.65);
background-color: #e9e9e9;
}
}
@ -541,12 +563,20 @@ mat-label {
}
}
.mat-cell,
.mat-header-cell {
white-space: nowrap;
}
.mat-cell, .mat-header-cell {
min-width: 40px;
word-wrap: initial;
display: table-cell;
line-break: unset;
width: 0px;
overflow: hidden;
vertical-align: middle;
border-width: 0;
border-bottom-width: 1px;
border-bottom-color: rgba(0, 0, 0, 0.12);
border-style: solid;
text-overflow: ellipsis;
&:last-child {
padding: 0 12px 0 0;
}
@ -561,8 +591,28 @@ mat-label {
text-overflow: ellipsis;
white-space: nowrap;
}
&.mat-table-sticky {
background: transparent;
}
.mat-header-cell {
white-space: nowrap;
button.mat-sort-header-button {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
&.mat-number-cell {
.mat-sort-header-container {
justify-content: flex-end;
}
}
}
.mat-cell {
&.mat-number-cell {
text-align: end;
}
mat-icon {
color: rgba(0, 0, 0, .54);
}
}
@ -575,6 +625,10 @@ mat-label {
height: 20px;
}
.mat-sort-header-sorted .mat-sort-header-arrow {
opacity: 1 !important;
}
.mat-toolbar-tools {
font-size: 20px;
letter-spacing: .005em;

Loading…
Cancel
Save