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() {
console.log(`Supported Langs: ${env.supportedLangs}`);
if (!env.production) {
console.log(`Supported Langs: ${env.supportedLangs}`);
}
this.translate.addLangs(env.supportedLangs);
console.log(`Default Lang: ${env.defaultLang}`);
if (!env.production) {
console.log(`Default Lang: ${env.defaultLang}`);
}
this.translate.setDefaultLang(env.defaultLang);
}

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

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

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

@ -24,6 +24,42 @@
<div id="gridster-parent"
fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
(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;">
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
<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;
}
}
.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 {
AfterViewInit,
Component,
DoCheck,
Input,
IterableDiffers,
KeyValueDiffers,
OnChanges,
OnInit,
QueryList,
SimpleChanges,
ViewChild,
ViewChildren
ViewChild
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -32,16 +33,15 @@ import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service';
import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2';
import { GridsterComponent, GridsterConfig } from 'angular-gridster2';
import {
DashboardCallbacks,
DashboardWidget,
DashboardWidgets,
IDashboardComponent,
WidgetsData,
DashboardWidgets
WidgetPosition
} from '../../models/dashboard-component.models';
import { merge, Observable, ReplaySubject, Subject } from 'rxjs';
import { map, share, tap } from 'rxjs/operators';
import { ReplaySubject, Subject } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
@ -49,13 +49,14 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { Widget } from '@app/shared/models/widget.models';
import { MatMenuTrigger } from '@angular/material';
@Component({
selector: 'tb-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit, OnChanges {
export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, AfterViewInit, OnChanges {
authUser: AuthUser;
@ -130,25 +131,38 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
gridsterOpts: GridsterConfig;
highlightedMode = false;
highlightedWidget: DashboardWidget = null;
selectedWidget: DashboardWidget = null;
isWidgetExpanded = false;
isMobileSize = false;
@ViewChild('gridster', {static: true}) gridster: GridsterComponent;
@ViewChildren(GridsterItemComponent) gridsterItems: QueryList<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;
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>,
private timeService: TimeService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver) {
private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers) {
super(store);
this.authUser = getCurrentAuthUser(store);
}
@ -175,7 +189,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
defaultItemRows: 6,
resizable: {enabled: this.isEdit},
draggable: {enabled: this.isEdit},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets()
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
}
};
this.updateMobileOpts();
@ -184,12 +201,17 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
.observe(MediaBreakpoints['gt-sm']).subscribe(
() => {
this.updateMobileOpts();
this.notifyGridsterOptionsChanged();
}
);
this.updateWidgets();
}
ngDoCheck() {
this.dashboardWidgets.doCheck();
}
ngOnChanges(changes: SimpleChanges): void {
let updateMobileOpts = false;
let updateLayoutOpts = false;
@ -206,6 +228,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
updateEditingOpts = true;
} else if (['widgets', 'widgetLayouts'].includes(propName)) {
updateWidgets = true;
} else if (propName === 'dashboardTimewindow') {
this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
}
}
}
@ -259,14 +283,34 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
}
}
openDashboardContextMenu($event: Event) {
// TODO:
// this.dialogService.todo();
openDashboardContextMenu($event: MouseEvent) {
if (this.callbacks && this.callbacks.prepareDashboardContextMenu) {
const items = this.callbacks.prepareDashboardContextMenu($event);
if (items && items.length) {
$event.preventDefault();
$event.stopPropagation();
this.dashboardContextMenuEvent = $event;
this.dashboardMenuPosition.x = $event.clientX + 'px';
this.dashboardMenuPosition.y = $event.clientY + 'px';
this.dashboardMenuTrigger.menuData = { items };
this.dashboardMenuTrigger.openMenu();
}
}
}
openWidgetContextMenu($event: Event, widget: DashboardWidget) {
// TODO:
// this.dialogService.todo();
openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.widgetIndex);
if (items && items.length) {
$event.preventDefault();
$event.stopPropagation();
this.widgetContextMenuEvent = $event;
this.widgetMenuPosition.x = $event.clientX + 'px';
this.widgetMenuPosition.y = $event.clientY + 'px';
this.widgetMenuTrigger.menuData = { items, widget: widget.widget };
this.widgetMenuTrigger.openMenu();
}
}
}
onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) {
@ -275,13 +319,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetMouseDown($event: Event, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.onWidgetMouseDown) {
this.callbacks.onWidgetMouseDown($event, widget.widget);
this.callbacks.onWidgetMouseDown($event, widget.widget, widget.widgetIndex);
}
}
widgetClicked($event: Event, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.onWidgetClicked) {
this.callbacks.onWidgetClicked($event, widget.widget);
this.callbacks.onWidgetClicked($event, widget.widget, widget.widgetIndex);
}
}
@ -290,7 +334,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation();
}
if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) {
this.callbacks.onEditWidget($event, widget.widget);
this.callbacks.onEditWidget($event, widget.widget, widget.widgetIndex);
}
}
@ -299,7 +343,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation();
}
if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) {
this.callbacks.onExportWidget($event, widget.widget);
this.callbacks.onExportWidget($event, widget.widget, widget.widgetIndex);
}
}
@ -308,56 +352,81 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation();
}
if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) {
this.callbacks.onRemoveWidget($event, widget.widget).subscribe(
(result) => {
if (result) {
this.dashboardWidgets.removeWidget(widget.widget);
}
}
);
this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex);
}
}
highlightWidget(widget: DashboardWidget, delay?: number) {
if (!this.highlightedMode || this.highlightedWidget !== widget) {
this.highlightedMode = true;
this.highlightedWidget = widget;
this.scrollToWidget(widget, delay);
highlightWidget(index: number, delay?: number) {
const highlighted = this.dashboardWidgets.highlightWidget(index);
if (highlighted) {
this.scrollToWidget(highlighted, delay);
}
}
selectWidget(widget: DashboardWidget, delay?: number) {
if (this.selectedWidget !== widget) {
this.selectedWidget = widget;
this.scrollToWidget(widget, delay);
selectWidget(index: number, delay?: number) {
const selected = this.dashboardWidgets.selectWidget(index);
if (selected) {
this.scrollToWidget(selected, delay);
}
}
getSelectedWidget(): Widget {
const dashboardWidget = this.dashboardWidgets.getSelectedWidget();
return dashboardWidget ? dashboardWidget.widget : null;
}
getEventGridPosition(event: Event): WidgetPosition {
const pos: WidgetPosition = {
row: 0,
column: 0
};
const parentElement = this.gridster.el as HTMLElement;
let pageX = 0;
let pageY = 0;
if (event instanceof MouseEvent) {
pageX = event.pageX;
pageY = event.pageY;
}
const x = pageX - parentElement.offsetLeft + parentElement.scrollLeft;
const y = pageY - parentElement.offsetTop + parentElement.scrollTop;
pos.row = this.gridster.pixelsToPositionY(y, Math.floor);
pos.column = this.gridster.pixelsToPositionX(x, Math.floor);
return pos;
}
resetHighlight() {
this.highlightedMode = false;
this.highlightedWidget = null;
this.selectedWidget = null;
const highlighted = this.dashboardWidgets.resetHighlight();
if (highlighted) {
setTimeout(() => {
this.scrollToWidget(highlighted, 0);
}, 0);
}
}
isHighlighted(widget: DashboardWidget) {
return (this.highlightedMode && this.highlightedWidget === widget) || (this.selectedWidget === widget);
return this.dashboardWidgets.isHighlighted(widget);
}
isNotHighlighted(widget: DashboardWidget) {
return this.highlightedMode && this.highlightedWidget !== widget;
return this.dashboardWidgets.isNotHighlighted(widget);
}
scrollToWidget(widget: DashboardWidget, delay?: number) {
if (this.gridsterItems) {
const gridsterItem = this.gridsterItems.find((item => item.item === widget));
const offset = (this.gridster.curHeight - gridsterItem.height) / 2;
let scrollTop = gridsterItem.top;
private scrollToWidget(widget: DashboardWidget, delay?: number) {
const parentElement = this.gridster.el as HTMLElement;
widget.gridsterItemComponent$().subscribe((gridsterItem) => {
const gridsterItemElement = gridsterItem.el as HTMLElement;
const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
let scrollTop;
if (this.isMobileSize) {
scrollTop = gridsterItemElement.offsetTop;
} else {
scrollTop = scrollTop = gridsterItem.top;
}
if (offset > 0) {
scrollTop -= offset;
}
const parentElement = this.gridster.el as HTMLElement;
animatedScroll(parentElement, scrollTop, delay);
}
});
}
private updateMobileOpts() {

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

@ -31,7 +31,7 @@
</button>
</div>
<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
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
@ -40,7 +40,7 @@
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)"
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
mat-fab
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
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() isReadOnly = false;
@Input() isAlwaysEdit = false;
@Input() theForm: NgForm;
theFormValue: NgForm;
@Input()
set theForm(value: NgForm) {
this.theFormValue = value;
}
get theForm(): NgForm {
return this.theFormValue;
}
@Output()
closeDetails = new EventEmitter<void>();
@Output()
@ -47,7 +58,7 @@ export class DetailsPanelComponent extends PageComponent {
@Input()
get isEdit() {
return this.isEditValue;
return this.isAlwaysEdit || this.isEditValue;
}
set isEdit(val: boolean) {
@ -72,7 +83,7 @@ export class DetailsPanelComponent extends PageComponent {
}
onApplyDetails() {
if (this.theForm.valid) {
if (this.theForm && this.theForm.valid) {
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 { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
@NgModule({
entryComponents: [
@ -76,7 +77,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent
AliasesEntitySelectComponent,
DashboardComponent,
WidgetComponent,
LegendComponent
LegendComponent,
WidgetConfigComponent
],
imports: [
CommonModule,
@ -97,7 +99,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent
AliasesEntitySelectComponent,
DashboardComponent,
WidgetComponent,
LegendComponent
LegendComponent,
WidgetConfigComponent
],
providers: [
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 {
console.log('Widget component destroyed!');
}
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 { UtilsService } from '@core/services/utils.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 { isFunction, isUndefined } from '@core/utils';
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> {
return this.init().pipe(
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 {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
ComponentRef,
ElementRef,
Injector,
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
ChangeDetectorRef,
ChangeDetectionStrategy, NgZone
ViewEncapsulation
} from '@angular/core';
import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models';
import {
@ -52,7 +53,7 @@ import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { UtilsService } from '@core/services/utils.service';
import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
import { isDefined, objToBase64, deepClone } from '@core/utils';
import { deepClone, isDefined, objToBase64 } from '@core/utils';
import {
IDynamicWidgetComponent,
WidgetContext,
@ -63,7 +64,8 @@ import {
import {
IWidgetSubscription,
StateObject,
StateParams, SubscriptionEntityInfo,
StateParams,
SubscriptionEntityInfo,
SubscriptionInfo,
WidgetSubscriptionContext,
WidgetSubscriptionOptions
@ -86,7 +88,6 @@ import { DashboardService } from '@core/http/dashboard.service';
import { DatasourceService } from '@core/api/datasource.service';
import { WidgetSubscription } from '@core/api/widget-subscription';
import { EntityService } from '@core/http/entity.service';
import { TimewindowComponent } from '@shared/components/time/timewindow.component';
@Component({
selector: 'tb-widget',
@ -362,10 +363,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'isEdit') {
console.log(`isEdit changed: ${this.isEdit}`);
this.onEditModeChanged();
} else if (propName === 'isMobile') {
console.log(`isMobile changed: ${this.isMobile}`);
this.onMobileModeChanged();
}
}

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

@ -14,30 +14,50 @@
/// 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 { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Observable } from 'rxjs';
import { Observable, of, Subject } from 'rxjs';
import { isDefined, isUndefined } from '@app/core/utils';
import { EventEmitter } from '@angular/core';
import { EntityId } from '@app/shared/models/id/entity-id';
import { IAliasController, IStateController, TimewindowFunctions } from '@app/core/api/widget-api.models';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import * as deepEqual from 'deep-equal';
export interface WidgetsData {
widgets: Array<Widget>;
widgetLayouts?: WidgetLayouts;
}
export interface ContextMenuItem {
enabled: boolean;
shortcut?: string;
icon: string;
value: string;
}
export interface DashboardContextMenuItem extends ContextMenuItem {
action: (contextMenuEvent: MouseEvent) => void;
}
export interface WidgetContextMenuItem extends ContextMenuItem {
action: (contextMenuEvent: MouseEvent, widget: Widget) => void;
}
export interface DashboardCallbacks {
onEditWidget?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => void;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => void;
onEditWidget?: ($event: Event, widget: Widget, index: number) => void;
onExportWidget?: ($event: Event, widget: Widget, index: number) => void;
onRemoveWidget?: ($event: Event, widget: Widget, index: number) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget, index: number) => void;
onWidgetClicked?: ($event: Event, widget: Widget, index: number) => void;
prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget, index: number) => Array<WidgetContextMenuItem>;
}
export interface WidgetPosition {
row: number;
column: number;
}
export interface IDashboardComponent {
@ -53,60 +73,181 @@ export interface IDashboardComponent {
stateController: IStateController;
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void;
onResetTimewindow(): void;
resetHighlight(): void;
highlightWidget(index: number, delay?: number);
selectWidget(index: number, delay?: number);
getSelectedWidget(): Widget;
getEventGridPosition(event: Event): WidgetPosition;
}
declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update';
interface DashboardWidgetUpdateRecord {
widget?: Widget;
widgetLayout?: WidgetLayout;
widgetIndex: number;
operation: DashboardWidgetUpdateOperation;
}
export class DashboardWidgets implements Iterable<DashboardWidget> {
highlightedMode = false;
dashboardWidgets: Array<DashboardWidget> = [];
widgets: Array<Widget>;
widgetLayouts: WidgetLayouts;
[Symbol.iterator](): Iterator<DashboardWidget> {
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) {
let maxRows = this.dashboard.gridsterOpts.maxRows;
this.dashboardWidgets.length = 0;
widgets.forEach((widget) => {
let widgetLayout: WidgetLayout;
if (widgetLayouts && widget.id) {
widgetLayout = widgetLayouts[widget.id];
doCheck() {
const widgetChange = this.widgetsDiffer.diff(this.widgets);
if (widgetChange !== null) {
const layouts: WidgetLayouts = {};
const updateRecords: Array<DashboardWidgetUpdateRecord> = [];
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);
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
this.dashboardWidgets.push(dashboardWidget);
});
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
widgetChange.forEachAddedItem((added) => {
updateRecords.push({
widget: added.item,
widgetLayout: layouts[added.item.id],
widgetIndex: added.currentIndex,
operation: 'add'
});
});
widgetChange.forEachRemovedItem((removed) => {
let operation = updateRecords.find((record) => record.widgetIndex === removed.previousIndex);
if (operation) {
operation.operation = 'update';
} else {
operation = {
widgetIndex: removed.previousIndex,
operation: 'remove'
};
updateRecords.push(operation);
}
});
updateRecords.forEach((record) => {
switch (record.operation) {
case 'add':
this.dashboardWidgets.push(
new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout)
);
break;
case 'remove':
let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex);
if (index > -1) {
this.dashboardWidgets.splice(index, 1);
}
break;
case 'update':
index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex);
if (index > -1) {
const prevDashboardWidget = this.dashboardWidgets[index];
if (!deepEqual(prevDashboardWidget.widget, record.widget)) {
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout);
this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
this.dashboardWidgets[index].selected = prevDashboardWidget.selected;
} else {
this.dashboardWidgets[index].widget = record.widget;
this.dashboardWidgets[index].widgetLayout = record.widgetLayout;
}
}
break;
}
});
if (updateRecords.length) {
this.updateRowsAndSort();
}
}
}
addWidget(widget: Widget, widgetLayout: WidgetLayout) {
const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
let maxRows = this.dashboard.gridsterOpts.maxRows;
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
this.dashboardWidgets.push(dashboardWidget);
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
this.highlightedMode = false;
this.widgets = widgets;
this.widgetLayouts = widgetLayouts;
}
removeWidget(widget: Widget): boolean {
const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widget === widget);
if (index > -1) {
this.dashboardWidgets.splice(index, 1);
let maxRows = this.dashboard.gridsterOpts.maxRows;
highlightWidget(index: number): DashboardWidget {
const widget = this.findWidgetAtIndex(index);
if (widget && (!this.highlightedMode || !widget.highlighted)) {
this.highlightedMode = true;
widget.highlighted = true;
this.dashboardWidgets.forEach((dashboardWidget) => {
const bottom = dashboardWidget.y + dashboardWidget.rows;
maxRows = Math.max(maxRows, bottom);
if (dashboardWidget !== widget) {
dashboardWidget.highlighted = false;
}
});
this.sortWidgets();
this.dashboard.gridsterOpts.maxRows = maxRows;
return true;
return widget;
} else {
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() {
@ -125,6 +266,9 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
export class DashboardWidget implements GridsterItem {
highlighted = false;
selected = false;
isFullscreen = false;
color: string;
@ -160,13 +304,31 @@ export class DashboardWidget implements GridsterItem {
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(
private dashboard: IDashboardComponent,
public widget: Widget,
private widgetLayout?: WidgetLayout) {
public widgetIndex: number,
public widgetLayout?: WidgetLayout) {
this.updateWidgetParams();
}
gridsterItemComponent$(): Observable<GridsterItemComponentInterface> {
if (this.gridsterItemComponentValue) {
return of(this.gridsterItemComponentValue);
} else {
return this.gridsterItemComponentSubject.asObservable();
}
}
updateWidgetParams() {
this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)';
this.backgroundColor = this.widget.config.backgroundColor || '#fff';
@ -221,11 +383,13 @@ export class DashboardWidget implements GridsterItem {
}
get x(): number {
let res;
if (this.widgetLayout) {
return this.widgetLayout.col;
res = this.widgetLayout.col;
} else {
return this.widget.col;
res = this.widget.col;
}
return Math.floor(res);
}
set x(x: number) {
@ -239,11 +403,13 @@ export class DashboardWidget implements GridsterItem {
}
get y(): number {
let res;
if (this.widgetLayout) {
return this.widgetLayout.row;
res = this.widgetLayout.row;
} else {
return this.widget.row;
res = this.widget.row;
}
return Math.floor(res);
}
set y(y: number) {
@ -257,11 +423,13 @@ export class DashboardWidget implements GridsterItem {
}
get cols(): number {
let res;
if (this.widgetLayout) {
return this.widgetLayout.sizeX;
res = this.widgetLayout.sizeX;
} else {
return this.widget.sizeX;
res = this.widget.sizeX;
}
return Math.floor(res);
}
set cols(cols: number) {
@ -275,6 +443,7 @@ export class DashboardWidget implements GridsterItem {
}
get rows(): number {
let res;
if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) {
let mobileHeight;
if (this.widgetLayout) {
@ -284,17 +453,18 @@ export class DashboardWidget implements GridsterItem {
mobileHeight = this.widget.config.mobileHeight;
}
if (mobileHeight) {
return mobileHeight;
res = mobileHeight;
} else {
return this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
res = this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
}
} else {
if (this.widgetLayout) {
return this.widgetLayout.sizeY;
res = this.widgetLayout.sizeY;
} else {
return this.widget.sizeY;
res = this.widget.sizeY;
}
}
return Math.floor(res);
}
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 { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetLayout } from '@shared/models/dashboard.models';
export interface IWidgetAction {
name: string;
@ -112,11 +113,16 @@ export interface IDynamicWidgetComponent {
export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
widgetName: string;
alias: string;
typeSettingsSchema?: string;
typeDataKeySettingsSchema?: string;
typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
}
export interface WidgetConfigComponentData {
config: WidgetConfig;
layout: WidgetLayout;
}
export const MissingWidgetType: WidgetInfo = {
type: widgetType.latest,
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"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</div>
<mat-sidenav-container *ngIf="layouts.right.show"
<mat-drawer-container *ngIf="layouts.right.show"
id="tb-right-layout">
<mat-sidenav
<mat-drawer
[ngStyle]="{minWidth: rightLayoutWidth(),
maxWidth: rightLayoutWidth(),
height: rightLayoutHeight(),
@ -157,13 +158,43 @@
[layoutCtx]="layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</mat-sidenav>
</mat-sidenav-container>
</mat-drawer>
</mat-drawer-container>
</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 -->
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<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 {
&.mat-content {
overflow: hidden;
background-color: #eee;
}
section.tb-dashboard-title {
@ -108,13 +109,30 @@ div.tb-dashboard-page {
z-index: 1;
}*/
#tb-right-layout {
mat-sidenav {
mat-drawer {
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 {
position: absolute;
right: 25px;

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

@ -14,7 +14,7 @@
/// 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -41,17 +41,25 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model';
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 { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service';
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 { IStateController } from '@core/api/widget-api.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.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({
selector: 'tb-dashboard-page',
@ -89,6 +97,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
editingWidgetLayoutOriginal: WidgetLayout = null;
editingWidgetSubtitle: string = null;
editingLayoutCtx: DashboardPageLayoutContext = null;
editingWidgetFormGroup: FormGroup;
thingsboardVersion: string = env.tbVersion;
@ -105,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false,
ctrl: null
ctrl: null,
dashboardCtrl: this
}
},
right: {
@ -116,7 +126,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false,
ctrl: null
ctrl: null,
dashboardCtrl: this
}
}
};
@ -175,9 +186,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private authService: AuthService,
private entityService: EntityService,
private dialogService: DialogService,
private dashboardService: DashboardService) {
private widgetComponentService: WidgetComponentService,
private dashboardService: DashboardService,
private itembuffer: ItemBufferService,
private fb: FormBuilder) {
super(store);
this.editingWidgetFormGroup = this.fb.group({
widgetConfig: [null]
});
this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
this.init(data);
@ -253,6 +271,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.currentDashboardId = null;
this.currentCustomerId = null;
this.currentDashboardScope = null;
this.dashboardCtx.state = null;
}
ngOnDestroy(): void {
@ -428,14 +448,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dialogService.todo();
}
private addWidget($event: Event) {
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
}
private importWidget($event: Event) {
if ($event) {
$event.stopPropagation();
@ -568,7 +580,221 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
};
this.window.parent.postMessage(JSON.stringify(message), '*');
} 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 { IAliasController, IStateController } from '@core/api/widget-api.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';
@ -34,6 +40,18 @@ export interface IDashboardController {
dashboardCtx: DashboardContext;
openRightLayout();
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 {
@ -42,6 +60,7 @@ export interface DashboardPageLayoutContext {
widgetLayouts: WidgetLayouts;
gridSettings: GridSettings;
ctrl: ILayoutController;
dashboardCtrl: IDashboardController;
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 { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module';
import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
import { EditWidgetComponent } from './edit-widget.component';
@NgModule({
entryComponents: [
@ -43,7 +44,8 @@ import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
MakeDashboardPublicDialogComponent,
DashboardToolbarComponent,
DashboardPageComponent,
DashboardLayoutComponent
DashboardLayoutComponent,
EditWidgetComponent
],
imports: [
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.
-->
<hotkeys-cheatsheet></hotkeys-cheatsheet>
<div class="mat-content" style="position: relative; width: 100%; height: 100%;"
[ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
@ -60,6 +61,7 @@
[isEditActionEnabled]="isEdit"
[isExportActionEnabled]="isEdit && !widgetEditMode"
[isRemoveActionEnabled]="isEdit && !widgetEditMode"
[callbacks]="this"
[ignoreLoading]="layoutCtx.ignoreLoading">
</tb-dashboard>
</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 { AppState } from '@core/core.state';
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 { 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({
selector: 'tb-dashboard-layout',
templateUrl: './dashboard-layout.component.html',
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;
@ -52,6 +61,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
@Input()
isEdit: boolean;
@Input()
isEditingWidget: boolean;
@Input()
isMobile: boolean;
@ -60,15 +72,97 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
private hotkeysService: HotkeysService,
private translate: TranslateService,
private itembuffer: ItemBufferService) {
super(store);
}
ngOnInit(): void {
this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe(
(dashboardTimewindow) => {
this.dashboardCtx.dashboardTimewindow = dashboardTimewindow;
}
)
);
this.initHotKeys();
}
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() {
@ -78,6 +172,65 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}
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.
///
import { Widget } from '@shared/models/widget.models';
import { WidgetLayout } from '@shared/models/dashboard.models';
export interface ILayoutController {
reload();
setResizing(layoutVisibilityChanged: boolean);
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',
rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
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;
icon: string;
isMdiIcon?: boolean;
configHelpLinkId: string;
template: WidgetTypeTemplate;
}
@ -50,6 +51,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{
name: 'widget.timeseries',
icon: 'timeline',
configHelpLinkId: 'widgetsConfigTimeseries',
template: {
bundleAlias: 'charts',
alias: 'basic_timeseries'
@ -61,6 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{
name: 'widget.latest-values',
icon: 'track_changes',
configHelpLinkId: 'widgetsConfigLatest',
template: {
bundleAlias: 'cards',
alias: 'attributes_card'
@ -72,6 +75,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{
name: 'widget.rpc',
icon: 'mdi:developer-board',
configHelpLinkId: 'widgetsConfigRpc',
isMdiIcon: true,
template: {
bundleAlias: 'gpio_widgets',
@ -84,6 +88,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{
name: 'widget.alarm',
icon: 'error',
configHelpLinkId: 'widgetsConfigAlarm',
template: {
bundleAlias: 'alarm_widgets',
alias: 'alarms_table'
@ -95,6 +100,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
{
name: 'widget.static',
icon: 'font_download',
configHelpLinkId: 'widgetsConfigStatic',
template: {
bundleAlias: 'cards',
alias: 'html_card'
@ -129,8 +135,8 @@ export interface WidgetTypeDescriptor {
templateHtml: string;
templateCss: string;
controllerScript: string;
settingsSchema?: string;
dataKeySettingsSchema?: string;
settingsSchema?: string | any;
dataKeySettingsSchema?: string | any;
defaultConfig: string;
sizeX: number;
sizeY: number;
@ -146,8 +152,8 @@ export interface WidgetTypeParameters {
export interface WidgetControllerDescriptor {
widgetTypeFunction?: any;
settingsSchema?: string;
dataKeySettingsSchema?: string;
settingsSchema?: string | any;
dataKeySettingsSchema?: string | any;
typeParameters?: WidgetTypeParameters;
actionSources?: {[key: string]: WidgetActionSource};
}
@ -309,7 +315,7 @@ export interface WidgetConfig {
showTitle?: boolean;
showTitleIcon?: boolean;
iconColor?: string;
iconSize?: number;
iconSize?: string;
dropShadow?: boolean;
enableFullscreen?: 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 { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
@NgModule({
providers: [
@ -150,7 +151,8 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe
HighlightPipe,
KeyboardShortcutPipe
],
imports: [
CommonModule,
@ -272,6 +274,7 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe,
KeyboardShortcutPipe,
TranslateModule
]
})

9
ui-ngx/src/theme.scss

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

Loading…
Cancel
Save