Commit 3cee41745194f0ba8af6943412fda032357b0330

Authored by Igor Kulikov
1 parent 2eb93dac

UI: Dashboard implementation

Showing 33 changed files with 1569 additions and 178 deletions
... ... @@ -49,9 +49,13 @@ export class AppComponent implements OnInit {
49 49 }
50 50
51 51 setupTranslate() {
52   - console.log(`Supported Langs: ${env.supportedLangs}`);
  52 + if (!env.production) {
  53 + console.log(`Supported Langs: ${env.supportedLangs}`);
  54 + }
53 55 this.translate.addLangs(env.supportedLangs);
54   - console.log(`Default Lang: ${env.defaultLang}`);
  56 + if (!env.production) {
  57 + console.log(`Default Lang: ${env.defaultLang}`);
  58 + }
55 59 this.translate.setDefaultLang(env.defaultLang);
56 60 }
57 61
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models';
  19 +
  20 +@Injectable({
  21 + providedIn: 'root'
  22 +})
  23 +export class ItemBufferService {
  24 + constructor() {}
  25 +
  26 + public hasWidget(): boolean {
  27 + // TODO:
  28 + return false;
  29 + }
  30 +
  31 + public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean {
  32 + // TODO:
  33 + return false;
  34 + }
  35 +}
... ...
... ... @@ -14,25 +14,31 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { environment } from '@env/environment';
  17 +import { environment as env } from '@env/environment';
18 18 import { TranslateService } from '@ngx-translate/core';
19 19
20 20 export function updateUserLang(translate: TranslateService, userLang: string) {
21 21 let targetLang = userLang;
22   - console.log(`User lang: ${targetLang}`);
  22 + if (!env.production) {
  23 + console.log(`User lang: ${targetLang}`);
  24 + }
23 25 if (!targetLang) {
24 26 targetLang = translate.getBrowserCultureLang();
25   - console.log(`Fallback to browser lang: ${targetLang}`);
  27 + if (!env.production) {
  28 + console.log(`Fallback to browser lang: ${targetLang}`);
  29 + }
26 30 }
27 31 const detectedSupportedLang = detectSupportedLang(targetLang);
28   - console.log(`Detected supported lang: ${detectedSupportedLang}`);
  32 + if (!env.production) {
  33 + console.log(`Detected supported lang: ${detectedSupportedLang}`);
  34 + }
29 35 translate.use(detectedSupportedLang);
30 36 }
31 37
32 38 function detectSupportedLang(targetLang: string): string {
33 39 const langTag = (targetLang || '').split('-').join('_');
34 40 if (langTag.length) {
35   - if (environment.supportedLangs.indexOf(langTag) > -1) {
  41 + if (env.supportedLangs.indexOf(langTag) > -1) {
36 42 return langTag;
37 43 } else {
38 44 const parts = langTag.split('_');
... ... @@ -42,7 +48,7 @@ function detectSupportedLang(targetLang: string): string {
42 48 } else {
43 49 lang = langTag;
44 50 }
45   - const foundLangs = environment.supportedLangs.filter(
  51 + const foundLangs = env.supportedLangs.filter(
46 52 (supportedLang: string) => {
47 53 const supportedLangParts = supportedLang.split('_');
48 54 return supportedLangParts[0] === lang;
... ... @@ -53,5 +59,5 @@ function detectSupportedLang(targetLang: string): string {
53 59 }
54 60 }
55 61 }
56   - return environment.defaultLang;
  62 + return env.defaultLang;
57 63 }
... ...
... ... @@ -61,11 +61,15 @@ export function animatedScroll(element: HTMLElement, scrollTop: number, delay?:
61 61 const duration = delay ? delay : 0;
62 62 const remaining = to - start;
63 63 const animateScroll = () => {
64   - currentTime += increment;
65   - const val = easeInOut(currentTime, start, remaining, duration);
66   - element.scrollTop = val;
67   - if (currentTime < duration) {
68   - setTimeout(animateScroll, increment);
  64 + if (duration === 0) {
  65 + element.scrollTop = to;
  66 + } else {
  67 + currentTime += increment;
  68 + const val = easeInOut(currentTime, start, remaining, duration);
  69 + element.scrollTop = val;
  70 + if (currentTime < duration) {
  71 + setTimeout(animateScroll, increment);
  72 + }
69 73 }
70 74 };
71 75 animateScroll();
... ...
... ... @@ -24,6 +24,42 @@
24 24 <div id="gridster-parent"
25 25 fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}"
26 26 (contextmenu)="openDashboardContextMenu($event)">
  27 + <div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
  28 + [style.left]="dashboardMenuPosition.x"
  29 + [style.top]="dashboardMenuPosition.y"
  30 + [matMenuTriggerFor]="dashboardMenu">
  31 + </div>
  32 + <mat-menu #dashboardMenu="matMenu">
  33 + <ng-template matMenuContent let-items="items">
  34 + <div class="tb-dashboard-context-menu-items">
  35 + <button mat-menu-item *ngFor="let item of items;"
  36 + [disabled]="!item.enabled"
  37 + (click)="item.action(dashboardContextMenuEvent)">
  38 + <span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
  39 + <mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
  40 + <span translate>{{item.value}}</span>
  41 + </button>
  42 + </div>
  43 + </ng-template>
  44 + </mat-menu>
  45 + <div #widgetMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
  46 + [style.left]="widgetMenuPosition.x"
  47 + [style.top]="widgetMenuPosition.y"
  48 + [matMenuTriggerFor]="widgetMenu">
  49 + </div>
  50 + <mat-menu #widgetMenu="matMenu">
  51 + <ng-template matMenuContent let-items="items" let-widget="widget">
  52 + <div class="tb-dashboard-context-menu-items">
  53 + <button mat-menu-item *ngFor="let item of items;"
  54 + [disabled]="!item.enabled"
  55 + (click)="item.action(widgetContextMenuEvent, widget)">
  56 + <span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
  57 + <mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
  58 + <span translate>{{item.value}}</span>
  59 + </button>
  60 + </div>
  61 + </ng-template>
  62 + </mat-menu>
27 63 <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
28 64 <gridster #gridster id="gridster-child" [options]="gridsterOpts">
29 65 <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets">
... ...
... ... @@ -123,3 +123,7 @@ div.tb-widget {
123 123 opacity: .5;
124 124 }
125 125 }
  126 +
  127 +.tb-dashboard-context-menu-items {
  128 + min-width: 256px;
  129 +}
... ...
... ... @@ -17,13 +17,14 @@
17 17 import {
18 18 AfterViewInit,
19 19 Component,
  20 + DoCheck,
20 21 Input,
  22 + IterableDiffers,
  23 + KeyValueDiffers,
21 24 OnChanges,
22 25 OnInit,
23   - QueryList,
24 26 SimpleChanges,
25   - ViewChild,
26   - ViewChildren
  27 + ViewChild
27 28 } from '@angular/core';
28 29 import { Store } from '@ngrx/store';
29 30 import { AppState } from '@core/core.state';
... ... @@ -32,16 +33,15 @@ import { AuthUser } from '@shared/models/user.model';
32 33 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
33 34 import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models';
34 35 import { TimeService } from '@core/services/time.service';
35   -import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2';
  36 +import { GridsterComponent, GridsterConfig } from 'angular-gridster2';
36 37 import {
37 38 DashboardCallbacks,
38 39 DashboardWidget,
  40 + DashboardWidgets,
39 41 IDashboardComponent,
40   - WidgetsData,
41   - DashboardWidgets
  42 + WidgetPosition
42 43 } from '../../models/dashboard-component.models';
43   -import { merge, Observable, ReplaySubject, Subject } from 'rxjs';
44   -import { map, share, tap } from 'rxjs/operators';
  44 +import { ReplaySubject, Subject } from 'rxjs';
45 45 import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
46 46 import { DialogService } from '@core/services/dialog.service';
47 47 import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
... ... @@ -49,13 +49,14 @@ import { BreakpointObserver } from '@angular/cdk/layout';
49 49 import { MediaBreakpoints } from '@shared/models/constants';
50 50 import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
51 51 import { Widget } from '@app/shared/models/widget.models';
  52 +import { MatMenuTrigger } from '@angular/material';
52 53
53 54 @Component({
54 55 selector: 'tb-dashboard',
55 56 templateUrl: './dashboard.component.html',
56 57 styleUrls: ['./dashboard.component.scss']
57 58 })
58   -export class DashboardComponent extends PageComponent implements IDashboardComponent, OnInit, AfterViewInit, OnChanges {
  59 +export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, AfterViewInit, OnChanges {
59 60
60 61 authUser: AuthUser;
61 62
... ... @@ -130,25 +131,38 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
130 131
131 132 gridsterOpts: GridsterConfig;
132 133
133   - highlightedMode = false;
134   - highlightedWidget: DashboardWidget = null;
135   - selectedWidget: DashboardWidget = null;
136   -
137 134 isWidgetExpanded = false;
138 135 isMobileSize = false;
139 136
140 137 @ViewChild('gridster', {static: true}) gridster: GridsterComponent;
141 138
142   - @ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>;
  139 + @ViewChild('dashboardMenuTrigger', {static: true}) dashboardMenuTrigger: MatMenuTrigger;
  140 +
  141 + dashboardMenuPosition = { x: '0px', y: '0px' };
  142 +
  143 + dashboardContextMenuEvent: MouseEvent;
  144 +
  145 + @ViewChild('widgetMenuTrigger', {static: true}) widgetMenuTrigger: MatMenuTrigger;
  146 +
  147 + widgetMenuPosition = { x: '0px', y: '0px' };
  148 +
  149 + widgetContextMenuEvent: MouseEvent;
143 150
144 151 dashboardLoading = true;
145 152
146   - dashboardWidgets = new DashboardWidgets(this);
  153 + dashboardWidgets = new DashboardWidgets(this,
  154 + this.differs.find([]).create<Widget>((index, item) => {
  155 + return item;
  156 + }),
  157 + this.kvDiffers.find([]).create<string, WidgetLayout>()
  158 + );
147 159
148 160 constructor(protected store: Store<AppState>,
149 161 private timeService: TimeService,
150 162 private dialogService: DialogService,
151   - private breakpointObserver: BreakpointObserver) {
  163 + private breakpointObserver: BreakpointObserver,
  164 + private differs: IterableDiffers,
  165 + private kvDiffers: KeyValueDiffers) {
152 166 super(store);
153 167 this.authUser = getCurrentAuthUser(store);
154 168 }
... ... @@ -175,7 +189,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
175 189 defaultItemRows: 6,
176 190 resizable: {enabled: this.isEdit},
177 191 draggable: {enabled: this.isEdit},
178   - itemChangeCallback: item => this.dashboardWidgets.sortWidgets()
  192 + itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
  193 + itemInitCallback: (item, itemComponent) => {
  194 + (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
  195 + }
179 196 };
180 197
181 198 this.updateMobileOpts();
... ... @@ -184,12 +201,17 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
184 201 .observe(MediaBreakpoints['gt-sm']).subscribe(
185 202 () => {
186 203 this.updateMobileOpts();
  204 + this.notifyGridsterOptionsChanged();
187 205 }
188 206 );
189 207
190 208 this.updateWidgets();
191 209 }
192 210
  211 + ngDoCheck() {
  212 + this.dashboardWidgets.doCheck();
  213 + }
  214 +
193 215 ngOnChanges(changes: SimpleChanges): void {
194 216 let updateMobileOpts = false;
195 217 let updateLayoutOpts = false;
... ... @@ -206,6 +228,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
206 228 updateEditingOpts = true;
207 229 } else if (['widgets', 'widgetLayouts'].includes(propName)) {
208 230 updateWidgets = true;
  231 + } else if (propName === 'dashboardTimewindow') {
  232 + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow);
209 233 }
210 234 }
211 235 }
... ... @@ -259,14 +283,34 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
259 283 }
260 284 }
261 285
262   - openDashboardContextMenu($event: Event) {
263   - // TODO:
264   - // this.dialogService.todo();
  286 + openDashboardContextMenu($event: MouseEvent) {
  287 + if (this.callbacks && this.callbacks.prepareDashboardContextMenu) {
  288 + const items = this.callbacks.prepareDashboardContextMenu($event);
  289 + if (items && items.length) {
  290 + $event.preventDefault();
  291 + $event.stopPropagation();
  292 + this.dashboardContextMenuEvent = $event;
  293 + this.dashboardMenuPosition.x = $event.clientX + 'px';
  294 + this.dashboardMenuPosition.y = $event.clientY + 'px';
  295 + this.dashboardMenuTrigger.menuData = { items };
  296 + this.dashboardMenuTrigger.openMenu();
  297 + }
  298 + }
265 299 }
266 300
267   - openWidgetContextMenu($event: Event, widget: DashboardWidget) {
268   - // TODO:
269   - // this.dialogService.todo();
  301 + openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
  302 + if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
  303 + const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.widgetIndex);
  304 + if (items && items.length) {
  305 + $event.preventDefault();
  306 + $event.stopPropagation();
  307 + this.widgetContextMenuEvent = $event;
  308 + this.widgetMenuPosition.x = $event.clientX + 'px';
  309 + this.widgetMenuPosition.y = $event.clientY + 'px';
  310 + this.widgetMenuTrigger.menuData = { items, widget: widget.widget };
  311 + this.widgetMenuTrigger.openMenu();
  312 + }
  313 + }
