Browse Source

UI: Dashboard implementation

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
3cee417451
  1. 8
      ui-ngx/src/app/app.component.ts
  2. 35
      ui-ngx/src/app/core/services/item-buffer.service.ts
  3. 20
      ui-ngx/src/app/core/settings/settings.utils.ts
  4. 14
      ui-ngx/src/app/core/utils.ts
  5. 36
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  6. 4
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss
  7. 179
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  8. 4
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  9. 17
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  10. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  11. 2
      ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts
  12. 11
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  13. 43
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  14. 32
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  15. 282
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  16. 15
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  17. 284
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  18. 10
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  19. 41
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  20. 20
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss
  21. 256
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  22. 19
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts
  23. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  24. 32
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html
  25. 115
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts
  26. 2
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html
  27. 161
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts
  28. 7
      ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts
  29. 8
      ui-ngx/src/app/shared/models/constants.ts
  30. 16
      ui-ngx/src/app/shared/models/widget.models.ts
  31. 49
      ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts
  32. 5
      ui-ngx/src/app/shared/shared.module.ts
  33. 9
      ui-ngx/src/theme.scss

8
ui-ngx/src/app/app.component.ts

@ -49,9 +49,13 @@ export class AppComponent implements OnInit {
} }
setupTranslate() { setupTranslate() {
console.log(`Supported Langs: ${env.supportedLangs}`); if (!env.production) {
console.log(`Supported Langs: ${env.supportedLangs}`);
}
this.translate.addLangs(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); this.translate.setDefaultLang(env.defaultLang);
} }

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

20
ui-ngx/src/app/core/settings/settings.utils.ts

@ -14,25 +14,31 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { environment } from '@env/environment'; import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
export function updateUserLang(translate: TranslateService, userLang: string) { export function updateUserLang(translate: TranslateService, userLang: string) {
let targetLang = userLang; let targetLang = userLang;
console.log(`User lang: ${targetLang}`); if (!env.production) {
console.log(`User lang: ${targetLang}`);
}
if (!targetLang) { if (!targetLang) {
targetLang = translate.getBrowserCultureLang(); targetLang = translate.getBrowserCultureLang();
console.log(`Fallback to browser lang: ${targetLang}`); if (!env.production) {
console.log(`Fallback to browser lang: ${targetLang}`);
}
} }
const detectedSupportedLang = detectSupportedLang(targetLang); const detectedSupportedLang = detectSupportedLang(targetLang);
console.log(`Detected supported lang: ${detectedSupportedLang}`); if (!env.production) {
console.log(`Detected supported lang: ${detectedSupportedLang}`);
}
translate.use(detectedSupportedLang); translate.use(detectedSupportedLang);
} }
function detectSupportedLang(targetLang: string): string { function detectSupportedLang(targetLang: string): string {
const langTag = (targetLang || '').split('-').join('_'); const langTag = (targetLang || '').split('-').join('_');
if (langTag.length) { if (langTag.length) {
if (environment.supportedLangs.indexOf(langTag) > -1) { if (env.supportedLangs.indexOf(langTag) > -1) {
return langTag; return langTag;
} else { } else {
const parts = langTag.split('_'); const parts = langTag.split('_');
@ -42,7 +48,7 @@ function detectSupportedLang(targetLang: string): string {
} else { } else {
lang = langTag; lang = langTag;
} }
const foundLangs = environment.supportedLangs.filter( const foundLangs = env.supportedLangs.filter(
(supportedLang: string) => { (supportedLang: string) => {
const supportedLangParts = supportedLang.split('_'); const supportedLangParts = supportedLang.split('_');
return supportedLangParts[0] === lang; return supportedLangParts[0] === lang;
@ -53,5 +59,5 @@ function detectSupportedLang(targetLang: string): string {
} }
} }
} }
return environment.defaultLang; return env.defaultLang;
} }

14
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 duration = delay ? delay : 0;
const remaining = to - start; const remaining = to - start;
const animateScroll = () => { const animateScroll = () => {
currentTime += increment; if (duration === 0) {
const val = easeInOut(currentTime, start, remaining, duration); element.scrollTop = to;
element.scrollTop = val; } else {
if (currentTime < duration) { currentTime += increment;
setTimeout(animateScroll, increment); const val = easeInOut(currentTime, start, remaining, duration);
element.scrollTop = val;
if (currentTime < duration) {
setTimeout(animateScroll, increment);
}
} }
}; };
animateScroll(); animateScroll();

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

@ -24,6 +24,42 @@
<div id="gridster-parent" <div id="gridster-parent"
fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}" fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
(contextmenu)="openDashboardContextMenu($event)"> (contextmenu)="openDashboardContextMenu($event)">
<div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="dashboardMenuPosition.x"
[style.top]="dashboardMenuPosition.y"
[matMenuTriggerFor]="dashboardMenu">
</div>
<mat-menu #dashboardMenu="matMenu">
<ng-template matMenuContent let-items="items">
<div class="tb-dashboard-context-menu-items">
<button mat-menu-item *ngFor="let item of items;"
[disabled]="!item.enabled"
(click)="item.action(dashboardContextMenuEvent)">
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
<span translate>{{item.value}}</span>
</button>
</div>
</ng-template>
</mat-menu>
<div #widgetMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="widgetMenuPosition.x"
[style.top]="widgetMenuPosition.y"
[matMenuTriggerFor]="widgetMenu">
</div>
<mat-menu #widgetMenu="matMenu">
<ng-template matMenuContent let-items="items" let-widget="widget">
<div class="tb-dashboard-context-menu-items">
<button mat-menu-item *ngFor="let item of items;"
[disabled]="!item.enabled"
(click)="item.action(widgetContextMenuEvent, widget)">
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
<span translate>{{item.value}}</span>
</button>
</div>
</ng-template>
</mat-menu>
<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">

4
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss

@ -123,3 +123,7 @@ div.tb-widget {
opacity: .5; opacity: .5;
} }
} }
.tb-dashboard-context-menu-items {
min-width: 256px;
}

