From 3cee41745194f0ba8af6943412fda032357b0330 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 25 Sep 2019 19:37:29 +0300 Subject: [PATCH] UI: Dashboard implementation --- ui-ngx/src/app/app.component.ts | 8 +- .../app/core/services/item-buffer.service.ts | 35 +++ .../src/app/core/settings/settings.utils.ts | 20 +- ui-ngx/src/app/core/utils.ts | 14 +- .../dashboard/dashboard.component.html | 36 +++ .../dashboard/dashboard.component.scss | 4 + .../dashboard/dashboard.component.ts | 179 +++++++---- .../components/details-panel.component.html | 4 +- .../components/details-panel.component.ts | 17 +- .../home/components/home-components.module.ts | 7 +- .../widget/dynamic-widget.component.ts | 2 +- .../widget/widget-component.service.ts | 11 +- .../widget/widget-config.component.html | 43 +++ .../widget/widget-config.component.scss | 32 ++ .../widget/widget-config.component.ts | 282 +++++++++++++++++ .../components/widget/widget.component.ts | 15 +- .../home/models/dashboard-component.models.ts | 284 ++++++++++++++---- .../home/models/widget-component.models.ts | 10 +- .../dashboard/dashboard-page.component.html | 41 ++- .../dashboard/dashboard-page.component.scss | 20 +- .../dashboard/dashboard-page.component.ts | 256 +++++++++++++++- .../pages/dashboard/dashboard-page.models.ts | 19 ++ .../home/pages/dashboard/dashboard.module.ts | 4 +- .../dashboard/edit-widget.component.html | 32 ++ .../pages/dashboard/edit-widget.component.ts | 115 +++++++ .../layout/dashboard-layout.component.html | 2 + .../layout/dashboard-layout.component.ts | 161 +++++++++- .../pages/dashboard/layout/layout.models.ts | 7 + ui-ngx/src/app/shared/models/constants.ts | 8 +- ui-ngx/src/app/shared/models/widget.models.ts | 16 +- .../app/shared/pipe/keyboard-shortcut.pipe.ts | 49 +++ ui-ngx/src/app/shared/shared.module.ts | 5 +- ui-ngx/src/theme.scss | 9 + 33 files changed, 1569 insertions(+), 178 deletions(-) create mode 100644 ui-ngx/src/app/core/services/item-buffer.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts create mode 100644 ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 19a06c3d0b..309e9a2150 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -49,9 +49,13 @@ export class AppComponent implements OnInit { } setupTranslate() { - console.log(`Supported Langs: ${env.supportedLangs}`); + if (!env.production) { + console.log(`Supported Langs: ${env.supportedLangs}`); + } this.translate.addLangs(env.supportedLangs); - console.log(`Default Lang: ${env.defaultLang}`); + if (!env.production) { + console.log(`Default Lang: ${env.defaultLang}`); + } this.translate.setDefaultLang(env.defaultLang); } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts new file mode 100644 index 0000000000..edcca2701b --- /dev/null +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -0,0 +1,35 @@ +/// +/// 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 { Injectable } from '@angular/core'; +import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; + +@Injectable({ + providedIn: 'root' +}) +export class ItemBufferService { + constructor() {} + + public hasWidget(): boolean { + // TODO: + return false; + } + + public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { + // TODO: + return false; + } +} diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts index 1f7a24d7d5..c5798ac793 100644 --- a/ui-ngx/src/app/core/settings/settings.utils.ts +++ b/ui-ngx/src/app/core/settings/settings.utils.ts @@ -14,25 +14,31 @@ /// limitations under the License. /// -import { environment } from '@env/environment'; +import { environment as env } from '@env/environment'; import { TranslateService } from '@ngx-translate/core'; export function updateUserLang(translate: TranslateService, userLang: string) { let targetLang = userLang; - console.log(`User lang: ${targetLang}`); + if (!env.production) { + console.log(`User lang: ${targetLang}`); + } if (!targetLang) { targetLang = translate.getBrowserCultureLang(); - console.log(`Fallback to browser lang: ${targetLang}`); + if (!env.production) { + console.log(`Fallback to browser lang: ${targetLang}`); + } } const detectedSupportedLang = detectSupportedLang(targetLang); - console.log(`Detected supported lang: ${detectedSupportedLang}`); + if (!env.production) { + console.log(`Detected supported lang: ${detectedSupportedLang}`); + } translate.use(detectedSupportedLang); } function detectSupportedLang(targetLang: string): string { const langTag = (targetLang || '').split('-').join('_'); if (langTag.length) { - if (environment.supportedLangs.indexOf(langTag) > -1) { + if (env.supportedLangs.indexOf(langTag) > -1) { return langTag; } else { const parts = langTag.split('_'); @@ -42,7 +48,7 @@ function detectSupportedLang(targetLang: string): string { } else { lang = langTag; } - const foundLangs = environment.supportedLangs.filter( + const foundLangs = env.supportedLangs.filter( (supportedLang: string) => { const supportedLangParts = supportedLang.split('_'); return supportedLangParts[0] === lang; @@ -53,5 +59,5 @@ function detectSupportedLang(targetLang: string): string { } } } - return environment.defaultLang; + return env.defaultLang; } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 6b8846f8d3..192387f853 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -61,11 +61,15 @@ export function animatedScroll(element: HTMLElement, scrollTop: number, delay?: const duration = delay ? delay : 0; const remaining = to - start; const animateScroll = () => { - currentTime += increment; - const val = easeInOut(currentTime, start, remaining, duration); - element.scrollTop = val; - if (currentTime < duration) { - setTimeout(animateScroll, increment); + if (duration === 0) { + element.scrollTop = to; + } else { + currentTime += increment; + const val = easeInOut(currentTime, start, remaining, duration); + element.scrollTop = val; + if (currentTime < duration) { + setTimeout(animateScroll, increment); + } } }; animateScroll(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index 467ce77fb5..135f397b20 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -24,6 +24,42 @@
+
+
+ + +
+ +
+
+
+
+
+ + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss index 642cec4192..b903b6fb2b 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss @@ -123,3 +123,7 @@ div.tb-widget { opacity: .5; } } + +.tb-dashboard-context-menu-items { + min-width: 256px; +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index d1c5fe976e..605b2d43eb 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -17,13 +17,14 @@ import { AfterViewInit, Component, + DoCheck, Input, + IterableDiffers, + KeyValueDiffers, OnChanges, OnInit, - QueryList, SimpleChanges, - ViewChild, - ViewChildren + ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -32,16 +33,15 @@ 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, GridsterItemComponent } from 'angular-gridster2'; +import { GridsterComponent, GridsterConfig } from 'angular-gridster2'; import { DashboardCallbacks, DashboardWidget, + DashboardWidgets, IDashboardComponent, - WidgetsData, - DashboardWidgets + WidgetPosition } from '../../models/dashboard-component.models'; -import { merge, Observable, ReplaySubject, Subject } from 'rxjs'; -import { map, share, tap } from 'rxjs/operators'; +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'; @@ -49,13 +49,14 @@ 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, OnInit, AfterViewInit, OnChanges { +export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, AfterViewInit, OnChanges { authUser: AuthUser; @@ -130,25 +131,38 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo gridsterOpts: GridsterConfig; - highlightedMode = false; - highlightedWidget: DashboardWidget = null; - selectedWidget: DashboardWidget = null; - isWidgetExpanded = false; isMobileSize = false; @ViewChild('gridster', {static: true}) gridster: GridsterComponent; - @ViewChildren(GridsterItemComponent) gridsterItems: QueryList; + @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); + 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 breakpointObserver: BreakpointObserver, + private differs: IterableDiffers, + private kvDiffers: KeyValueDiffers) { super(store); this.authUser = getCurrentAuthUser(store); } @@ -175,7 +189,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo defaultItemRows: 6, resizable: {enabled: this.isEdit}, draggable: {enabled: this.isEdit}, - itemChangeCallback: item => this.dashboardWidgets.sortWidgets() + itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), + itemInitCallback: (item, itemComponent) => { + (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; + } }; this.updateMobileOpts(); @@ -184,12 +201,17 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo .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; @@ -206,6 +228,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo updateEditingOpts = true; } else if (['widgets', 'widgetLayouts'].includes(propName)) { updateWidgets = true; + } else if (propName === 'dashboardTimewindow') { + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); } } } @@ -259,14 +283,34 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - openDashboardContextMenu($event: Event) { - // TODO: - // this.dialogService.todo(); + 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: Event, widget: DashboardWidget) { - // TODO: - // this.dialogService.todo(); + 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) { @@ -275,13 +319,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo widgetMouseDown($event: Event, widget: DashboardWidget) { if (this.callbacks && this.callbacks.onWidgetMouseDown) { - this.callbacks.onWidgetMouseDown($event, widget.widget); + 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); + this.callbacks.onWidgetClicked($event, widget.widget, widget.widgetIndex); } } @@ -290,7 +334,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo $event.stopPropagation(); } if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { - this.callbacks.onEditWidget($event, widget.widget); + this.callbacks.onEditWidget($event, widget.widget, widget.widgetIndex); } } @@ -299,7 +343,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo $event.stopPropagation(); } if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { - this.callbacks.onExportWidget($event, widget.widget); + this.callbacks.onExportWidget($event, widget.widget, widget.widgetIndex); } } @@ -308,56 +352,81 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo $event.stopPropagation(); } if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { - this.callbacks.onRemoveWidget($event, widget.widget).subscribe( - (result) => { - if (result) { - this.dashboardWidgets.removeWidget(widget.widget); - } - } - ); + this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex); } } - highlightWidget(widget: DashboardWidget, delay?: number) { - if (!this.highlightedMode || this.highlightedWidget !== widget) { - this.highlightedMode = true; - this.highlightedWidget = widget; - this.scrollToWidget(widget, delay); + highlightWidget(index: number, delay?: number) { + const highlighted = this.dashboardWidgets.highlightWidget(index); + if (highlighted) { + this.scrollToWidget(highlighted, delay); } } - selectWidget(widget: DashboardWidget, delay?: number) { - if (this.selectedWidget !== widget) { - this.selectedWidget = widget; - this.scrollToWidget(widget, 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() { - this.highlightedMode = false; - this.highlightedWidget = null; - this.selectedWidget = null; + const highlighted = this.dashboardWidgets.resetHighlight(); + if (highlighted) { + setTimeout(() => { + this.scrollToWidget(highlighted, 0); + }, 0); + } } isHighlighted(widget: DashboardWidget) { - return (this.highlightedMode && this.highlightedWidget === widget) || (this.selectedWidget === widget); + return this.dashboardWidgets.isHighlighted(widget); } isNotHighlighted(widget: DashboardWidget) { - return this.highlightedMode && this.highlightedWidget !== widget; + return this.dashboardWidgets.isNotHighlighted(widget); } - scrollToWidget(widget: DashboardWidget, delay?: number) { - if (this.gridsterItems) { - const gridsterItem = this.gridsterItems.find((item => item.item === widget)); - const offset = (this.gridster.curHeight - gridsterItem.height) / 2; - let scrollTop = gridsterItem.top; + 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; } - const parentElement = this.gridster.el as HTMLElement; animatedScroll(parentElement, scrollTop, delay); - } + }); } private updateMobileOpts() { diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index 05f855fc43..4268147c72 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -31,7 +31,7 @@
- -
- - - - + + - + + + +
+
+
+
+ + +
+
+
+