270 314 }
271 315
272 316 onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) {
... ... @@ -275,13 +319,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
275 319
276 320 widgetMouseDown($event: Event, widget: DashboardWidget) {
277 321 if (this.callbacks && this.callbacks.onWidgetMouseDown) {
278   - this.callbacks.onWidgetMouseDown($event, widget.widget);
  322 + this.callbacks.onWidgetMouseDown($event, widget.widget, widget.widgetIndex);
279 323 }
280 324 }
281 325
282 326 widgetClicked($event: Event, widget: DashboardWidget) {
283 327 if (this.callbacks && this.callbacks.onWidgetClicked) {
284   - this.callbacks.onWidgetClicked($event, widget.widget);
  328 + this.callbacks.onWidgetClicked($event, widget.widget, widget.widgetIndex);
285 329 }
286 330 }
287 331
... ... @@ -290,7 +334,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
290 334 $event.stopPropagation();
291 335 }
292 336 if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) {
293   - this.callbacks.onEditWidget($event, widget.widget);
  337 + this.callbacks.onEditWidget($event, widget.widget, widget.widgetIndex);
294 338 }
295 339 }
296 340
... ... @@ -299,7 +343,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
299 343 $event.stopPropagation();
300 344 }
301 345 if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) {
302   - this.callbacks.onExportWidget($event, widget.widget);
  346 + this.callbacks.onExportWidget($event, widget.widget, widget.widgetIndex);
303 347 }
304 348 }
305 349
... ... @@ -308,56 +352,81 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
308 352 $event.stopPropagation();
309 353 }
310 354 if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) {
311   - this.callbacks.onRemoveWidget($event, widget.widget).subscribe(
312   - (result) => {
313   - if (result) {
314   - this.dashboardWidgets.removeWidget(widget.widget);
315   - }
316   - }
317   - );
  355 + this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex);