179
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -17,13 +17,14 @@
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
DoCheck,
Input, Input,
IterableDiffers,
KeyValueDiffers,
OnChanges, OnChanges,
OnInit, OnInit,
QueryList,
SimpleChanges, SimpleChanges,
ViewChild, ViewChild
ViewChildren
} from '@angular/core'; } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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 { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig } from 'angular-gridster2';
import { import {
DashboardCallbacks, DashboardCallbacks,
DashboardWidget, DashboardWidget,
DashboardWidgets,
IDashboardComponent, IDashboardComponent,
WidgetsData, WidgetPosition
DashboardWidgets
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { merge, Observable, ReplaySubject, Subject } from 'rxjs'; import { ReplaySubject, Subject } from 'rxjs';
import { map, share, tap } from 'rxjs/operators';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; 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 { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { Widget } from '@app/shared/models/widget.models'; import { Widget } from '@app/shared/models/widget.models';
import { MatMenuTrigger } from '@angular/material';
@Component({ @Component({
selector: 'tb-dashboard', selector: 'tb-dashboard',
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'] 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; authUser: AuthUser;
@ -130,25 +131,38 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
gridsterOpts: GridsterConfig; gridsterOpts: GridsterConfig;
highlightedMode = false;
highlightedWidget: DashboardWidget = null;
selectedWidget: DashboardWidget = null;
isWidgetExpanded = false; isWidgetExpanded = false;
isMobileSize = false; isMobileSize = false;
@ViewChild('gridster', {static: true}) gridster: GridsterComponent; @ViewChild('gridster', {static: true}) gridster: GridsterComponent;
@ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>; @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; dashboardLoading = true;
dashboardWidgets = new DashboardWidgets(this); dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((index, item) => {
return item;
}),
this.kvDiffers.find([]).create<string, WidgetLayout>()
);
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private timeService: TimeService, private timeService: TimeService,
private dialogService: DialogService, private dialogService: DialogService,
private breakpointObserver: BreakpointObserver) { private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers) {
super(store); super(store);
this.authUser = getCurrentAuthUser(store); this.authUser = getCurrentAuthUser(store);
} }
@ -175,7 +189,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemRows: 6, defaultItemRows: 6,
resizable: {enabled: this.isEdit}, resizable: {enabled: this.isEdit},
draggable: {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(); this.updateMobileOpts();
@ -184,12 +201,17 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
.observe(MediaBreakpoints['gt-sm']).subscribe( .observe(MediaBreakpoints['gt-sm']).subscribe(
() => { () => {
this.updateMobileOpts(); this.updateMobileOpts();
this.notifyGridsterOptionsChanged();
} }
); );
this.updateWidgets(); this.updateWidgets();
} }
ngDoCheck() {
this.dashboardWidgets.doCheck();
}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
let updateMobileOpts = false; let updateMobileOpts = false;
let updateLayoutOpts = false; let updateLayoutOpts = false;
@ -206,6 +228,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
updateEditingOpts = true; updateEditingOpts = true;
} else if (['widgets', 'widgetLayouts'].includes(propName)) { } else if (['widgets', 'widgetLayouts'].includes(propName)) {
updateWidgets = true; 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) { openDashboardContextMenu($event: MouseEvent) {
// TODO: if (this.callbacks && this.callbacks.prepareDashboardContextMenu) {
// this.dialogService.todo(); 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) { openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
// TODO: if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
// this.dialogService.todo(); 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) { onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) {
@ -275,13 +319,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetMouseDown($event: Event, widget: DashboardWidget) { 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, widget.widgetIndex);
} }
} }
widgetClicked($event: Event, widget: DashboardWidget) { 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, widget.widgetIndex);
} }
} }
@ -290,7 +334,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation(); $event.stopPropagation();
} }
if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { 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(); $event.stopPropagation();
} }
if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { 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(); $event.stopPropagation();
} }
if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) {
this.callbacks.onRemoveWidget($event, widget.widget).subscribe( this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex);
(result) => {
if (result) {
this.dashboardWidgets.removeWidget(widget.widget);
}
}
);
} }
} }
highlightWidget(widget: DashboardWidget, delay?: number) { highlightWidget(index: number, delay?: number) {
if (!this.highlightedMode || this.highlightedWidget !== widget) { const highlighted = this.dashboardWidgets.highlightWidget(index);
this.highlightedMode = true; if (highlighted) {
this.highlightedWidget = widget; this.scrollToWidget(highlighted, delay);
this.scrollToWidget(widget, delay);
} }
} }
selectWidget(widget: DashboardWidget, delay?: number) { selectWidget(index: number, delay?: number) {
if (this.selectedWidget !== widget) { const selected = this.dashboardWidgets.selectWidget(index);
this.selectedWidget = widget; if (selected) {
this.scrollToWidget(widget, delay); 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() { resetHighlight() {
this.highlightedMode = false; const highlighted = this.dashboardWidgets.resetHighlight();
this.highlightedWidget = null; if (highlighted) {
this.selectedWidget = null; setTimeout(() => {
this.scrollToWidget(highlighted, 0);
}, 0);
}
} }
isHighlighted(widget: DashboardWidget) { isHighlighted(widget: DashboardWidget) {
return (this.highlightedMode && this.highlightedWidget === widget) || (this.selectedWidget === widget); return this.dashboardWidgets.isHighlighted(widget);
} }
isNotHighlighted(widget: DashboardWidget) { isNotHighlighted(widget: DashboardWidget) {
return this.highlightedMode && this.highlightedWidget !== widget; return this.dashboardWidgets.isNotHighlighted(widget);
} }
scrollToWidget(widget: DashboardWidget, delay?: number) { private scrollToWidget(widget: DashboardWidget, delay?: number) {
if (this.gridsterItems) { const parentElement = this.gridster.el as HTMLElement;
const gridsterItem = this.gridsterItems.find((item => item.item === widget)); widget.gridsterItemComponent$().subscribe((gridsterItem) => {
const offset = (this.gridster.curHeight - gridsterItem.height) / 2; const gridsterItemElement = gridsterItem.el as HTMLElement;
let scrollTop = gridsterItem.top; const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
let scrollTop;
if (this.isMobileSize) {
scrollTop = gridsterItemElement.offsetTop;
} else {
scrollTop = scrollTop = gridsterItem.top;
}
if (offset > 0) { if (offset > 0) {
scrollTop -= offset; scrollTop -= offset;
} }
const parentElement = this.gridster.el as HTMLElement;
animatedScroll(parentElement, scrollTop, delay); animatedScroll(parentElement, scrollTop, delay);
} });
} }
private updateMobileOpts() { private updateMobileOpts() {

4
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -31,7 +31,7 @@
</button> </button>
</div> </div>
<section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons"> <section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons">
<button [disabled]="(isLoading$ | async) || theForm.invalid || !theForm.dirty" <button [disabled]="(isLoading$ | async) || theForm?.invalid || !theForm?.dirty"
mat-fab mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}" matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -40,7 +40,7 @@
(click)="onApplyDetails()"> (click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon> <mat-icon class="material-icons">done</mat-icon>
</button> </button>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)" <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
mat-fab mat-fab
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
matTooltipPosition="above" matTooltipPosition="above"

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

@ -32,7 +32,18 @@ export class DetailsPanelComponent extends PageComponent {
@Input() headerSubtitle = ''; @Input() headerSubtitle = '';
@Input() isReadOnly = false; @Input() isReadOnly = false;
@Input() isAlwaysEdit = false; @Input() isAlwaysEdit = false;
@Input() theForm: NgForm;
theFormValue: NgForm;
@Input()
set theForm(value: NgForm) {
this.theFormValue = value;
}
get theForm(): NgForm {
return this.theFormValue;
}
@Output() @Output()
closeDetails = new EventEmitter<void>(); closeDetails = new EventEmitter<void>();
@Output() @Output()
@ -47,7 +58,7 @@ export class DetailsPanelComponent extends PageComponent {
@Input() @Input()
get isEdit() { get isEdit() {
return this.isEditValue; return this.isAlwaysEdit || this.isEditValue;
} }
set isEdit(val: boolean) { set isEdit(val: boolean) {
@ -72,7 +83,7 @@ export class DetailsPanelComponent extends PageComponent {
} }
onApplyDetails() { onApplyDetails() {
if (this.theForm.valid) { if (this.theForm && this.theForm.valid) {
this.applyDetails.emit(); this.applyDetails.emit();
} }
} }

7
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -40,6 +40,7 @@ import { WidgetComponentService } from './widget/widget-component.service';
import { LegendComponent } from '@home/components/widget/legend.component'; import { LegendComponent } from '@home/components/widget/legend.component';
import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
@ -76,7 +77,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
DashboardComponent, DashboardComponent,
WidgetComponent, WidgetComponent,
LegendComponent LegendComponent,
WidgetConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -97,7 +99,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
DashboardComponent, DashboardComponent,
WidgetComponent, WidgetComponent,
LegendComponent LegendComponent,
WidgetConfigComponent
], ],
providers: [ providers: [
WidgetComponentService WidgetComponentService

2
ui-ngx/src/app/modules/home/components/widget/dynamic-widget.component.ts

@ -48,7 +48,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
} }
ngOnDestroy(): void { ngOnDestroy(): void {
console.log('Widget component destroyed!');
} }
clearRpcError() { clearRpcError() {

11
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -29,7 +29,7 @@ import {
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; import { Widget, widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models';
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
import { isFunction, isUndefined } from '@core/utils'; import { isFunction, isUndefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -130,6 +130,15 @@ export class WidgetComponentService {
} }
} }
public getInstantWidgetInfo(widget: Widget): WidgetInfo {
const widgetInfo = this.getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType);
if (widgetInfo) {
return widgetInfo;
} else {
return {} as WidgetInfo;
}
}
public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
return this.init().pipe( return this.init().pipe(
mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem)) mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem))

43
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -0,0 +1,43 @@
<!--
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.
-->
<mat-tab-group [ngClass]="{'tb-headless': (widgetType === widgetTypes.static && !displayAdvanced())}"
fxFlex class="tb-widget-config tb-absolute-fill" [selectedIndex]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" [fxShow]="widgetType !== widgetTypes.static">
<div class="mat-content mat-padding" fxLayout="column">
<div [fxShow]="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm"
fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
<div fxLayout="column" fxFlex>
<mat-checkbox fxFlex [(ngModel)]="useDashboardTimewindow" (ngModelChange)="updateModel()">
{{ 'widget-config.use-dashboard-timewindow' | translate }}
</mat-checkbox>
<mat-checkbox [disabled]="useDashboardTimewindow" fxFlex [(ngModel)]="displayTimewindow" (ngModelChange)="updateModel()">
{{ 'widget-config.display-timewindow' | translate }}
</mat-checkbox>
</div>
<section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
<span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow [disabled]="useDashboardTimewindow" asButton="true"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex [(ngModel)]="timewindow" (ngModelChange)="updateModel()"></tb-timewindow>
</section>
</div>
</div>
</mat-tab>
<mat-tab label="{{ 'widget-config.settings' | translate }}">
</mat-tab>
</mat-tab-group>

32
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -0,0 +1,32 @@
/**
* 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.
*/
:host {
.tb-widget-config {
.tb-advanced-widget-config {
height: 100%;
}
}
}
:host ::ng-deep {
.tb-widget-config {
.mat-tab-body.mat-tab-body-active {
.mat-tab-body-content > div {
height: 100%;
}
}
}
}

282
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -0,0 +1,282 @@
///
/// 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 { Component, forwardRef, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
Datasource,
LegendConfig,
WidgetActionDescriptor,
WidgetActionSource, WidgetConfigSettings,
widgetType,
WidgetTypeParameters
} from '@shared/models/widget.models';
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined } from '@app/core/utils';
import { Timewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias } from '@shared/models/alias.models';
@Component({
selector: 'tb-widget-config',
templateUrl: './widget-config.component.html',
styleUrls: ['./widget-config.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => WidgetConfigComponent),
multi: true,
}
]
})
export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
widgetTypes = widgetType;
@Input()
forceExpandDatasources: boolean;
@Input()
isDataEnabled: boolean;
@Input()
widgetType: widgetType;
@Input()
typeParameters: WidgetTypeParameters;
@Input()
actionSources: {[key: string]: WidgetActionSource};
@Input()
aliasController: IAliasController;
@Input()
widgetSettingsSchema: any;
@Input()
dataKeySettingsSchema: any;
@Input()
functionsOnly: boolean;
@Input() disabled: boolean;
selectedTab: number;
title: string;
showTitleIcon: boolean;
titleIcon: string;
iconColor: string;
iconSize: string;
showTitle: boolean;
dropShadow: boolean;
enableFullscreen: boolean;
backgroundColor: string;
color: string;
padding: string;
margin: string;
widgetStyle: string;
titleStyle: string;
units: string;
decimals: number;
useDashboardTimewindow: boolean;
displayTimewindow: boolean;
timewindow: Timewindow;
showLegend: boolean;
legendConfig: LegendConfig;
actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
datasources: Array<Datasource>;
targetDeviceAlias: EntityAlias;
alarmSource: Datasource;
alarmSearchStatus: AlarmSearchStatus;
alarmsPollingInterval: number;
settings: WidgetConfigSettings;
mobileOrder: number;
mobileHeight: number;
emptySettingsSchema = {
type: 'object',
properties: {}
};
emptySettingsGroupInfoes = [];
defaultSettingsForm = [
'*'
];
currentSettingsSchema = deepClone(this.emptySettingsSchema);
currentSettings: WidgetConfigSettings = {};
currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);
currentSettingsForm: any;
private modelValue: WidgetConfigComponentData;
private propagateChange = null;
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value;
if (this.modelValue) {
const config = this.modelValue.config;
const layout = this.modelValue.layout;
if (config) {
this.selectedTab = 0;
this.title = config.title;
this.showTitleIcon = isDefined(config.showTitleIcon) ? config.showTitleIcon : false;
this.titleIcon = isDefined(config.titleIcon) ? config.titleIcon : '';
this.iconColor = isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)';
this.iconSize = isDefined(config.iconSize) ? config.iconSize : '24px';
this.showTitle = config.showTitle;
this.dropShadow = isDefined(config.dropShadow) ? config.dropShadow : true;
this.enableFullscreen = isDefined(config.enableFullscreen) ? config.enableFullscreen : true;
this.backgroundColor = config.backgroundColor;
this.color = config.color;
this.padding = config.padding;
this.margin = config.margin;
this.widgetStyle =
JSON.stringify(isDefined(config.widgetStyle) ? config.widgetStyle : {}, null, 2);
this.titleStyle =
JSON.stringify(isDefined(config.titleStyle) ? config.titleStyle : {
fontSize: '16px',
fontWeight: 400
}, null, 2);
this.units = config.units;
this.decimals = config.decimals;
this.useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true;
this.displayTimewindow = isDefined(config.displayTimewindow) ?
config.displayTimewindow : true;
this.timewindow = config.timewindow;
this.actions = config.actions;
if (!this.actions) {
this.actions = {};
}
if (this.isDataEnabled) {
if (this.widgetType !== widgetType.rpc &&
this.widgetType !== widgetType.alarm &&
this.widgetType !== widgetType.static) {
if (config.datasources) {
this.datasources = config.datasources;
} else {
this.datasources = [];
}
} else if (this.widgetType === widgetType.rpc) {
if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
const aliasId = config.targetDeviceAliasIds[0];
const entityAliases = this.aliasController.getEntityAliases();
if (entityAliases[aliasId]) {
this.targetDeviceAlias = entityAliases[aliasId];
} else {
this.targetDeviceAlias = null;
}
} else {
this.targetDeviceAlias = null;
}
} else if (this.widgetType === widgetType.alarm) {
this.alarmSearchStatus = isDefined(config.alarmSearchStatus) ?
config.alarmSearchStatus : AlarmSearchStatus.ANY;
this.alarmsPollingInterval = isDefined(config.alarmsPollingInterval) ?
config.alarmsPollingInterval : 5;
if (config.alarmSource) {
this.alarmSource = config.alarmSource;
} else {
this.alarmSource = null;
}
}
}
this.settings = config.settings;
this.updateSchemaForm();
if (layout) {
this.mobileOrder = layout.mobileOrder;
this.mobileHeight = layout.mobileHeight;
} else {
this.mobileOrder = undefined;
this.mobileHeight = undefined;
}
}
}
}
private updateSchemaForm() {
if (this.widgetSettingsSchema && this.widgetSettingsSchema.schema) {
this.currentSettingsSchema = this.widgetSettingsSchema.schema;
this.currentSettingsForm = this.widgetSettingsSchema.form || deepClone(this.defaultSettingsForm);
this.currentSettingsGroupInfoes = this.widgetSettingsSchema.groupInfoes;
this.currentSettings = this.settings;
} else {
this.currentSettingsForm = deepClone(this.defaultSettingsForm);
this.currentSettingsSchema = deepClone(this.emptySettingsSchema);
this.currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);
this.currentSettings = {};
}
}
public updateModel() {
if (this.modelValue) {
if (this.modelValue.config) {
const config = this.modelValue.config;
config.useDashboardTimewindow = this.useDashboardTimewindow;
config.displayTimewindow = this.displayTimewindow;
config.timewindow = this.timewindow;
}
this.propagateChange(this.modelValue);
}
}
public displayAdvanced(): boolean {
return this.widgetSettingsSchema && this.widgetSettingsSchema.schema;
}
public validate(c: FormControl) {
return null; /*{
targetDeviceAliasIds: {
valid: false,
},
};*/
}
}

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

