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"> | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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; | ... | ... |