Browse Source

UI: Improve dashboard performance - move widget header to widget container

pull/4630/head
Igor Kulikov 5 years ago
parent
commit
07926350f5
  1. 109
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  2. 99
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss
  3. 51
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  4. 29
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  5. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  6. 112
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  7. 117
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  8. 153
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  9. 8
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  10. 29
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  11. 15
      ui-ngx/src/app/modules/home/models/widget-component.models.ts

109
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -64,101 +64,20 @@
<div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
<gridster #gridster id="gridster-child" [options]="gridsterOpts"> <gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets"> <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets">
<div tb-fullscreen [fullscreen]="widget.isFullscreen" <tb-widget-container
[fullscreenBackgroundStyle]="dashboardStyle" [widget]="widget"
[fullscreenBackgroundImage]="backgroundImage" [dashboardWidgets]="dashboardWidgets"
(fullscreenChanged)="onWidgetFullscreenChanged($event, widget)" [dashboardStyle]="dashboardStyle"
fxLayout="column" [backgroundImage]="backgroundImage"
class="tb-widget" [isEdit]="isEdit"
[ngClass]="{ [isMobile]="isMobile"
'tb-highlighted': isHighlighted(widget), [isEditActionEnabled]="isEditActionEnabled"
'tb-not-highlighted': isNotHighlighted(widget), [isExportActionEnabled]="isExportActionEnabled"
'mat-elevation-z4': widget.dropShadow, [isRemoveActionEnabled]="isRemoveActionEnabled"
'tb-has-timewindow': widget.hasTimewindow [disableWidgetInteraction]="disableWidgetInteraction"
}" (widgetFullscreenChanged)="onWidgetFullscreenChanged($event)"
[ngStyle]="widget.style" (widgetComponentAction)="onWidgetComponentAction($event, widget)">
(mousedown)="widgetMouseDown($event, widget)" </tb-widget-container>
(click)="widgetClicked($event, widget)"
(contextmenu)="openWidgetContextMenu($event, widget)">
<div *ngIf="!!widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
<span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}"
[ngStyle]="widget.titleStyle"
[matTooltip]="widget.titleTooltip"
matTooltipClass="tb-tooltip-multiline"
matTooltipPosition="above"
class="mat-subheading-2 title">
<mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
{{widget.customTranslatedTitle}}
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
timezone="true"
[isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>
<div *ngIf="widget.showWidgetActions"
class="tb-widget-actions"
[ngClass]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.showTitle||widget.hasAggregation))}"
fxLayout="row"
fxLayoutAlign="start center"
(mousedown)="$event.stopPropagation()">
<button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions"
[fxShow]="!isEdit"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<mat-icon>{{ action.icon }}</mat-icon>
</button>
<button mat-button mat-icon-button *ngFor="let action of widget.widgetActions"
[fxShow]="!isEdit && action.show"
(click)="action.onAction($event)"
matTooltip="{{ action.name | translate }}"
matTooltipPosition="above">
<mat-icon>{{ action.icon }}</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="!isEdit && widget.enableFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen"
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="isEditActionEnabled && !widget.isFullscreen"
(click)="editWidget($event, widget)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="isExportActionEnabled && !widget.isFullscreen"
(click)="exportWidget($event, widget)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<mat-icon>file_download</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="isRemoveActionEnabled && !widget.isFullscreen"
(click)="removeWidget($event, widget);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
<tb-widget fxFlex
#widgetComponent
[dashboardWidget]="widget"
[isEdit]="isEdit"
[isMobile]="isMobileSize">
</tb-widget>
</div>
</div>
</gridster-item> </gridster-item>
</gridster> </gridster>
</div> </div>

99
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 { .tb-dashboard-context-menu-items {
min-width: 256px; min-width: 256px;
} }