@ -16,21 +16,22 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ComponentRef, ComponentRef,
ElementRef, ElementRef,
Injector, Injector,
Input, Input,
NgZone,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
ViewEncapsulation, ViewEncapsulation
ChangeDetectorRef,
ChangeDetectionStrategy, NgZone
} from '@angular/core'; } from '@angular/core';
import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models'; import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models';
import { import {
@ -52,7 +53,7 @@ import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import { isDefined, objToBase64, deepClone } from '@core/utils'; import { deepClone, isDefined, objToBase64 } from '@core/utils';
import { import {
IDynamicWidgetComponent, IDynamicWidgetComponent,
WidgetContext, WidgetContext,
@ -63,7 +64,8 @@ import {
import { import {
IWidgetSubscription, IWidgetSubscription,
StateObject, StateObject,
StateParams, SubscriptionEntityInfo, StateParams,
SubscriptionEntityInfo,
SubscriptionInfo, SubscriptionInfo,
WidgetSubscriptionContext, WidgetSubscriptionContext,
WidgetSubscriptionOptions WidgetSubscriptionOptions
@ -86,7 +88,6 @@ import { DashboardService } from '@core/http/dashboard.service';
import { DatasourceService } from '@core/api/datasource.service'; import { DatasourceService } from '@core/api/datasource.service';
import { WidgetSubscription } from '@core/api/widget-subscription'; import { WidgetSubscription } from '@core/api/widget-subscription';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { TimewindowComponent } from '@shared/components/time/timewindow.component';
@Component({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -362,10 +363,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const change = changes[propName]; const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) { if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'isEdit') { if (propName === 'isEdit') {
console.log(`isEdit changed: ${this.isEdit}`);
this.onEditModeChanged(); this.onEditModeChanged();
} else if (propName === 'isMobile') { } else if (propName === 'isMobile') {
console.log(`isMobile changed: ${this.isMobile}`);
this.onMobileModeChanged(); this.onMobileModeChanged();
} }
} }

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

@ -14,30 +14,50 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { GridsterConfig, GridsterItem, GridsterComponent } from 'angular-gridster2'; import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { Widget, widgetType } from '@app/shared/models/widget.models'; import { Widget, widgetType } from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { Observable } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { isDefined, isUndefined } from '@app/core/utils'; import { isDefined, isUndefined } from '@app/core/utils';
import { EventEmitter } from '@angular/core'; import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { EntityId } from '@app/shared/models/id/entity-id'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { IAliasController, IStateController, TimewindowFunctions } from '@app/core/api/widget-api.models'; import * as deepEqual from 'deep-equal';
export interface WidgetsData { export interface WidgetsData {
widgets: Array<Widget>; widgets: Array<Widget>;
widgetLayouts?: WidgetLayouts; 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 { export interface DashboardCallbacks {
onEditWidget?: ($event: Event, widget: Widget) => void; onEditWidget?: ($event: Event, widget: Widget, index: number) => void;
onExportWidget?: ($event: Event, widget: Widget) => void; onExportWidget?: ($event: Event, widget: Widget, index: number) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>; onRemoveWidget?: ($event: Event, widget: Widget, index: number) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void; onWidgetMouseDown?: ($event: Event, widget: Widget, index: number) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void; onWidgetClicked?: ($event: Event, widget: Widget, index: number) => void;
prepareDashboardContextMenu?: ($event: Event) => void; prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => void; prepareWidgetContextMenu?: ($event: Event, widget: Widget, index: number) => Array<WidgetContextMenuItem>;
}
export interface WidgetPosition {
row: number;
column: number;
} }
export interface IDashboardComponent { export interface IDashboardComponent {
@ -53,60 +73,181 @@ export interface IDashboardComponent {
stateController: IStateController; stateController: IStateController;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void;
onResetTimewindow(): 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<DashboardWidget> { export class DashboardWidgets implements Iterable<DashboardWidget> {
highlightedMode = false;
dashboardWidgets: Array<DashboardWidget> = []; dashboardWidgets: Array<DashboardWidget> = [];
widgets: Array<Widget>;
widgetLayouts: WidgetLayouts;
[Symbol.iterator](): Iterator<DashboardWidget> { [Symbol.iterator](): Iterator<DashboardWidget> {
return this.dashboardWidgets[Symbol.iterator](); return this.dashboardWidgets[Symbol.iterator]();
} }
constructor(private dashboard: IDashboardComponent) { constructor(private dashboard: IDashboardComponent,
private widgetsDiffer: IterableDiffer<Widget>,
private widgetLayoutsDiffer: KeyValueDiffer<string, WidgetLayout>) {
} }
setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) { doCheck() {
let maxRows = this.dashboard.gridsterOpts.maxRows; const widgetChange = this.widgetsDiffer.diff(this.widgets);
this.dashboardWidgets.length = 0; if (widgetChange !== null) {
widgets.forEach((widget) => {
let widgetLayout: WidgetLayout; const layouts: WidgetLayouts = {};
if (widgetLayouts && widget.id) { const updateRecords: Array<DashboardWidgetUpdateRecord> = [];
widgetLayout = widgetLayouts[widget.id];
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;
});
} }
const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout); widgetChange.forEachAddedItem((added) => {
const bottom = dashboardWidget.y + dashboardWidget.rows; updateRecords.push({
maxRows = Math.max(maxRows, bottom); widget: added.item,
this.dashboardWidgets.push(dashboardWidget); widgetLayout: layouts[added.item.id],
}); widgetIndex: added.currentIndex,
this.sortWidgets(); operation: 'add'
this.dashboard.gridsterOpts.maxRows = maxRows; });
});
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();
}
}
} }
addWidget(widget: Widget, widgetLayout: WidgetLayout) { setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout); this.highlightedMode = false;
let maxRows = this.dashboard.gridsterOpts.maxRows; this.widgets = widgets;
const bottom = dashboardWidget.y + dashboardWidget.rows; this.widgetLayouts = widgetLayouts;
maxRows = Math.max(maxRows, bottom);
this.dashboardWidgets.push(dashboardWidget);
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
} }
removeWidget(widget: Widget): boolean { highlightWidget(index: number): DashboardWidget {
const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widget === widget); const widget = this.findWidgetAtIndex(index);
if (index > -1) { if (widget && (!this.highlightedMode || !widget.highlighted)) {
this.dashboardWidgets.splice(index, 1); this.highlightedMode = true;
let maxRows = this.dashboard.gridsterOpts.maxRows; widget.highlighted = true;
this.dashboardWidgets.forEach((dashboardWidget) => { this.dashboardWidgets.forEach((dashboardWidget) => {
const bottom = dashboardWidget.y + dashboardWidget.rows; if (dashboardWidget !== widget) {
maxRows = Math.max(maxRows, bottom); dashboardWidget.highlighted = false;
}
}); });
this.sortWidgets(); return widget;
this.dashboard.gridsterOpts.maxRows = maxRows; } else {
return true; return null;
} }
return false; }
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() { sortWidgets() {
@ -125,6 +266,9 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
export class DashboardWidget implements GridsterItem { export class DashboardWidget implements GridsterItem {
highlighted = false;
selected = false;
isFullscreen = false; isFullscreen = false;
color: string; color: string;
@ -160,13 +304,31 @@ export class DashboardWidget implements GridsterItem {
widgetContext: WidgetContext = {}; widgetContext: WidgetContext = {};
private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
private gridsterItemComponentValue: GridsterItemComponentInterface;
set gridsterItemComponent(item: GridsterItemComponentInterface) {
this.gridsterItemComponentValue = item;
this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue);
this.gridsterItemComponentSubject.complete();
}
constructor( constructor(
private dashboard: IDashboardComponent, private dashboard: IDashboardComponent,
public widget: Widget, public widget: Widget,
private widgetLayout?: WidgetLayout) { public widgetIndex: number,
public widgetLayout?: WidgetLayout) {
this.updateWidgetParams(); this.updateWidgetParams();
} }
gridsterItemComponent$(): Observable<GridsterItemComponentInterface> {
if (this.gridsterItemComponentValue) {
return of(this.gridsterItemComponentValue);
} else {
return this.gridsterItemComponentSubject.asObservable();
}
}
updateWidgetParams() { updateWidgetParams() {
this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)'; this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)';
this.backgroundColor = this.widget.config.backgroundColor || '#fff'; this.backgroundColor = this.widget.config.backgroundColor || '#fff';
@ -221,11 +383,13 @@ export class DashboardWidget implements GridsterItem {
} }
get x(): number { get x(): number {
let res;
if (this.widgetLayout) { if (this.widgetLayout) {
return this.widgetLayout.col; res = this.widgetLayout.col;
} else { } else {
return this.widget.col; res = this.widget.col;
} }
return Math.floor(res);
} }
set x(x: number) { set x(x: number) {
@ -239,11 +403,13 @@ export class DashboardWidget implements GridsterItem {
} }
get y(): number { get y(): number {
let res;
if (this.widgetLayout) { if (this.widgetLayout) {
return this.widgetLayout.row; res = this.widgetLayout.row;
} else { } else {
return this.widget.row; res = this.widget.row;
} }
return Math.floor(res);
} }
set y(y: number) { set y(y: number) {
@ -257,11 +423,13 @@ export class DashboardWidget implements GridsterItem {
} }
get cols(): number { get cols(): number {
let res;
if (this.widgetLayout) { if (this.widgetLayout) {
return this.widgetLayout.sizeX; res = this.widgetLayout.sizeX;
} else { } else {
return this.widget.sizeX; res = this.widget.sizeX;
} }
return Math.floor(res);
} }
set cols(cols: number) { set cols(cols: number) {
@ -275,6 +443,7 @@ export class DashboardWidget implements GridsterItem {
} }
get rows(): number { get rows(): number {
let res;
if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) { if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) {
let mobileHeight; let mobileHeight;
if (this.widgetLayout) { if (this.widgetLayout) {
@ -284,17 +453,18 @@ export class DashboardWidget implements GridsterItem {
mobileHeight = this.widget.config.mobileHeight; mobileHeight = this.widget.config.mobileHeight;
} }
if (mobileHeight) { if (mobileHeight) {
return mobileHeight; res = mobileHeight;
} else { } else {
return this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols; res = this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
} }
} else { } else {
if (this.widgetLayout) { if (this.widgetLayout) {
return this.widgetLayout.sizeY; res = this.widgetLayout.sizeY;
} else { } else {
return this.widget.sizeY; res = this.widget.sizeY;
} }
} }
return Math.floor(res);
} }
set rows(rows: number) { set rows(rows: number) {

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

@ -45,6 +45,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetLayout } from '@shared/models/dashboard.models';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -112,11 +113,16 @@ export interface IDynamicWidgetComponent {
export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
widgetName: string; widgetName: string;
alias: string; alias: string;
typeSettingsSchema?: string; typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string; typeDataKeySettingsSchema?: string | any;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>; componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
} }
export interface WidgetConfigComponentData {
config: WidgetConfig;
layout: WidgetLayout;
}
export const MissingWidgetType: WidgetInfo = { export const MissingWidgetType: WidgetInfo = {
type: widgetType.latest, type: widgetType.latest,
widgetName: 'Widget type not found', widgetName: 'Widget type not found',

41
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -138,13 +138,14 @@
[layoutCtx]="layouts.main.layoutCtx" [layoutCtx]="layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx" [dashboardCtx]="dashboardCtx"
[isEdit]="isEdit" [isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode" [isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"> [widgetEditMode]="widgetEditMode">
</tb-dashboard-layout> </tb-dashboard-layout>
</div> </div>
<mat-sidenav-container *ngIf="layouts.right.show" <mat-drawer-container *ngIf="layouts.right.show"
id="tb-right-layout"> id="tb-right-layout">
<mat-sidenav <mat-drawer
[ngStyle]="{minWidth: rightLayoutWidth(), [ngStyle]="{minWidth: rightLayoutWidth(),
maxWidth: rightLayoutWidth(), maxWidth: rightLayoutWidth(),
height: rightLayoutHeight(), height: rightLayoutHeight(),
@ -157,13 +158,43 @@
[layoutCtx]="layouts.right.layoutCtx" [layoutCtx]="layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx" [dashboardCtx]="dashboardCtx"
[isEdit]="isEdit" [isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode" [isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"> [widgetEditMode]="widgetEditMode">
</tb-dashboard-layout> </tb-dashboard-layout>
</mat-sidenav> </mat-drawer>
</mat-sidenav-container> </mat-drawer-container>
</div> </div>
<!--tb-details-sidenav TODO --> <mat-drawer-container hasBackdrop="false" class="tb-widget-details-sidenav">
<mat-drawer class="tb-details-drawer"
[opened]="isEditingWidget"
mode="over"
position="end">
<tb-details-panel fxFlex
headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="false"
[isAlwaysEdit]="true"
(closeDetails)="onEditWidgetClosed()"
(toggleDetailsEditMode)="onRevertWidgetEdit()"
(applyDetails)="saveWidget()"
[theForm]="widgetForm">
<div class="details-buttons">
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<form #widgetForm="ngForm" [formGroup]="editingWidgetFormGroup">
<tb-edit-widget *ngIf="isEditingWidget"
[dashboard]="dashboard"
[aliasController]="dashboardCtx.aliasController"
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"
[widgetFormGroup]="editingWidgetFormGroup">
</tb-edit-widget>
</form>
</tb-details-panel>
</mat-drawer>
</mat-drawer-container>
<!--tb-details-sidenav TODO --> <!--tb-details-sidenav TODO -->
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode"

20
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss

@ -28,6 +28,7 @@ tb-dashboard-page {
div.tb-dashboard-page { div.tb-dashboard-page {
&.mat-content { &.mat-content {
overflow: hidden;
background-color: #eee; background-color: #eee;
} }
section.tb-dashboard-title { section.tb-dashboard-title {
@ -108,13 +109,30 @@ div.tb-dashboard-page {
z-index: 1; z-index: 1;
}*/ }*/
#tb-right-layout { #tb-right-layout {
mat-sidenav { mat-drawer {
z-index: 1; z-index: 1;
} }
} }
} }
} }
mat-drawer-container.tb-widget-details-sidenav {
position: initial;
mat-drawer.tb-details-drawer {
@media #{$mat-gt-sm} {
width: 85% !important;
}
@media #{$mat-gt-md} {
width: 75% !important;
}
@media #{$mat-gt-xl} {
width: 60% !important;
}
}
}
section.tb-powered-by-footer { section.tb-powered-by-footer {
position: absolute; position: absolute;
right: 25px; right: 25px;

256
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation, ViewChild, NgZone } 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';
@ -41,17 +41,25 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Widget } from '@app/shared/models/widget.models'; import { Widget, widgetTypesData } from '@app/shared/models/widget.models';
import { environment as env } from '@env/environment'; import { environment as env } from '@env/environment';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller'; import { AliasController } from '@core/api/alias-controller';
import { Subscription } from 'rxjs'; import { Observable, Subscription, of } from 'rxjs';
import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
import { IStateController } from '@core/api/widget-api.models'; import { IStateController } from '@core/api/widget-api.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import {
WidgetContextMenuItem,
DashboardContextMenuItem,
IDashboardComponent, WidgetPosition
} from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service';
@Component({ @Component({
selector: 'tb-dashboard-page', selector: 'tb-dashboard-page',
@ -89,6 +97,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
editingWidgetLayoutOriginal: WidgetLayout = null; editingWidgetLayoutOriginal: WidgetLayout = null;
editingWidgetSubtitle: string = null; editingWidgetSubtitle: string = null;
editingLayoutCtx: DashboardPageLayoutContext = null; editingLayoutCtx: DashboardPageLayoutContext = null;
editingWidgetFormGroup: FormGroup;
thingsboardVersion: string = env.tbVersion; thingsboardVersion: string = env.tbVersion;
@ -105,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: false, ignoreLoading: false,
ctrl: null ctrl: null,
dashboardCtrl: this
} }
}, },
right: { right: {
@ -116,7 +126,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: false, ignoreLoading: false,
ctrl: null ctrl: null,
dashboardCtrl: this
} }
} }
}; };
@ -175,9 +186,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private authService: AuthService, private authService: AuthService,
private entityService: EntityService, private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private dashboardService: DashboardService) { private widgetComponentService: WidgetComponentService,
private dashboardService: DashboardService,
private itembuffer: ItemBufferService,
private fb: FormBuilder) {
super(store); super(store);
this.editingWidgetFormGroup = this.fb.group({
widgetConfig: [null]
});
this.rxSubscriptions.push(this.route.data.subscribe( this.rxSubscriptions.push(this.route.data.subscribe(
(data) => { (data) => {
this.init(data); this.init(data);
@ -253,6 +271,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.currentDashboardId = null; this.currentDashboardId = null;
this.currentCustomerId = null; this.currentCustomerId = null;
this.currentDashboardScope = null; this.currentDashboardScope = null;
this.dashboardCtx.state = null;
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -428,14 +448,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dialogService.todo(); this.dialogService.todo();
} }
private addWidget($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
private importWidget($event: Event) { private importWidget($event: Event) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -568,7 +580,221 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}; };
this.window.parent.postMessage(JSON.stringify(message), '*'); this.window.parent.postMessage(JSON.stringify(message), '*');
} else { } else {
this.dashboardService.saveDashboard(this.dashboard); this.dashboardService.saveDashboard(this.dashboard).subscribe();
}
}
helpLinkIdForWidgetType(): string {
let link = 'widgetsConfig';
if (this.editingWidget && this.editingWidget.type) {
link = widgetTypesData.get(this.editingWidget.type).configHelpLinkId;
}
return link;
}
addWidget($event: Event, layoutCtx?: DashboardPageLayoutContext) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
onRevertWidgetEdit() {
if (this.editingWidgetFormGroup.dirty) {
this.editingWidgetFormGroup.markAsPristine();
this.editingWidget = deepClone(this.editingWidgetOriginal);
this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
}
}
saveWidget() {
this.editingWidgetFormGroup.markAsPristine();
const widget = deepClone(this.editingWidget);
const widgetLayout = deepClone(this.editingWidgetLayout);
const id = this.editingWidgetOriginal.id;
const index = this.editingLayoutCtx.widgets.indexOf(this.editingWidgetOriginal);
this.dashboardConfiguration.widgets[id] = widget;
this.editingWidgetOriginal = widget;
this.editingWidgetLayoutOriginal = widgetLayout;
this.editingLayoutCtx.widgets[index] = widget;
this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout;
this.editingLayoutCtx.ctrl.highlightWidget(index, 0);
}
onEditWidgetClosed() {
this.editingWidgetOriginal = null;
this.editingWidget = null;
this.editingWidgetLayoutOriginal = null;
this.editingWidgetLayout = null;
this.editingLayoutCtx = null;
this.editingWidgetSubtitle = null;
this.isEditingWidget = false;
this.resetHighlight();
this.forceDashboardMobileMode = false;
}
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
$event.stopPropagation();
if (this.editingWidgetOriginal === widget) {
this.onEditWidgetClosed();
} else {
const transition = !this.forceDashboardMobileMode;
this.editingWidgetOriginal = widget;
this.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id];
this.editingWidget = deepClone(this.editingWidgetOriginal);
this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
this.editingLayoutCtx = layoutCtx;
this.editingWidgetSubtitle = this.widgetComponentService.getInstantWidgetInfo(this.editingWidget).widgetName;
this.forceDashboardMobileMode = true;
this.isEditingWidget = true;
if (layoutCtx) {
const delayOffset = transition ? 350 : 0;
const delay = transition ? 400 : 300;
setTimeout(() => {
layoutCtx.ctrl.highlightWidget(index, delay);
}, delayOffset);
}
}
}
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
// TODO:
this.dialogService.todo();
}
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
// TODO:
this.dialogService.todo();
}
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
// TODO:
this.dialogService.todo();
}
pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
// TODO:
this.dialogService.todo();
}
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
// TODO:
this.dialogService.todo();
}
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
$event.stopPropagation();
// TODO:
this.dialogService.todo();
}
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
if (this.isEditingWidget) {
this.editWidget($event, layoutCtx, widget, index);
}
}
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
if (this.isEdit && !this.isEditingWidget) {
layoutCtx.ctrl.selectWidget(index, 0);
}
}
prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem> {
const dashboardContextActions: Array<DashboardContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
dashboardContextActions.push(
{
action: this.openDashboardSettings.bind(this),
enabled: true,
value: 'dashboard.settings',
icon: 'settings'
}
);
dashboardContextActions.push(
{
action: this.openEntityAliases.bind(this),
enabled: true,
value: 'entity.aliases',
icon: 'devices_other'
}
);
dashboardContextActions.push(
{
action: ($event) => {
layoutCtx.ctrl.pasteWidget($event);
},
enabled: this.itembuffer.hasWidget(),
value: 'action.paste',
icon: 'content_paste',
shortcut: 'M-V'
}
);
dashboardContextActions.push(
{
action: ($event) => {
layoutCtx.ctrl.pasteWidgetReference($event);
},
enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id),
value: 'action.paste-reference',
icon: 'content_paste',
shortcut: 'M-I'
}
);
}
return dashboardContextActions;
}
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem> {
const widgetContextActions: Array<WidgetContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget) {
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.editWidget(event, layoutCtx, currentWidget, index);
},
enabled: true,
value: 'action.edit',
icon: 'edit'
}
);
if (!this.widgetEditMode) {
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.copyWidget(event, layoutCtx, currentWidget);
},
enabled: true,
value: 'action.copy',
icon: 'content_copy',
shortcut: 'M-C'
}
);
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.copyWidgetReference(event, layoutCtx, currentWidget);
},
enabled: true,
value: 'action.copy-reference',
icon: 'content_copy',
shortcut: 'M-R'
}
);
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.removeWidget(event, layoutCtx, currentWidget);
},
enabled: true,
value: 'action.delete',
icon: 'clear',
shortcut: 'M-X'
}
);
}
} }
return widgetContextActions;
} }
} }

