/// /// 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, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { PageLink } from '@shared/models/page/page-link'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { fromEvent, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityId } from '@shared/models/id/entity-id'; import { AttributeData, AttributeScope, isClientSideTelemetryType, LatestTelemetry, TelemetryType, telemetryTypeTranslations } from '@shared/models/telemetry/telemetry.models'; import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; import { AttributeService } from '@app/core/http/attribute.service'; import { EntityType } from '@shared/models/entity-type.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; import { AddAttributeDialogComponent, AddAttributeDialogData } from '@home/components/attribute/add-attribute-dialog.component'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { TIMEWINDOW_PANEL_DATA, TimewindowPanelComponent } from '@shared/components/time/timewindow-panel.component'; import { EDIT_ATTRIBUTE_VALUE_PANEL_DATA, EditAttributeValuePanelComponent, EditAttributeValuePanelData } from './edit-attribute-value-panel.component'; import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; @Component({ selector: 'tb-attribute-table', templateUrl: './attribute-table.component.html', styleUrls: ['./attribute-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class AttributeTableComponent extends PageComponent implements AfterViewInit, OnInit { telemetryTypeTranslationsMap = telemetryTypeTranslations; isClientSideTelemetryTypeMap = isClientSideTelemetryType; latestTelemetryTypes = LatestTelemetry; mode: 'default' | 'widget' = 'default'; attributeScopes: Array = []; attributeScope: TelemetryType; displayedColumns = ['select', 'lastUpdateTs', 'key', 'value']; pageLink: PageLink; textSearchMode = false; dataSource: AttributeDatasource; activeValue = false; dirtyValue = false; entityIdValue: EntityId; attributeScopeSelectionReadonly = false; viewsInited = false; private disableAttributeScopeSelectionValue: boolean; get disableAttributeScopeSelection(): boolean { return this.disableAttributeScopeSelectionValue; } @Input() set disableAttributeScopeSelection(value: boolean) { this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value); } @Input() defaultAttributeScope: TelemetryType; @Input() set active(active: boolean) { if (this.activeValue !== active) { this.activeValue = active; if (this.activeValue && this.dirtyValue) { this.dirtyValue = false; if (this.viewsInited) { this.updateData(true); } } } } @Input() set entityId(entityId: EntityId) { if (this.entityIdValue !== entityId) { this.entityIdValue = entityId; this.resetSortAndFilter(this.activeValue); if (!this.activeValue) { this.dirtyValue = true; } } } @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; @ViewChild(MatSort, {static: false}) sort: MatSort; constructor(protected store: Store, private attributeService: AttributeService, public translate: TranslateService, public dialog: MatDialog, private overlay: Overlay, private viewContainerRef: ViewContainerRef, private dialogService: DialogService) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); this.dataSource = new AttributeDatasource(this.attributeService, this.translate); } ngOnInit() { } attributeScopeChanged(attributeScope: TelemetryType) { this.attributeScope = attributeScope; this.mode = 'default'; this.updateData(true); } 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(); this.viewsInited = true; if (this.activeValue && this.entityIdValue) { this.updateData(true); } } updateData(reload: boolean = 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()]; this.dataSource.loadAttributes(this.entityIdValue, this.attributeScope, this.pageLink, reload); } 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) { const entityType = this.entityIdValue.entityType; if (entityType === EntityType.DEVICE || entityType === EntityType.ENTITY_VIEW) { this.attributeScopes = Object.keys(AttributeScope); this.attributeScopeSelectionReadonly = false; } else { this.attributeScopes = [AttributeScope.SERVER_SCOPE]; this.attributeScopeSelectionReadonly = true; } this.mode = 'default'; this.attributeScope = this.defaultAttributeScope; this.pageLink.textSearch = null; if (this.viewsInited) { this.paginator.pageIndex = 0; const sortable = this.sort.sortables.get('key'); this.sort.active = sortable.id; this.sort.direction = 'asc'; if (update) { this.updateData(true); } } } reloadAttributes() { this.updateData(true); } addAttribute($event: Event) { if ($event) { $event.stopPropagation(); } this.dialog.open(AddAttributeDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { entityId: this.entityIdValue, attributeScope: this.attributeScope as AttributeScope } }).afterClosed().subscribe( (res) => { if (res) { this.reloadAttributes(); } } ); } editAttribute($event: Event, attribute: AttributeData) { 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: 'center', overlayX: 'end', overlayY: 'center' }; config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions([connectedPosition]); const overlayRef = this.overlay.create(config); overlayRef.backdropClick().subscribe(() => { overlayRef.dispose(); }); const injectionTokens = new WeakMap([ [EDIT_ATTRIBUTE_VALUE_PANEL_DATA, { attributeValue: attribute.value } as EditAttributeValuePanelData], [OverlayRef, overlayRef] ]); const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent, this.viewContainerRef, injector)); componentRef.onDestroy(() => { if (componentRef.instance.result !== null) { const attributeValue = componentRef.instance.result; const updatedAttribute = {...attribute}; updatedAttribute.value = attributeValue; this.attributeService.saveEntityAttributes(this.entityIdValue, this.attributeScope as AttributeScope, [updatedAttribute]).subscribe( () => { this.reloadAttributes(); } ); } }); } deleteAttributes($event: Event) { if ($event) { $event.stopPropagation(); } if (this.dataSource.selection.selected.length > 0) { this.dialogService.confirm( this.translate.instant('attribute.delete-attributes-title', {count: this.dataSource.selection.selected.length}), this.translate.instant('attribute.delete-attributes-text'), this.translate.instant('action.no'), this.translate.instant('action.yes'), true ).subscribe((result) => { if (result) { this.attributeService.deleteEntityAttributes(this.entityIdValue, this.attributeScope as AttributeScope, this.dataSource.selection.selected).subscribe( () => { this.reloadAttributes(); } ); } }); } } enterWidgetMode() { this.mode = 'widget'; // TODO: } exitWidgetMode() { this.mode = 'default'; this.reloadAttributes(); // TODO: } }