318 356 }
319 357 }
320 358
321   - highlightWidget(widget: DashboardWidget, delay?: number) {
322   - if (!this.highlightedMode || this.highlightedWidget !== widget) {
323   - this.highlightedMode = true;
324   - this.highlightedWidget = widget;
325   - this.scrollToWidget(widget, delay);
  359 + highlightWidget(index: number, delay?: number) {
  360 + const highlighted = this.dashboardWidgets.highlightWidget(index);
  361 + if (highlighted) {
  362 + this.scrollToWidget(highlighted, delay);
326 363 }
327 364 }
328 365
329   - selectWidget(widget: DashboardWidget, delay?: number) {
330   - if (this.selectedWidget !== widget) {
331   - this.selectedWidget = widget;
332   - this.scrollToWidget(widget, delay);
  366 + selectWidget(index: number, delay?: number) {
  367 + const selected = this.dashboardWidgets.selectWidget(index);
  368 + if (selected) {
  369 + this.scrollToWidget(selected, delay);
333 370 }
334 371 }
335 372
  373 + getSelectedWidget(): Widget {
  374 + const dashboardWidget = this.dashboardWidgets.getSelectedWidget();
  375 + return dashboardWidget ? dashboardWidget.widget : null;
  376 + }
  377 +
  378 + getEventGridPosition(event: Event): WidgetPosition {
  379 + const pos: WidgetPosition = {
  380 + row: 0,
  381 + column: 0
  382 + };
  383 + const parentElement = this.gridster.el as HTMLElement;
  384 + let pageX = 0;
  385 + let pageY = 0;
  386 + if (event instanceof MouseEvent) {
  387 + pageX = event.pageX;
  388 + pageY = event.pageY;
  389 + }
  390 + const x = pageX - parentElement.offsetLeft + parentElement.scrollLeft;
  391 + const y = pageY - parentElement.offsetTop + parentElement.scrollTop;
  392 + pos.row = this.gridster.pixelsToPositionY(y, Math.floor);
  393 + pos.column = this.gridster.pixelsToPositionX(x, Math.floor);
  394 + return pos;
  395 + }
  396 +
336 397 resetHighlight() {
337   - this.highlightedMode = false;
338   - this.highlightedWidget = null;
339   - this.selectedWidget = null;
  398 + const highlighted = this.dashboardWidgets.resetHighlight();
  399 + if (highlighted) {
  400 + setTimeout(() => {
  401 + this.scrollToWidget(highlighted, 0);
  402 + }, 0);
  403 + }
340 404 }
341 405
342 406 isHighlighted(widget: DashboardWidget) {
343   - return (this.highlightedMode && this.highlightedWidget === widget) || (this.selectedWidget === widget);
  407 + return this.dashboardWidgets.isHighlighted(widget);
344 408 }
345 409
346 410 isNotHighlighted(widget: DashboardWidget) {
347   - return this.highlightedMode && this.highlightedWidget !== widget;
  411 + return this.dashboardWidgets.isNotHighlighted(widget);
348 412 }
349 413
350   - scrollToWidget(widget: DashboardWidget, delay?: number) {
351   - if (this.gridsterItems) {
352   - const gridsterItem = this.gridsterItems.find((item => item.item === widget));
353   - const offset = (this.gridster.curHeight - gridsterItem.height) / 2;
354   - let scrollTop = gridsterItem.top;
  414 + private scrollToWidget(widget: DashboardWidget, delay?: number) {
  415 + const parentElement = this.gridster.el as HTMLElement;
  416 + widget.gridsterItemComponent$().subscribe((gridsterItem) => {
  417 + const gridsterItemElement = gridsterItem.el as HTMLElement;
  418 + const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
  419 + let scrollTop;
  420 + if (this.isMobileSize) {
  421 + scrollTop = gridsterItemElement.offsetTop;
  422 + } else {
  423 + scrollTop = scrollTop = gridsterItem.top;
  424 + }
355 425 if (offset > 0) {
356 426 scrollTop -= offset;
357 427 }
358   - const parentElement = this.gridster.el as HTMLElement;
359 428 animatedScroll(parentElement, scrollTop, delay);
360   - }
  429 + });
361 430 }
362 431
363 432 private updateMobileOpts() {
... ...
... ... @@ -31,7 +31,7 @@
31 31 </button>
32 32 </div>
33 33 <section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons">
34   - <button [disabled]="(isLoading$ | async) || theForm.invalid || !theForm.dirty"
  34 + <button [disabled]="(isLoading$ | async) || theForm?.invalid || !theForm?.dirty"
35 35 mat-fab
36 36 matTooltip="{{ 'action.apply-changes' | translate }}"
37 37 matTooltipPosition="above"
... ... @@ -40,7 +40,7 @@
40 40 (click)="onApplyDetails()">
41 41 <mat-icon class="material-icons">done</mat-icon>
42 42 </button>
43   - <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)"
  43 + <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
44 44 mat-fab
45 45 matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
46 46 matTooltipPosition="above"
... ...
... ... @@ -32,7 +32,18 @@ export class DetailsPanelComponent extends PageComponent {
32 32 @Input() headerSubtitle = '';
33 33 @Input() isReadOnly = false;
34 34 @Input() isAlwaysEdit = false;
35   - @Input() theForm: NgForm;
  35 +
  36 + theFormValue: NgForm;
  37 +
  38 + @Input()
  39 + set theForm(value: NgForm) {
  40 + this.theFormValue = value;
  41 + }
  42 +
  43 + get theForm(): NgForm {
  44 + return this.theFormValue;
  45 + }
  46 +
36 47 @Output()
37 48 closeDetails = new EventEmitter<void>();
38 49 @Output()
... ... @@ -47,7 +58,7 @@ export class DetailsPanelComponent extends PageComponent {
47 58
48 59 @Input()
49 60 get isEdit() {
50   - return this.isEditValue;
  61 + return this.isAlwaysEdit || this.isEditValue;
51 62 }
52 63
53 64 set isEdit(val: boolean) {
... ... @@ -72,7 +83,7 @@ export class DetailsPanelComponent extends PageComponent {
72 83 }
73 84
74 85 onApplyDetails() {
75   - if (this.theForm.valid) {
  86 + if (this.theForm && this.theForm.valid) {
76 87 this.applyDetails.emit();
77 88 }
78 89 }
... ...
... ... @@ -40,6 +40,7 @@ import { WidgetComponentService } from './widget/widget-component.service';
40 40 import { LegendComponent } from '@home/components/widget/legend.component';
41 41 import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
42 42 import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
  43 +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
43 44
44 45 @NgModule({
45 46 entryComponents: [
... ... @@ -76,7 +77,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent
76 77 AliasesEntitySelectComponent,
77 78 DashboardComponent,
78 79 WidgetComponent,
79   - LegendComponent
  80 + LegendComponent,
  81 + WidgetConfigComponent
80 82 ],
81 83 imports: [
82 84 CommonModule,
... ... @@ -97,7 +99,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent
97 99 AliasesEntitySelectComponent,
98 100 DashboardComponent,
99 101 WidgetComponent,
100   - LegendComponent
  102 + LegendComponent,
  103 + WidgetConfigComponent
101 104 ],
102 105 providers: [
103 106 WidgetComponentService
... ...
... ... @@ -48,7 +48,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
48 48 }
49 49
50 50 ngOnDestroy(): void {
51   - console.log('Widget component destroyed!');
  51 +
52 52 }
53 53
54 54 clearRpcError() {
... ...
... ... @@ -29,7 +29,7 @@ import {
29 29 import cssjs from '@core/css/css';
30 30 import { UtilsService } from '@core/services/utils.service';
31 31 import { ResourcesService } from '@core/services/resources.service';
32   -import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models';
  32 +import { Widget, widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models';
33 33 import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
34 34 import { isFunction, isUndefined } from '@core/utils';
35 35 import { TranslateService } from '@ngx-translate/core';
... ... @@ -130,6 +130,15 @@ export class WidgetComponentService {
130 130 }
131 131 }
132 132
  133 + public getInstantWidgetInfo(widget: Widget): WidgetInfo {
  134 + const widgetInfo = this.getWidgetInfoFromCache(widget.bundleAlias, widget.typeAlias, widget.isSystemType);
  135 + if (widgetInfo) {
  136 + return widgetInfo;
  137 + } else {
  138 + return {} as WidgetInfo;
  139 + }
  140 + }
  141 +
133 142 public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> {
134 143 return this.init().pipe(
135 144 mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem))
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-tab-group [ngClass]="{'tb-headless': (widgetType === widgetTypes.static && !displayAdvanced())}"
  19 + fxFlex class="tb-widget-config tb-absolute-fill" [selectedIndex]="selectedTab">
  20 + <mat-tab label="{{ 'widget-config.data' | translate }}" [fxShow]="widgetType !== widgetTypes.static">
  21 + <div class="mat-content mat-padding" fxLayout="column">
  22 + <div [fxShow]="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm"
  23 + fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center">
  24 + <div fxLayout="column" fxFlex>
  25 + <mat-checkbox fxFlex [(ngModel)]="useDashboardTimewindow" (ngModelChange)="updateModel()">
  26 + {{ 'widget-config.use-dashboard-timewindow' | translate }}
  27 + </mat-checkbox>
  28 + <mat-checkbox [disabled]="useDashboardTimewindow" fxFlex [(ngModel)]="displayTimewindow" (ngModelChange)="updateModel()">
  29 + {{ 'widget-config.display-timewindow' | translate }}
  30 + </mat-checkbox>
  31 + </div>
  32 + <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">
  33 + <span [ngClass]="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
  34 + <tb-timewindow [disabled]="useDashboardTimewindow" asButton="true"
  35 + aggregation="{{ widgetType === widgetTypes.timeseries }}"
  36 + fxFlex [(ngModel)]="timewindow" (ngModelChange)="updateModel()"></tb-timewindow>
  37 + </section>
  38 + </div>
  39 + </div>
  40 + </mat-tab>
  41 + <mat-tab label="{{ 'widget-config.settings' | translate }}">
  42 + </mat-tab>
  43 +</mat-tab-group>
... ...
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .tb-widget-config {
  18 + .tb-advanced-widget-config {
  19 + height: 100%;
  20 + }
  21 + }
  22 +}
  23 +
  24 +:host ::ng-deep {
  25 + .tb-widget-config {
  26 + .mat-tab-body.mat-tab-body-active {
  27 + .mat-tab-body-content > div {
  28 + height: 100%;
  29 + }
  30 + }
  31 + }
  32 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnInit } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import {
  22 + Datasource,
  23 + LegendConfig,
  24 + WidgetActionDescriptor,
  25 + WidgetActionSource, WidgetConfigSettings,
  26 + widgetType,
  27 + WidgetTypeParameters
  28 +} from '@shared/models/widget.models';
  29 +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
  30 +import { WidgetConfigComponentData } from '@home/models/widget-component.models';
  31 +import { deepClone, isDefined } from '@app/core/utils';
  32 +import { Timewindow } from '@shared/models/time/time.models';
  33 +import { AlarmSearchStatus } from '@shared/models/alarm.models';
  34 +import { IAliasController } from '@core/api/widget-api.models';
  35 +import { EntityAlias } from '@shared/models/alias.models';
  36 +
  37 +@Component({
  38 + selector: 'tb-widget-config',
  39 + templateUrl: './widget-config.component.html',
  40 + styleUrls: ['./widget-config.component.scss'],
  41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => WidgetConfigComponent),
  45 + multi: true
  46 + },
  47 + {
  48 + provide: NG_VALIDATORS,
  49 + useExisting: forwardRef(() => WidgetConfigComponent),
  50 + multi: true,
  51 + }
  52 + ]
  53 +})
  54 +export class WidgetConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator {
  55 +
  56 + widgetTypes = widgetType;
  57 +
  58 + @Input()
  59 + forceExpandDatasources: boolean;
  60 +
  61 + @Input()
  62 + isDataEnabled: boolean;
  63 +
  64 + @Input()
  65 + widgetType: widgetType;
  66 +
  67 + @Input()
  68 + typeParameters: WidgetTypeParameters;
  69 +
  70 + @Input()
  71 + actionSources: {[key: string]: WidgetActionSource};
  72 +
  73 + @Input()
  74 + aliasController: IAliasController;
  75 +
  76 + @Input()
  77 + widgetSettingsSchema: any;
  78 +
  79 + @Input()
  80 + dataKeySettingsSchema: any;
  81 +
  82 + @Input()
  83 + functionsOnly: boolean;
  84 +
  85 + @Input() disabled: boolean;
  86 +
  87 + selectedTab: number;
  88 + title: string;
  89 + showTitleIcon: boolean;
  90 + titleIcon: string;
  91 + iconColor: string;
  92 + iconSize: string;
  93 + showTitle: boolean;
  94 + dropShadow: boolean;
  95 + enableFullscreen: boolean;
  96 + backgroundColor: string;
  97 + color: string;
  98 + padding: string;
  99 + margin: string;
  100 + widgetStyle: string;
  101 + titleStyle: string;
  102 + units: string;
  103 + decimals: number;
  104 + useDashboardTimewindow: boolean;
  105 + displayTimewindow: boolean;
  106 + timewindow: Timewindow;
  107 + showLegend: boolean;
  108 + legendConfig: LegendConfig;
  109 + actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
  110 + datasources: Array<Datasource>;
  111 + targetDeviceAlias: EntityAlias;
  112 + alarmSource: Datasource;
  113 + alarmSearchStatus: AlarmSearchStatus;
  114 + alarmsPollingInterval: number;
  115 + settings: WidgetConfigSettings;
  116 + mobileOrder: number;
  117 + mobileHeight: number;
  118 +
  119 + emptySettingsSchema = {
  120 + type: 'object',
  121 + properties: {}
  122 + };
  123 +
  124 + emptySettingsGroupInfoes = [];
  125 +
  126 + defaultSettingsForm = [
  127 + '*'
  128 + ];
  129 +
  130 + currentSettingsSchema = deepClone(this.emptySettingsSchema);
  131 +
  132 + currentSettings: WidgetConfigSettings = {};
  133 + currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);
  134 +
  135 + currentSettingsForm: any;
  136 +
  137 + private modelValue: WidgetConfigComponentData;
  138 +
  139 + private propagateChange = null;
  140 +
  141 + constructor(protected store: Store<AppState>) {
  142 + super(store);
  143 + }
  144 +
  145 + ngOnInit(): void {
  146 + }
  147 +
  148 + registerOnChange(fn: any): void {
  149 + this.propagateChange = fn;
  150 + }
  151 +
  152 + registerOnTouched(fn: any): void {
  153 + }
  154 +
  155 + setDisabledState(isDisabled: boolean): void {
  156 + this.disabled = isDisabled;
  157 + }
  158 +
  159 + writeValue(value: WidgetConfigComponentData): void {
  160 + this.modelValue = value;
  161 + if (this.modelValue) {
  162 + const config = this.modelValue.config;
  163 + const layout = this.modelValue.layout;
  164 + if (config) {
  165 + this.selectedTab = 0;
  166 + this.title = config.title;
  167 + this.showTitleIcon = isDefined(config.showTitleIcon) ? config.showTitleIcon : false;
  168 + this.titleIcon = isDefined(config.titleIcon) ? config.titleIcon : '';
  169 + this.iconColor = isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)';
  170 + this.iconSize = isDefined(config.iconSize) ? config.iconSize : '24px';
  171 + this.showTitle = config.showTitle;
  172 + this.dropShadow = isDefined(config.dropShadow) ? config.dropShadow : true;
  173 + this.enableFullscreen = isDefined(config.enableFullscreen) ? config.enableFullscreen : true;
  174 + this.backgroundColor = config.backgroundColor;
  175 + this.color = config.color;
  176 + this.padding = config.padding;
  177 + this.margin = config.margin;
  178 + this.widgetStyle =
  179 + JSON.stringify(isDefined(config.widgetStyle) ? config.widgetStyle : {}, null, 2);
  180 + this.titleStyle =
  181 + JSON.stringify(isDefined(config.titleStyle) ? config.titleStyle : {
  182 + fontSize: '16px',
  183 + fontWeight: 400
  184 + }, null, 2);
  185 + this.units = config.units;
  186 + this.decimals = config.decimals;
  187 + this.useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
  188 + config.useDashboardTimewindow : true;
  189 + this.displayTimewindow = isDefined(config.displayTimewindow) ?
  190 + config.displayTimewindow : true;
  191 + this.timewindow = config.timewindow;
  192 + this.actions = config.actions;
  193 + if (!this.actions) {
  194 + this.actions = {};
  195 + }
  196 + if (this.isDataEnabled) {
  197 + if (this.widgetType !== widgetType.rpc &&
  198 + this.widgetType !== widgetType.alarm &&
  199 + this.widgetType !== widgetType.static) {
  200 + if (config.datasources) {
  201 + this.datasources = config.datasources;
  202 + } else {
  203 + this.datasources = [];
  204 + }
  205 + } else if (this.widgetType === widgetType.rpc) {
  206 + if (config.targetDeviceAliasIds && config.targetDeviceAliasIds.length > 0) {
  207 + const aliasId = config.targetDeviceAliasIds[0];
  208 + const entityAliases = this.aliasController.getEntityAliases();
  209 + if (entityAliases[aliasId]) {
  210 + this.targetDeviceAlias = entityAliases[aliasId];
  211 + } else {
  212 + this.targetDeviceAlias = null;
  213 + }
  214 + } else {
  215 + this.targetDeviceAlias = null;
  216 + }
  217 + } else if (this.widgetType === widgetType.alarm) {
  218 + this.alarmSearchStatus = isDefined(config.alarmSearchStatus) ?
  219 + config.alarmSearchStatus : AlarmSearchStatus.ANY;
  220 + this.alarmsPollingInterval = isDefined(config.alarmsPollingInterval) ?
  221 + config.alarmsPollingInterval : 5;
  222 + if (config.alarmSource) {
  223 + this.alarmSource = config.alarmSource;
  224 + } else {
  225 + this.alarmSource = null;
  226 + }
  227 + }
  228 + }
  229 + this.settings = config.settings;
  230 +
  231 + this.updateSchemaForm();
  232 +
  233 + if (layout) {
  234 + this.mobileOrder = layout.mobileOrder;
  235 + this.mobileHeight = layout.mobileHeight;
  236 + } else {
  237 + this.mobileOrder = undefined;
  238 + this.mobileHeight = undefined;
  239 + }
  240 + }
  241 + }
  242 + }
  243 +
  244 + private updateSchemaForm() {
  245 + if (this.widgetSettingsSchema && this.widgetSettingsSchema.schema) {
  246 + this.currentSettingsSchema = this.widgetSettingsSchema.schema;
  247 + this.currentSettingsForm = this.widgetSettingsSchema.form || deepClone(this.defaultSettingsForm);
  248 + this.currentSettingsGroupInfoes = this.widgetSettingsSchema.groupInfoes;
  249 + this.currentSettings = this.settings;
  250 + } else {
  251 + this.currentSettingsForm = deepClone(this.defaultSettingsForm);
  252 + this.currentSettingsSchema = deepClone(this.emptySettingsSchema);
  253 + this.currentSettingsGroupInfoes = deepClone(this.emptySettingsGroupInfoes);
  254 + this.currentSettings = {};
  255 + }
  256 + }
  257 +
  258 + public updateModel() {
  259 + if (this.modelValue) {
  260 + if (this.modelValue.config) {
  261 + const config = this.modelValue.config;
  262 + config.useDashboardTimewindow = this.useDashboardTimewindow;
  263 + config.displayTimewindow = this.displayTimewindow;
  264 + config.timewindow = this.timewindow;
  265 + }
  266 + this.propagateChange(this.modelValue);
  267 + }
  268 + }
  269 +
  270 + public displayAdvanced(): boolean {
  271 + return this.widgetSettingsSchema && this.widgetSettingsSchema.schema;
  272 + }
  273 +
  274 + public validate(c: FormControl) {
  275 + return null; /*{
  276 + targetDeviceAliasIds: {
  277 + valid: false,
  278 + },
  279 + };*/
  280 + }
  281 +
  282 +}
... ...
... ... @@ -16,21 +16,22 @@
16 16
17 17 import {
18 18 AfterViewInit,
  19 + ChangeDetectionStrategy,
  20 + ChangeDetectorRef,
19 21 Component,
20 22 ComponentFactoryResolver,
21 23 ComponentRef,
22 24 ElementRef,
23 25 Injector,
24 26 Input,
  27 + NgZone,
25 28 OnChanges,
26 29 OnDestroy,
27 30 OnInit,
28 31 SimpleChanges,
29 32 ViewChild,
30 33 ViewContainerRef,
31   - ViewEncapsulation,
32   - ChangeDetectorRef,
33   - ChangeDetectionStrategy, NgZone
  34 + ViewEncapsulation
34 35 } from '@angular/core';
35 36 import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models';
36 37 import {
... ... @@ -52,7 +53,7 @@ import { AppState } from '@core/core.state';
52 53 import { WidgetService } from '@core/http/widget.service';
53 54 import { UtilsService } from '@core/services/utils.service';
54 55 import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs';
55   -import { isDefined, objToBase64, deepClone } from '@core/utils';
  56 +import { deepClone, isDefined, objToBase64 } from '@core/utils';
56 57 import {
57 58 IDynamicWidgetComponent,
58 59 WidgetContext,
... ... @@ -63,7 +64,8 @@ import {
63 64 import {
64 65 IWidgetSubscription,
65 66 StateObject,
66   - StateParams, SubscriptionEntityInfo,
  67 + StateParams,
  68 + SubscriptionEntityInfo,
67 69 SubscriptionInfo,
68 70 WidgetSubscriptionContext,
69 71 WidgetSubscriptionOptions
... ... @@ -86,7 +88,6 @@ import { DashboardService } from '@core/http/dashboard.service';
86 88 import { DatasourceService } from '@core/api/datasource.service';
87 89 import { WidgetSubscription } from '@core/api/widget-subscription';
88 90 import { EntityService } from '@core/http/entity.service';
89   -import { TimewindowComponent } from '@shared/components/time/timewindow.component';
90 91
91 92 @Component({
92 93 selector: 'tb-widget',
... ... @@ -362,10 +363,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
362 363 const change = changes[propName];
363 364 if (!change.firstChange && change.currentValue !== change.previousValue) {
364 365 if (propName === 'isEdit') {
365   - console.log(`isEdit changed: ${this.isEdit}`);
366 366 this.onEditModeChanged();
367 367 } else if (propName === 'isMobile') {
368   - console.log(`isMobile changed: ${this.isMobile}`);
369 368 this.onMobileModeChanged();
370 369 }
371 370 }
... ...
... ... @@ -14,30 +14,50 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { GridsterConfig, GridsterItem, GridsterComponent } from 'angular-gridster2';
  17 +import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
18 18 import { Widget, widgetType } from '@app/shared/models/widget.models';
19 19 import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
20 20 import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
21 21 import { Timewindow } from '@shared/models/time/time.models';
22   -import { Observable } from 'rxjs';
  22 +import { Observable, of, Subject } from 'rxjs';
23 23 import { isDefined, isUndefined } from '@app/core/utils';
24   -import { EventEmitter } from '@angular/core';
25   -import { EntityId } from '@app/shared/models/id/entity-id';
26   -import { IAliasController, IStateController, TimewindowFunctions } from '@app/core/api/widget-api.models';
  24 +import { IterableDiffer, KeyValueDiffer } from '@angular/core';
  25 +import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
  26 +import * as deepEqual from 'deep-equal';
27 27
28 28 export interface WidgetsData {
29 29 widgets: Array<Widget>;
30 30 widgetLayouts?: WidgetLayouts;
31 31 }
32 32
  33 +export interface ContextMenuItem {
  34 + enabled: boolean;
  35 + shortcut?: string;
  36 + icon: string;
  37 + value: string;
  38 +}
  39 +
  40 +export interface DashboardContextMenuItem extends ContextMenuItem {
  41 + action: (contextMenuEvent: MouseEvent) => void;
  42 +}
  43 +
  44 +export interface WidgetContextMenuItem extends ContextMenuItem {
  45 + action: (contextMenuEvent: MouseEvent, widget: Widget) => void;
  46 +}
  47 +
33 48 export interface DashboardCallbacks {
34   - onEditWidget?: ($event: Event, widget: Widget) => void;
35   - onExportWidget?: ($event: Event, widget: Widget) => void;
36   - onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>;
37   - onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
38   - onWidgetClicked?: ($event: Event, widget: Widget) => void;
39   - prepareDashboardContextMenu?: ($event: Event) => void;
40   - prepareWidgetContextMenu?: ($event: Event, widget: Widget) => void;
  49 + onEditWidget?: ($event: Event, widget: Widget, index: number) => void;
  50 + onExportWidget?: ($event: Event, widget: Widget, index: number) => void;
  51 + onRemoveWidget?: ($event: Event, widget: Widget, index: number) => void;
  52 + onWidgetMouseDown?: ($event: Event, widget: Widget, index: number) => void;
  53 + onWidgetClicked?: ($event: Event, widget: Widget, index: number) => void;
  54 + prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
  55 + prepareWidgetContextMenu?: ($event: Event, widget: Widget, index: number) => Array<WidgetContextMenuItem>;
  56 +}
  57 +
  58 +export interface WidgetPosition {
  59 + row: number;
  60 + column: number;
41 61 }
42 62
43 63 export interface IDashboardComponent {
... ... @@ -53,60 +73,181 @@ export interface IDashboardComponent {
53 73 stateController: IStateController;
54 74 onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void;
55 75 onResetTimewindow(): void;
  76 + resetHighlight(): void;
  77 + highlightWidget(index: number, delay?: number);
  78 + selectWidget(index: number, delay?: number);
  79 + getSelectedWidget(): Widget;
  80 + getEventGridPosition(event: Event): WidgetPosition;
  81 +}
  82 +
  83 +declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update';
  84 +
  85 +interface DashboardWidgetUpdateRecord {
  86 + widget?: Widget;
  87 + widgetLayout?: WidgetLayout;
  88 + widgetIndex: number;
  89 + operation: DashboardWidgetUpdateOperation;
56 90 }
57 91
58 92 export class DashboardWidgets implements Iterable<DashboardWidget> {
59 93
  94 + highlightedMode = false;
  95 +
60 96 dashboardWidgets: Array<DashboardWidget> = [];
  97 + widgets: Array<Widget>;
  98 + widgetLayouts: WidgetLayouts;
61 99
62 100 [Symbol.iterator](): Iterator<DashboardWidget> {
63 101 return this.dashboardWidgets[Symbol.iterator]();
64 102 }
65 103
66   - constructor(private dashboard: IDashboardComponent) {
  104 + constructor(private dashboard: IDashboardComponent,
  105 + private widgetsDiffer: IterableDiffer<Widget>,
  106 + private widgetLayoutsDiffer: KeyValueDiffer<string, WidgetLayout>) {
67 107 }
68 108
69   - setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
70   - let maxRows = this.dashboard.gridsterOpts.maxRows;
71   - this.dashboardWidgets.length = 0;
72   - widgets.forEach((widget) => {
73   - let widgetLayout: WidgetLayout;
74   - if (widgetLayouts && widget.id) {
75   - widgetLayout = widgetLayouts[widget.id];
  109 + doCheck() {
  110 + const widgetChange = this.widgetsDiffer.diff(this.widgets);
  111 + if (widgetChange !== null) {
  112 +
  113 + const layouts: WidgetLayouts = {};
  114 + const updateRecords: Array<DashboardWidgetUpdateRecord> = [];
  115 +
  116 + const widgetLayoutChange = this.widgetLayoutsDiffer.diff(this.widgetLayouts);
  117 + if (widgetLayoutChange !== null) {
  118 + widgetLayoutChange.forEachAddedItem((added) => {
  119 + layouts[added.key] = added.currentValue;
  120 + });
  121 + widgetLayoutChange.forEachChangedItem((changed) => {
  122 + layouts[changed.key] = changed.currentValue;
  123 + });
76 124 }
77   - const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
78   - const bottom = dashboardWidget.y + dashboardWidget.rows;
79   - maxRows = Math.max(maxRows, bottom);
80   - this.dashboardWidgets.push(dashboardWidget);
81   - });
82   - this.sortWidgets();
83   - this.dashboard.gridsterOpts.maxRows = maxRows;
  125 + widgetChange.forEachAddedItem((added) => {
  126 + updateRecords.push({
  127 + widget: added.item,
  128 + widgetLayout: layouts[added.item.id],
  129 + widgetIndex: added.currentIndex,
  130 + operation: 'add'
  131 + });
  132 + });
  133 + widgetChange.forEachRemovedItem((removed) => {
  134 + let operation = updateRecords.find((record) => record.widgetIndex === removed.previousIndex);
  135 + if (operation) {
  136 + operation.operation = 'update';
  137 + } else {
  138 + operation = {
  139 + widgetIndex: removed.previousIndex,
  140 + operation: 'remove'
  141 + };
  142 + updateRecords.push(operation);
  143 + }
  144 + });
  145 + updateRecords.forEach((record) => {
  146 + switch (record.operation) {
  147 + case 'add':
  148 + this.dashboardWidgets.push(
  149 + new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout)
  150 + );
  151 + break;
  152 + case 'remove':
  153 + let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex);
  154 + if (index > -1) {
  155 + this.dashboardWidgets.splice(index, 1);
  156 + }
  157 + break;
  158 + case 'update':
  159 + index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex);
  160 + if (index > -1) {
  161 + const prevDashboardWidget = this.dashboardWidgets[index];
  162 + if (!deepEqual(prevDashboardWidget.widget, record.widget)) {
  163 + this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout);
  164 + this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
  165 + this.dashboardWidgets[index].selected = prevDashboardWidget.selected;
  166 + } else {
  167 + this.dashboardWidgets[index].widget = record.widget;
  168 + this.dashboardWidgets[index].widgetLayout = record.widgetLayout;
  169 + }
  170 + }
  171 + break;
  172 + }
  173 + });
  174 + if (updateRecords.length) {
  175 + this.updateRowsAndSort();
  176 + }
  177 + }
84 178 }
85 179
86   - addWidget(widget: Widget, widgetLayout: WidgetLayout) {
87   - const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
88   - let maxRows = this.dashboard.gridsterOpts.maxRows;
89   - const bottom = dashboardWidget.y + dashboardWidget.rows;
90   - maxRows = Math.max(maxRows, bottom);
91   - this.dashboardWidgets.push(dashboardWidget);
92   - this.sortWidgets();
93   - this.dashboard.gridsterOpts.maxRows = maxRows;
  180 + setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
  181 + this.highlightedMode = false;
  182 + this.widgets = widgets;
  183 + this.widgetLayouts = widgetLayouts;
94 184 }
95 185
96   - removeWidget(widget: Widget): boolean {
97   - const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widget === widget);
98   - if (index > -1) {
99   - this.dashboardWidgets.splice(index, 1);
100   - let maxRows = this.dashboard.gridsterOpts.maxRows;
  186 + highlightWidget(index: number): DashboardWidget {
  187 + const widget = this.findWidgetAtIndex(index);
  188 + if (widget && (!this.highlightedMode || !widget.highlighted)) {
  189 + this.highlightedMode = true;
  190 + widget.highlighted = true;
101 191 this.dashboardWidgets.forEach((dashboardWidget) => {
102   - const bottom = dashboardWidget.y + dashboardWidget.rows;
103   - maxRows = Math.max(maxRows, bottom);
  192 + if (dashboardWidget !== widget) {
  193 + dashboardWidget.highlighted = false;
  194 + }
104 195 });
105   - this.sortWidgets();
106   - this.dashboard.gridsterOpts.maxRows = maxRows;
107   - return true;
  196 + return widget;
  197 + } else {
  198 + return null;
108 199 }
109   - return false;
  200 + }
  201 +
  202 + selectWidget(index: number): DashboardWidget {
  203 + const widget = this.findWidgetAtIndex(index);
  204 + if (widget && (!widget.selected)) {
  205 + widget.selected = true;
  206 + this.dashboardWidgets.forEach((dashboardWidget) => {
  207 + if (dashboardWidget !== widget) {
  208 + dashboardWidget.selected = false;
  209 + }
  210 + });
  211 + return widget;
  212 + } else {
  213 + return null;
  214 + }
  215 + }
  216 +
  217 + resetHighlight(): DashboardWidget {
  218 + const highlighted = this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.highlighted);
  219 + this.highlightedMode = false;
  220 + this.dashboardWidgets.forEach((dashboardWidget) => {
  221 + dashboardWidget.highlighted = false;
  222 + dashboardWidget.selected = false;
  223 + });
  224 + return highlighted;
  225 + }
  226 +
  227 + isHighlighted(widget: DashboardWidget): boolean {
  228 + return (this.highlightedMode && widget.highlighted) || (widget.selected);
  229 + }
  230 +
  231 + isNotHighlighted(widget: DashboardWidget): boolean {
  232 + return this.highlightedMode && !widget.highlighted;
  233 + }
  234 +
  235 + getSelectedWidget(): DashboardWidget {
  236 + return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.selected);
  237 + }
  238 +
  239 + private findWidgetAtIndex(index: number): DashboardWidget {
  240 + return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetIndex === index);
  241 + }
  242 +
  243 + private updateRowsAndSort() {
  244 + let maxRows = this.dashboard.gridsterOpts.maxRows;
  245 + this.dashboardWidgets.forEach((dashboardWidget) => {
  246 + const bottom = dashboardWidget.y + dashboardWidget.rows;
  247 + maxRows = Math.max(maxRows, bottom);
  248 + });
  249 + this.sortWidgets();
  250 + this.dashboard.gridsterOpts.maxRows = maxRows;
110 251 }
111 252
112 253 sortWidgets() {
... ... @@ -125,6 +266,9 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
125 266
126 267 export class DashboardWidget implements GridsterItem {
127 268
  269 + highlighted = false;
  270 + selected = false;
  271 +
128 272 isFullscreen = false;
129 273
130 274 color: string;
... ... @@ -160,13 +304,31 @@ export class DashboardWidget implements GridsterItem {
160 304
161 305 widgetContext: WidgetContext = {};
162 306
  307 + private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
  308 + private gridsterItemComponentValue: GridsterItemComponentInterface;
  309 +
  310 + set gridsterItemComponent(item: GridsterItemComponentInterface) {
  311 + this.gridsterItemComponentValue = item;
  312 + this.gridsterItemComponentSubject.next(this.gridsterItemComponentValue);
  313 + this.gridsterItemComponentSubject.complete();
  314 + }
  315 +
163 316 constructor(
164 317 private dashboard: IDashboardComponent,
165 318 public widget: Widget,
166   - private widgetLayout?: WidgetLayout) {
  319 + public widgetIndex: number,
  320 + public widgetLayout?: WidgetLayout) {
167 321 this.updateWidgetParams();
168 322 }
169 323
  324 + gridsterItemComponent$(): Observable<GridsterItemComponentInterface> {
  325 + if (this.gridsterItemComponentValue) {
  326 + return of(this.gridsterItemComponentValue);
  327 + } else {
  328 + return this.gridsterItemComponentSubject.asObservable();
  329 + }
  330 + }
  331 +
170 332 updateWidgetParams() {
171 333 this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)';
172 334 this.backgroundColor = this.widget.config.backgroundColor || '#fff';
... ... @@ -221,11 +383,13 @@ export class DashboardWidget implements GridsterItem {
221 383 }
222 384
223 385 get x(): number {
  386 + let res;
224 387 if (this.widgetLayout) {
225   - return this.widgetLayout.col;
  388 + res = this.widgetLayout.col;
226 389 } else {
227   - return this.widget.col;
  390 + res = this.widget.col;
228 391 }
  392 + return Math.floor(res);
229 393 }
230 394
231 395 set x(x: number) {
... ... @@ -239,11 +403,13 @@ export class DashboardWidget implements GridsterItem {
239 403 }
240 404
241 405 get y(): number {
  406 + let res;
242 407 if (this.widgetLayout) {
243   - return this.widgetLayout.row;
  408 + res = this.widgetLayout.row;
244 409 } else {
245   - return this.widget.row;
  410 + res = this.widget.row;
246 411 }
  412 + return Math.floor(res);
247 413 }
248 414
249 415 set y(y: number) {
... ... @@ -257,11 +423,13 @@ export class DashboardWidget implements GridsterItem {
257 423 }
258 424
259 425 get cols(): number {
  426 + let res;
260 427 if (this.widgetLayout) {
261   - return this.widgetLayout.sizeX;
  428 + res = this.widgetLayout.sizeX;
262 429 } else {
263   - return this.widget.sizeX;
  430 + res = this.widget.sizeX;
264 431 }
  432 + return Math.floor(res);
265 433 }
266 434
267 435 set cols(cols: number) {
... ... @@ -275,6 +443,7 @@ export class DashboardWidget implements GridsterItem {
275 443 }
276 444
277 445 get rows(): number {
  446 + let res;
278 447 if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) {
279 448 let mobileHeight;
280 449 if (this.widgetLayout) {
... ... @@ -284,17 +453,18 @@ export class DashboardWidget implements GridsterItem {
284 453 mobileHeight = this.widget.config.mobileHeight;
285 454 }
286 455 if (mobileHeight) {
287   - return mobileHeight;
  456 + res = mobileHeight;
288 457 } else {
289   - return this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
  458 + res = this.widget.sizeY * 24 / this.dashboard.gridsterOpts.minCols;
290 459 }
291 460 } else {
292 461 if (this.widgetLayout) {
293   - return this.widgetLayout.sizeY;
  462 + res = this.widgetLayout.sizeY;
294 463 } else {
295   - return this.widget.sizeY;
  464 + res = this.widget.sizeY;
296 465 }
297 466 }
  467 + return Math.floor(res);
298 468 }
299 469
300 470 set rows(rows: number) {
... ...
... ... @@ -45,6 +45,7 @@ import { HttpErrorResponse } from '@angular/common/http';
45 45 import { RafService } from '@core/services/raf.service';
46 46 import { WidgetTypeId } from '@shared/models/id/widget-type-id';
47 47 import { TenantId } from '@shared/models/id/tenant-id';
  48 +import { WidgetLayout } from '@shared/models/dashboard.models';
48 49
49 50 export interface IWidgetAction {
50 51 name: string;
... ... @@ -112,11 +113,16 @@ export interface IDynamicWidgetComponent {
112 113 export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
113 114 widgetName: string;
114 115 alias: string;
115   - typeSettingsSchema?: string;
116   - typeDataKeySettingsSchema?: string;
  116 + typeSettingsSchema?: string | any;
  117 + typeDataKeySettingsSchema?: string | any;
117 118 componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
118 119 }
119 120
  121 +export interface WidgetConfigComponentData {
  122 + config: WidgetConfig;
  123 + layout: WidgetLayout;
  124 +}
  125 +
120 126 export const MissingWidgetType: WidgetInfo = {
121 127 type: widgetType.latest,
122 128 widgetName: 'Widget type not found',
... ...
... ... @@ -138,13 +138,14 @@
138 138 [layoutCtx]="layouts.main.layoutCtx"
139 139 [dashboardCtx]="dashboardCtx"
140 140 [isEdit]="isEdit"
  141 + [isEditingWidget]="isEditingWidget"
141 142 [isMobile]="forceDashboardMobileMode"
142 143 [widgetEditMode]="widgetEditMode">
143 144 </tb-dashboard-layout>
144 145 </div>
145   - <mat-sidenav-container *ngIf="layouts.right.show"
  146 + <mat-drawer-container *ngIf="layouts.right.show"
146 147 id="tb-right-layout">
147   - <mat-sidenav
  148 + <mat-drawer
148 149 [ngStyle]="{minWidth: rightLayoutWidth(),
149 150 maxWidth: rightLayoutWidth(),
150 151 height: rightLayoutHeight(),
... ... @@ -157,13 +158,43 @@
157 158 [layoutCtx]="layouts.right.layoutCtx"
158 159 [dashboardCtx]="dashboardCtx"
159 160 [isEdit]="isEdit"
  161 + [isEditingWidget]="isEditingWidget"
160 162 [isMobile]="forceDashboardMobileMode"
161 163 [widgetEditMode]="widgetEditMode">
162 164 </tb-dashboard-layout>
163   - </mat-sidenav>
164   - </mat-sidenav-container>
  165 + </mat-drawer>
  166 + </mat-drawer-container>
165 167 </div>
166   - <!--tb-details-sidenav TODO -->
  168 + <mat-drawer-container hasBackdrop="false" class="tb-widget-details-sidenav">
  169 + <mat-drawer class="tb-details-drawer"
  170 + [opened]="isEditingWidget"
  171 + mode="over"
  172 + position="end">
  173 + <tb-details-panel fxFlex
  174 + headerTitle="{{editingWidget?.config.title}}"
  175 + headerSubtitle="{{ editingWidgetSubtitle }}"
  176 + [isReadOnly]="false"
  177 + [isAlwaysEdit]="true"
  178 + (closeDetails)="onEditWidgetClosed()"
  179 + (toggleDetailsEditMode)="onRevertWidgetEdit()"
  180 + (applyDetails)="saveWidget()"
  181 + [theForm]="widgetForm">
  182 + <div class="details-buttons">
  183 + <div [tb-help]="helpLinkIdForWidgetType()"></div>
  184 + </div>
  185 + <form #widgetForm="ngForm" [formGroup]="editingWidgetFormGroup">
  186 + <tb-edit-widget *ngIf="isEditingWidget"
  187 + [dashboard]="dashboard"
  188 + [aliasController]="dashboardCtx.aliasController"
  189 + [widgetEditMode]="widgetEditMode"
  190 + [widget]="editingWidget"
  191 + [widgetLayout]="editingWidgetLayout"
  192 + [widgetFormGroup]="editingWidgetFormGroup">
  193 + </tb-edit-widget>
  194 + </form>
  195 + </tb-details-panel>
  196 + </mat-drawer>
  197 + </mat-drawer-container>
167 198 <!--tb-details-sidenav TODO -->
168 199 <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
169 200 <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode"
... ...
... ... @@ -28,6 +28,7 @@ tb-dashboard-page {
28 28
29 29 div.tb-dashboard-page {
30 30 &.mat-content {
  31 + overflow: hidden;
31 32 background-color: #eee;
32 33 }
33 34 section.tb-dashboard-title {
... ... @@ -108,13 +109,30 @@ div.tb-dashboard-page {
108 109 z-index: 1;
109 110 }*/
110 111 #tb-right-layout {
111   - mat-sidenav {
  112 + mat-drawer {
112 113 z-index: 1;
113 114 }
114 115 }
115 116 }
116 117 }
117 118
  119 + mat-drawer-container.tb-widget-details-sidenav {
  120 + position: initial;
  121 + mat-drawer.tb-details-drawer {
  122 + @media #{$mat-gt-sm} {
  123 + width: 85% !important;
  124 + }
  125 +
  126 + @media #{$mat-gt-md} {
  127 + width: 75% !important;
  128 + }
  129 +
  130 + @media #{$mat-gt-xl} {
  131 + width: 60% !important;
  132 + }
  133 + }
  134 + }
  135 +
118 136 section.tb-powered-by-footer {
119 137 position: absolute;
120 138 right: 25px;
... ...
... ... @@ -14,7 +14,7 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
  17 +import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation, ViewChild, NgZone } from '@angular/core';
18 18 import { PageComponent } from '@shared/components/page.component';
19 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
... ... @@ -41,17 +41,25 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
41 41 import { MediaBreakpoints } from '@shared/models/constants';
42 42 import { AuthUser } from '@shared/models/user.model';
43 43 import { getCurrentAuthUser } from '@core/auth/auth.selectors';
44   -import { Widget } from '@app/shared/models/widget.models';
  44 +import { Widget, widgetTypesData } from '@app/shared/models/widget.models';
45 45 import { environment as env } from '@env/environment';
46 46 import { Authority } from '@shared/models/authority.enum';
47 47 import { DialogService } from '@core/services/dialog.service';
48 48 import { EntityService } from '@core/http/entity.service';
49 49 import { AliasController } from '@core/api/alias-controller';
50   -import { Subscription } from 'rxjs';
  50 +import { Observable, Subscription, of } from 'rxjs';
51 51 import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
52 52 import { IStateController } from '@core/api/widget-api.models';
53 53 import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
54 54 import { DashboardService } from '@core/http/dashboard.service';
  55 +import {
  56 + WidgetContextMenuItem,
  57 + DashboardContextMenuItem,
  58 + IDashboardComponent, WidgetPosition
  59 +} from '../../models/dashboard-component.models';
  60 +import { WidgetComponentService } from '../../components/widget/widget-component.service';
  61 +import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
  62 +import { ItemBufferService } from '@core/services/item-buffer.service';
55 63
56 64 @Component({
57 65 selector: 'tb-dashboard-page',
... ... @@ -89,6 +97,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
89 97 editingWidgetLayoutOriginal: WidgetLayout = null;
90 98 editingWidgetSubtitle: string = null;
91 99 editingLayoutCtx: DashboardPageLayoutContext = null;
  100 + editingWidgetFormGroup: FormGroup;
92 101
93 102 thingsboardVersion: string = env.tbVersion;
94 103
... ... @@ -105,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
105 114 widgetLayouts: {},
106 115 gridSettings: {},
107 116 ignoreLoading: false,
108   - ctrl: null
  117 + ctrl: null,
  118 + dashboardCtrl: this
109 119 }
110 120 },
111 121 right: {
... ... @@ -116,7 +126,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
116 126 widgetLayouts: {},
117 127 gridSettings: {},
118 128 ignoreLoading: false,
119   - ctrl: null
  129 + ctrl: null,
  130 + dashboardCtrl: this
120 131 }
121 132 }
122 133 };
... ... @@ -175,9 +186,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
175 186 private authService: AuthService,
176 187 private entityService: EntityService,
177 188 private dialogService: DialogService,
178   - private dashboardService: DashboardService) {
  189 + private widgetComponentService: WidgetComponentService,
  190 + private dashboardService: DashboardService,
  191 + private itembuffer: ItemBufferService,
  192 + private fb: FormBuilder) {
179 193 super(store);
180 194
  195 + this.editingWidgetFormGroup = this.fb.group({
  196 + widgetConfig: [null]
  197 + });
  198 +
181 199 this.rxSubscriptions.push(this.route.data.subscribe(
182 200 (data) => {
183 201 this.init(data);
... ... @@ -253,6 +271,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
253 271 this.currentDashboardId = null;
254 272 this.currentCustomerId = null;
255 273 this.currentDashboardScope = null;
  274 +
  275 + this.dashboardCtx.state = null;
256 276 }
257 277
258 278 ngOnDestroy(): void {
... ... @@ -428,14 +448,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
428 448 this.dialogService.todo();
429 449 }
430 450
431   - private addWidget($event: Event) {
432   - if ($event) {
433   - $event.stopPropagation();
434   - }
435   - // TODO:
436   - this.dialogService.todo();
437   - }
438   -
439 451 private importWidget($event: Event) {
440 452 if ($event) {
441 453 $event.stopPropagation();
... ... @@ -568,7 +580,221 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
568 580 };
569 581 this.window.parent.postMessage(JSON.stringify(message), '*');
570 582 } else {
571   - this.dashboardService.saveDashboard(this.dashboard);
  583 + this.dashboardService.saveDashboard(this.dashboard).subscribe();
  584 + }
  585 + }
  586 +
  587 + helpLinkIdForWidgetType(): string {
  588 + let link = 'widgetsConfig';
  589 + if (this.editingWidget && this.editingWidget.type) {
  590 + link = widgetTypesData.get(this.editingWidget.type).configHelpLinkId;
  591 + }
  592 + return link;
  593 + }
  594 +
  595 + addWidget($event: Event, layoutCtx?: DashboardPageLayoutContext) {
  596 + if ($event) {
  597 + $event.stopPropagation();
  598 + }
  599 + // TODO:
  600 + this.dialogService.todo();
  601 + }
  602 +
  603 + onRevertWidgetEdit() {
  604 + if (this.editingWidgetFormGroup.dirty) {
  605 + this.editingWidgetFormGroup.markAsPristine();
  606 + this.editingWidget = deepClone(this.editingWidgetOriginal);
  607 + this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
  608 + }
  609 + }
  610 +
  611 + saveWidget() {
  612 + this.editingWidgetFormGroup.markAsPristine();
  613 + const widget = deepClone(this.editingWidget);
  614 + const widgetLayout = deepClone(this.editingWidgetLayout);
  615 + const id = this.editingWidgetOriginal.id;
  616 + const index = this.editingLayoutCtx.widgets.indexOf(this.editingWidgetOriginal);
  617 + this.dashboardConfiguration.widgets[id] = widget;
  618 + this.editingWidgetOriginal = widget;
  619 + this.editingWidgetLayoutOriginal = widgetLayout;
  620 + this.editingLayoutCtx.widgets[index] = widget;
  621 + this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout;
  622 + this.editingLayoutCtx.ctrl.highlightWidget(index, 0);
  623 + }
  624 +
  625 + onEditWidgetClosed() {
  626 + this.editingWidgetOriginal = null;
  627 + this.editingWidget = null;
  628 + this.editingWidgetLayoutOriginal = null;
  629 + this.editingWidgetLayout = null;
  630 + this.editingLayoutCtx = null;
  631 + this.editingWidgetSubtitle = null;
  632 + this.isEditingWidget = false;
  633 + this.resetHighlight();
  634 + this.forceDashboardMobileMode = false;
  635 + }
  636 +
  637 + editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
  638 + $event.stopPropagation();
  639 + if (this.editingWidgetOriginal === widget) {
  640 + this.onEditWidgetClosed();
  641 + } else {
  642 + const transition = !this.forceDashboardMobileMode;
  643 + this.editingWidgetOriginal = widget;
  644 + this.editingWidgetLayoutOriginal = layoutCtx.widgetLayouts[widget.id];
  645 + this.editingWidget = deepClone(this.editingWidgetOriginal);
  646 + this.editingWidgetLayout = deepClone(this.editingWidgetLayoutOriginal);
  647 + this.editingLayoutCtx = layoutCtx;
  648 + this.editingWidgetSubtitle = this.widgetComponentService.getInstantWidgetInfo(this.editingWidget).widgetName;
  649 + this.forceDashboardMobileMode = true;
  650 + this.isEditingWidget = true;
  651 + if (layoutCtx) {
  652 + const delayOffset = transition ? 350 : 0;
  653 + const delay = transition ? 400 : 300;
  654 + setTimeout(() => {
  655 + layoutCtx.ctrl.highlightWidget(index, delay);
  656 + }, delayOffset);
  657 + }
  658 + }
  659 + }
  660 +
  661 + copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
  662 + // TODO:
  663 + this.dialogService.todo();
  664 + }
  665 +
  666 + copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
  667 + // TODO:
  668 + this.dialogService.todo();
  669 + }
  670 +
  671 + pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
  672 + // TODO:
  673 + this.dialogService.todo();
  674 + }
  675 +
  676 + pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
  677 + // TODO:
  678 + this.dialogService.todo();
  679 + }
  680 +
  681 + removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
  682 + // TODO:
  683 + this.dialogService.todo();
  684 + }
  685 +
  686 + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
  687 + $event.stopPropagation();
  688 + // TODO:
  689 + this.dialogService.todo();
  690 + }
  691 +
  692 + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
  693 + if (this.isEditingWidget) {
  694 + this.editWidget($event, layoutCtx, widget, index);
  695 + }
  696 + }
  697 +
  698 + widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
  699 + if (this.isEdit && !this.isEditingWidget) {
  700 + layoutCtx.ctrl.selectWidget(index, 0);
  701 + }
  702 + }
  703 +
  704 + prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem> {
  705 + const dashboardContextActions: Array<DashboardContextMenuItem> = [];
  706 + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
  707 + dashboardContextActions.push(
  708 + {
  709 + action: this.openDashboardSettings.bind(this),
  710 + enabled: true,
  711 + value: 'dashboard.settings',
  712 + icon: 'settings'
  713 + }
  714 + );
  715 + dashboardContextActions.push(
  716 + {
  717 + action: this.openEntityAliases.bind(this),
  718 + enabled: true,
  719 + value: 'entity.aliases',
  720 + icon: 'devices_other'
  721 + }
  722 + );
  723 + dashboardContextActions.push(
  724 + {
  725 + action: ($event) => {
  726 + layoutCtx.ctrl.pasteWidget($event);
  727 + },
  728 + enabled: this.itembuffer.hasWidget(),
  729 + value: 'action.paste',
  730 + icon: 'content_paste',
  731 + shortcut: 'M-V'
  732 + }
  733 + );
  734 + dashboardContextActions.push(
  735 + {
  736 + action: ($event) => {
  737 + layoutCtx.ctrl.pasteWidgetReference($event);
  738 + },
  739 + enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id),
  740 + value: 'action.paste-reference',
  741 + icon: 'content_paste',
  742 + shortcut: 'M-I'
  743 + }
  744 + );
  745 + }
  746 + return dashboardContextActions;
  747 + }
  748 +
  749 + prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem> {
  750 + const widgetContextActions: Array<WidgetContextMenuItem> = [];
  751 + if (this.isEdit && !this.isEditingWidget) {
  752 + widgetContextActions.push(
  753 + {
  754 + action: (event, currentWidget) => {
  755 + this.editWidget(event, layoutCtx, currentWidget, index);
  756 + },
  757 + enabled: true,
  758 + value: 'action.edit',
  759 + icon: 'edit'
  760 + }
  761 + );
  762 + if (!this.widgetEditMode) {
  763 + widgetContextActions.push(
  764 + {
  765 + action: (event, currentWidget) => {
  766 + this.copyWidget(event, layoutCtx, currentWidget);
  767 + },
  768 + enabled: true,
  769 + value: 'action.copy',
  770 + icon: 'content_copy',
  771 + shortcut: 'M-C'
  772 + }
  773 + );
  774 + widgetContextActions.push(
  775 + {
  776 + action: (event, currentWidget) => {
  777 + this.copyWidgetReference(event, layoutCtx, currentWidget);
  778 + },
  779 + enabled: true,
  780 + value: 'action.copy-reference',
  781 + icon: 'content_copy',
  782 + shortcut: 'M-R'
  783 + }
  784 + );
  785 + widgetContextActions.push(
  786 + {
  787 + action: (event, currentWidget) => {
  788 + this.removeWidget(event, layoutCtx, currentWidget);
  789 + },
  790 + enabled: true,
  791 + value: 'action.delete',
  792 + icon: 'clear',
  793 + shortcut: 'M-X'
  794 + }
  795 + );
  796 + }
572 797 }
  798 + return widgetContextActions;
573 799 }
574 800 }
... ...
... ... @@ -19,6 +19,12 @@ import { Widget } from '@app/shared/models/widget.models';
19 19 import { Timewindow } from '@shared/models/time/time.models';
20 20 import { IAliasController, IStateController } from '@core/api/widget-api.models';
21 21 import { ILayoutController } from './layout/layout.models';
  22 +import {
  23 + DashboardContextMenuItem,
  24 + WidgetContextMenuItem,
  25 + WidgetPosition
  26 +} from '@home/models/dashboard-component.models';
  27 +import { Observable } from 'rxjs';
22 28
23 29 export declare type DashboardPageScope = 'tenant' | 'customer';
24 30
... ... @@ -34,6 +40,18 @@ export interface IDashboardController {
34 40 dashboardCtx: DashboardContext;
35 41 openRightLayout();
36 42 openDashboardState(stateId: string, openRightLayout: boolean);
  43 + addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
  44 + editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
  45 + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
  46 + removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
  47 + widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
  48 + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
  49 + prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>;
  50 + prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem>;
  51 + copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
  52 + copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
  53 + pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
  54 + pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
37 55 }
38 56
39 57 export interface DashboardPageLayoutContext {
... ... @@ -42,6 +60,7 @@ export interface DashboardPageLayoutContext {
42 60 widgetLayouts: WidgetLayouts;
43 61 gridSettings: GridSettings;
44 62 ctrl: ILayoutController;
  63 + dashboardCtrl: IDashboardController;
45 64 ignoreLoading: boolean;
46 65 }
47 66
... ...
... ... @@ -28,6 +28,7 @@ import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.com
28 28 import { DashboardToolbarComponent } from './dashboard-toolbar.component';
29 29 import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module';
30 30 import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
  31 +import { EditWidgetComponent } from './edit-widget.component';
31 32
32 33 @NgModule({
33 34 entryComponents: [
... ... @@ -43,7 +44,8 @@ import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
43 44 MakeDashboardPublicDialogComponent,
44 45 DashboardToolbarComponent,
45 46 DashboardPageComponent,
46   - DashboardLayoutComponent
  47 + DashboardLayoutComponent,
  48 + EditWidgetComponent
47 49 ],
48 50 imports: [
49 51 CommonModule,
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<form [formGroup]="widgetFormGroup">
  19 + <fieldset [disabled]="isLoading$ | async">
  20 + <tb-widget-config
  21 + [widgetType]="widget.type"
  22 + [typeParameters]="typeParameters"
  23 + [actionSources]="actionSources"
  24 + [isDataEnabled]="isDataEnabled"
  25 + [widgetSettingsSchema]="settingsSchema"
  26 + [dataKeySettingsSchema]="dataKeySettingsSchema"
  27 + [aliasController]="aliasController"
  28 + [functionsOnly]="widgetEditMode"
  29 + formControlName="widgetConfig">
  30 + </tb-widget-config>
  31 + </fieldset>
  32 +</form>
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
  18 +import { PageComponent } from '@shared/components/page.component';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { ActivatedRoute, Router } from '@angular/router';
  22 +import { WidgetService } from '@core/http/widget.service';
  23 +import { DialogService } from '@core/services/dialog.service';
  24 +import { MatDialog } from '@angular/material/dialog';
  25 +import { TranslateService } from '@ngx-translate/core';
  26 +import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models';
  27 +import { IAliasController } from '@core/api/widget-api.models';
  28 +import { Widget, WidgetActionSource, WidgetTypeParameters } from '@shared/models/widget.models';
  29 +import { WidgetComponentService } from '@home/components/widget/widget-component.service';
  30 +import { WidgetConfigComponentData } from '../../models/widget-component.models';
  31 +import { isString } from '@core/utils';
  32 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  33 +
  34 +@Component({
  35 + selector: 'tb-edit-widget',
  36 + templateUrl: './edit-widget.component.html',
  37 + styleUrls: []
  38 +})
  39 +export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges {
  40 +
  41 + @Input()
  42 + dashboard: Dashboard;
  43 +
  44 + @Input()
  45 + aliasController: IAliasController;
  46 +
  47 + @Input()
  48 + widgetEditMode: boolean;
  49 +
  50 + @Input()
  51 + widget: Widget;
  52 +
  53 + @Input()
  54 + widgetLayout: WidgetLayout;
  55 +
  56 + @Input()
  57 + widgetFormGroup: FormGroup;
  58 +
  59 + widgetConfig: WidgetConfigComponentData;
  60 + typeParameters: WidgetTypeParameters;
  61 + actionSources: {[key: string]: WidgetActionSource};
  62 + isDataEnabled: boolean;
  63 + settingsSchema: any;
  64 + dataKeySettingsSchema: any;
  65 + functionsOnly: boolean;
  66 +
  67 + constructor(protected store: Store<AppState>,
  68 + private widgetComponentService: WidgetComponentService) {
  69 + super(store);
  70 + }
  71 +
  72 + ngOnInit(): void {
  73 + this.loadWidgetConfig();
  74 + }
  75 +
  76 + ngOnChanges(changes: SimpleChanges): void {
  77 + let reloadConfig = false;
  78 + for (const propName of Object.keys(changes)) {
  79 + const change = changes[propName];
  80 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  81 + if (['widget', 'widgetLayout'].includes(propName)) {
  82 + reloadConfig = true;
  83 + }
  84 + }
  85 + }
  86 + if (reloadConfig) {
  87 + this.loadWidgetConfig();
  88 + }
  89 + }
  90 +
  91 + private loadWidgetConfig() {
  92 + const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
  93 + this.widgetConfig = {
  94 + config: this.widget.config,
  95 + layout: this.widgetLayout
  96 + };
  97 + const settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
  98 + const dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
  99 + this.typeParameters = widgetInfo.typeParameters;
  100 + this.actionSources = widgetInfo.actionSources;
  101 + this.isDataEnabled = widgetInfo.typeParameters && !widgetInfo.typeParameters.useCustomDatasources;
  102 + if (!settingsSchema || settingsSchema === '') {
  103 + this.settingsSchema = {};
  104 + } else {
  105 + this.settingsSchema = isString(settingsSchema) ? JSON.parse(settingsSchema) : settingsSchema;
  106 + }
  107 + if (!dataKeySettingsSchema || dataKeySettingsSchema === '') {
  108 + this.dataKeySettingsSchema = {};
  109 + } else {
  110 + this.dataKeySettingsSchema = isString(dataKeySettingsSchema) ? JSON.parse(dataKeySettingsSchema) : dataKeySettingsSchema;
  111 + }
  112 + this.functionsOnly = this.dashboard ? false : true;
  113 + this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
  114 + }
  115 +}
... ...
... ... @@ -15,6 +15,7 @@
15 15 limitations under the License.
16 16
17 17 -->
  18 +<hotkeys-cheatsheet></hotkeys-cheatsheet>
18 19 <div class="mat-content" style="position: relative; width: 100%; height: 100%;"
19 20 [ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
20 21 'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
... ... @@ -60,6 +61,7 @@
60 61 [isEditActionEnabled]="isEdit"
61 62 [isExportActionEnabled]="isEdit && !widgetEditMode"
62 63 [isRemoveActionEnabled]="isEdit && !widgetEditMode"
  64 + [callbacks]="this"
63 65 [ignoreLoading]="layoutCtx.ignoreLoading">
64 66 </tb-dashboard>
65 67 </div>
... ...
... ... @@ -22,16 +22,25 @@ import { PageComponent } from '@shared/components/page.component';
22 22 import { Store } from '@ngrx/store';
23 23 import { AppState } from '@core/core.state';
24 24 import { Widget } from '@shared/models/widget.models';
25   -import { WidgetLayouts } from '@shared/models/dashboard.models';
  25 +import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
26 26 import { GridsterComponent } from 'angular-gridster2';
27   -import { IDashboardComponent } from '@home/models/dashboard-component.models';
  27 +import {
  28 + DashboardCallbacks,
  29 + DashboardContextMenuItem,
  30 + IDashboardComponent, WidgetContextMenuItem
  31 +} from '@home/models/dashboard-component.models';
  32 +import { Observable, of, Subscription } from 'rxjs';
  33 +import { Hotkey, HotkeysService } from 'angular2-hotkeys';
  34 +import { getCurrentIsLoading } from '@core/interceptors/load.selectors';
  35 +import { TranslateService } from '@ngx-translate/core';
  36 +import { ItemBufferService } from '@app/core/services/item-buffer.service';
28 37
29 38 @Component({
30 39 selector: 'tb-dashboard-layout',
31 40 templateUrl: './dashboard-layout.component.html',
32 41 styleUrls: ['./dashboard-layout.component.scss']
33 42 })
34   -export class DashboardLayoutComponent extends PageComponent implements ILayoutController, OnInit, OnDestroy {
  43 +export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy {
35 44
36 45 layoutCtxValue: DashboardPageLayoutContext;
37 46
... ... @@ -53,6 +62,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
53 62 isEdit: boolean;
54 63
55 64 @Input()
  65 + isEditingWidget: boolean;
  66 +
  67 + @Input()
56 68 isMobile: boolean;
57 69
58 70 @Input()
... ... @@ -60,15 +72,97 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
60 72
61 73 @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
62 74
  75 + private rxSubscriptions = new Array<Subscription>();
  76 +
63 77 constructor(protected store: Store<AppState>,
64   - private cd: ChangeDetectorRef) {
  78 + private hotkeysService: HotkeysService,
  79 + private translate: TranslateService,
  80 + private itembuffer: ItemBufferService) {
65 81 super(store);
66 82 }
67 83
68 84 ngOnInit(): void {
  85 + this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe(
  86 + (dashboardTimewindow) => {
  87 + this.dashboardCtx.dashboardTimewindow = dashboardTimewindow;
  88 + }
  89 + )
  90 + );
  91 + this.initHotKeys();
69 92 }
70 93
71 94 ngOnDestroy(): void {
  95 + this.rxSubscriptions.forEach((subscription) => {
  96 + subscription.unsubscribe();
  97 + });
  98 + this.rxSubscriptions.length = 0;
  99 + }
  100 +
  101 + private initHotKeys(): void {
  102 + this.hotkeysService.add(
  103 + new Hotkey('ctrl+c', (event: KeyboardEvent) => {
  104 + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
  105 + const widget = this.dashboard.getSelectedWidget();
  106 + if (widget) {
  107 + event.preventDefault();
  108 + this.copyWidget(event, widget);
  109 + }
  110 + }
  111 + return false;
  112 + }, null,
  113 + this.translate.instant('action.copy'))
  114 + );
  115 + this.hotkeysService.add(
  116 + new Hotkey('ctrl+r', (event: KeyboardEvent) => {
  117 + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
  118 + const widget = this.dashboard.getSelectedWidget();
  119 + if (widget) {
  120 + event.preventDefault();
  121 + this.copyWidgetReference(event, widget);
  122 + }
  123 + }
  124 + return false;
  125 + }, null,
  126 + this.translate.instant('action.copy-reference'))
  127 + );
  128 + this.hotkeysService.add(
  129 + new Hotkey('ctrl+v', (event: KeyboardEvent) => {
  130 + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
  131 + if (this.itembuffer.hasWidget()) {
  132 + event.preventDefault();
  133 + this.pasteWidget(event);
  134 + }
  135 + }
  136 + return false;
  137 + }, null,
  138 + this.translate.instant('action.paste'))
  139 + );
  140 + this.hotkeysService.add(
  141 + new Hotkey('ctrl+i', (event: KeyboardEvent) => {
  142 + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
  143 + if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.dashboard,
  144 + this.dashboardCtx.state, this.layoutCtx.id)) {
  145 + event.preventDefault();
  146 + this.pasteWidgetReference(event);
  147 + }
  148 + }
  149 + return false;
  150 + }, null,
  151 + this.translate.instant('action.paste-reference'))
  152 + );
  153 + this.hotkeysService.add(
  154 + new Hotkey('ctrl+x', (event: KeyboardEvent) => {
  155 + if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
  156 + const widget = this.dashboard.getSelectedWidget();
  157 + if (widget) {
  158 + event.preventDefault();
  159 + this.layoutCtx.dashboardCtrl.removeWidget(event, this.layoutCtx, widget);
  160 + }
  161 + }
  162 + return false;
  163 + }, null,
  164 + this.translate.instant('action.delete'))
  165 + );
72 166 }
73 167
74 168 reload() {
... ... @@ -78,6 +172,65 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
78 172 }
79 173
80 174 resetHighlight() {
  175 + this.dashboard.resetHighlight();
  176 + }
  177 +
  178 + highlightWidget(index: number, delay?: number) {
  179 + this.dashboard.highlightWidget(index, delay);
  180 + }
  181 +
  182 + selectWidget(index: number, delay?: number) {
  183 + this.dashboard.selectWidget(index, delay);
  184 + }
  185 +
  186 + addWidget($event: Event) {
  187 + this.layoutCtx.dashboardCtrl.addWidget($event, this.layoutCtx);
  188 + }
  189 +
  190 + onEditWidget($event: Event, widget: Widget, index: number): void {
  191 + this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget, index);
  192 + }
  193 +
  194 + onExportWidget($event: Event, widget: Widget, index: number): void {
  195 + this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, index);
  196 + }
  197 +
  198 + onRemoveWidget($event: Event, widget: Widget, index: number): void {
  199 + return this.layoutCtx.dashboardCtrl.removeWidget($event, this.layoutCtx, widget);
  200 + }
  201 +
  202 + onWidgetMouseDown($event: Event, widget: Widget, index: number): void {
  203 + this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget, index);
  204 + }
  205 +
  206 + onWidgetClicked($event: Event, widget: Widget, index: number): void {
  207 + this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget, index);
  208 + }
  209 +
  210 + prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> {
  211 + return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
  212 + }
  213 +
  214 + prepareWidgetContextMenu($event: Event, widget: Widget, index: number): Array<WidgetContextMenuItem> {
  215 + return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, index);
  216 + }
  217 +
  218 + copyWidget($event: Event, widget: Widget) {
  219 + this.layoutCtx.dashboardCtrl.copyWidget($event, this.layoutCtx, widget);
  220 + }
  221 +
  222 + copyWidgetReference($event: Event, widget: Widget) {
  223 + this.layoutCtx.dashboardCtrl.copyWidgetReference($event, this.layoutCtx, widget);
  224 + }
  225 +
  226 + pasteWidget($event: Event) {
  227 + const pos = this.dashboard.getEventGridPosition($event);
  228 + this.layoutCtx.dashboardCtrl.pasteWidget($event, this.layoutCtx, pos);
  229 + }
  230 +
  231 + pasteWidgetReference($event: Event) {
  232 + const pos = this.dashboard.getEventGridPosition($event);
  233 + this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos);