19
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts

@ -19,6 +19,12 @@ import { Widget } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models'; import { ILayoutController } from './layout/layout.models';
import {
DashboardContextMenuItem,
WidgetContextMenuItem,
WidgetPosition
} from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs';
export declare type DashboardPageScope = 'tenant' | 'customer'; export declare type DashboardPageScope = 'tenant' | 'customer';
@ -34,6 +40,18 @@ export interface IDashboardController {
dashboardCtx: DashboardContext; dashboardCtx: DashboardContext;
openRightLayout(); openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean); openDashboardState(stateId: string, openRightLayout: boolean);
addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>;
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem>;
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
} }
export interface DashboardPageLayoutContext { export interface DashboardPageLayoutContext {
@ -42,6 +60,7 @@ export interface DashboardPageLayoutContext {
widgetLayouts: WidgetLayouts; widgetLayouts: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;
ctrl: ILayoutController; ctrl: ILayoutController;
dashboardCtrl: IDashboardController;
ignoreLoading: boolean; ignoreLoading: boolean;
} }

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts

@ -28,6 +28,7 @@ import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.com
import { DashboardToolbarComponent } from './dashboard-toolbar.component'; import { DashboardToolbarComponent } from './dashboard-toolbar.component';
import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module';
import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
import { EditWidgetComponent } from './edit-widget.component';
@NgModule({ @NgModule({
entryComponents: [ entryComponents: [
@ -43,7 +44,8 @@ import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
MakeDashboardPublicDialogComponent, MakeDashboardPublicDialogComponent,
DashboardToolbarComponent, DashboardToolbarComponent,
DashboardPageComponent, DashboardPageComponent,
DashboardLayoutComponent DashboardLayoutComponent,
EditWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

32
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.html

@ -0,0 +1,32 @@
<!--
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.
-->
<form [formGroup]="widgetFormGroup">
<fieldset [disabled]="isLoading$ | async">
<tb-widget-config
[widgetType]="widget.type"
[typeParameters]="typeParameters"
[actionSources]="actionSources"
[isDataEnabled]="isDataEnabled"
[widgetSettingsSchema]="settingsSchema"
[dataKeySettingsSchema]="dataKeySettingsSchema"
[aliasController]="aliasController"
[functionsOnly]="widgetEditMode"
formControlName="widgetConfig">
</tb-widget-config>
</fieldset>
</form>

115
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts

@ -0,0 +1,115 @@
///
/// 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 { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router';
import { WidgetService } from '@core/http/widget.service';
import { DialogService } from '@core/services/dialog.service';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { isString } from '@core/utils';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'tb-edit-widget',
templateUrl: './edit-widget.component.html',
styleUrls: []
})
export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges {
@Input()
dashboard: Dashboard;
@Input()
aliasController: IAliasController;
@Input()
widgetEditMode: boolean;
@Input()
widget: Widget;
@Input()
widgetLayout: WidgetLayout;
@Input()
widgetFormGroup: FormGroup;
widgetConfig: WidgetConfigComponentData;
typeParameters: WidgetTypeParameters;
actionSources: {[key: string]: WidgetActionSource};
isDataEnabled: boolean;
settingsSchema: any;
dataKeySettingsSchema: any;
functionsOnly: boolean;
constructor(protected store: Store<AppState>,
private widgetComponentService: WidgetComponentService) {
super(store);
}
ngOnInit(): void {
this.loadWidgetConfig();
}
ngOnChanges(changes: SimpleChanges): void {
let reloadConfig = false;
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['widget', 'widgetLayout'].includes(propName)) {
reloadConfig = true;
}
}
}
if (reloadConfig) {
this.loadWidgetConfig();
}
}
private loadWidgetConfig() {
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
this.widgetConfig = {
config: this.widget.config,
layout: this.widgetLayout
};
const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
this.typeParameters = widgetInfo.typeParameters;
this.actionSources = widgetInfo.actionSources;
this.isDataEnabled = widgetInfo.typeParameters && !widgetInfo.typeParameters.useCustomDatasources;
if (!settingsSchema || settingsSchema === '') {
this.settingsSchema = {};
} else {
this.settingsSchema = isString(settingsSchema) ? JSON.parse(settingsSchema) : settingsSchema;
}
if (!dataKeySettingsSchema || dataKeySettingsSchema === '') {
this.dataKeySettingsSchema = {};
} else {
this.dataKeySettingsSchema = isString(dataKeySettingsSchema) ? JSON.parse(dataKeySettingsSchema) : dataKeySettingsSchema;
}
this.functionsOnly = this.dashboard ? false : true;
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
}
}

2
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html

@ -15,6 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<hotkeys-cheatsheet></hotkeys-cheatsheet>
<div class="mat-content" style="position: relative; width: 100%; height: 100%;" <div class="mat-content" style="position: relative; width: 100%; height: 100%;"
[ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor, [ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
'background-image': layoutCtx.gridSettings.backgroundImageUrl ? 'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
@ -60,6 +61,7 @@
[isEditActionEnabled]="isEdit" [isEditActionEnabled]="isEdit"
[isExportActionEnabled]="isEdit && !widgetEditMode" [isExportActionEnabled]="isEdit && !widgetEditMode"
[isRemoveActionEnabled]="isEdit && !widgetEditMode" [isRemoveActionEnabled]="isEdit && !widgetEditMode"
[callbacks]="this"
[ignoreLoading]="layoutCtx.ignoreLoading"> [ignoreLoading]="layoutCtx.ignoreLoading">
</tb-dashboard> </tb-dashboard>
</div> </div>

161
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts

@ -22,16 +22,25 @@ 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 { Widget } from '@shared/models/widget.models'; import { Widget } from '@shared/models/widget.models';
import { WidgetLayouts } from '@shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { GridsterComponent } from 'angular-gridster2'; import { GridsterComponent } from 'angular-gridster2';
import { IDashboardComponent } from '@home/models/dashboard-component.models'; import {
DashboardCallbacks,
DashboardContextMenuItem,
IDashboardComponent, WidgetContextMenuItem
} from '@home/models/dashboard-component.models';
import { Observable, of, Subscription } from 'rxjs';
import { Hotkey, HotkeysService } from 'angular2-hotkeys';
import { getCurrentIsLoading } from '@core/interceptors/load.selectors';
import { TranslateService } from '@ngx-translate/core';
import { ItemBufferService } from '@app/core/services/item-buffer.service';
@Component({ @Component({
selector: 'tb-dashboard-layout', selector: 'tb-dashboard-layout',
templateUrl: './dashboard-layout.component.html', templateUrl: './dashboard-layout.component.html',
styleUrls: ['./dashboard-layout.component.scss'] styleUrls: ['./dashboard-layout.component.scss']
}) })
export class DashboardLayoutComponent extends PageComponent implements ILayoutController, OnInit, OnDestroy { export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy {
layoutCtxValue: DashboardPageLayoutContext; layoutCtxValue: DashboardPageLayoutContext;
@ -52,6 +61,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
@Input() @Input()
isEdit: boolean; isEdit: boolean;
@Input()
isEditingWidget: boolean;
@Input() @Input()
isMobile: boolean; isMobile: boolean;
@ -60,15 +72,97 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef) { private hotkeysService: HotkeysService,
private translate: TranslateService,
private itembuffer: ItemBufferService) {
super(store); super(store);
} }
ngOnInit(): void { ngOnInit(): void {
this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => {
this.dashboardCtx.dashboardTimewindow = dashboardTimewindow;
}
)
);
this.initHotKeys();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.rxSubscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
this.rxSubscriptions.length = 0;
}
private initHotKeys(): void {
this.hotkeysService.add(
new Hotkey('ctrl+c', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
const widget = this.dashboard.getSelectedWidget();
if (widget) {
event.preventDefault();
this.copyWidget(event, widget);
}
}
return false;
}, null,
this.translate.instant('action.copy'))
);
this.hotkeysService.add(
new Hotkey('ctrl+r', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
const widget = this.dashboard.getSelectedWidget();
if (widget) {
event.preventDefault();
this.copyWidgetReference(event, widget);
}
}
return false;
}, null,
this.translate.instant('action.copy-reference'))
);
this.hotkeysService.add(
new Hotkey('ctrl+v', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.hasWidget()) {
event.preventDefault();
this.pasteWidget(event);
}
}
return false;
}, null,
this.translate.instant('action.paste'))
);
this.hotkeysService.add(
new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.dashboard,
this.dashboardCtx.state, this.layoutCtx.id)) {
event.preventDefault();
this.pasteWidgetReference(event);
}
}
return false;
}, null,
this.translate.instant('action.paste-reference'))
);
this.hotkeysService.add(
new Hotkey('ctrl+x', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
const widget = this.dashboard.getSelectedWidget();
if (widget) {
event.preventDefault();
this.layoutCtx.dashboardCtrl.removeWidget(event, this.layoutCtx, widget);
}
}
return false;
}, null,
this.translate.instant('action.delete'))
);
} }
reload() { reload() {
@ -78,6 +172,65 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
} }
resetHighlight() { resetHighlight() {
this.dashboard.resetHighlight();
}
highlightWidget(index: number, delay?: number) {
this.dashboard.highlightWidget(index, delay);
}
selectWidget(index: number, delay?: number) {
this.dashboard.selectWidget(index, delay);
}
addWidget($event: Event) {
this.layoutCtx.dashboardCtrl.addWidget($event, this.layoutCtx);
}
onEditWidget($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget, index);
}
onExportWidget($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, index);
}
onRemoveWidget($event: Event, widget: Widget, index: number): void {
return this.layoutCtx.dashboardCtrl.removeWidget($event, this.layoutCtx, widget);
}
onWidgetMouseDown($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget, index);
}
onWidgetClicked($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget, index);
}
prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
}
prepareWidgetContextMenu($event: Event, widget: Widget, index: number): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, index);
}
copyWidget($event: Event, widget: Widget) {
this.layoutCtx.dashboardCtrl.copyWidget($event, this.layoutCtx, widget);
}
copyWidgetReference($event: Event, widget: Widget) {
this.layoutCtx.dashboardCtrl.copyWidgetReference($event, this.layoutCtx, widget);
}
pasteWidget($event: Event) {
const pos = this.dashboard.getEventGridPosition($event);
this.layoutCtx.dashboardCtrl.pasteWidget($event, this.layoutCtx, pos);
}
pasteWidgetReference($event: Event) {
const pos = this.dashboard.getEventGridPosition($event);
this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos);
} }
} }

