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 c050b524cf..e5341f241b 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 @@ -64,101 +64,20 @@
-
-
-
- - {{widget.titleIcon}} - {{widget.customTranslatedTitle}} - - - -
-
- - - - - - -
-
-
- - -
-
+ +
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 5414291787..9d9057b77d 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 @@ -48,105 +48,6 @@ } } -tb-widget.tb-widget { - position: relative; - height: 100%; - margin: 0; - overflow: hidden; - outline: none; - - transition: all .2s ease-in-out; -} - -div.tb-widget { - position: relative; - height: 100%; - margin: 0; - overflow: hidden; - outline: none; - - transition: all .2s ease-in-out; - - .tb-widget-title { - max-height: 65px; - padding-top: 5px; - padding-left: 5px; - overflow: hidden; - - tb-timewindow { - font-size: 14px; - opacity: .85; - margin: 0; - } - - .title { - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - line-height: 24px; - letter-spacing: .01em; - margin: 0; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - - &.single-row{ - -webkit-line-clamp: 1; - } - } - } - - .tb-widget-actions { - z-index: 19; - margin: 5px 0 0; - - &-absolute { - position: absolute; - top: 3px; - right: 8px; - z-index: 150; - } - - button.mat-icon-button { - width: 32px; - min-width: 32px; - height: 32px; - min-height: 32px; - padding: 0 !important; - margin: 0 !important; - line-height: 20px; - - mat-icon { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - font-size: 20px; - line-height: 20px; - } - } - } - - .tb-widget-content { - &.tb-no-interaction { - pointer-events: none; - } - tb-widget { - position: relative; - width: 100%; - } - } - - &.tb-highlighted { - border: 1px solid #039be5; - box-shadow: 0 0 20px #039be5; - } - - &.tb-not-highlighted { - 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 c871587ba2..72c242c308 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 @@ -55,6 +55,7 @@ import { SafeStyle } from '@angular/platform-browser'; import { distinct } from 'rxjs/operators'; import { ResizeObserver } from '@juggle/resize-observer'; import { UtilsService } from '@core/services/utils.service'; +import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component'; @Component({ selector: 'tb-dashboard', @@ -348,7 +349,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { + private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget); if (items && items.length) { @@ -363,23 +364,47 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) { + onWidgetFullscreenChanged(expanded: boolean) { this.isWidgetExpanded = expanded; } - widgetMouseDown($event: Event, widget: DashboardWidget) { + onWidgetComponentAction(action: WidgetComponentAction, widget: DashboardWidget) { + const $event = action.event; + switch (action.actionType) { + case WidgetComponentActionType.MOUSE_DOWN: + this.widgetMouseDown($event, widget); + break; + case WidgetComponentActionType.CLICKED: + this.widgetClicked($event, widget); + break; + case WidgetComponentActionType.CONTEXT_MENU: + this.openWidgetContextMenu($event, widget); + break; + case WidgetComponentActionType.EDIT: + this.editWidget($event, widget); + break; + case WidgetComponentActionType.EXPORT: + this.exportWidget($event, widget); + break; + case WidgetComponentActionType.REMOVE: + this.removeWidget($event, widget); + break; + } + } + + private widgetMouseDown($event: Event, widget: DashboardWidget) { if (this.callbacks && this.callbacks.onWidgetMouseDown) { this.callbacks.onWidgetMouseDown($event, widget.widget); } } - widgetClicked($event: Event, widget: DashboardWidget) { + private widgetClicked($event: Event, widget: DashboardWidget) { if (this.callbacks && this.callbacks.onWidgetClicked) { this.callbacks.onWidgetClicked($event, widget.widget); } } - editWidget($event: Event, widget: DashboardWidget) { + private editWidget($event: Event, widget: DashboardWidget) { if ($event) { $event.stopPropagation(); } @@ -388,7 +413,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - exportWidget($event: Event, widget: DashboardWidget) { + private exportWidget($event: Event, widget: DashboardWidget) { if ($event) { $event.stopPropagation(); } @@ -397,7 +422,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - removeWidget($event: Event, widget: DashboardWidget) { + private removeWidget($event: Event, widget: DashboardWidget) { if ($event) { $event.stopPropagation(); } @@ -454,14 +479,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - 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) => { @@ -534,10 +551,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo this.updateWidgetLayouts(); } - public detectChanges() { - this.cd.detectChanges(); - } - private detectRowSize(isMobile: boolean, autofillHeight: boolean, parentHeight?: number): number | null { let rowHeight = null; if (!autofillHeight) { diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.ts b/ui-ngx/src/app/modules/home/components/details-panel.component.ts index 26391db994..edb1d3c405 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.ts @@ -14,18 +14,19 @@ /// limitations under the License. /// -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs'; @Component({ selector: 'tb-details-panel', templateUrl: './details-panel.component.html', styleUrls: ['./details-panel.component.scss'] }) -export class DetailsPanelComponent extends PageComponent { +export class DetailsPanelComponent extends PageComponent implements OnDestroy { @Input() headerHeightPx = 100; @Input() headerTitle = ''; @@ -35,11 +36,21 @@ export class DetailsPanelComponent extends PageComponent { @Input() isShowSearch = false; @Input() backgroundColor = '#FFF'; - theFormValue: FormGroup; + private theFormValue: FormGroup; + private formSubscription: Subscription = null; @Input() set theForm(value: FormGroup) { - this.theFormValue = value; + if (this.theFormValue !== value) { + if (this.formSubscription !== null) { + this.formSubscription.unsubscribe(); + this.formSubscription = null; + } + this.theFormValue = value; + if (this.theFormValue !== null) { + this.formSubscription = this.theFormValue.valueChanges.subscribe(() => this.cd.detectChanges()); + } + } } get theForm(): FormGroup { @@ -72,10 +83,18 @@ export class DetailsPanelComponent extends PageComponent { } - constructor(protected store: Store) { + constructor(protected store: Store, + private cd: ChangeDetectorRef) { super(store); } + ngOnDestroy() { + if (this.formSubscription !== null) { + this.formSubscription.unsubscribe(); + } + super.ngOnDestroy(); + } + onCloseDetails() { this.closeDetails.emit(); } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 98525ffa90..3958830088 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -142,6 +142,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag import { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component'; import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component'; import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; +import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; @NgModule({ declarations: @@ -172,6 +173,7 @@ import { DashboardImageDialogComponent } from '@home/components/dashboard-page/d EntityAliasesDialogComponent, EntityAliasDialogComponent, DashboardComponent, + WidgetContainerComponent, WidgetComponent, LegendComponent, WidgetConfigComponent, @@ -290,6 +292,7 @@ import { DashboardImageDialogComponent } from '@home/components/dashboard-page/d EntityAliasesDialogComponent, EntityAliasDialogComponent, DashboardComponent, + WidgetContainerComponent, WidgetComponent, LegendComponent, WidgetConfigComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html new file mode 100644 index 0000000000..09f3199b06 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -0,0 +1,112 @@ + +
+
+
+ + {{widget.titleIcon}} + {{widget.customTranslatedTitle}} + + + +
+
+ + + + + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss new file mode 100644 index 0000000000..ed66a2b235 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2021 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. + */ +:host { + +} + +tb-widget.tb-widget { + position: relative; + height: 100%; + margin: 0; + overflow: hidden; + outline: none; + + transition: all .2s ease-in-out; +} + +div.tb-widget { + position: relative; + height: 100%; + margin: 0; + overflow: hidden; + outline: none; + + transition: all .2s ease-in-out; + + .tb-widget-title { + max-height: 65px; + padding-top: 5px; + padding-left: 5px; + overflow: hidden; + + tb-timewindow { + font-size: 14px; + opacity: .85; + margin: 0; + } + + .title { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + line-height: 24px; + letter-spacing: .01em; + margin: 0; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &.single-row{ + -webkit-line-clamp: 1; + } + } + } + + .tb-widget-actions { + z-index: 19; + margin: 5px 0 0; + + &-absolute { + position: absolute; + top: 3px; + right: 8px; + z-index: 150; + } + + button.mat-icon-button { + width: 32px; + min-width: 32px; + height: 32px; + min-height: 32px; + padding: 0 !important; + margin: 0 !important; + line-height: 20px; + + mat-icon { + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + font-size: 20px; + line-height: 20px; + } + } + } + + .tb-widget-content { + &.tb-no-interaction { + pointer-events: none; + } + tb-widget { + position: relative; + width: 100%; + } + } + + &.tb-highlighted { + border: 1px solid #039be5; + box-shadow: 0 0 20px #039be5; + } + + &.tb-not-highlighted { + opacity: .5; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts new file mode 100644 index 0000000000..cd2b322420 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -0,0 +1,153 @@ +/// +/// Copyright © 2016-2021 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 { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnInit, + Output +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { SafeStyle } from '@angular/platform-browser'; + +export enum WidgetComponentActionType { + MOUSE_DOWN, + CLICKED, + CONTEXT_MENU, + EDIT, + EXPORT, + REMOVE +} + +export class WidgetComponentAction { + event: MouseEvent; + actionType: WidgetComponentActionType; +} + +@Component({ + selector: 'tb-widget-container', + templateUrl: './widget-container.component.html', + styleUrls: ['./widget-container.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class WidgetContainerComponent extends PageComponent implements OnInit { + + @Input() + widget: DashboardWidget; + + @Input() + dashboardStyle: {[klass: string]: any}; + + @Input() + backgroundImage: SafeStyle | string; + + @Input() + isEdit: boolean; + + @Input() + isMobile: boolean; + + @Input() + dashboardWidgets: DashboardWidgets; + + @Input() + isEditActionEnabled: boolean; + + @Input() + isExportActionEnabled: boolean; + + @Input() + isRemoveActionEnabled: boolean; + + @Input() + disableWidgetInteraction = false; + + @Output() + widgetFullscreenChanged: EventEmitter = new EventEmitter(); + + @Output() + widgetComponentAction: EventEmitter = new EventEmitter(); + + constructor(protected store: Store, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.widget.widgetContext.containerChangeDetector = this.cd; + } + + isHighlighted(widget: DashboardWidget) { + return this.dashboardWidgets.isHighlighted(widget); + } + + isNotHighlighted(widget: DashboardWidget) { + return this.dashboardWidgets.isNotHighlighted(widget); + } + + onFullscreenChanged(expanded: boolean) { + this.widgetFullscreenChanged.emit(expanded); + } + + onMouseDown(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.MOUSE_DOWN + }); + } + + onClicked(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.CLICKED + }); + } + + onContextMenu(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.CONTEXT_MENU + }); + } + + onEdit(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.EDIT + }); + } + + onExport(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.EXPORT + }); + } + + onRemove(event: MouseEvent) { + this.widgetComponentAction.emit({ + event, + actionType: WidgetComponentActionType.REMOVE + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 4caf5f3edd..fea6e14291 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -468,12 +468,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ); } - private detectChanges(detectDashboardChanges = false) { + private detectChanges(detectContainerChanges = false) { if (!this.destroyed) { try { this.cd.detectChanges(); - if (detectDashboardChanges) { - this.widgetContext.dashboard.detectChanges(); + if (detectContainerChanges) { + this.widgetContext.detectContainerChanges(); } } catch (e) { // console.log(e); @@ -494,7 +494,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } if (!this.widgetContext.inited && this.isReady()) { this.widgetContext.inited = true; - this.widgetContext.dashboard.detectChanges(); + this.widgetContext.detectContainerChanges(); if (this.cafs.init) { this.cafs.init(); this.cafs.init = null; diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 88d09ac855..2edd7f9afa 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -79,7 +79,6 @@ export interface IDashboardComponent { pauseChangeNotifications(); resumeChangeNotifications(); notifyLayoutUpdated(); - detectChanges(); } declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; @@ -287,8 +286,8 @@ export class DashboardWidgets implements Iterable { export class DashboardWidget implements GridsterItem, IDashboardWidget { - highlighted = false; - selected = false; + private highlightedValue = false; + private selectedValue = false; isFullscreen = false; @@ -335,6 +334,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.gridsterItemComponentSubject.complete(); } + get highlighted() { + return this.highlightedValue; + } + + set highlighted(highlighted: boolean) { + if (this.highlightedValue !== highlighted) { + this.highlightedValue = highlighted; + this.widgetContext.detectContainerChanges(); + } + } + + get selected() { + return this.selectedValue; + } + + set selected(selected: boolean) { + if (this.selectedValue !== selected) { + this.selectedValue = selected; + this.widgetContext.detectContainerChanges(); + } + } + constructor( private dashboard: IDashboardComponent, public widget: Widget, @@ -407,7 +428,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; if (detectChanges) { - this.dashboard.detectChanges(); + this.widgetContext.detectContainerChanges(); } } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 64fbe59efa..50466d7600 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -136,6 +136,10 @@ export class WidgetContext { this.changeDetectorValue = cd; } + set containerChangeDetector(cd: ChangeDetectorRef) { + this.containerChangeDetectorValue = cd; + } + get currentUser(): AuthUser { if (this.store) { return getCurrentAuthUser(this.store); @@ -162,6 +166,7 @@ export class WidgetContext { router: Router; private changeDetectorValue: ChangeDetectorRef; + private containerChangeDetectorValue: ChangeDetectorRef; inited = false; destroyed = false; @@ -309,6 +314,16 @@ export class WidgetContext { } } + detectContainerChanges() { + if (!this.destroyed) { + try { + this.containerChangeDetectorValue.detectChanges(); + } catch (e) { + // console.log(e); + } + } + } + updateWidgetParams() { if (!this.destroyed) { setTimeout(() => {