/// /// Copyright © 2016-2020 The Thingsboard Authors /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import { AfterViewInit, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageLink, TimePageLink } from '@shared/models/page/page-link'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute } from '@angular/router'; import { CellActionDescriptor, EntityActionTableColumn, EntityColumn, EntityTableColumn, EntityTableConfig, GroupActionDescriptor, HeaderActionDescriptor } from '@home/models/entity/entities-table-config.models'; import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; import { historyInterval, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; @Component({ selector: 'tb-entities-table', templateUrl: './entities-table.component.html', styleUrls: ['./entities-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit { @Input() entitiesTableConfig: EntityTableConfig>; translations: EntityTypeTranslation; headerActionDescriptors: Array; groupActionDescriptors: Array>>; cellActionDescriptors: Array>>; actionColumns: Array>>; entityColumns: Array>>; displayedColumns: string[]; headerCellStyleCache: Array = []; cellContentCache: Array = []; cellTooltipCache: Array = []; cellStyleCache: Array = []; selectionEnabled; pageLink: PageLink; textSearchMode = false; timewindow: Timewindow; dataSource: EntitiesDataSource>; isDetailsOpen = false; @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; constructor(protected store: Store, private route: ActivatedRoute, public translate: TranslateService, public dialog: MatDialog, private dialogService: DialogService, private domSanitizer: DomSanitizer, private componentFactoryResolver: ComponentFactoryResolver) { super(store); } ngOnInit() { this.entitiesTableConfig = this.entitiesTableConfig || this.route.snapshot.data.entitiesTableConfig; if (this.entitiesTableConfig.headerComponent) { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.entitiesTableConfig.headerComponent); const viewContainerRef = this.entityTableHeaderAnchor.viewContainerRef; viewContainerRef.clear(); const componentRef = viewContainerRef.createComponent(componentFactory); const headerComponent = componentRef.instance; headerComponent.entitiesTableConfig = this.entitiesTableConfig; } this.entitiesTableConfig.table = this; this.translations = this.entitiesTableConfig.entityTranslations; this.headerActionDescriptors = [...this.entitiesTableConfig.headerActionDescriptors]; this.groupActionDescriptors = [...this.entitiesTableConfig.groupActionDescriptors]; this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; if (this.entitiesTableConfig.entitiesDeleteEnabled) { this.cellActionDescriptors.push( { name: this.translate.instant('action.delete'), icon: 'delete', isEnabled: entity => this.entitiesTableConfig.deleteEnabled(entity), onAction: ($event, entity) => this.deleteEntity($event, entity) } ); } this.groupActionDescriptors.push( { name: this.translate.instant('action.delete'), icon: 'delete', isEnabled: this.entitiesTableConfig.entitiesDeleteEnabled, onAction: ($event, entities) => this.deleteEntities($event, entities) } ); const enabledGroupActionDescriptors = this.groupActionDescriptors.filter((descriptor) => descriptor.isEnabled); this.selectionEnabled = this.entitiesTableConfig.selectionEnabled && enabledGroupActionDescriptors.length; this.columnsUpdated(); const sortOrder: SortOrder = { property: this.entitiesTableConfig.defaultSortOrder.property, direction: this.entitiesTableConfig.defaultSortOrder.direction }; if (this.entitiesTableConfig.useTimePageLink) { this.timewindow = historyInterval(24 * 60 * 60 * 1000); const currentTime = Date.now(); this.pageLink = new TimePageLink(10, 0, null, sortOrder, currentTime - this.timewindow.history.timewindowMs, currentTime); } else { this.pageLink = new PageLink(10, 0, null, sortOrder); } this.dataSource = new EntitiesDataSource>( this.entitiesTableConfig.entitiesFetchFunction, this.entitiesTableConfig.entitySelectionEnabled, () => { this.dataLoaded(); } ); if (this.entitiesTableConfig.onLoadAction) { this.entitiesTableConfig.onLoadAction(this.route); } if (this.entitiesTableConfig.loadDataOnInit) { this.dataSource.loadEntities(this.pageLink); } } ngAfterViewInit() { fromEvent(this.searchInputField.nativeElement, 'keyup') .pipe( debounceTime(150), distinctUntilChanged(), tap(() => { this.paginator.pageIndex = 0; this.updateData(); }) ) .subscribe(); this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); merge(this.sort.sortChange, this.paginator.page) .pipe( tap(() => this.updateData()) ) .subscribe(); } addEnabled() { return this.entitiesTableConfig.addEnabled; } updateData(closeDetails: boolean = true) { if (closeDetails) { this.isDetailsOpen = false; } this.pageLink.page = this.paginator.pageIndex; this.pageLink.pageSize = this.paginator.pageSize; this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; if (this.entitiesTableConfig.useTimePageLink) { const timePageLink = this.pageLink as TimePageLink; if (this.timewindow.history.timewindowMs) { const currentTime = Date.now(); timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; timePageLink.endTime = currentTime; } else { timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs; timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs; } } this.dataSource.loadEntities(this.pageLink); } private dataLoaded() { this.headerCellStyleCache.length = 0; this.cellContentCache.length = 0; this.cellTooltipCache.length = 0; this.cellStyleCache.length = 0; } onRowClick($event: Event, entity) { if ($event) { $event.stopPropagation(); } if (this.dataSource.toggleCurrentEntity(entity)) { this.isDetailsOpen = true; } else { this.isDetailsOpen = !this.isDetailsOpen; } } addEntity($event: Event) { let entity$: Observable>; if (this.entitiesTableConfig.addEntity) { entity$ = this.entitiesTableConfig.addEntity(); } else { entity$ = this.dialog.open>, BaseData>(AddEntityDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { entitiesTableConfig: this.entitiesTableConfig } }).afterClosed(); } entity$.subscribe( (entity) => { if (entity) { this.updateData(); } } ); } onEntityUpdated(entity: BaseData) { this.updateData(false); } onEntityAction(action: EntityAction>) { if (action.action === 'delete') { this.deleteEntity(action.event, action.entity); } } deleteEntity($event: Event, entity: BaseData) { if ($event) { $event.stopPropagation(); } this.dialogService.confirm( this.entitiesTableConfig.deleteEntityTitle(entity), this.entitiesTableConfig.deleteEntityContent(entity), this.translate.instant('action.no'), this.translate.instant('action.yes'), true ).subscribe((result) => { if (result) { this.entitiesTableConfig.deleteEntity(entity.id).subscribe( () => { this.updateData(); } ); } }); } deleteEntities($event: Event, entities: BaseData[]) { if ($event) { $event.stopPropagation(); } this.dialogService.confirm( this.entitiesTableConfig.deleteEntitiesTitle(entities.length), this.entitiesTableConfig.deleteEntitiesContent(entities.length), this.translate.instant('action.no'), this.translate.instant('action.yes'), true ).subscribe((result) => { if (result) { const tasks: Observable[] = []; entities.forEach((entity) => { if (this.entitiesTableConfig.deleteEnabled(entity)) { tasks.push(this.entitiesTableConfig.deleteEntity(entity.id)); } }); forkJoin(tasks).subscribe( () => { this.updateData(); } ); } }); } onTimewindowChange() { this.updateData(); } enterFilterMode() { this.textSearchMode = true; this.pageLink.textSearch = ''; setTimeout(() => { this.searchInputField.nativeElement.focus(); this.searchInputField.nativeElement.setSelectionRange(0, 0); }, 10); } exitFilterMode() { this.textSearchMode = false; this.pageLink.textSearch = null; this.paginator.pageIndex = 0; this.updateData(); } resetSortAndFilter(update: boolean = true) { this.pageLink.textSearch = null; if (this.entitiesTableConfig.useTimePageLink) { this.timewindow = historyInterval(24 * 60 * 60 * 1000); } this.paginator.pageIndex = 0; const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); this.sort.active = sortable.id; this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; if (update) { this.updateData(); } } columnsUpdated(resetData: boolean = false) { this.entityColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityTableColumn) .map(column => column as EntityTableColumn>); this.actionColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityActionTableColumn) .map(column => column as EntityActionTableColumn>); this.displayedColumns = []; if (this.selectionEnabled) { this.displayedColumns.push('select'); } this.entitiesTableConfig.columns.forEach( (column) => { this.displayedColumns.push(column.key); } ); this.displayedColumns.push('actions'); this.headerCellStyleCache.length = 0; this.cellContentCache.length = 0; this.cellTooltipCache.length = 0; this.cellStyleCache.length = 0; if (resetData) { this.dataSource.reset(); } } headerCellStyle(column: EntityColumn>) { const index = this.entitiesTableConfig.columns.indexOf(column); let res = this.headerCellStyleCache[index]; if (!res) { const widthStyle: any = {width: column.width}; if (column.width !== '0px') { widthStyle.minWidth = column.width; widthStyle.maxWidth = column.width; } if (column instanceof EntityTableColumn) { res = {...column.headerCellStyleFunction(column.key), ...widthStyle}; } else { res = widthStyle; } this.headerCellStyleCache[index] = res; } return res; } cellContent(entity: BaseData, column: EntityColumn>, row: number) { if (column instanceof EntityTableColumn) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; let res = this.cellContentCache[index]; if (!res) { res = this.domSanitizer.bypassSecurityTrustHtml(column.cellContentFunction(entity, column.key)); this.cellContentCache[index] = res; } return res; } else { return ''; } } cellTooltip(entity: BaseData, column: EntityColumn>, row: number) { if (column instanceof EntityTableColumn) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; let res = this.cellTooltipCache[index]; if (isUndefined(res)) { res = column.cellTooltipFunction(entity, column.key); res = isDefined(res) ? res : null; this.cellTooltipCache[index] = res; } else { return res !== null ? res : undefined; } } else { return undefined; } } cellStyle(entity: BaseData, column: EntityColumn>, row: number) { const col = this.entitiesTableConfig.columns.indexOf(column); const index = row * this.entitiesTableConfig.columns.length + col; let res = this.cellStyleCache[index]; if (!res) { const widthStyle: any = {width: column.width}; if (column.width !== '0px') { widthStyle.minWidth = column.width; widthStyle.maxWidth = column.width; } if (column instanceof EntityTableColumn) { res = {...column.cellStyleFunction(entity, column.key), ...widthStyle}; } else { res = widthStyle; } this.cellStyleCache[index] = res; } return res; } trackByColumnKey(index, column: EntityTableColumn>) { return column.key; } trackByEntityId(index: number, entity: BaseData) { return entity.id.id; } }