7
ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts

@ -14,8 +14,15 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Widget } from '@shared/models/widget.models';
import { WidgetLayout } from '@shared/models/dashboard.models';
export interface ILayoutController { export interface ILayoutController {
reload(); reload();
setResizing(layoutVisibilityChanged: boolean); setResizing(layoutVisibilityChanged: boolean);
resetHighlight(); resetHighlight();
highlightWidget(index: number, delay?: number);
selectWidget(index: number, delay?: number);
pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent);
} }

8
ui-ngx/src/app/shared/models/constants.ts

@ -65,7 +65,13 @@ export const HelpLinks = {
entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles' widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',
widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration',
widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries',
widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest',
widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc',
widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm',
widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static'
} }
}; };

16
ui-ngx/src/app/shared/models/widget.models.ts

@ -40,6 +40,7 @@ export interface WidgetTypeData {
name: string; name: string;
icon: string; icon: string;
isMdiIcon?: boolean; isMdiIcon?: boolean;
configHelpLinkId: string;
template: WidgetTypeTemplate; template: WidgetTypeTemplate;
} }
@ -50,6 +51,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{ {
name: 'widget.timeseries', name: 'widget.timeseries',
icon: 'timeline', icon: 'timeline',
configHelpLinkId: 'widgetsConfigTimeseries',
template: { template: {
bundleAlias: 'charts', bundleAlias: 'charts',
alias: 'basic_timeseries' alias: 'basic_timeseries'
@ -61,6 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{ {
name: 'widget.latest-values', name: 'widget.latest-values',
icon: 'track_changes', icon: 'track_changes',
configHelpLinkId: 'widgetsConfigLatest',
template: { template: {
bundleAlias: 'cards', bundleAlias: 'cards',
alias: 'attributes_card' alias: 'attributes_card'
@ -72,6 +75,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{ {
name: 'widget.rpc', name: 'widget.rpc',
icon: 'mdi:developer-board', icon: 'mdi:developer-board',
configHelpLinkId: 'widgetsConfigRpc',
isMdiIcon: true, isMdiIcon: true,
template: { template: {
bundleAlias: 'gpio_widgets', bundleAlias: 'gpio_widgets',
@ -84,6 +88,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{ {
name: 'widget.alarm', name: 'widget.alarm',
icon: 'error', icon: 'error',
configHelpLinkId: 'widgetsConfigAlarm',
template: { template: {
bundleAlias: 'alarm_widgets', bundleAlias: 'alarm_widgets',
alias: 'alarms_table' alias: 'alarms_table'
@ -95,6 +100,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{ {
name: 'widget.static', name: 'widget.static',
icon: 'font_download', icon: 'font_download',
configHelpLinkId: 'widgetsConfigStatic',
template: { template: {
bundleAlias: 'cards', bundleAlias: 'cards',
alias: 'html_card' alias: 'html_card'
@ -129,8 +135,8 @@ export interface WidgetTypeDescriptor {
templateHtml: string; templateHtml: string;
templateCss: string; templateCss: string;
controllerScript: string; controllerScript: string;
settingsSchema?: string; settingsSchema?: string | any;
dataKeySettingsSchema?: string; dataKeySettingsSchema?: string | any;
defaultConfig: string; defaultConfig: string;
sizeX: number; sizeX: number;
sizeY: number; sizeY: number;
@ -146,8 +152,8 @@ export interface WidgetTypeParameters {
export interface WidgetControllerDescriptor { export interface WidgetControllerDescriptor {
widgetTypeFunction?: any; widgetTypeFunction?: any;
settingsSchema?: string; settingsSchema?: string | any;
dataKeySettingsSchema?: string; dataKeySettingsSchema?: string | any;
typeParameters?: WidgetTypeParameters; typeParameters?: WidgetTypeParameters;
actionSources?: {[key: string]: WidgetActionSource}; actionSources?: {[key: string]: WidgetActionSource};
} }
@ -309,7 +315,7 @@ export interface WidgetConfig {
showTitle?: boolean; showTitle?: boolean;
showTitleIcon?: boolean; showTitleIcon?: boolean;
iconColor?: string; iconColor?: string;
iconSize?: number; iconSize?: string;
dropShadow?: boolean; dropShadow?: boolean;
enableFullscreen?: boolean; enableFullscreen?: boolean;
useDashboardTimewindow?: boolean; useDashboardTimewindow?: boolean;

49
ui-ngx/src/app/shared/pipe/keyboard-shortcut.pipe.ts

@ -0,0 +1,49 @@
///
/// 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 { Inject, Pipe, PipeTransform } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
@Pipe({
name: 'keyboardShortcut'
})
export class KeyboardShortcutPipe implements PipeTransform {
constructor(@Inject(WINDOW) private window: Window) {}
transform(value: string): string {
if (!value) {
return;
}
const keys = value.split('-');
const isOSX = /Mac OS X/.test(this.window.navigator.userAgent);
const seperator = (!isOSX || keys.length > 2) ? '+' : '';
const abbreviations = {
M: isOSX ? '⌘' : 'Ctrl',
A: isOSX ? 'Option' : 'Alt',
S: 'Shift'
};
return keys.map((key, index) => {
const last = index === keys.length - 1;
return last ? key : abbreviations[key];
}).join(seperator);
return (!value) ? '' : value.replace(/ /g, '');
}
}

5
ui-ngx/src/app/shared/shared.module.ts

@ -95,6 +95,7 @@ import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from '.
import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
@NgModule({ @NgModule({
providers: [ providers: [
@ -150,7 +151,8 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
HighlightPipe HighlightPipe,
KeyboardShortcutPipe
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -272,6 +274,7 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
HighlightPipe, HighlightPipe,
KeyboardShortcutPipe,
TranslateModule TranslateModule
] ]
}) })

9
ui-ngx/src/theme.scss

@ -277,6 +277,15 @@ $tb-dark-theme: get-tb-dark-theme(
display: block; display: block;
} }
button.mat-menu-item {
overflow: hidden;
fill: #737373;
display: block;
.tb-alt-text {
float: right;
}
}
mat-toolbar.mat-table-toolbar { mat-toolbar.mat-table-toolbar {
background: #fff; background: #fff;
padding: 0 24px; padding: 0 24px;

Loading…
Cancel
Save