Showing
33 changed files
with
1569 additions
and
178 deletions
@@ -49,9 +49,13 @@ export class AppComponent implements OnInit { | @@ -49,9 +49,13 @@ export class AppComponent implements OnInit { | ||
49 | } | 49 | } |
50 | 50 | ||
51 | setupTranslate() { | 51 | setupTranslate() { |
52 | - console.log(`Supported Langs: ${env.supportedLangs}`); | 52 | + if (!env.production) { |
53 | + console.log(`Supported Langs: ${env.supportedLangs}`); | ||
54 | + } | ||
53 | this.translate.addLangs(env.supportedLangs); | 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 | this.translate.setDefaultLang(env.defaultLang); | 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,25 +14,31 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { environment } from '@env/environment'; | 17 | +import { environment as env } from '@env/environment'; |
18 | import { TranslateService } from '@ngx-translate/core'; | 18 | import { TranslateService } from '@ngx-translate/core'; |
19 | 19 | ||
20 | export function updateUserLang(translate: TranslateService, userLang: string) { | 20 | export function updateUserLang(translate: TranslateService, userLang: string) { |
21 | let targetLang = userLang; | 21 | let targetLang = userLang; |
22 | - console.log(`User lang: ${targetLang}`); | 22 | + if (!env.production) { |
23 | + console.log(`User lang: ${targetLang}`); | ||
24 | + } | ||
23 | if (!targetLang) { | 25 | if (!targetLang) { |
24 | targetLang = translate.getBrowserCultureLang(); | 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 | const detectedSupportedLang = detectSupportedLang(targetLang); | 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 | translate.use(detectedSupportedLang); | 35 | translate.use(detectedSupportedLang); |
30 | } | 36 | } |
31 | 37 | ||
32 | function detectSupportedLang(targetLang: string): string { | 38 | function detectSupportedLang(targetLang: string): string { |
33 | const langTag = (targetLang || '').split('-').join('_'); | 39 | const langTag = (targetLang || '').split('-').join('_'); |
34 | if (langTag.length) { | 40 | if (langTag.length) { |
35 | - if (environment.supportedLangs.indexOf(langTag) > -1) { | 41 | + if (env.supportedLangs.indexOf(langTag) > -1) { |
36 | return langTag; | 42 | return langTag; |
37 | } else { | 43 | } else { |
38 | const parts = langTag.split('_'); | 44 | const parts = langTag.split('_'); |
@@ -42,7 +48,7 @@ function detectSupportedLang(targetLang: string): string { | @@ -42,7 +48,7 @@ function detectSupportedLang(targetLang: string): string { | ||
42 | } else { | 48 | } else { |
43 | lang = langTag; | 49 | lang = langTag; |
44 | } | 50 | } |
45 | - const foundLangs = environment.supportedLangs.filter( | 51 | + const foundLangs = env.supportedLangs.filter( |
46 | (supportedLang: string) => { | 52 | (supportedLang: string) => { |
47 | const supportedLangParts = supportedLang.split('_'); | 53 | const supportedLangParts = supportedLang.split('_'); |
48 | return supportedLangParts[0] === lang; | 54 | return supportedLangParts[0] === lang; |
@@ -53,5 +59,5 @@ function detectSupportedLang(targetLang: string): string { | @@ -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,11 +61,15 @@ export function animatedScroll(element: HTMLElement, scrollTop: number, delay?: | ||
61 | const duration = delay ? delay : 0; | 61 | const duration = delay ? delay : 0; |
62 | const remaining = to - start; | 62 | const remaining = to - start; |
63 | const animateScroll = () => { | 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 | animateScroll(); | 75 | animateScroll(); |
@@ -24,6 +24,42 @@ | @@ -24,6 +24,42 @@ | ||
24 | <div id="gridster-parent" | 24 | <div id="gridster-parent" |
25 | fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}" | 25 | fxFlex class="tb-dashboard-content layout-wrap" [ngStyle]="{overflowY: isAutofillHeight() ? 'hidden' : 'auto'}" |
26 | (contextmenu)="openDashboardContextMenu($event)"> | 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 | <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> | 63 | <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> |
28 | <gridster #gridster id="gridster-child" [options]="gridsterOpts"> | 64 | <gridster #gridster id="gridster-child" [options]="gridsterOpts"> |
29 | <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets"> | 65 | <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets"> |
@@ -17,13 +17,14 @@ | @@ -17,13 +17,14 @@ | ||
17 | import { | 17 | import { |
18 | AfterViewInit, | 18 | AfterViewInit, |
19 | Component, | 19 | Component, |
20 | + DoCheck, | ||
20 | Input, | 21 | Input, |
22 | + IterableDiffers, | ||
23 | + KeyValueDiffers, | ||
21 | OnChanges, | 24 | OnChanges, |
22 | OnInit, | 25 | OnInit, |
23 | - QueryList, | ||
24 | SimpleChanges, | 26 | SimpleChanges, |
25 | - ViewChild, | ||
26 | - ViewChildren | 27 | + ViewChild |
27 | } from '@angular/core'; | 28 | } from '@angular/core'; |
28 | import { Store } from '@ngrx/store'; | 29 | import { Store } from '@ngrx/store'; |
29 | import { AppState } from '@core/core.state'; | 30 | import { AppState } from '@core/core.state'; |
@@ -32,16 +33,15 @@ import { AuthUser } from '@shared/models/user.model'; | @@ -32,16 +33,15 @@ import { AuthUser } from '@shared/models/user.model'; | ||
32 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | 33 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
33 | import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; | 34 | import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; |
34 | import { TimeService } from '@core/services/time.service'; | 35 | import { TimeService } from '@core/services/time.service'; |
35 | -import { GridsterComponent, GridsterConfig, GridsterItemComponent } from 'angular-gridster2'; | 36 | +import { GridsterComponent, GridsterConfig } from 'angular-gridster2'; |
36 | import { | 37 | import { |
37 | DashboardCallbacks, | 38 | DashboardCallbacks, |
38 | DashboardWidget, | 39 | DashboardWidget, |
40 | + DashboardWidgets, | ||
39 | IDashboardComponent, | 41 | IDashboardComponent, |
40 | - WidgetsData, | ||
41 | - DashboardWidgets | 42 | + WidgetPosition |
42 | } from '../../models/dashboard-component.models'; | 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 | import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; | 45 | import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; |
46 | import { DialogService } from '@core/services/dialog.service'; | 46 | import { DialogService } from '@core/services/dialog.service'; |
47 | import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; | 47 | import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; |
@@ -49,13 +49,14 @@ import { BreakpointObserver } from '@angular/cdk/layout'; | @@ -49,13 +49,14 @@ import { BreakpointObserver } from '@angular/cdk/layout'; | ||
49 | import { MediaBreakpoints } from '@shared/models/constants'; | 49 | import { MediaBreakpoints } from '@shared/models/constants'; |
50 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; | 50 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
51 | import { Widget } from '@app/shared/models/widget.models'; | 51 | import { Widget } from '@app/shared/models/widget.models'; |
52 | +import { MatMenuTrigger } from '@angular/material'; | ||
52 | 53 | ||
53 | @Component({ | 54 | @Component({ |
54 | selector: 'tb-dashboard', | 55 | selector: 'tb-dashboard', |
55 | templateUrl: './dashboard.component.html', | 56 | templateUrl: './dashboard.component.html', |
56 | styleUrls: ['./dashboard.component.scss'] | 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 | authUser: AuthUser; | 61 | authUser: AuthUser; |
61 | 62 | ||
@@ -130,25 +131,38 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -130,25 +131,38 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
130 | 131 | ||
131 | gridsterOpts: GridsterConfig; | 132 | gridsterOpts: GridsterConfig; |
132 | 133 | ||
133 | - highlightedMode = false; | ||
134 | - highlightedWidget: DashboardWidget = null; | ||
135 | - selectedWidget: DashboardWidget = null; | ||
136 | - | ||
137 | isWidgetExpanded = false; | 134 | isWidgetExpanded = false; |
138 | isMobileSize = false; | 135 | isMobileSize = false; |
139 | 136 | ||
140 | @ViewChild('gridster', {static: true}) gridster: GridsterComponent; | 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 | dashboardLoading = true; | 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 | constructor(protected store: Store<AppState>, | 160 | constructor(protected store: Store<AppState>, |
149 | private timeService: TimeService, | 161 | private timeService: TimeService, |
150 | private dialogService: DialogService, | 162 | private dialogService: DialogService, |
151 | - private breakpointObserver: BreakpointObserver) { | 163 | + private breakpointObserver: BreakpointObserver, |
164 | + private differs: IterableDiffers, | ||
165 | + private kvDiffers: KeyValueDiffers) { | ||
152 | super(store); | 166 | super(store); |
153 | this.authUser = getCurrentAuthUser(store); | 167 | this.authUser = getCurrentAuthUser(store); |
154 | } | 168 | } |
@@ -175,7 +189,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -175,7 +189,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
175 | defaultItemRows: 6, | 189 | defaultItemRows: 6, |
176 | resizable: {enabled: this.isEdit}, | 190 | resizable: {enabled: this.isEdit}, |
177 | draggable: {enabled: this.isEdit}, | 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 | this.updateMobileOpts(); | 198 | this.updateMobileOpts(); |
@@ -184,12 +201,17 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -184,12 +201,17 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
184 | .observe(MediaBreakpoints['gt-sm']).subscribe( | 201 | .observe(MediaBreakpoints['gt-sm']).subscribe( |
185 | () => { | 202 | () => { |
186 | this.updateMobileOpts(); | 203 | this.updateMobileOpts(); |
204 | + this.notifyGridsterOptionsChanged(); | ||
187 | } | 205 | } |
188 | ); | 206 | ); |
189 | 207 | ||
190 | this.updateWidgets(); | 208 | this.updateWidgets(); |
191 | } | 209 | } |
192 | 210 | ||
211 | + ngDoCheck() { | ||
212 | + this.dashboardWidgets.doCheck(); | ||
213 | + } | ||
214 | + | ||
193 | ngOnChanges(changes: SimpleChanges): void { | 215 | ngOnChanges(changes: SimpleChanges): void { |
194 | let updateMobileOpts = false; | 216 | let updateMobileOpts = false; |
195 | let updateLayoutOpts = false; | 217 | let updateLayoutOpts = false; |
@@ -206,6 +228,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -206,6 +228,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
206 | updateEditingOpts = true; | 228 | updateEditingOpts = true; |
207 | } else if (['widgets', 'widgetLayouts'].includes(propName)) { | 229 | } else if (['widgets', 'widgetLayouts'].includes(propName)) { |
208 | updateWidgets = true; | 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,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 | onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) { | 316 | onWidgetFullscreenChanged(expanded: boolean, widget: DashboardWidget) { |
@@ -275,13 +319,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -275,13 +319,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
275 | 319 | ||
276 | widgetMouseDown($event: Event, widget: DashboardWidget) { | 320 | widgetMouseDown($event: Event, widget: DashboardWidget) { |
277 | if (this.callbacks && this.callbacks.onWidgetMouseDown) { | 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 | widgetClicked($event: Event, widget: DashboardWidget) { | 326 | widgetClicked($event: Event, widget: DashboardWidget) { |
283 | if (this.callbacks && this.callbacks.onWidgetClicked) { | 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,7 +334,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
290 | $event.stopPropagation(); | 334 | $event.stopPropagation(); |
291 | } | 335 | } |
292 | if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { | 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,7 +343,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
299 | $event.stopPropagation(); | 343 | $event.stopPropagation(); |
300 | } | 344 | } |
301 | if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { | 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,56 +352,81 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
308 | $event.stopPropagation(); | 352 | $event.stopPropagation(); |
309 | } | 353 | } |
310 | if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { | 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 | resetHighlight() { | 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 | isHighlighted(widget: DashboardWidget) { | 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 | isNotHighlighted(widget: DashboardWidget) { | 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 | if (offset > 0) { | 425 | if (offset > 0) { |
356 | scrollTop -= offset; | 426 | scrollTop -= offset; |
357 | } | 427 | } |
358 | - const parentElement = this.gridster.el as HTMLElement; | ||
359 | animatedScroll(parentElement, scrollTop, delay); | 428 | animatedScroll(parentElement, scrollTop, delay); |
360 | - } | 429 | + }); |
361 | } | 430 | } |
362 | 431 | ||
363 | private updateMobileOpts() { | 432 | private updateMobileOpts() { |
@@ -31,7 +31,7 @@ | @@ -31,7 +31,7 @@ | ||
31 | </button> | 31 | </button> |
32 | </div> | 32 | </div> |
33 | <section *ngIf="!isReadOnly" fxLayout="row" class="layout-wrap tb-header-buttons"> | 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 | mat-fab | 35 | mat-fab |
36 | matTooltip="{{ 'action.apply-changes' | translate }}" | 36 | matTooltip="{{ 'action.apply-changes' | translate }}" |
37 | matTooltipPosition="above" | 37 | matTooltipPosition="above" |
@@ -40,7 +40,7 @@ | @@ -40,7 +40,7 @@ | ||
40 | (click)="onApplyDetails()"> | 40 | (click)="onApplyDetails()"> |
41 | <mat-icon class="material-icons">done</mat-icon> | 41 | <mat-icon class="material-icons">done</mat-icon> |
42 | </button> | 42 | </button> |
43 | - <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm.dirty)" | 43 | + <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)" |
44 | mat-fab | 44 | mat-fab |
45 | matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" | 45 | matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" |
46 | matTooltipPosition="above" | 46 | matTooltipPosition="above" |
@@ -32,7 +32,18 @@ export class DetailsPanelComponent extends PageComponent { | @@ -32,7 +32,18 @@ export class DetailsPanelComponent extends PageComponent { | ||
32 | @Input() headerSubtitle = ''; | 32 | @Input() headerSubtitle = ''; |
33 | @Input() isReadOnly = false; | 33 | @Input() isReadOnly = false; |
34 | @Input() isAlwaysEdit = false; | 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 | @Output() | 47 | @Output() |
37 | closeDetails = new EventEmitter<void>(); | 48 | closeDetails = new EventEmitter<void>(); |
38 | @Output() | 49 | @Output() |
@@ -47,7 +58,7 @@ export class DetailsPanelComponent extends PageComponent { | @@ -47,7 +58,7 @@ export class DetailsPanelComponent extends PageComponent { | ||
47 | 58 | ||
48 | @Input() | 59 | @Input() |
49 | get isEdit() { | 60 | get isEdit() { |
50 | - return this.isEditValue; | 61 | + return this.isAlwaysEdit || this.isEditValue; |
51 | } | 62 | } |
52 | 63 | ||
53 | set isEdit(val: boolean) { | 64 | set isEdit(val: boolean) { |
@@ -72,7 +83,7 @@ export class DetailsPanelComponent extends PageComponent { | @@ -72,7 +83,7 @@ export class DetailsPanelComponent extends PageComponent { | ||
72 | } | 83 | } |
73 | 84 | ||
74 | onApplyDetails() { | 85 | onApplyDetails() { |
75 | - if (this.theForm.valid) { | 86 | + if (this.theForm && this.theForm.valid) { |
76 | this.applyDetails.emit(); | 87 | this.applyDetails.emit(); |
77 | } | 88 | } |
78 | } | 89 | } |
@@ -40,6 +40,7 @@ import { WidgetComponentService } from './widget/widget-component.service'; | @@ -40,6 +40,7 @@ import { WidgetComponentService } from './widget/widget-component.service'; | ||
40 | import { LegendComponent } from '@home/components/widget/legend.component'; | 40 | import { LegendComponent } from '@home/components/widget/legend.component'; |
41 | import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; | 41 | import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; |
42 | import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; | 42 | import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; |
43 | +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; | ||
43 | 44 | ||
44 | @NgModule({ | 45 | @NgModule({ |
45 | entryComponents: [ | 46 | entryComponents: [ |
@@ -76,7 +77,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent | @@ -76,7 +77,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent | ||
76 | AliasesEntitySelectComponent, | 77 | AliasesEntitySelectComponent, |
77 | DashboardComponent, | 78 | DashboardComponent, |
78 | WidgetComponent, | 79 | WidgetComponent, |
79 | - LegendComponent | 80 | + LegendComponent, |
81 | + WidgetConfigComponent | ||
80 | ], | 82 | ], |
81 | imports: [ | 83 | imports: [ |
82 | CommonModule, | 84 | CommonModule, |
@@ -97,7 +99,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent | @@ -97,7 +99,8 @@ import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-ent | ||
97 | AliasesEntitySelectComponent, | 99 | AliasesEntitySelectComponent, |
98 | DashboardComponent, | 100 | DashboardComponent, |
99 | WidgetComponent, | 101 | WidgetComponent, |
100 | - LegendComponent | 102 | + LegendComponent, |
103 | + WidgetConfigComponent | ||
101 | ], | 104 | ], |
102 | providers: [ | 105 | providers: [ |
103 | WidgetComponentService | 106 | WidgetComponentService |
@@ -48,7 +48,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid | @@ -48,7 +48,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid | ||
48 | } | 48 | } |
49 | 49 | ||
50 | ngOnDestroy(): void { | 50 | ngOnDestroy(): void { |
51 | - console.log('Widget component destroyed!'); | 51 | + |
52 | } | 52 | } |
53 | 53 | ||
54 | clearRpcError() { | 54 | clearRpcError() { |
@@ -29,7 +29,7 @@ import { | @@ -29,7 +29,7 @@ import { | ||
29 | import cssjs from '@core/css/css'; | 29 | import cssjs from '@core/css/css'; |
30 | import { UtilsService } from '@core/services/utils.service'; | 30 | import { UtilsService } from '@core/services/utils.service'; |
31 | import { ResourcesService } from '@core/services/resources.service'; | 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 | import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; | 33 | import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; |
34 | import { isFunction, isUndefined } from '@core/utils'; | 34 | import { isFunction, isUndefined } from '@core/utils'; |
35 | import { TranslateService } from '@ngx-translate/core'; | 35 | import { TranslateService } from '@ngx-translate/core'; |
@@ -130,6 +130,15 @@ export class WidgetComponentService { | @@ -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 | public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { | 142 | public getWidgetInfo(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean): Observable<WidgetInfo> { |
134 | return this.init().pipe( | 143 | return this.init().pipe( |
135 | mergeMap(() => this.getWidgetInfoInternal(bundleAlias, widgetTypeAlias, isSystem)) | 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,21 +16,22 @@ | ||
16 | 16 | ||
17 | import { | 17 | import { |
18 | AfterViewInit, | 18 | AfterViewInit, |
19 | + ChangeDetectionStrategy, | ||
20 | + ChangeDetectorRef, | ||
19 | Component, | 21 | Component, |
20 | ComponentFactoryResolver, | 22 | ComponentFactoryResolver, |
21 | ComponentRef, | 23 | ComponentRef, |
22 | ElementRef, | 24 | ElementRef, |
23 | Injector, | 25 | Injector, |
24 | Input, | 26 | Input, |
27 | + NgZone, | ||
25 | OnChanges, | 28 | OnChanges, |
26 | OnDestroy, | 29 | OnDestroy, |
27 | OnInit, | 30 | OnInit, |
28 | SimpleChanges, | 31 | SimpleChanges, |
29 | ViewChild, | 32 | ViewChild, |
30 | ViewContainerRef, | 33 | ViewContainerRef, |
31 | - ViewEncapsulation, | ||
32 | - ChangeDetectorRef, | ||
33 | - ChangeDetectionStrategy, NgZone | 34 | + ViewEncapsulation |
34 | } from '@angular/core'; | 35 | } from '@angular/core'; |
35 | import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models'; | 36 | import { DashboardWidget, IDashboardComponent } from '@home/models/dashboard-component.models'; |
36 | import { | 37 | import { |
@@ -52,7 +53,7 @@ import { AppState } from '@core/core.state'; | @@ -52,7 +53,7 @@ import { AppState } from '@core/core.state'; | ||
52 | import { WidgetService } from '@core/http/widget.service'; | 53 | import { WidgetService } from '@core/http/widget.service'; |
53 | import { UtilsService } from '@core/services/utils.service'; | 54 | import { UtilsService } from '@core/services/utils.service'; |
54 | import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; | 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 | import { | 57 | import { |
57 | IDynamicWidgetComponent, | 58 | IDynamicWidgetComponent, |
58 | WidgetContext, | 59 | WidgetContext, |
@@ -63,7 +64,8 @@ import { | @@ -63,7 +64,8 @@ import { | ||
63 | import { | 64 | import { |
64 | IWidgetSubscription, | 65 | IWidgetSubscription, |
65 | StateObject, | 66 | StateObject, |
66 | - StateParams, SubscriptionEntityInfo, | 67 | + StateParams, |
68 | + SubscriptionEntityInfo, | ||
67 | SubscriptionInfo, | 69 | SubscriptionInfo, |
68 | WidgetSubscriptionContext, | 70 | WidgetSubscriptionContext, |
69 | WidgetSubscriptionOptions | 71 | WidgetSubscriptionOptions |
@@ -86,7 +88,6 @@ import { DashboardService } from '@core/http/dashboard.service'; | @@ -86,7 +88,6 @@ import { DashboardService } from '@core/http/dashboard.service'; | ||
86 | import { DatasourceService } from '@core/api/datasource.service'; | 88 | import { DatasourceService } from '@core/api/datasource.service'; |
87 | import { WidgetSubscription } from '@core/api/widget-subscription'; | 89 | import { WidgetSubscription } from '@core/api/widget-subscription'; |
88 | import { EntityService } from '@core/http/entity.service'; | 90 | import { EntityService } from '@core/http/entity.service'; |
89 | -import { TimewindowComponent } from '@shared/components/time/timewindow.component'; | ||
90 | 91 | ||
91 | @Component({ | 92 | @Component({ |
92 | selector: 'tb-widget', | 93 | selector: 'tb-widget', |
@@ -362,10 +363,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -362,10 +363,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
362 | const change = changes[propName]; | 363 | const change = changes[propName]; |
363 | if (!change.firstChange && change.currentValue !== change.previousValue) { | 364 | if (!change.firstChange && change.currentValue !== change.previousValue) { |
364 | if (propName === 'isEdit') { | 365 | if (propName === 'isEdit') { |
365 | - console.log(`isEdit changed: ${this.isEdit}`); | ||
366 | this.onEditModeChanged(); | 366 | this.onEditModeChanged(); |
367 | } else if (propName === 'isMobile') { | 367 | } else if (propName === 'isMobile') { |
368 | - console.log(`isMobile changed: ${this.isMobile}`); | ||
369 | this.onMobileModeChanged(); | 368 | this.onMobileModeChanged(); |
370 | } | 369 | } |
371 | } | 370 | } |
@@ -14,30 +14,50 @@ | @@ -14,30 +14,50 @@ | ||
14 | /// limitations under the License. | 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 | import { Widget, widgetType } from '@app/shared/models/widget.models'; | 18 | import { Widget, widgetType } from '@app/shared/models/widget.models'; |
19 | import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; | 19 | import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; |
20 | import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; | 20 | import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; |
21 | import { Timewindow } from '@shared/models/time/time.models'; | 21 | import { Timewindow } from '@shared/models/time/time.models'; |
22 | -import { Observable } from 'rxjs'; | 22 | +import { Observable, of, Subject } from 'rxjs'; |
23 | import { isDefined, isUndefined } from '@app/core/utils'; | 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 | export interface WidgetsData { | 28 | export interface WidgetsData { |
29 | widgets: Array<Widget>; | 29 | widgets: Array<Widget>; |
30 | widgetLayouts?: WidgetLayouts; | 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 | export interface DashboardCallbacks { | 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 | export interface IDashboardComponent { | 63 | export interface IDashboardComponent { |
@@ -53,60 +73,181 @@ export interface IDashboardComponent { | @@ -53,60 +73,181 @@ export interface IDashboardComponent { | ||
53 | stateController: IStateController; | 73 | stateController: IStateController; |
54 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; | 74 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; |
55 | onResetTimewindow(): void; | 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 | export class DashboardWidgets implements Iterable<DashboardWidget> { | 92 | export class DashboardWidgets implements Iterable<DashboardWidget> { |
59 | 93 | ||
94 | + highlightedMode = false; | ||
95 | + | ||
60 | dashboardWidgets: Array<DashboardWidget> = []; | 96 | dashboardWidgets: Array<DashboardWidget> = []; |
97 | + widgets: Array<Widget>; | ||
98 | + widgetLayouts: WidgetLayouts; | ||
61 | 99 | ||
62 | [Symbol.iterator](): Iterator<DashboardWidget> { | 100 | [Symbol.iterator](): Iterator<DashboardWidget> { |
63 | return this.dashboardWidgets[Symbol.iterator](); | 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 | this.dashboardWidgets.forEach((dashboardWidget) => { | 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 | sortWidgets() { | 253 | sortWidgets() { |
@@ -125,6 +266,9 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -125,6 +266,9 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
125 | 266 | ||
126 | export class DashboardWidget implements GridsterItem { | 267 | export class DashboardWidget implements GridsterItem { |
127 | 268 | ||
269 | + highlighted = false; | ||
270 | + selected = false; | ||
271 | + | ||
128 | isFullscreen = false; | 272 | isFullscreen = false; |
129 | 273 | ||
130 | color: string; | 274 | color: string; |
@@ -160,13 +304,31 @@ export class DashboardWidget implements GridsterItem { | @@ -160,13 +304,31 @@ export class DashboardWidget implements GridsterItem { | ||
160 | 304 | ||
161 | widgetContext: WidgetContext = {}; | 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 | constructor( | 316 | constructor( |
164 | private dashboard: IDashboardComponent, | 317 | private dashboard: IDashboardComponent, |
165 | public widget: Widget, | 318 | public widget: Widget, |
166 | - private widgetLayout?: WidgetLayout) { | 319 | + public widgetIndex: number, |
320 | + public widgetLayout?: WidgetLayout) { | ||
167 | this.updateWidgetParams(); | 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 | updateWidgetParams() { | 332 | updateWidgetParams() { |
171 | this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)'; | 333 | this.color = this.widget.config.color || 'rgba(0, 0, 0, 0.87)'; |
172 | this.backgroundColor = this.widget.config.backgroundColor || '#fff'; | 334 | this.backgroundColor = this.widget.config.backgroundColor || '#fff'; |
@@ -221,11 +383,13 @@ export class DashboardWidget implements GridsterItem { | @@ -221,11 +383,13 @@ export class DashboardWidget implements GridsterItem { | ||
221 | } | 383 | } |
222 | 384 | ||
223 | get x(): number { | 385 | get x(): number { |
386 | + let res; | ||
224 | if (this.widgetLayout) { | 387 | if (this.widgetLayout) { |
225 | - return this.widgetLayout.col; | 388 | + res = this.widgetLayout.col; |
226 | } else { | 389 | } else { |
227 | - return this.widget.col; | 390 | + res = this.widget.col; |
228 | } | 391 | } |
392 | + return Math.floor(res); | ||
229 | } | 393 | } |
230 | 394 | ||
231 | set x(x: number) { | 395 | set x(x: number) { |
@@ -239,11 +403,13 @@ export class DashboardWidget implements GridsterItem { | @@ -239,11 +403,13 @@ export class DashboardWidget implements GridsterItem { | ||
239 | } | 403 | } |
240 | 404 | ||
241 | get y(): number { | 405 | get y(): number { |
406 | + let res; | ||
242 | if (this.widgetLayout) { | 407 | if (this.widgetLayout) { |
243 | - return this.widgetLayout.row; | 408 | + res = this.widgetLayout.row; |
244 | } else { | 409 | } else { |
245 | - return this.widget.row; | 410 | + res = this.widget.row; |
246 | } | 411 | } |
412 | + return Math.floor(res); | ||
247 | } | 413 | } |
248 | 414 | ||
249 | set y(y: number) { | 415 | set y(y: number) { |
@@ -257,11 +423,13 @@ export class DashboardWidget implements GridsterItem { | @@ -257,11 +423,13 @@ export class DashboardWidget implements GridsterItem { | ||
257 | } | 423 | } |
258 | 424 | ||
259 | get cols(): number { | 425 | get cols(): number { |
426 | + let res; | ||
260 | if (this.widgetLayout) { | 427 | if (this.widgetLayout) { |
261 | - return this.widgetLayout.sizeX; | 428 | + res = this.widgetLayout.sizeX; |
262 | } else { | 429 | } else { |
263 | - return this.widget.sizeX; | 430 | + res = this.widget.sizeX; |
264 | } | 431 | } |
432 | + return Math.floor(res); | ||
265 | } | 433 | } |
266 | 434 | ||
267 | set cols(cols: number) { | 435 | set cols(cols: number) { |
@@ -275,6 +443,7 @@ export class DashboardWidget implements GridsterItem { | @@ -275,6 +443,7 @@ export class DashboardWidget implements GridsterItem { | ||
275 | } | 443 | } |
276 | 444 | ||
277 | get rows(): number { | 445 | get rows(): number { |
446 | + let res; | ||
278 | if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) { | 447 | if (this.dashboard.isMobileSize && !this.dashboard.mobileAutofillHeight) { |
279 | let mobileHeight; | 448 | let mobileHeight; |
280 | if (this.widgetLayout) { | 449 | if (this.widgetLayout) { |
@@ -284,17 +453,18 @@ export class DashboardWidget implements GridsterItem { | @@ -284,17 +453,18 @@ export class DashboardWidget implements GridsterItem { | ||
284 | mobileHeight = this.widget.config.mobileHeight; | 453 | mobileHeight = this.widget.config.mobileHeight; |
285 | } | 454 | } |
286 | if (mobileHeight) { | 455 | if (mobileHeight) { |
287 | - return mobileHeight; | 456 | + res = mobileHeight; |
288 | } else { | 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 | } else { | 460 | } else { |
292 | if (this.widgetLayout) { | 461 | if (this.widgetLayout) { |
293 | - return this.widgetLayout.sizeY; | 462 | + res = this.widgetLayout.sizeY; |
294 | } else { | 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 | set rows(rows: number) { | 470 | set rows(rows: number) { |
@@ -45,6 +45,7 @@ import { HttpErrorResponse } from '@angular/common/http'; | @@ -45,6 +45,7 @@ import { HttpErrorResponse } from '@angular/common/http'; | ||
45 | import { RafService } from '@core/services/raf.service'; | 45 | import { RafService } from '@core/services/raf.service'; |
46 | import { WidgetTypeId } from '@shared/models/id/widget-type-id'; | 46 | import { WidgetTypeId } from '@shared/models/id/widget-type-id'; |
47 | import { TenantId } from '@shared/models/id/tenant-id'; | 47 | import { TenantId } from '@shared/models/id/tenant-id'; |
48 | +import { WidgetLayout } from '@shared/models/dashboard.models'; | ||
48 | 49 | ||
49 | export interface IWidgetAction { | 50 | export interface IWidgetAction { |
50 | name: string; | 51 | name: string; |
@@ -112,11 +113,16 @@ export interface IDynamicWidgetComponent { | @@ -112,11 +113,16 @@ export interface IDynamicWidgetComponent { | ||
112 | export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { | 113 | export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { |
113 | widgetName: string; | 114 | widgetName: string; |
114 | alias: string; | 115 | alias: string; |
115 | - typeSettingsSchema?: string; | ||
116 | - typeDataKeySettingsSchema?: string; | 116 | + typeSettingsSchema?: string | any; |
117 | + typeDataKeySettingsSchema?: string | any; | ||
117 | componentFactory?: ComponentFactory<IDynamicWidgetComponent>; | 118 | componentFactory?: ComponentFactory<IDynamicWidgetComponent>; |
118 | } | 119 | } |
119 | 120 | ||
121 | +export interface WidgetConfigComponentData { | ||
122 | + config: WidgetConfig; | ||
123 | + layout: WidgetLayout; | ||
124 | +} | ||
125 | + | ||
120 | export const MissingWidgetType: WidgetInfo = { | 126 | export const MissingWidgetType: WidgetInfo = { |
121 | type: widgetType.latest, | 127 | type: widgetType.latest, |
122 | widgetName: 'Widget type not found', | 128 | widgetName: 'Widget type not found', |
@@ -138,13 +138,14 @@ | @@ -138,13 +138,14 @@ | ||
138 | [layoutCtx]="layouts.main.layoutCtx" | 138 | [layoutCtx]="layouts.main.layoutCtx" |
139 | [dashboardCtx]="dashboardCtx" | 139 | [dashboardCtx]="dashboardCtx" |
140 | [isEdit]="isEdit" | 140 | [isEdit]="isEdit" |
141 | + [isEditingWidget]="isEditingWidget" | ||
141 | [isMobile]="forceDashboardMobileMode" | 142 | [isMobile]="forceDashboardMobileMode" |
142 | [widgetEditMode]="widgetEditMode"> | 143 | [widgetEditMode]="widgetEditMode"> |
143 | </tb-dashboard-layout> | 144 | </tb-dashboard-layout> |
144 | </div> | 145 | </div> |
145 | - <mat-sidenav-container *ngIf="layouts.right.show" | 146 | + <mat-drawer-container *ngIf="layouts.right.show" |
146 | id="tb-right-layout"> | 147 | id="tb-right-layout"> |
147 | - <mat-sidenav | 148 | + <mat-drawer |
148 | [ngStyle]="{minWidth: rightLayoutWidth(), | 149 | [ngStyle]="{minWidth: rightLayoutWidth(), |
149 | maxWidth: rightLayoutWidth(), | 150 | maxWidth: rightLayoutWidth(), |
150 | height: rightLayoutHeight(), | 151 | height: rightLayoutHeight(), |
@@ -157,13 +158,43 @@ | @@ -157,13 +158,43 @@ | ||
157 | [layoutCtx]="layouts.right.layoutCtx" | 158 | [layoutCtx]="layouts.right.layoutCtx" |
158 | [dashboardCtx]="dashboardCtx" | 159 | [dashboardCtx]="dashboardCtx" |
159 | [isEdit]="isEdit" | 160 | [isEdit]="isEdit" |
161 | + [isEditingWidget]="isEditingWidget" | ||
160 | [isMobile]="forceDashboardMobileMode" | 162 | [isMobile]="forceDashboardMobileMode" |
161 | [widgetEditMode]="widgetEditMode"> | 163 | [widgetEditMode]="widgetEditMode"> |
162 | </tb-dashboard-layout> | 164 | </tb-dashboard-layout> |
163 | - </mat-sidenav> | ||
164 | - </mat-sidenav-container> | 165 | + </mat-drawer> |
166 | + </mat-drawer-container> | ||
165 | </div> | 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 | <!--tb-details-sidenav TODO --> | 198 | <!--tb-details-sidenav TODO --> |
168 | <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> | 199 | <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> |
169 | <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" | 200 | <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" |
@@ -28,6 +28,7 @@ tb-dashboard-page { | @@ -28,6 +28,7 @@ tb-dashboard-page { | ||
28 | 28 | ||
29 | div.tb-dashboard-page { | 29 | div.tb-dashboard-page { |
30 | &.mat-content { | 30 | &.mat-content { |
31 | + overflow: hidden; | ||
31 | background-color: #eee; | 32 | background-color: #eee; |
32 | } | 33 | } |
33 | section.tb-dashboard-title { | 34 | section.tb-dashboard-title { |
@@ -108,13 +109,30 @@ div.tb-dashboard-page { | @@ -108,13 +109,30 @@ div.tb-dashboard-page { | ||
108 | z-index: 1; | 109 | z-index: 1; |
109 | }*/ | 110 | }*/ |
110 | #tb-right-layout { | 111 | #tb-right-layout { |
111 | - mat-sidenav { | 112 | + mat-drawer { |
112 | z-index: 1; | 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 | section.tb-powered-by-footer { | 136 | section.tb-powered-by-footer { |
119 | position: absolute; | 137 | position: absolute; |
120 | right: 25px; | 138 | right: 25px; |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 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 | import { PageComponent } from '@shared/components/page.component'; | 18 | import { PageComponent } from '@shared/components/page.component'; |
19 | import { Store } from '@ngrx/store'; | 19 | import { Store } from '@ngrx/store'; |
20 | import { AppState } from '@core/core.state'; | 20 | import { AppState } from '@core/core.state'; |
@@ -41,17 +41,25 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; | @@ -41,17 +41,25 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; | ||
41 | import { MediaBreakpoints } from '@shared/models/constants'; | 41 | import { MediaBreakpoints } from '@shared/models/constants'; |
42 | import { AuthUser } from '@shared/models/user.model'; | 42 | import { AuthUser } from '@shared/models/user.model'; |
43 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | 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 | import { environment as env } from '@env/environment'; | 45 | import { environment as env } from '@env/environment'; |
46 | import { Authority } from '@shared/models/authority.enum'; | 46 | import { Authority } from '@shared/models/authority.enum'; |
47 | import { DialogService } from '@core/services/dialog.service'; | 47 | import { DialogService } from '@core/services/dialog.service'; |
48 | import { EntityService } from '@core/http/entity.service'; | 48 | import { EntityService } from '@core/http/entity.service'; |
49 | import { AliasController } from '@core/api/alias-controller'; | 49 | import { AliasController } from '@core/api/alias-controller'; |
50 | -import { Subscription } from 'rxjs'; | 50 | +import { Observable, Subscription, of } from 'rxjs'; |
51 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; | 51 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; |
52 | import { IStateController } from '@core/api/widget-api.models'; | 52 | import { IStateController } from '@core/api/widget-api.models'; |
53 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | 53 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
54 | import { DashboardService } from '@core/http/dashboard.service'; | 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 | @Component({ | 64 | @Component({ |
57 | selector: 'tb-dashboard-page', | 65 | selector: 'tb-dashboard-page', |
@@ -89,6 +97,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -89,6 +97,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
89 | editingWidgetLayoutOriginal: WidgetLayout = null; | 97 | editingWidgetLayoutOriginal: WidgetLayout = null; |
90 | editingWidgetSubtitle: string = null; | 98 | editingWidgetSubtitle: string = null; |
91 | editingLayoutCtx: DashboardPageLayoutContext = null; | 99 | editingLayoutCtx: DashboardPageLayoutContext = null; |
100 | + editingWidgetFormGroup: FormGroup; | ||
92 | 101 | ||
93 | thingsboardVersion: string = env.tbVersion; | 102 | thingsboardVersion: string = env.tbVersion; |
94 | 103 | ||
@@ -105,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -105,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
105 | widgetLayouts: {}, | 114 | widgetLayouts: {}, |
106 | gridSettings: {}, | 115 | gridSettings: {}, |
107 | ignoreLoading: false, | 116 | ignoreLoading: false, |
108 | - ctrl: null | 117 | + ctrl: null, |
118 | + dashboardCtrl: this | ||
109 | } | 119 | } |
110 | }, | 120 | }, |
111 | right: { | 121 | right: { |
@@ -116,7 +126,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -116,7 +126,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
116 | widgetLayouts: {}, | 126 | widgetLayouts: {}, |
117 | gridSettings: {}, | 127 | gridSettings: {}, |
118 | ignoreLoading: false, | 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,9 +186,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
175 | private authService: AuthService, | 186 | private authService: AuthService, |
176 | private entityService: EntityService, | 187 | private entityService: EntityService, |
177 | private dialogService: DialogService, | 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 | super(store); | 193 | super(store); |
180 | 194 | ||
195 | + this.editingWidgetFormGroup = this.fb.group({ | ||
196 | + widgetConfig: [null] | ||
197 | + }); | ||
198 | + | ||
181 | this.rxSubscriptions.push(this.route.data.subscribe( | 199 | this.rxSubscriptions.push(this.route.data.subscribe( |
182 | (data) => { | 200 | (data) => { |
183 | this.init(data); | 201 | this.init(data); |
@@ -253,6 +271,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -253,6 +271,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
253 | this.currentDashboardId = null; | 271 | this.currentDashboardId = null; |
254 | this.currentCustomerId = null; | 272 | this.currentCustomerId = null; |
255 | this.currentDashboardScope = null; | 273 | this.currentDashboardScope = null; |
274 | + | ||
275 | + this.dashboardCtx.state = null; | ||
256 | } | 276 | } |
257 | 277 | ||
258 | ngOnDestroy(): void { | 278 | ngOnDestroy(): void { |
@@ -428,14 +448,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -428,14 +448,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
428 | this.dialogService.todo(); | 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 | private importWidget($event: Event) { | 451 | private importWidget($event: Event) { |
440 | if ($event) { | 452 | if ($event) { |
441 | $event.stopPropagation(); | 453 | $event.stopPropagation(); |
@@ -568,7 +580,221 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -568,7 +580,221 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
568 | }; | 580 | }; |
569 | this.window.parent.postMessage(JSON.stringify(message), '*'); | 581 | this.window.parent.postMessage(JSON.stringify(message), '*'); |
570 | } else { | 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,6 +19,12 @@ import { Widget } from '@app/shared/models/widget.models'; | ||
19 | import { Timewindow } from '@shared/models/time/time.models'; | 19 | import { Timewindow } from '@shared/models/time/time.models'; |
20 | import { IAliasController, IStateController } from '@core/api/widget-api.models'; | 20 | import { IAliasController, IStateController } from '@core/api/widget-api.models'; |
21 | import { ILayoutController } from './layout/layout.models'; | 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 | export declare type DashboardPageScope = 'tenant' | 'customer'; | 29 | export declare type DashboardPageScope = 'tenant' | 'customer'; |
24 | 30 | ||
@@ -34,6 +40,18 @@ export interface IDashboardController { | @@ -34,6 +40,18 @@ export interface IDashboardController { | ||
34 | dashboardCtx: DashboardContext; | 40 | dashboardCtx: DashboardContext; |
35 | openRightLayout(); | 41 | openRightLayout(); |
36 | openDashboardState(stateId: string, openRightLayout: boolean); | 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 | export interface DashboardPageLayoutContext { | 57 | export interface DashboardPageLayoutContext { |
@@ -42,6 +60,7 @@ export interface DashboardPageLayoutContext { | @@ -42,6 +60,7 @@ export interface DashboardPageLayoutContext { | ||
42 | widgetLayouts: WidgetLayouts; | 60 | widgetLayouts: WidgetLayouts; |
43 | gridSettings: GridSettings; | 61 | gridSettings: GridSettings; |
44 | ctrl: ILayoutController; | 62 | ctrl: ILayoutController; |
63 | + dashboardCtrl: IDashboardController; | ||
45 | ignoreLoading: boolean; | 64 | ignoreLoading: boolean; |
46 | } | 65 | } |
47 | 66 |
@@ -28,6 +28,7 @@ import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.com | @@ -28,6 +28,7 @@ import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.com | ||
28 | import { DashboardToolbarComponent } from './dashboard-toolbar.component'; | 28 | import { DashboardToolbarComponent } from './dashboard-toolbar.component'; |
29 | import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; | 29 | import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; |
30 | import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; | 30 | import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; |
31 | +import { EditWidgetComponent } from './edit-widget.component'; | ||
31 | 32 | ||
32 | @NgModule({ | 33 | @NgModule({ |
33 | entryComponents: [ | 34 | entryComponents: [ |
@@ -43,7 +44,8 @@ import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; | @@ -43,7 +44,8 @@ import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; | ||
43 | MakeDashboardPublicDialogComponent, | 44 | MakeDashboardPublicDialogComponent, |
44 | DashboardToolbarComponent, | 45 | DashboardToolbarComponent, |
45 | DashboardPageComponent, | 46 | DashboardPageComponent, |
46 | - DashboardLayoutComponent | 47 | + DashboardLayoutComponent, |
48 | + EditWidgetComponent | ||
47 | ], | 49 | ], |
48 | imports: [ | 50 | imports: [ |
49 | CommonModule, | 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,6 +15,7 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | +<hotkeys-cheatsheet></hotkeys-cheatsheet> | ||
18 | <div class="mat-content" style="position: relative; width: 100%; height: 100%;" | 19 | <div class="mat-content" style="position: relative; width: 100%; height: 100%;" |
19 | [ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor, | 20 | [ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor, |
20 | 'background-image': layoutCtx.gridSettings.backgroundImageUrl ? | 21 | 'background-image': layoutCtx.gridSettings.backgroundImageUrl ? |
@@ -60,6 +61,7 @@ | @@ -60,6 +61,7 @@ | ||
60 | [isEditActionEnabled]="isEdit" | 61 | [isEditActionEnabled]="isEdit" |
61 | [isExportActionEnabled]="isEdit && !widgetEditMode" | 62 | [isExportActionEnabled]="isEdit && !widgetEditMode" |
62 | [isRemoveActionEnabled]="isEdit && !widgetEditMode" | 63 | [isRemoveActionEnabled]="isEdit && !widgetEditMode" |
64 | + [callbacks]="this" | ||
63 | [ignoreLoading]="layoutCtx.ignoreLoading"> | 65 | [ignoreLoading]="layoutCtx.ignoreLoading"> |
64 | </tb-dashboard> | 66 | </tb-dashboard> |
65 | </div> | 67 | </div> |
@@ -22,16 +22,25 @@ import { PageComponent } from '@shared/components/page.component'; | @@ -22,16 +22,25 @@ import { PageComponent } from '@shared/components/page.component'; | ||
22 | import { Store } from '@ngrx/store'; | 22 | import { Store } from '@ngrx/store'; |
23 | import { AppState } from '@core/core.state'; | 23 | import { AppState } from '@core/core.state'; |
24 | import { Widget } from '@shared/models/widget.models'; | 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 | import { GridsterComponent } from 'angular-gridster2'; | 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 | @Component({ | 38 | @Component({ |
30 | selector: 'tb-dashboard-layout', | 39 | selector: 'tb-dashboard-layout', |
31 | templateUrl: './dashboard-layout.component.html', | 40 | templateUrl: './dashboard-layout.component.html', |
32 | styleUrls: ['./dashboard-layout.component.scss'] | 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 | layoutCtxValue: DashboardPageLayoutContext; | 45 | layoutCtxValue: DashboardPageLayoutContext; |
37 | 46 | ||
@@ -53,6 +62,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -53,6 +62,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
53 | isEdit: boolean; | 62 | isEdit: boolean; |
54 | 63 | ||
55 | @Input() | 64 | @Input() |
65 | + isEditingWidget: boolean; | ||
66 | + | ||
67 | + @Input() | ||
56 | isMobile: boolean; | 68 | isMobile: boolean; |
57 | 69 | ||
58 | @Input() | 70 | @Input() |
@@ -60,15 +72,97 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -60,15 +72,97 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
60 | 72 | ||
61 | @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; | 73 | @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; |
62 | 74 | ||
75 | + private rxSubscriptions = new Array<Subscription>(); | ||
76 | + | ||
63 | constructor(protected store: Store<AppState>, | 77 | constructor(protected store: Store<AppState>, |
64 | - private cd: ChangeDetectorRef) { | 78 | + private hotkeysService: HotkeysService, |
79 | + private translate: TranslateService, | ||
80 | + private itembuffer: ItemBufferService) { | ||
65 | super(store); | 81 | super(store); |
66 | } | 82 | } |
67 | 83 | ||
68 | ngOnInit(): void { | 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 | ngOnDestroy(): void { | 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 | reload() { | 168 | reload() { |
@@ -78,6 +172,65 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -78,6 +172,65 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
78 | } | 172 | } |
79 | 173 | ||
80 | resetHighlight() { | 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,8 +14,15 @@ | ||
14 | /// limitations under the License. | 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 | export interface ILayoutController { | 20 | export interface ILayoutController { |
18 | reload(); | 21 | reload(); |
19 | setResizing(layoutVisibilityChanged: boolean); | 22 | setResizing(layoutVisibilityChanged: boolean); |
20 | resetHighlight(); | 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,7 +65,13 @@ export const HelpLinks = { | ||
65 | entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', | 65 | entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', |
66 | rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', | 66 | rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', |
67 | dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', | 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,6 +40,7 @@ export interface WidgetTypeData { | ||
40 | name: string; | 40 | name: string; |
41 | icon: string; | 41 | icon: string; |
42 | isMdiIcon?: boolean; | 42 | isMdiIcon?: boolean; |
43 | + configHelpLinkId: string; | ||
43 | template: WidgetTypeTemplate; | 44 | template: WidgetTypeTemplate; |
44 | } | 45 | } |
45 | 46 | ||
@@ -50,6 +51,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | @@ -50,6 +51,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | ||
50 | { | 51 | { |
51 | name: 'widget.timeseries', | 52 | name: 'widget.timeseries', |
52 | icon: 'timeline', | 53 | icon: 'timeline', |
54 | + configHelpLinkId: 'widgetsConfigTimeseries', | ||
53 | template: { | 55 | template: { |
54 | bundleAlias: 'charts', | 56 | bundleAlias: 'charts', |
55 | alias: 'basic_timeseries' | 57 | alias: 'basic_timeseries' |
@@ -61,6 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | @@ -61,6 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | ||
61 | { | 63 | { |
62 | name: 'widget.latest-values', | 64 | name: 'widget.latest-values', |
63 | icon: 'track_changes', | 65 | icon: 'track_changes', |
66 | + configHelpLinkId: 'widgetsConfigLatest', | ||
64 | template: { | 67 | template: { |
65 | bundleAlias: 'cards', | 68 | bundleAlias: 'cards', |
66 | alias: 'attributes_card' | 69 | alias: 'attributes_card' |
@@ -72,6 +75,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | @@ -72,6 +75,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | ||
72 | { | 75 | { |
73 | name: 'widget.rpc', | 76 | name: 'widget.rpc', |
74 | icon: 'mdi:developer-board', | 77 | icon: 'mdi:developer-board', |
78 | + configHelpLinkId: 'widgetsConfigRpc', | ||
75 | isMdiIcon: true, | 79 | isMdiIcon: true, |
76 | template: { | 80 | template: { |
77 | bundleAlias: 'gpio_widgets', | 81 | bundleAlias: 'gpio_widgets', |
@@ -84,6 +88,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | @@ -84,6 +88,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | ||
84 | { | 88 | { |
85 | name: 'widget.alarm', | 89 | name: 'widget.alarm', |
86 | icon: 'error', | 90 | icon: 'error', |
91 | + configHelpLinkId: 'widgetsConfigAlarm', | ||
87 | template: { | 92 | template: { |
88 | bundleAlias: 'alarm_widgets', | 93 | bundleAlias: 'alarm_widgets', |
89 | alias: 'alarms_table' | 94 | alias: 'alarms_table' |
@@ -95,6 +100,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | @@ -95,6 +100,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( | ||
95 | { | 100 | { |
96 | name: 'widget.static', | 101 | name: 'widget.static', |
97 | icon: 'font_download', | 102 | icon: 'font_download', |
103 | + configHelpLinkId: 'widgetsConfigStatic', | ||
98 | template: { | 104 | template: { |
99 | bundleAlias: 'cards', | 105 | bundleAlias: 'cards', |
100 | alias: 'html_card' | 106 | alias: 'html_card' |
@@ -129,8 +135,8 @@ export interface WidgetTypeDescriptor { | @@ -129,8 +135,8 @@ export interface WidgetTypeDescriptor { | ||
129 | templateHtml: string; | 135 | templateHtml: string; |
130 | templateCss: string; | 136 | templateCss: string; |
131 | controllerScript: string; | 137 | controllerScript: string; |
132 | - settingsSchema?: string; | ||
133 | - dataKeySettingsSchema?: string; | 138 | + settingsSchema?: string | any; |
139 | + dataKeySettingsSchema?: string | any; | ||
134 | defaultConfig: string; | 140 | defaultConfig: string; |
135 | sizeX: number; | 141 | sizeX: number; |
136 | sizeY: number; | 142 | sizeY: number; |
@@ -146,8 +152,8 @@ export interface WidgetTypeParameters { | @@ -146,8 +152,8 @@ export interface WidgetTypeParameters { | ||
146 | 152 | ||
147 | export interface WidgetControllerDescriptor { | 153 | export interface WidgetControllerDescriptor { |
148 | widgetTypeFunction?: any; | 154 | widgetTypeFunction?: any; |
149 | - settingsSchema?: string; | ||
150 | - dataKeySettingsSchema?: string; | 155 | + settingsSchema?: string | any; |
156 | + dataKeySettingsSchema?: string | any; | ||
151 | typeParameters?: WidgetTypeParameters; | 157 | typeParameters?: WidgetTypeParameters; |
152 | actionSources?: {[key: string]: WidgetActionSource}; | 158 | actionSources?: {[key: string]: WidgetActionSource}; |
153 | } | 159 | } |
@@ -309,7 +315,7 @@ export interface WidgetConfig { | @@ -309,7 +315,7 @@ export interface WidgetConfig { | ||
309 | showTitle?: boolean; | 315 | showTitle?: boolean; |
310 | showTitleIcon?: boolean; | 316 | showTitleIcon?: boolean; |
311 | iconColor?: string; | 317 | iconColor?: string; |
312 | - iconSize?: number; | 318 | + iconSize?: string; |
313 | dropShadow?: boolean; | 319 | dropShadow?: boolean; |
314 | enableFullscreen?: boolean; | 320 | enableFullscreen?: boolean; |
315 | useDashboardTimewindow?: boolean; | 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,6 +95,7 @@ import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from '. | ||
95 | import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; | 95 | import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; |
96 | import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; | 96 | import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; |
97 | import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; | 97 | import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; |
98 | +import { KeyboardShortcutPipe } from './pipe/keyboard-shortcut.pipe'; | ||
98 | 99 | ||
99 | @NgModule({ | 100 | @NgModule({ |
100 | providers: [ | 101 | providers: [ |
@@ -150,7 +151,8 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select | @@ -150,7 +151,8 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select | ||
150 | NospacePipe, | 151 | NospacePipe, |
151 | MillisecondsToTimeStringPipe, | 152 | MillisecondsToTimeStringPipe, |
152 | EnumToArrayPipe, | 153 | EnumToArrayPipe, |
153 | - HighlightPipe | 154 | + HighlightPipe, |
155 | + KeyboardShortcutPipe | ||
154 | ], | 156 | ], |
155 | imports: [ | 157 | imports: [ |
156 | CommonModule, | 158 | CommonModule, |
@@ -272,6 +274,7 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select | @@ -272,6 +274,7 @@ import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select | ||
272 | MillisecondsToTimeStringPipe, | 274 | MillisecondsToTimeStringPipe, |
273 | EnumToArrayPipe, | 275 | EnumToArrayPipe, |
274 | HighlightPipe, | 276 | HighlightPipe, |
277 | + KeyboardShortcutPipe, | ||
275 | TranslateModule | 278 | TranslateModule |
276 | ] | 279 | ] |
277 | }) | 280 | }) |
@@ -277,6 +277,15 @@ $tb-dark-theme: get-tb-dark-theme( | @@ -277,6 +277,15 @@ $tb-dark-theme: get-tb-dark-theme( | ||
277 | display: block; | 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 | mat-toolbar.mat-table-toolbar { | 289 | mat-toolbar.mat-table-toolbar { |
281 | background: #fff; | 290 | background: #fff; |
282 | padding: 0 24px; | 291 | padding: 0 24px; |