/// /// 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 { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; import { Widget, widgetType } from '@app/shared/models/widget.models'; import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; import { Timewindow } from '@shared/models/time/time.models'; import { Observable, of, Subject } from 'rxjs'; import { isDefined, isUndefined } from '@app/core/utils'; import { IterableDiffer, KeyValueDiffer } from '@angular/core'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import * as deepEqual from 'deep-equal'; export interface WidgetsData { widgets: Array; widgetLayouts?: WidgetLayouts; } export interface ContextMenuItem { enabled: boolean; shortcut?: string; icon: string; value: string; } export interface DashboardContextMenuItem extends ContextMenuItem { action: (contextMenuEvent: MouseEvent) => void; } export interface WidgetContextMenuItem extends ContextMenuItem { action: (contextMenuEvent: MouseEvent, widget: Widget) => void; } export interface DashboardCallbacks { onEditWidget?: ($event: Event, widget: Widget, index: number) => void; onExportWidget?: ($event: Event, widget: Widget, index: number) => void; onRemoveWidget?: ($event: Event, widget: Widget, index: number) => void; onWidgetMouseDown?: ($event: Event, widget: Widget, index: number) => void; onWidgetClicked?: ($event: Event, widget: Widget, index: number) => void; prepareDashboardContextMenu?: ($event: Event) => Array; prepareWidgetContextMenu?: ($event: Event, widget: Widget, index: number) => Array; } export interface WidgetPosition { row: number; column: number; } export interface IDashboardComponent { gridsterOpts: GridsterConfig; gridster: GridsterComponent; dashboardWidgets: DashboardWidgets; mobileAutofillHeight: boolean; isMobileSize: boolean; autofillHeight: boolean; dashboardTimewindow: Timewindow; dashboardTimewindowChanged: Observable; aliasController: IAliasController; stateController: IStateController; onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; onResetTimewindow(): void; resetHighlight(): void; highlightWidget(index: number, delay?: number); selectWidget(index: number, delay?: number); getSelectedWidget(): Widget; getEventGridPosition(event: Event): WidgetPosition; } declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; interface DashboardWidgetUpdateRecord { widget?: Widget; widgetLayout?: WidgetLayout; widgetIndex: number; operation: DashboardWidgetUpdateOperation; } export class DashboardWidgets implements Iterable { highlightedMode = false; dashboardWidgets: Array = []; widgets: Array; widgetLayouts: WidgetLayouts; [Symbol.iterator](): Iterator { return this.dashboardWidgets[Symbol.iterator](); } constructor(private dashboard: IDashboardComponent, private widgetsDiffer: IterableDiffer, private widgetLayoutsDiffer: KeyValueDiffer) { } doCheck() { const widgetChange = this.widgetsDiffer.diff(this.widgets); if (widgetChange !== null) { const layouts: WidgetLayouts = {}; const updateRecords: Array = []; const widgetLayoutChange = this.widgetLayoutsDiffer.diff(this.widgetLayouts); if (widgetLayoutChange !== null) { widgetLayoutChange.forEachAddedItem((added) => { layouts[added.key] = added.currentValue; }); widgetLayoutChange.forEachChangedItem((changed) => { layouts[changed.key] = changed.currentValue; }); } widgetChange.forEachAddedItem((added) => { updateRecords.push({ widget: added.item, widgetLayout: layouts[added.item.id], widgetIndex: added.currentIndex, operation: 'add' }); }); widgetChange.forEachRemovedItem((removed) => { let operation = updateRecords.find((record) => record.widgetIndex === removed.previousIndex); if (operation) { operation.operation = 'update'; } else { operation = { widgetIndex: removed.previousIndex, operation: 'remove' }; updateRecords.push(operation); } }); updateRecords.forEach((record) => { switch (record.operation) { case 'add': this.dashboardWidgets.push( new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout) ); break; case 'remove': let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex); if (index > -1) { this.dashboardWidgets.splice(index, 1); } break; case 'update': index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex); if (index > -1) { const prevDashboardWidget = this.dashboardWidgets[index]; if (!deepEqual(prevDashboardWidget.widget, record.widget)) { this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout); this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; this.dashboardWidgets[index].selected = prevDashboardWidget.selected; } else { this.dashboardWidgets[index].widget = record.widget; this.dashboardWidgets[index].widgetLayout = record.widgetLayout; } } break; } }); if (updateRecords.length) { this.updateRowsAndSort(); } } } setWidgets(widgets: Array, widgetLayouts: WidgetLayouts) { this.highlightedMode = false; this.widgets = widgets; this.widgetLayouts = widgetLayouts; } highlightWidget(index: number): DashboardWidget { const widget = this.findWidgetAtIndex(index); if (widget && (!this.highlightedMode || !widget.highlighted)) { this.highlightedMode = true; widget.highlighted = true; this.dashboardWidgets.forEach((dashboardWidget) => { if (dashboardWidget !== widget) { dashboardWidget.highlighted = false; } }); return widget; } else { return null; } } selectWidget(index: number): DashboardWidget { const widget = this.findWidgetAtIndex(index); if (widget && (!widget.selected)) { widget.selected = true; this.dashboardWidgets.forEach((dashboardWidget) => { if (dashboardWidget !== widget) { dashboardWidget.selected = false; } }); return widget; } else { return null; } } resetHighlight(): DashboardWidget { const highlighted = this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.highlighted); this.highlightedMode = false; this.dashboardWidgets.forEach((dashboardWidget) => { dashboardWidget.highlighted = false; dashboardWidget.selected = false; }); return highlighted; } isHighlighted(widget: DashboardWidget): boolean { return (this.highlightedMode && widget.highlighted) || (widget.selected); } isNotHighlighted(widget: DashboardWidget): boolean { return this.highlightedMode && !widget.highlighted; } getSelectedWidget(): DashboardWidget { return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.selected); } private findWidgetAtIndex(index: number): DashboardWidget { return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetIndex === index); } private updateRowsAndSort() { let maxRows = this.dashboard.gridsterOpts.maxRows; this.dashboardWidgets.forEach((dashboardWidget) => { const bottom = dashboardWidget.y + dashboardWidget.rows; maxRows = Math.max(maxRows, bottom); }); this.sortWidgets(); this.dashboard.gridsterOpts.maxRows = maxRows; } sortWidgets() { this.dashboardWidgets.sort((widget1, widget2) => { const row1 = widget1.widgetOrder; const row2 = widget2.widgetOrder; let res = row1 - row2; if (res === 0) { res = widget1.x - widget2.x; } return res; }); } } export class DashboardWidget implements GridsterItem { highlighted = false; selected = false; isFullscreen = false; color: string; backgroundColor: string; padding: string; margin: string; title: string; showTitle: boolean; titleStyle: {[klass: string]: any}; titleIcon: string; showTitleIcon: boolean; titleIconStyle: {[klass: string]: any}; dropShadow: boolean; enableFullscreen: boolean; hasTimewindow: boolean; hasAggregation: boolean; style: {[klass: string]: any}; hasWidgetTitleTemplate: boolean; widgetTitleTemplate: string; showWidgetTitlePanel: boolean; showWidgetActions: boolean; customHeaderActions: Array; widgetActions: Array; widgetContext: WidgetContext = {}; private gridsterItemComponentSubject = new Subject(); private gridsterItemComponentValue: GridsterItemComponentInterface; set gridsterItemComponent(item: GridsterItemComponentInterface) { this.gridsterItemComponentValue = item; this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue); this.gridsterItemComponentSubject.complete(); } constructor( private dashboard: IDashboardComponent, public widget: Widget, public widgetIndex: number, public widgetLayout?: WidgetLayout) { this.updateWidgetParams(); } gridsterItemComponent$(): Observable { if (this.gridsterItemComponentValue) { return of(this.gridsterItemComponentValue); } else { return this.gridsterItemComponentSubject.asObservable(); } } updateWidgetParams() { this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)'; this.backgroundColor = this.widget.config.backgroundColor || '#fff'; this.padding = this.widget.config.padding || '8px'; this.margin = this.widget.config.margin || '0px'; this.title = isDefined(this.widgetContext.widgetTitle) && this.widgetContext.widgetTitle.length ? this.widgetContext.widgetTitle : this.widget.config.title; this.showTitle = isDefined(this.widget.config.showTitle) ? this.widget.config.showTitle : true; this.titleStyle = this.widget.config.titleStyle ? this.widget.config.titleStyle : {}; this.titleIcon = isDefined(this.widget.config.titleIcon) ? this.widget.config.titleIcon : ''; this.showTitleIcon = isDefined(this.widget.config.showTitleIcon) ? this.widget.config.showTitleIcon : false; this.titleIconStyle = {}; if (this.widget.config.iconColor) { this.titleIconStyle.color = this.widget.config.iconColor; } if (this.widget.config.iconSize) { this.titleIconStyle.fontSize = this.widget.config.iconSize; } this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true; this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true; this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ? (isDefined(this.widget.config.useDashboardTimewindow) ? (!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow) || this.widget.config.displayTimewindow)) : false) : false; this.hasAggregation = this.widget.type === widgetType.timeseries; this.style = {cursor: 'pointer', color: this.color, backgroundColor: this.backgroundColor, padding: this.padding, margin: this.margin}; if (this.widget.config.widgetStyle) { this.style = {...this.widget.config.widgetStyle, ...this.style}; } this.hasWidgetTitleTemplate = this.widgetContext.widgetTitleTemplate ? true : false; this.widgetTitleTemplate = this.widgetContext.widgetTitleTemplate ? this.widgetContext.widgetTitleTemplate : ''; this.showWidgetTitlePanel = this.widgetContext.hideTitlePanel ? false : this.hasWidgetTitleTemplate || this.showTitle || this.hasTimewindow; this.showWidgetActions = this.widgetContext.hideTitlePanel ? false : true; this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; } get x(): number { let res; if (this.widgetLayout) { res = this.widgetLayout.col; } else { res = this.widget.col; } return Math.floor(res); } set x(x: number) { if (!this.dashboard.isMobileSize) { if (this.widgetLayout) { this.widgetLayout.col = x; } else { this.widget.col = x; } } } get y(): number { let res; if (this.widgetLayout) { res = this.widgetLayout.row; } else { res = this.widget.row; } return Math.floor(res); } set y(y: number) { if (!this.dashboard.isMobileSize) { if (this.widgetLayout) { this.widgetLayout.row = y; } else { this.widget.row = y; } } } get cols(): number { let res; if (this.widgetLayout) { res = this.widgetLayout.sizeX; } else { res = this.widget.sizeX; } return Math.floor(res); } set cols(cols: number) { if (!this.dashboard.isMobileSize) { if (this.widgetLayout) { this.widgetLayout.sizeX = cols; } else { this.widget.sizeX = cols; } } } get rows(): number { let res; if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) { let mobileHeight; if (this.widgetLayout) { mobileHeight = this.widgetLayout.mobileHeight; } if (!mobileHeight && this.widget.config.mobileHeight) { mobileHeight = this.widget.config.mobileHeight; } if (mobileHeight) { res = mobileHeight; } else { res = this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols; } } else { if (this.widgetLayout) { res = this.widgetLayout.sizeY; } else { res = this.widget.sizeY; } } return Math.floor(res); } set rows(rows: number) { if (!this.dashboard.isMobileSize && !this.dashboard.autofillHeight) { if (this.widgetLayout) { this.widgetLayout.sizeY = rows; } else { this.widget.sizeY = rows; } } } get widgetOrder(): number { let order; if (this.widgetLayout && isDefined(this.widgetLayout.mobileOrder) && this.widgetLayout.mobileOrder >= 0) { order = this.widgetLayout.mobileOrder; } else if (isDefined(this.widget.config.mobileOrder) && this.widget.config.mobileOrder >= 0) { order = this.widget.config.mobileOrder; } else if (this.widgetLayout) { order = this.widgetLayout.row; } else { order = this.widget.row; } return order; } }