51
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 { distinct } from 'rxjs/operators';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetComponentAction, WidgetComponentActionType } from '@home/components/widget/widget-container.component';
@Component({ @Component({
selector: 'tb-dashboard', 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) { if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget); const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget);
if (items && items.length) { 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; 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) { if (this.callbacks && this.callbacks.onWidgetMouseDown) {
this.callbacks.onWidgetMouseDown($event, widget.widget); this.callbacks.onWidgetMouseDown($event, widget.widget);
} }
} }
widgetClicked($event: Event, widget: DashboardWidget) { private widgetClicked($event: Event, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.onWidgetClicked) { if (this.callbacks && this.callbacks.onWidgetClicked) {
this.callbacks.onWidgetClicked($event, widget.widget); this.callbacks.onWidgetClicked($event, widget.widget);
} }
} }
editWidget($event: Event, widget: DashboardWidget) { private editWidget($event: Event, widget: DashboardWidget) {
if ($event) { if ($event) {
$event.stopPropagation(); $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) { if ($event) {
$event.stopPropagation(); $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) { if ($event) {
$event.stopPropagation(); $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) { private scrollToWidget(widget: DashboardWidget, delay?: number) {
const parentElement = this.gridster.el as HTMLElement; const parentElement = this.gridster.el as HTMLElement;
widget.gridsterItemComponent$().subscribe((gridsterItem) => { widget.gridsterItemComponent$().subscribe((gridsterItem) => {
@ -534,10 +551,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.updateWidgetLayouts(); this.updateWidgetLayouts();
} }
public detectChanges() {
this.cd.detectChanges();
}
private detectRowSize(isMobile: boolean, autofillHeight: boolean, parentHeight?: number): number | null { private detectRowSize(isMobile: boolean, autofillHeight: boolean, parentHeight?: number): number | null {
let rowHeight = null; let rowHeight = null;
if (!autofillHeight) { if (!autofillHeight) {

29
ui-ngx/src/app/modules/home/components/details-panel.component.ts

@ -14,18 +14,19 @@
/// limitations under the License. /// 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 { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'tb-details-panel', selector: 'tb-details-panel',
templateUrl: './details-panel.component.html', templateUrl: './details-panel.component.html',
styleUrls: ['./details-panel.component.scss'] styleUrls: ['./details-panel.component.scss']
}) })
export class DetailsPanelComponent extends PageComponent { export class DetailsPanelComponent extends PageComponent implements OnDestroy {
@Input() headerHeightPx = 100; @Input() headerHeightPx = 100;
@Input() headerTitle = ''; @Input() headerTitle = '';
@ -35,11 +36,21 @@ export class DetailsPanelComponent extends PageComponent {
@Input() isShowSearch = false; @Input() isShowSearch = false;
@Input() backgroundColor = '#FFF'; @Input() backgroundColor = '#FFF';
theFormValue: FormGroup; private theFormValue: FormGroup;
private formSubscription: Subscription = null;
@Input() @Input()
set theForm(value: FormGroup) { 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 { get theForm(): FormGroup {
@ -72,10 +83,18 @@ export class DetailsPanelComponent extends PageComponent {
} }
constructor(protected store: Store<AppState>) { constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
super(store); super(store);
} }
ngOnDestroy() {
if (this.formSubscription !== null) {
this.formSubscription.unsubscribe();
}
super.ngOnDestroy();
}
onCloseDetails() { onCloseDetails() {
this.closeDetails.emit(); this.closeDetails.emit();
} }

3
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 { SecurityConfigLwm2mComponent } from '@home/components/device/security-config-lwm2m.component';
import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component'; import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component';
import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { WidgetContainerComponent } from '@home/components/widget/widget-container.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -172,6 +173,7 @@ import { DashboardImageDialogComponent } from '@home/components/dashboard-page/d
EntityAliasesDialogComponent, EntityAliasesDialogComponent,
EntityAliasDialogComponent, EntityAliasDialogComponent,
DashboardComponent, DashboardComponent,
WidgetContainerComponent,
WidgetComponent, WidgetComponent,
LegendComponent, LegendComponent,
WidgetConfigComponent, WidgetConfigComponent,
@ -290,6 +292,7 @@ import { DashboardImageDialogComponent } from '@home/components/dashboard-page/d
EntityAliasesDialogComponent, EntityAliasesDialogComponent,
EntityAliasDialogComponent, EntityAliasDialogComponent,
DashboardComponent, DashboardComponent,
WidgetContainerComponent,
WidgetComponent, WidgetComponent,
LegendComponent, LegendComponent,
WidgetConfigComponent, WidgetConfigComponent,

112
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -0,0 +1,112 @@
<!--
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.
-->
<div tb-fullscreen [fullscreen]="widget.isFullscreen"
[fullscreenBackgroundStyle]="dashboardStyle"
[fullscreenBackgroundImage]="backgroundImage"
(fullscreenChanged)="onFullscreenChanged($event)"
fxLayout="column"
class="tb-widget"
[ngClass]="{
'tb-highlighted': isHighlighted(widget),
'tb-not-highlighted': isNotHighlighted(widget),
'mat-elevation-z4': widget.dropShadow,
'tb-has-timewindow': widget.hasTimewindow
}"
[ngStyle]="widget.style"
(mousedown)="onMouseDown($event)"
(click)="onClicked($event)"
(contextmenu)="onContextMenu($event)">
<div *ngIf="!!widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
<span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}"
[ngStyle]="widget.titleStyle"
[matTooltip]="widget.titleTooltip"
matTooltipClass="tb-tooltip-multiline"
matTooltipPosition="above"
class="mat-subheading-2 title">
<mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
{{widget.customTranslatedTitle}}
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
timezone="true"
[isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow"
(ngModelChange)="widgetComponent.onTimewindowChanged($event)">
</tb-timewindow>
</div>
<div *ngIf="widget.showWidgetActions"
class="tb-widget-actions"
[ngClass]="{'tb-widget-actions-absolute': !(widget.showWidgetTitlePanel&&(widget.showTitle||widget.hasAggregation))}"
fxLayout="row"
fxLayoutAlign="start center"
(mousedown)="$event.stopPropagation()">
<button mat-button mat-icon-button *ngFor="let action of widget.customHeaderActions"
[fxShow]="!isEdit"
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<mat-icon>{{ action.icon }}</mat-icon>
</button>
<button mat-button mat-icon-button *ngFor="let action of widget.widgetActions"
[fxShow]="!isEdit && action.show"
(click)="action.onAction($event)"
matTooltip="{{ action.name | translate }}"
matTooltipPosition="above">
<mat-icon>{{ action.icon }}</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="!isEdit && widget.enableFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen"
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="isEditActionEnabled && !widget.isFullscreen"
(click)="onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="isExportActionEnabled && !widget.isFullscreen"
(click)="onExport($event)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<mat-icon>file_download</mat-icon>
</button>
<button mat-button mat-icon-button
[fxShow]="isRemoveActionEnabled && !widget.isFullscreen"
(click)="onRemove($event);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
<div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
<tb-widget fxFlex
#widgetComponent
[dashboardWidget]="widget"
[isEdit]="isEdit"
[isMobile]="isMobile">
</tb-widget>
</div>
</div>

117
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;
}
}

153
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<boolean> = new EventEmitter<boolean>();
@Output()
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
constructor(protected store: Store<AppState>,
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
});
}
}

8
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) { if (!this.destroyed) {
try { try {
this.cd.detectChanges(); this.cd.detectChanges();
if (detectDashboardChanges) { if (detectContainerChanges) {
this.widgetContext.dashboard.detectChanges(); this.widgetContext.detectContainerChanges();
} }
} catch (e) { } catch (e) {
// console.log(e); // console.log(e);
@ -494,7 +494,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
if (!this.widgetContext.inited && this.isReady()) { if (!this.widgetContext.inited && this.isReady()) {
this.widgetContext.inited = true; this.widgetContext.inited = true;
this.widgetContext.dashboard.detectChanges(); this.widgetContext.detectContainerChanges();
if (this.cafs.init) { if (this.cafs.init) {
this.cafs.init(); this.cafs.init();
this.cafs.init = null; this.cafs.init = null;

29
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -79,7 +79,6 @@ export interface IDashboardComponent {
pauseChangeNotifications(); pauseChangeNotifications();
resumeChangeNotifications(); resumeChangeNotifications();
notifyLayoutUpdated(); notifyLayoutUpdated();
detectChanges();
} }
declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update';
@ -287,8 +286,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
export class DashboardWidget implements GridsterItem, IDashboardWidget { export class DashboardWidget implements GridsterItem, IDashboardWidget {
highlighted = false; private highlightedValue = false;
selected = false; private selectedValue = false;
isFullscreen = false; isFullscreen = false;
@ -335,6 +334,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.gridsterItemComponentSubject.complete(); 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( constructor(
private dashboard: IDashboardComponent, private dashboard: IDashboardComponent,
public widget: Widget, public widget: Widget,
@ -407,7 +428,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : []; this.customHeaderActions = this.widgetContext.customHeaderActions ? this.widgetContext.customHeaderActions : [];
this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : []; this.widgetActions = this.widgetContext.widgetActions ? this.widgetContext.widgetActions : [];
if (detectChanges) { if (detectChanges) {
this.dashboard.detectChanges(); this.widgetContext.detectContainerChanges();
} }
} }

15
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -136,6 +136,10 @@ export class WidgetContext {
this.changeDetectorValue = cd; this.changeDetectorValue = cd;
} }
set containerChangeDetector(cd: ChangeDetectorRef) {
this.containerChangeDetectorValue = cd;
}
get currentUser(): AuthUser { get currentUser(): AuthUser {
if (this.store) { if (this.store) {
return getCurrentAuthUser(this.store); return getCurrentAuthUser(this.store);
@ -162,6 +166,7 @@ export class WidgetContext {
router: Router; router: Router;
private changeDetectorValue: ChangeDetectorRef; private changeDetectorValue: ChangeDetectorRef;
private containerChangeDetectorValue: ChangeDetectorRef;
inited = false; inited = false;
destroyed = false; destroyed = false;
@ -309,6 +314,16 @@ export class WidgetContext {
} }
} }
detectContainerChanges() {
if (!this.destroyed) {
try {
this.containerChangeDetectorValue.detectChanges();
} catch (e) {
// console.log(e);
}
}
}
updateWidgetParams() { updateWidgetParams() {
if (!this.destroyed) { if (!this.destroyed) {
setTimeout(() => { setTimeout(() => {

Loading…
Cancel
Save