/// /// 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, DoCheck, Input, IterableDiffers, KeyValueDiffers, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; import { AuthUser } from '@shared/models/user.model'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; import { TimeService } from '@core/services/time.service'; import { GridsterComponent, GridsterConfig } from 'angular-gridster2'; import { DashboardCallbacks, DashboardWidget, DashboardWidgets, IDashboardComponent, WidgetPosition } from '../../models/dashboard-component.models'; import { ReplaySubject, Subject } from 'rxjs'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; import { DialogService } from '@core/services/dialog.service'; import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { Widget } from '@app/shared/models/widget.models'; import { MatMenuTrigger } from '@angular/material'; @Component({ selector: 'tb-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, AfterViewInit, OnChanges { authUser: AuthUser; @Input() widgets: Array; @Input() widgetLayouts: WidgetLayouts; @Input() callbacks: DashboardCallbacks; @Input() aliasController: IAliasController; @Input() stateController: IStateController; @Input() columns: number; @Input() horizontalMargin: number; @Input() verticalMargin: number; @Input() isEdit: boolean; @Input() autofillHeight: boolean; @Input() mobileAutofillHeight: boolean; @Input() mobileRowHeight: number; @Input() isMobile: boolean; @Input() isMobileDisabled: boolean; @Input() isEditActionEnabled: boolean; @Input() isExportActionEnabled: boolean; @Input() isRemoveActionEnabled: boolean; @Input() dashboardStyle: {[klass: string]: any}; @Input() dashboardClass: string; @Input() ignoreLoading: boolean; @Input() dashboardTimewindow: Timewindow; dashboardTimewindowChangedSubject: Subject = new ReplaySubject(); dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable(); originalDashboardTimewindow: Timewindow; gridsterOpts: GridsterConfig; isWidgetExpanded = false; isMobileSize = false; @ViewChild('gridster', {static: true}) gridster: GridsterComponent; @ViewChild('dashboardMenuTrigger', {static: true}) dashboardMenuTrigger: MatMenuTrigger; dashboardMenuPosition = { x: '0px', y: '0px' }; dashboardContextMenuEvent: MouseEvent; @ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger; widgetMenuPosition = { x: '0px', y: '0px' }; widgetContextMenuEvent: MouseEvent; dashboardLoading = true; dashboardWidgets = new DashboardWidgets(this, this.differs.find([]).create((index, item) => { return item; }), this.kvDiffers.find([]).create() ); constructor(protected store: Store, private timeService: TimeService, private dialogService: DialogService, private breakpointObserver: BreakpointObserver, private differs: IterableDiffers, private kvDiffers: KeyValueDiffers) { super(store); this.authUser = getCurrentAuthUser(store); } ngOnInit(): void { if (!this.dashboardTimewindow) { this.dashboardTimewindow = this.timeService.defaultTimewindow(); } this.gridsterOpts = { gridType: 'scrollVertical', keepFixedHeightInMobile: true, pushItems: false, swap: false, maxRows: 100, minCols: this.columns ? this.columns : 24, outerMargin: true, outerMarginLeft: this.horizontalMargin ? this.horizontalMargin : 10, outerMarginRight: this.horizontalMargin ? this.horizontalMargin : 10, outerMarginTop: this.verticalMargin ? this.verticalMargin : 10, outerMarginBottom: this.horizontalMargin ? this.horizontalMargin : 10, minItemCols: 1, minItemRows: 1, defaultItemCols: 8, defaultItemRows: 6, resizable: {enabled: this.isEdit}, draggable: {enabled: this.isEdit}, itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), itemInitCallback: (item, itemComponent) => { (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; } }; this.updateMobileOpts(); this.breakpointObserver .observe(MediaBreakpoints['gt-sm']).subscribe( () => { this.updateMobileOpts(); this.notifyGridsterOptionsChanged(); } ); this.updateWidgets(); } ngDoCheck() { this.dashboardWidgets.doCheck(); } ngOnChanges(changes: SimpleChanges): void { let updateMobileOpts = false; let updateLayoutOpts = false; let updateEditingOpts = false; let updateWidgets = false; for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { updateMobileOpts = true; } else if (['horizontalMargin', 'verticalMargin'].includes(propName)) { updateLayoutOpts = true; } else if (propName === 'isEdit') { updateEditingOpts = true; } else if (['widgets', 'widgetLayouts'].includes(propName)) { updateWidgets = true; } else if (propName === 'dashboardTimewindow') { this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } } } if (updateWidgets) { this.updateWidgets(); } if (updateMobileOpts) { this.updateMobileOpts(); } if (updateLayoutOpts) { this.updateLayoutOpts(); } if (updateEditingOpts) { this.updateEditingOpts(); } if (updateMobileOpts || updateLayoutOpts || updateEditingOpts) { this.notifyGridsterOptionsChanged(); } } private updateWidgets() { this.dashboardWidgets.setWidgets(this.widgets, this.widgetLayouts); this.dashboardLoading = false; } ngAfterViewInit(): void { } onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { if (!this.originalDashboardTimewindow) { this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow); } this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow, startTimeMs, endTimeMs, interval, this.timeService); this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } onResetTimewindow(): void { if (this.originalDashboardTimewindow) { this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow); this.originalDashboardTimewindow = null; this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } } isAutofillHeight(): boolean { if (this.isMobileSize) { return isDefined(this.mobileAutofillHeight) ? this.mobileAutofillHeight : false; } else { return isDefined(this.autofillHeight) ? this.autofillHeight : false; } } openDashboardContextMenu($event: MouseEvent) { if (this.callbacks && this.callbacks.prepareDashboardContextMenu) { const items = this.callbacks.prepareDashboardContextMenu($event); if (items && items.length) { $event.preventDefault(); $event.stopPropagation(); this.dashboardContextMenuEvent = $event; this.dashboardMenuPosition.x = $event.clientX + 'px'; this.dashboardMenuPosition.y = $event.clientY + 'px'; this.dashboardMenuTrigger.menuData = { items }; this.dashboardMenuTrigger.openMenu(); } } } openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.widgetIndex); if (items && items.length) { $event.preventDefault(); $event.stopPropagation(); this.widgetContextMenuEvent = $event; this.widgetMenuPosition.x = $event.clientX + 'px'; this.widgetMenuPosition.y = $event.clientY + 'px'; this.widgetMenuTrigger.menuData = { items, widget: widget.widget }; this.widgetMenuTrigger.openMenu(); } } } onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) { this.isWidgetExpanded = expanded; } widgetMouseDown($event: Event, widget: DashboardWidget) { if (this.callbacks && this.callbacks.onWidgetMouseDown) { this.callbacks.onWidgetMouseDown($event, widget.widget, widget.widgetIndex); } } widgetClicked($event: Event, widget: DashboardWidget) { if (this.callbacks && this.callbacks.onWidgetClicked) { this.callbacks.onWidgetClicked($event, widget.widget, widget.widgetIndex); } } editWidget($event: Event, widget: DashboardWidget) { if ($event) { $event.stopPropagation(); } if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { this.callbacks.onEditWidget($event, widget.widget, widget.widgetIndex); } } exportWidget($event: Event, widget: DashboardWidget) { if ($event) { $event.stopPropagation(); } if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { this.callbacks.onExportWidget($event, widget.widget, widget.widgetIndex); } } removeWidget($event: Event, widget: DashboardWidget) { if ($event) { $event.stopPropagation(); } if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex); } } highlightWidget(index: number, delay?: number) { const highlighted = this.dashboardWidgets.highlightWidget(index); if (highlighted) { this.scrollToWidget(highlighted, delay); } } selectWidget(index: number, delay?: number) { const selected = this.dashboardWidgets.selectWidget(index); if (selected) { this.scrollToWidget(selected, delay); } } getSelectedWidget(): Widget { const dashboardWidget = this.dashboardWidgets.getSelectedWidget(); return dashboardWidget ? dashboardWidget.widget : null; } getEventGridPosition(event: Event): WidgetPosition { const pos: WidgetPosition = { row: 0, column: 0 }; const parentElement = this.gridster.el as HTMLElement; let pageX = 0; let pageY = 0; if (event instanceof MouseEvent) { pageX = event.pageX; pageY = event.pageY; } const x = pageX - parentElement.offsetLeft + parentElement.scrollLeft; const y = pageY - parentElement.offsetTop + parentElement.scrollTop; pos.row = this.gridster.pixelsToPositionY(y, Math.floor); pos.column = this.gridster.pixelsToPositionX(x, Math.floor); return pos; } resetHighlight() { const highlighted = this.dashboardWidgets.resetHighlight(); if (highlighted) { setTimeout(() => { this.scrollToWidget(highlighted, 0); }, 0); } } isHighlighted(widget: DashboardWidget) { return this.dashboardWidgets.isHighlighted(widget); } isNotHighlighted(widget: DashboardWidget) { return this.dashboardWidgets.isNotHighlighted(widget); } private scrollToWidget(widget: DashboardWidget, delay?: number) { const parentElement = this.gridster.el as HTMLElement; widget.gridsterItemComponent$().subscribe((gridsterItem) => { const gridsterItemElement = gridsterItem.el as HTMLElement; const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2; let scrollTop; if (this.isMobileSize) { scrollTop = gridsterItemElement.offsetTop; } else { scrollTop = scrollTop = gridsterItem.top; } if (offset > 0) { scrollTop -= offset; } animatedScroll(parentElement, scrollTop, delay); }); } private updateMobileOpts() { this.isMobileSize = this.checkIsMobileSize(); const mobileBreakPoint = this.isMobileSize ? 20000 : 0; this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; const rowSize = this.detectRowSize(this.isMobileSize); if (this.gridsterOpts.fixedRowHeight !== rowSize) { this.gridsterOpts.fixedRowHeight = rowSize; } if (this.isAutofillHeight()) { this.gridsterOpts.gridType = 'fit'; } else { this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical'; } } private updateLayoutOpts() { this.gridsterOpts.outerMarginLeft = this.horizontalMargin ? this.horizontalMargin : 10; this.gridsterOpts.outerMarginRight = this.horizontalMargin ? this.horizontalMargin : 10; this.gridsterOpts.outerMarginTop = this.verticalMargin ? this.verticalMargin : 10; this.gridsterOpts.outerMarginBottom = this.horizontalMargin ? this.horizontalMargin : 10; } private updateEditingOpts() { this.gridsterOpts.resizable.enabled = this.isEdit; this.gridsterOpts.draggable.enabled = this.isEdit; } private notifyGridsterOptionsChanged() { if (this.gridster && this.gridster.options) { this.gridster.optionsChanged(); } } private detectRowSize(isMobile: boolean): number | null { let rowHeight = null; if (!this.isAutofillHeight()) { if (isMobile) { rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70; } } return rowHeight; } private checkIsMobileSize(): boolean { const isMobileDisabled = this.isMobileDisabled === true; let isMobileSize = this.isMobile === true && !isMobileDisabled; if (!isMobileSize && !isMobileDisabled) { isMobileSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); } return isMobileSize; } }