81 234 }
82 235
83 236 }
... ...
... ... @@ -14,8 +14,15 @@
14 14 /// limitations under the License.
15 15 ///
16 16
  17 +import { Widget } from '@shared/models/widget.models';
  18 +import { WidgetLayout } from '@shared/models/dashboard.models';
  19 +
17 20 export interface ILayoutController {
18 21 reload();
19 22 setResizing(layoutVisibilityChanged: boolean);
20 23 resetHighlight();
  24 + highlightWidget(index: number, delay?: number);
  25 + selectWidget(index: number, delay?: number);
  26 + pasteWidget($event: MouseEvent);
  27 + pasteWidgetReference($event: MouseEvent);
21 28 }
... ...
... ... @@ -65,7 +65,13 @@ export const HelpLinks = {
65 65 entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views',
66 66 rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains',
67 67 dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
68   - widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles'
  68 + widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles',
  69 + widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration',
  70 + widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries',
  71 + widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest',
  72 + widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc',
  73 + widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm',
  74 + widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static'
69 75 }
70 76 };
71 77
... ...
... ... @@ -40,6 +40,7 @@ export interface WidgetTypeData {
40 40 name: string;
41 41 icon: string;
42 42 isMdiIcon?: boolean;
  43 + configHelpLinkId: string;
43 44 template: WidgetTypeTemplate;
44 45 }
45 46
... ... @@ -50,6 +51,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
50 51 {
51 52 name: 'widget.timeseries',
52 53 icon: 'timeline',
  54 + configHelpLinkId: 'widgetsConfigTimeseries',
53 55 template: {
54 56 bundleAlias: 'charts',
55 57 alias: 'basic_timeseries'
... ... @@ -61,6 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
61 63 {
62 64 name: 'widget.latest-values',
63 65 icon: 'track_changes',
  66 + configHelpLinkId: 'widgetsConfigLatest',
64 67 template: {
65 68 bundleAlias: 'cards',
66 69 alias: 'attributes_card'
... ... @@ -72,6 +75,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
72 75 {
73 76 name: 'widget.rpc',
74 77 icon: 'mdi:developer-board',
  78 + configHelpLinkId: 'widgetsConfigRpc',
75 79 isMdiIcon: true,
76 80 template: {
77 81 bundleAlias: 'gpio_widgets',
... ... @@ -84,6 +88,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
84 88 {
85 89 name: 'widget.alarm',
86 90 icon: 'error',
  91 + configHelpLinkId: 'widgetsConfigAlarm',
87 92 template: {
88 93 bundleAlias: 'alarm_widgets',
89 94 alias: 'alarms_table'
... ... @@ -95,6 +100,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
95 100 {
96 101 name: 'widget.static',
97 102 icon: 'font_download',
  103 + configHelpLinkId: 'widgetsConfigStatic',
98 104 template: {
99 105 bundleAlias: 'cards',
100 106 alias: 'html_card'
... ... @@ -129,8 +135,8 @@ export interface WidgetTypeDescriptor {
129 135 templateHtml: string;
130 136 templateCss: string;
131 137 controllerScript: string;
132   - settingsSchema?: string;
133   - dataKeySettingsSchema?: string;
  138 + settingsSchema?: string | any;
  139 + dataKeySettingsSchema?: string | any;
134 140 defaultConfig: string;
135 141 sizeX: number;
136 142 sizeY: number;
... ... @@ -146,8 +152,8 @@ export interface WidgetTypeParameters {
146 152
147 153 export interface WidgetControllerDescriptor {
148 154 widgetTypeFunction?: any;
149   - settingsSchema?: string;
150   - dataKeySettingsSchema?: string;
  155 + settingsSchema?: string | any;
  156 + dataKeySettingsSchema?: string | any;
151 157 typeParameters?: WidgetTypeParameters;
152 158 actionSources?: {[key: string]: WidgetActionSource};
153 159 }
... ... @@ -309,7 +315,7 @@ export interface WidgetConfig {
309 315 showTitle?: boolean;
310 316 showTitleIcon?: boolean;
311 317 iconColor?: string;
312   - iconSize?: number;
  318 + iconSize?: string;
313 319 dropShadow?: boolean;
314 320 enableFullscreen?: boolean;
315 321 useDashboardTimewindow?: boolean;
... ...
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Inject, Pipe, PipeTransform } from '@angular/core';
  18 +import { WINDOW } from '@core/services/window.service';
  19 +
  20 +@Pipe({
  21 + name: 'keyboardShortcut'
  22 +})
  23 +export class KeyboardShortcutPipe implements PipeTransform {
  24 +
  25 + constructor(@Inject(WINDOW) private window: Window) {}
  26 +
  27 + transform(value: string): string {
  28 + if (!value) {
  29 + return;
  30 + }
  31 + const keys = value.split('-');
  32 + const isOSX = /Mac OS X/.test(this.window.navigator.userAgent);
  33 +
  34 + const seperator = (!isOSX || keys.length > 2) ? '+' : '';
  35 +
  36 + const abbreviations = {
  37 + M: isOSX ? '⌘' : 'Ctrl',
  38 + A: isOSX ? 'Option' : 'Alt',
  39 + S: 'Shift'
  40 + };
  41 +
  42 + return keys.map((key, index) => {
  43 + const last = index === keys.length - 1;
  44 + return last ? key : abbreviations[key];
  45 + }).join(seperator);
  46 + return (!value) ? '' : value.replace(/ /g, '');
  47 + }
  48 +
  49 +}
... ...
... ... @@ -95,6 +95,7 @@ import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from '.
95 95 import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component';
96 96 import { DashboardSelectComponent } from '@shared/components/dashboard-select.component';
97 97 import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component';
  98 +import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe';
98 99
99 100 @NgModule({
100 101 providers: [
... ... @@ -150,7 +151,8 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select
150 151 NospacePipe,
151 152 MillisecondsToTimeStringPipe,
152 153 EnumToArrayPipe,
153   - HighlightPipe
  154 + HighlightPipe,
  155 + KeyboardShortcutPipe
154 156 ],
155 157 imports: [
156 158 CommonModule,
... ... @@ -272,6 +274,7 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select
272 274 MillisecondsToTimeStringPipe,
273 275 EnumToArrayPipe,
274 276 HighlightPipe,
  277 + KeyboardShortcutPipe,
275 278 TranslateModule
276 279 ]
277 280 })
... ...
... ... @@ -277,6 +277,15 @@ $tb-dark-theme: get-tb-dark-theme(
277 277 display: block;
278 278 }
279 279
  280 + button.mat-menu-item {
  281 + overflow: hidden;
  282 + fill: #737373;
  283 + display: block;
  284 + .tb-alt-text {
  285 + float: right;
  286 + }
  287 + }
  288 +
280 289 mat-toolbar.mat-table-toolbar {
281 290 background: #fff;
282 291 padding: 0 24px;
... ...