Showing
26 changed files
with
1077 additions
and
195 deletions
... | ... | @@ -39,6 +39,7 @@ import { EntityInfo } from '@app/shared/models/entity.models'; |
39 | 39 | import { Type } from '@angular/core'; |
40 | 40 | import { AssetService } from '@core/http/asset.service'; |
41 | 41 | import { DialogService } from '@core/services/dialog.service'; |
42 | +import { IDashboardComponent } from '@home/models/dashboard-component.models'; | |
42 | 43 | |
43 | 44 | export interface TimewindowFunctions { |
44 | 45 | onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; |
... | ... | @@ -148,7 +149,19 @@ export interface SubscriptionInfo { |
148 | 149 | deviceIds?: Array<string>; |
149 | 150 | } |
150 | 151 | |
151 | -export interface WidgetSubscriptionContext { | |
152 | +export class WidgetSubscriptionContext { | |
153 | + | |
154 | + constructor(private dashboard: IDashboardComponent) {} | |
155 | + | |
156 | + get aliasController(): IAliasController { | |
157 | + return this.dashboard.aliasController; | |
158 | + } | |
159 | + | |
160 | + dashboardTimewindowApi: TimewindowFunctions = { | |
161 | + onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard), | |
162 | + onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard) | |
163 | + }; | |
164 | + | |
152 | 165 | timeService: TimeService; |
153 | 166 | deviceService: DeviceService; |
154 | 167 | alarmService: AlarmService; |
... | ... | @@ -156,11 +169,7 @@ export interface WidgetSubscriptionContext { |
156 | 169 | utils: UtilsService; |
157 | 170 | raf: RafService; |
158 | 171 | widgetUtils: IWidgetUtils; |
159 | - dashboardTimewindowApi: TimewindowFunctions; | |
160 | 172 | getServerTimeDiff: () => Observable<number>; |
161 | - aliasController: IAliasController; | |
162 | - [key: string]: any; | |
163 | - // TODO: | |
164 | 173 | } |
165 | 174 | |
166 | 175 | export interface WidgetSubscriptionCallbacks { | ... | ... |
... | ... | @@ -382,6 +382,13 @@ export class WidgetSubscription implements IWidgetSubscription { |
382 | 382 | } |
383 | 383 | |
384 | 384 | onAliasesChanged(aliasIds: Array<string>): boolean { |
385 | + if (this.type === widgetType.rpc) { | |
386 | + return this.checkRpcTarget(aliasIds); | |
387 | + } else if (this.type === widgetType.alarm) { | |
388 | + return this.checkAlarmSource(aliasIds); | |
389 | + } else { | |
390 | + return this.checkSubscriptions(aliasIds); | |
391 | + } | |
385 | 392 | return false; |
386 | 393 | } |
387 | 394 | |
... | ... | @@ -566,6 +573,35 @@ export class WidgetSubscription implements IWidgetSubscription { |
566 | 573 | // TODO: |
567 | 574 | } |
568 | 575 | |
576 | + private checkRpcTarget(aliasIds: Array<string>): boolean { | |
577 | + if (aliasIds.indexOf(this.targetDeviceAliasId) > -1) { | |
578 | + return true; | |
579 | + } else { | |
580 | + return false; | |
581 | + } | |
582 | + } | |
583 | + | |
584 | + private checkAlarmSource(aliasIds: Array<string>): boolean { | |
585 | + if (this.alarmSource && this.alarmSource.entityAliasId) { | |
586 | + return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1; | |
587 | + } else { | |
588 | + return false; | |
589 | + } | |
590 | + } | |
591 | + | |
592 | + private checkSubscriptions(aliasIds: Array<string>): boolean { | |
593 | + let subscriptionsChanged = false; | |
594 | + for (const listener of this.datasourceListeners) { | |
595 | + if (listener.datasource.entityAliasId) { | |
596 | + if (aliasIds.indexOf(listener.datasource.entityAliasId) > -1) { | |
597 | + subscriptionsChanged = true; | |
598 | + break; | |
599 | + } | |
600 | + } | |
601 | + } | |
602 | + return subscriptionsChanged; | |
603 | + } | |
604 | + | |
569 | 605 | destroy(): void { |
570 | 606 | this.unsubscribe(); |
571 | 607 | for (const cafId of Object.keys(this.cafs)) { | ... | ... |
... | ... | @@ -22,26 +22,29 @@ import {PageLink} from '@shared/models/page/page-link'; |
22 | 22 | import {PageData} from '@shared/models/page/page-data'; |
23 | 23 | import {Dashboard, DashboardInfo} from '@shared/models/dashboard.models'; |
24 | 24 | import {WINDOW} from '@core/services/window.service'; |
25 | -import { ActivationEnd, Router } from '@angular/router'; | |
26 | -import { filter } from 'rxjs/operators'; | |
25 | +import { ActivationEnd, NavigationEnd, Router } from '@angular/router'; | |
26 | +import { filter, map, publishReplay, refCount } from 'rxjs/operators'; | |
27 | 27 | |
28 | 28 | @Injectable({ |
29 | 29 | providedIn: 'root' |
30 | 30 | }) |
31 | 31 | export class DashboardService { |
32 | 32 | |
33 | - stDiffSubject: Subject<number>; | |
33 | + stDiffObservable: Observable<number>; | |
34 | + currentUrl: string; | |
34 | 35 | |
35 | 36 | constructor( |
36 | 37 | private http: HttpClient, |
37 | 38 | private router: Router, |
38 | 39 | @Inject(WINDOW) private window: Window |
39 | 40 | ) { |
40 | - this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( | |
41 | + this.currentUrl = this.router.url.split('?')[0]; | |
42 | + this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe( | |
41 | 43 | () => { |
42 | - if (this.stDiffSubject) { | |
43 | - this.stDiffSubject.complete(); | |
44 | - this.stDiffSubject = null; | |
44 | + const newUrl = this.router.url.split('?')[0]; | |
45 | + if (this.currentUrl !== newUrl) { | |
46 | + this.stDiffObservable = null; | |
47 | + this.currentUrl = newUrl; | |
45 | 48 | } |
46 | 49 | } |
47 | 50 | ); |
... | ... | @@ -139,24 +142,20 @@ export class DashboardService { |
139 | 142 | } |
140 | 143 | |
141 | 144 | public getServerTimeDiff(): Observable<number> { |
142 | - if (this.stDiffSubject) { | |
143 | - return this.stDiffSubject.asObservable(); | |
144 | - } else { | |
145 | - this.stDiffSubject = new ReplaySubject<number>(1); | |
145 | + if (!this.stDiffObservable) { | |
146 | 146 | const url = '/api/dashboard/serverTime'; |
147 | 147 | const ct1 = Date.now(); |
148 | - this.http.get<number>(url, defaultHttpOptions(true)).subscribe( | |
149 | - (st) => { | |
148 | + this.stDiffObservable = this.http.get<number>(url, defaultHttpOptions(true)).pipe( | |
149 | + map((st) => { | |
150 | 150 | const ct2 = Date.now(); |
151 | 151 | const stDiff = Math.ceil(st - (ct1 + ct2) / 2); |
152 | - this.stDiffSubject.next(stDiff); | |
153 | - }, | |
154 | - () => { | |
155 | - this.stDiffSubject.error(null); | |
156 | - } | |
152 | + return stDiff; | |
153 | + }), | |
154 | + publishReplay(1), | |
155 | + refCount() | |
157 | 156 | ); |
158 | - return this.stDiffSubject.asObservable(); | |
159 | 157 | } |
158 | + return this.stDiffObservable; | |
160 | 159 | } |
161 | 160 | |
162 | 161 | } | ... | ... |
... | ... | @@ -479,7 +479,7 @@ export class DashboardUtilsService { |
479 | 479 | } |
480 | 480 | } |
481 | 481 | |
482 | - private removeUnusedWidgets(dashboard: Dashboard) { | |
482 | + public removeUnusedWidgets(dashboard: Dashboard) { | |
483 | 483 | const dashboardConfiguration = dashboard.configuration; |
484 | 484 | const states = dashboardConfiguration.states; |
485 | 485 | const widgets = dashboardConfiguration.widgets; | ... | ... |
... | ... | @@ -99,10 +99,34 @@ export function isNumber(value: any): boolean { |
99 | 99 | return typeof value === 'number'; |
100 | 100 | } |
101 | 101 | |
102 | +export function isNumeric(value: any): boolean { | |
103 | + return (value - parseFloat( value ) + 1) >= 0; | |
104 | +} | |
105 | + | |
102 | 106 | export function isString(value: any): boolean { |
103 | 107 | return typeof value === 'string'; |
104 | 108 | } |
105 | 109 | |
110 | +export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { | |
111 | + if (isDefined(value) && | |
112 | + value != null && isNumeric(value)) { | |
113 | + let formatted: string | number = Number(value); | |
114 | + if (isDefined(dec)) { | |
115 | + formatted = formatted.toFixed(dec); | |
116 | + } | |
117 | + if (!showZeroDecimals) { | |
118 | + formatted = (Number(formatted) * 1); | |
119 | + } | |
120 | + formatted = formatted.toString(); | |
121 | + if (isDefined(units) && units.length > 0) { | |
122 | + formatted += ' ' + units; | |
123 | + } | |
124 | + return formatted; | |
125 | + } else { | |
126 | + return value; | |
127 | + } | |
128 | +} | |
129 | + | |
106 | 130 | export function deleteNullProperties(obj: any) { |
107 | 131 | if (isUndefined(obj) || obj == null) { |
108 | 132 | return; | ... | ... |
... | ... | @@ -52,7 +52,7 @@ export class AliasesEntitySelectPanelComponent { |
52 | 52 | const resolvedEntities = this.entityAliasesInfo[aliasId].resolvedEntities; |
53 | 53 | const selected = resolvedEntities.find((entity) => entity.id === selectedId); |
54 | 54 | if (selected) { |
55 | - this.data.aliasController.updateCurrentAliasEntity(aliasId, selected[0]); | |
55 | + this.data.aliasController.updateCurrentAliasEntity(aliasId, selected); | |
56 | 56 | } |
57 | 57 | } |
58 | 58 | ... | ... |
... | ... | @@ -180,6 +180,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
180 | 180 | this.gridsterOpts = { |
181 | 181 | gridType: 'scrollVertical', |
182 | 182 | keepFixedHeightInMobile: true, |
183 | + disableWarnings: false, | |
184 | + disableAutoPositionOnConflict: false, | |
183 | 185 | pushItems: false, |
184 | 186 | swap: false, |
185 | 187 | maxRows: 100, |
... | ... | @@ -228,7 +230,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
228 | 230 | } |
229 | 231 | |
230 | 232 | ngDoCheck() { |
231 | - this.dashboardWidgets.doCheck(); | |
233 | + if (!this.optionsChangeNotificationsPaused) { | |
234 | + this.dashboardWidgets.doCheck(); | |
235 | + } | |
232 | 236 | } |
233 | 237 | |
234 | 238 | ngOnChanges(changes: SimpleChanges): void { | ... | ... |
... | ... | @@ -116,9 +116,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
116 | 116 | isMobile: boolean; |
117 | 117 | |
118 | 118 | @Input() |
119 | - dashboard: IDashboardComponent; | |
120 | - | |
121 | - @Input() | |
122 | 119 | dashboardWidget: DashboardWidget; |
123 | 120 | |
124 | 121 | @ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef; |
... | ... | @@ -146,11 +143,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
146 | 143 | subscriptionContext: WidgetSubscriptionContext; |
147 | 144 | |
148 | 145 | subscriptionInited = false; |
146 | + destroyed = false; | |
149 | 147 | widgetSizeDetected = false; |
150 | 148 | |
151 | 149 | cafs: {[cafId: string]: CancelAnimationFrame} = {}; |
152 | 150 | |
153 | - onResizeListener = this.onResize.bind(this); | |
151 | + onResizeListener = null; | |
154 | 152 | |
155 | 153 | private cssParser = new cssjs(); |
156 | 154 | |
... | ... | @@ -252,30 +250,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
252 | 250 | |
253 | 251 | this.widgetContext = this.dashboardWidget.widgetContext; |
254 | 252 | this.widgetContext.servicesMap = ServicesMap; |
255 | - this.widgetContext.inited = false; | |
256 | - this.widgetContext.hideTitlePanel = false; | |
257 | 253 | this.widgetContext.isEdit = this.isEdit; |
258 | 254 | this.widgetContext.isMobile = this.isMobile; |
259 | - this.widgetContext.dashboard = this.dashboard; | |
260 | - this.widgetContext.widgetConfig = this.widget.config; | |
261 | - this.widgetContext.settings = this.widget.config.settings; | |
262 | - this.widgetContext.units = this.widget.config.units || ''; | |
263 | - this.widgetContext.decimals = isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2; | |
264 | - this.widgetContext.subscriptions = {}; | |
265 | - this.widgetContext.defaultSubscription = null; | |
266 | - this.widgetContext.dashboardTimewindow = this.dashboard.dashboardTimewindow; | |
267 | - this.widgetContext.timewindowFunctions = { | |
268 | - onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { | |
269 | - if (this.widgetContext.defaultSubscription) { | |
270 | - this.widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); | |
271 | - } | |
272 | - }, | |
273 | - onResetTimewindow: () => { | |
274 | - if (this.widgetContext.defaultSubscription) { | |
275 | - this.widgetContext.defaultSubscription.onResetTimewindow(); | |
276 | - } | |
277 | - } | |
278 | - }; | |
255 | + | |
279 | 256 | this.widgetContext.subscriptionApi = { |
280 | 257 | createSubscription: this.createSubscription.bind(this), |
281 | 258 | createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this), |
... | ... | @@ -287,33 +264,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
287 | 264 | } |
288 | 265 | } |
289 | 266 | }; |
290 | - this.widgetContext.controlApi = { | |
291 | - sendOneWayCommand: (method, params, timeout) => { | |
292 | - if (this.widgetContext.defaultSubscription) { | |
293 | - return this.widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout); | |
294 | - } else { | |
295 | - return of(null); | |
296 | - } | |
297 | - }, | |
298 | - sendTwoWayCommand: (method, params, timeout) => { | |
299 | - if (this.widgetContext.defaultSubscription) { | |
300 | - return this.widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout); | |
301 | - } else { | |
302 | - return of(null); | |
303 | - } | |
304 | - } | |
305 | - }; | |
306 | - this.widgetContext.utils = { | |
307 | - formatValue: this.formatValue.bind(this) | |
308 | - }; | |
267 | + | |
309 | 268 | this.widgetContext.actionsApi = { |
310 | 269 | actionDescriptorsBySourceId, |
311 | 270 | getActionDescriptors: this.getActionDescriptors.bind(this), |
312 | 271 | handleWidgetAction: this.handleWidgetAction.bind(this), |
313 | 272 | elementClick: this.elementClick.bind(this) |
314 | 273 | }; |
315 | - this.widgetContext.stateController = this.dashboard.stateController; | |
316 | - this.widgetContext.aliasController = this.dashboard.aliasController; | |
317 | 274 | |
318 | 275 | this.widgetContext.customHeaderActions = []; |
319 | 276 | const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); |
... | ... | @@ -333,21 +290,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
333 | 290 | this.widgetContext.customHeaderActions.push(headerAction); |
334 | 291 | }); |
335 | 292 | |
336 | - this.subscriptionContext = { | |
337 | - timeService: this.timeService, | |
338 | - deviceService: this.deviceService, | |
339 | - alarmService: this.alarmService, | |
340 | - datasourceService: this.datasourceService, | |
341 | - utils: this.utils, | |
342 | - raf: this.raf, | |
343 | - widgetUtils: this.widgetContext.utils, | |
344 | - dashboardTimewindowApi: { | |
345 | - onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard), | |
346 | - onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard) | |
347 | - }, | |
348 | - getServerTimeDiff: this.dashboardService.getServerTimeDiff.bind(this.dashboardService), | |
349 | - aliasController: this.dashboard.aliasController | |
350 | - }; | |
293 | + this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); | |
294 | + this.subscriptionContext.timeService = this.timeService; | |
295 | + this.subscriptionContext.deviceService = this.deviceService; | |
296 | + this.subscriptionContext.alarmService = this.alarmService; | |
297 | + this.subscriptionContext.datasourceService = this.datasourceService; | |
298 | + this.subscriptionContext.utils = this.utils; | |
299 | + this.subscriptionContext.raf = this.raf; | |
300 | + this.subscriptionContext.widgetUtils = this.widgetContext.utils; | |
301 | + this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); | |
351 | 302 | |
352 | 303 | this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( |
353 | 304 | (widgetInfo) => { |
... | ... | @@ -382,6 +333,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
382 | 333 | } |
383 | 334 | |
384 | 335 | ngOnDestroy(): void { |
336 | + this.destroyed = true; | |
385 | 337 | this.rxSubscriptions.forEach((subscription) => { |
386 | 338 | subscription.unsubscribe(); |
387 | 339 | }); |
... | ... | @@ -481,7 +433,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
481 | 433 | } |
482 | 434 | |
483 | 435 | private onInit(skipSizeCheck?: boolean) { |
484 | - if (!this.widgetContext.$containerParent) { | |
436 | + if (!this.widgetContext.$containerParent || this.destroyed) { | |
485 | 437 | return; |
486 | 438 | } |
487 | 439 | if (!skipSizeCheck) { |
... | ... | @@ -565,17 +517,35 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
565 | 517 | } |
566 | 518 | |
567 | 519 | private reInit() { |
520 | + if (this.cafs.reinit) { | |
521 | + this.cafs.reinit(); | |
522 | + this.cafs.reinit = null; | |
523 | + } | |
524 | + this.cafs.reinit = this.raf.raf(() => { | |
525 | + this.reInitImpl(); | |
526 | + }); | |
527 | + } | |
528 | + | |
529 | + private reInitImpl() { | |
568 | 530 | this.onDestroy(); |
569 | 531 | this.configureDynamicWidgetComponent(); |
570 | 532 | if (!this.typeParameters.useCustomDatasources) { |
571 | 533 | this.createDefaultSubscription().subscribe( |
572 | 534 | () => { |
573 | - this.subscriptionInited = true; | |
574 | - this.onInit(); | |
535 | + if (this.destroyed) { | |
536 | + this.onDestroy(); | |
537 | + } else { | |
538 | + this.subscriptionInited = true; | |
539 | + this.onInit(); | |
540 | + } | |
575 | 541 | }, |
576 | 542 | () => { |
577 | - this.subscriptionInited = true; | |
578 | - this.onInit(); | |
543 | + if (this.destroyed) { | |
544 | + this.onDestroy(); | |
545 | + } else { | |
546 | + this.subscriptionInited = true; | |
547 | + this.onInit(); | |
548 | + } | |
579 | 549 | } |
580 | 550 | ); |
581 | 551 | } else { |
... | ... | @@ -588,7 +558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
588 | 558 | |
589 | 559 | const initSubject = new ReplaySubject(); |
590 | 560 | |
591 | - this.rxSubscriptions.push(this.dashboard.aliasController.entityAliasesChanged.subscribe( | |
561 | + this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe( | |
592 | 562 | (aliasIds) => { |
593 | 563 | let subscriptionChanged = false; |
594 | 564 | for (const id of Object.keys(this.widgetContext.subscriptions)) { |
... | ... | @@ -601,7 +571,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
601 | 571 | } |
602 | 572 | )); |
603 | 573 | |
604 | - this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( | |
574 | + this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( | |
605 | 575 | (dashboardTimewindow) => { |
606 | 576 | for (const id of Object.keys(this.widgetContext.subscriptions)) { |
607 | 577 | const subscription = this.widgetContext.subscriptions[id]; |
... | ... | @@ -634,9 +604,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
634 | 604 | } |
635 | 605 | |
636 | 606 | private destroyDynamicWidgetComponent() { |
637 | - if (this.widgetContext.$containerParent) { | |
607 | + if (this.widgetContext.$containerParent && this.onResizeListener) { | |
638 | 608 | // @ts-ignore |
639 | 609 | removeResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); |
610 | + this.onResizeListener = null; | |
640 | 611 | } |
641 | 612 | if (this.dynamicWidgetComponentRef) { |
642 | 613 | this.dynamicWidgetComponentRef.destroy(); |
... | ... | @@ -661,7 +632,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
661 | 632 | |
662 | 633 | const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); |
663 | 634 | |
664 | - this.widgetContext.$container = $('> ng-component', containerElement); | |
635 | + // this.widgetContext.$container = $('> ng-component:not([id="container"])', containerElement); | |
636 | + this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); | |
665 | 637 | this.widgetContext.$container.css('display', 'block'); |
666 | 638 | this.widgetContext.$container.css('user-select', 'none'); |
667 | 639 | this.widgetContext.$container.attr('id', 'container'); |
... | ... | @@ -672,13 +644,14 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
672 | 644 | this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); |
673 | 645 | } |
674 | 646 | |
647 | + this.onResizeListener = this.onResize.bind(this); | |
675 | 648 | // @ts-ignore |
676 | 649 | addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); |
677 | 650 | } |
678 | 651 | |
679 | 652 | private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable<IWidgetSubscription> { |
680 | 653 | const createSubscriptionSubject = new ReplaySubject<IWidgetSubscription>(); |
681 | - options.dashboardTimewindow = this.dashboard.dashboardTimewindow; | |
654 | + options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; | |
682 | 655 | const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); |
683 | 656 | subscription.init$.subscribe( |
684 | 657 | () => { |
... | ... | @@ -747,7 +720,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
747 | 720 | ? this.widget.config.useDashboardTimewindow : true; |
748 | 721 | options.displayTimewindow = isDefined(this.widget.config.displayTimewindow) |
749 | 722 | ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; |
750 | - options.timeWindowConfig = options.useDashboardTimewindow ? this.dashboard.dashboardTimewindow : this.widget.config.timewindow; | |
723 | + options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; | |
751 | 724 | options.legendConfig = null; |
752 | 725 | if (this.displayLegend) { |
753 | 726 | options.legendConfig = this.legendConfig; |
... | ... | @@ -875,30 +848,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
875 | 848 | return createSubscriptionSubject.asObservable(); |
876 | 849 | } |
877 | 850 | |
878 | - private isNumeric(value: any): boolean { | |
879 | - return (value - parseFloat( value ) + 1) >= 0; | |
880 | - } | |
881 | - | |
882 | - private formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { | |
883 | - if (isDefined(value) && | |
884 | - value != null && this.isNumeric(value)) { | |
885 | - let formatted: string | number = Number(value); | |
886 | - if (isDefined(dec)) { | |
887 | - formatted = formatted.toFixed(dec); | |
888 | - } | |
889 | - if (!showZeroDecimals) { | |
890 | - formatted = (Number(formatted) * 1); | |
891 | - } | |
892 | - formatted = formatted.toString(); | |
893 | - if (isDefined(units) && units.length > 0) { | |
894 | - formatted += ' ' + units; | |
895 | - } | |
896 | - return formatted; | |
897 | - } else { | |
898 | - return value; | |
899 | - } | |
900 | - } | |
901 | - | |
902 | 851 | private getActionDescriptors(actionSourceId: string): Array<WidgetActionDescriptor> { |
903 | 852 | let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId]; |
904 | 853 | if (!result) { | ... | ... |
... | ... | @@ -302,7 +302,7 @@ export class DashboardWidget implements GridsterItem { |
302 | 302 | customHeaderActions: Array<WidgetHeaderAction>; |
303 | 303 | widgetActions: Array<WidgetAction>; |
304 | 304 | |
305 | - widgetContext: WidgetContext = {}; | |
305 | + widgetContext = new WidgetContext(this.dashboard, this.widget); | |
306 | 306 | |
307 | 307 | widgetId: string; |
308 | 308 | ... | ... |
... | ... | @@ -26,7 +26,8 @@ import { |
26 | 26 | WidgetType, |
27 | 27 | widgetType, |
28 | 28 | WidgetTypeDescriptor, |
29 | - WidgetTypeParameters | |
29 | + WidgetTypeParameters, | |
30 | + Widget | |
30 | 31 | } from '@shared/models/widget.models'; |
31 | 32 | import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; |
32 | 33 | import { |
... | ... | @@ -34,10 +35,10 @@ import { |
34 | 35 | IStateController, |
35 | 36 | IWidgetSubscription, |
36 | 37 | IWidgetUtils, |
37 | - RpcApi, SubscriptionEntityInfo, | |
38 | + RpcApi, SubscriptionEntityInfo, SubscriptionInfo, | |
38 | 39 | TimewindowFunctions, |
39 | 40 | WidgetActionsApi, |
40 | - WidgetSubscriptionApi | |
41 | + WidgetSubscriptionApi, WidgetSubscriptionContext, WidgetSubscriptionOptions | |
41 | 42 | } from '@core/api/widget-api.models'; |
42 | 43 | import { ComponentFactory, Type } from '@angular/core'; |
43 | 44 | import { HttpErrorResponse } from '@angular/common/http'; |
... | ... | @@ -49,6 +50,9 @@ import { DeviceService } from '@core/http/device.service'; |
49 | 50 | import { AssetService } from '@app/core/http/asset.service'; |
50 | 51 | import { DialogService } from '@core/services/dialog.service'; |
51 | 52 | import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; |
53 | +import { isDefined, formatValue } from '@core/utils'; | |
54 | +import { Observable, of, ReplaySubject } from 'rxjs'; | |
55 | +import { WidgetSubscription } from '@core/api/widget-subscription'; | |
52 | 56 | |
53 | 57 | export interface IWidgetAction { |
54 | 58 | name: string; |
... | ... | @@ -65,30 +69,89 @@ export interface WidgetAction extends IWidgetAction { |
65 | 69 | show: boolean; |
66 | 70 | } |
67 | 71 | |
68 | -export interface WidgetContext { | |
69 | - inited?: boolean; | |
70 | - $container?: JQuery<any>; | |
71 | - $containerParent?: JQuery<any>; | |
72 | - width?: number; | |
73 | - height?: number; | |
74 | - $scope?: IDynamicWidgetComponent; | |
75 | - isEdit?: boolean; | |
76 | - isMobile?: boolean; | |
77 | - dashboard?: IDashboardComponent; | |
78 | - widgetConfig?: WidgetConfig; | |
79 | - settings?: any; | |
80 | - units?: string; | |
81 | - decimals?: number; | |
82 | - subscriptions?: {[id: string]: IWidgetSubscription}; | |
83 | - defaultSubscription?: IWidgetSubscription; | |
84 | - dashboardTimewindow?: Timewindow; | |
85 | - timewindowFunctions?: TimewindowFunctions; | |
72 | +export class WidgetContext { | |
73 | + | |
74 | + constructor(public dashboard: IDashboardComponent, | |
75 | + private widget: Widget) {} | |
76 | + | |
77 | + get stateController(): IStateController { | |
78 | + return this.dashboard.stateController; | |
79 | + } | |
80 | + | |
81 | + get aliasController(): IAliasController { | |
82 | + return this.dashboard.aliasController; | |
83 | + } | |
84 | + | |
85 | + get dashboardTimewindow(): Timewindow { | |
86 | + return this.dashboard.dashboardTimewindow; | |
87 | + } | |
88 | + | |
89 | + get widgetConfig(): WidgetConfig { | |
90 | + return this.widget.config; | |
91 | + } | |
92 | + | |
93 | + get settings(): any { | |
94 | + return this.widget.config.settings; | |
95 | + } | |
96 | + | |
97 | + get units(): string { | |
98 | + return this.widget.config.units || ''; | |
99 | + } | |
100 | + | |
101 | + get decimals(): number { | |
102 | + return isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2; | |
103 | + } | |
104 | + | |
105 | + inited = false; | |
106 | + | |
107 | + subscriptions: {[id: string]: IWidgetSubscription} = {}; | |
108 | + defaultSubscription: IWidgetSubscription = null; | |
109 | + | |
110 | + timewindowFunctions: TimewindowFunctions = { | |
111 | + onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { | |
112 | + if (this.defaultSubscription) { | |
113 | + this.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); | |
114 | + } | |
115 | + }, | |
116 | + onResetTimewindow: () => { | |
117 | + if (this.defaultSubscription) { | |
118 | + this.defaultSubscription.onResetTimewindow(); | |
119 | + } | |
120 | + } | |
121 | + }; | |
122 | + | |
123 | + controlApi: RpcApi = { | |
124 | + sendOneWayCommand: (method, params, timeout) => { | |
125 | + if (this.defaultSubscription) { | |
126 | + return this.defaultSubscription.sendOneWayCommand(method, params, timeout); | |
127 | + } else { | |
128 | + return of(null); | |
129 | + } | |
130 | + }, | |
131 | + sendTwoWayCommand: (method, params, timeout) => { | |
132 | + if (this.defaultSubscription) { | |
133 | + return this.defaultSubscription.sendTwoWayCommand(method, params, timeout); | |
134 | + } else { | |
135 | + return of(null); | |
136 | + } | |
137 | + } | |
138 | + }; | |
139 | + | |
140 | + utils: IWidgetUtils = { | |
141 | + formatValue | |
142 | + }; | |
143 | + | |
144 | + $container: JQuery<any>; | |
145 | + $containerParent: JQuery<any>; | |
146 | + width: number; | |
147 | + height: number; | |
148 | + $scope: IDynamicWidgetComponent; | |
149 | + isEdit: boolean; | |
150 | + isMobile: boolean; | |
151 | + | |
86 | 152 | subscriptionApi?: WidgetSubscriptionApi; |
87 | - controlApi?: RpcApi; | |
88 | - utils?: IWidgetUtils; | |
153 | + | |
89 | 154 | actionsApi?: WidgetActionsApi; |
90 | - stateController?: IStateController; | |
91 | - aliasController?: IAliasController; | |
92 | 155 | activeEntityInfo?: SubscriptionEntityInfo; |
93 | 156 | |
94 | 157 | datasources?: Array<Datasource>; |
... | ... | @@ -96,7 +159,8 @@ export interface WidgetContext { |
96 | 159 | hiddenData?: Array<{data: DataSet}>; |
97 | 160 | timeWindow?: WidgetTimewindow; |
98 | 161 | |
99 | - hideTitlePanel?: boolean; | |
162 | + hideTitlePanel = false; | |
163 | + | |
100 | 164 | widgetTitleTemplate?: string; |
101 | 165 | widgetTitle?: string; |
102 | 166 | customHeaderActions?: Array<WidgetHeaderAction>; | ... | ... |
... | ... | @@ -26,7 +26,7 @@ import { |
26 | 26 | DashboardConfiguration, |
27 | 27 | DashboardLayoutId, |
28 | 28 | DashboardLayoutInfo, |
29 | - DashboardLayoutsInfo, | |
29 | + DashboardLayoutsInfo, DashboardState, | |
30 | 30 | DashboardStateLayouts, GridSettings, |
31 | 31 | WidgetLayout |
32 | 32 | } from '@app/shared/models/dashboard.models'; |
... | ... | @@ -79,6 +79,10 @@ import { |
79 | 79 | DashboardSettingsDialogComponent, |
80 | 80 | DashboardSettingsDialogData |
81 | 81 | } from '@home/pages/dashboard/dashboard-settings-dialog.component'; |
82 | +import { | |
83 | + ManageDashboardStatesDialogComponent, | |
84 | + ManageDashboardStatesDialogData | |
85 | +} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component'; | |
82 | 86 | |
83 | 87 | @Component({ |
84 | 88 | selector: 'tb-dashboard-page', |
... | ... | @@ -130,6 +134,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
130 | 134 | |
131 | 135 | addingLayoutCtx: DashboardPageLayoutContext; |
132 | 136 | |
137 | + | |
138 | + dashboardCtx: DashboardContext = { | |
139 | + getDashboard: () => this.dashboard, | |
140 | + dashboardTimewindow: null, | |
141 | + state: null, | |
142 | + stateController: null, | |
143 | + aliasController: null, | |
144 | + runChangeDetection: this.runChangeDetection.bind(this) | |
145 | + }; | |
146 | + | |
133 | 147 | layouts: DashboardPageLayouts = { |
134 | 148 | main: { |
135 | 149 | show: false, |
... | ... | @@ -157,15 +171,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
157 | 171 | } |
158 | 172 | }; |
159 | 173 | |
160 | - dashboardCtx: DashboardContext = { | |
161 | - dashboard: null, | |
162 | - dashboardTimewindow: null, | |
163 | - state: null, | |
164 | - stateController: null, | |
165 | - aliasController: null, | |
166 | - runChangeDetection: this.runChangeDetection.bind(this) | |
167 | - }; | |
168 | - | |
169 | 174 | addWidgetFabButtons: FooterFabButtons = { |
170 | 175 | fabTogglerName: 'dashboard.add-widget', |
171 | 176 | fabTogglerIcon: 'add', |
... | ... | @@ -255,13 +260,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
255 | 260 | |
256 | 261 | this.dashboard = data.dashboard; |
257 | 262 | this.dashboardConfiguration = this.dashboard.configuration; |
258 | - this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard); | |
259 | - this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard); | |
263 | + this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; | |
264 | + this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); | |
265 | + this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); | |
260 | 266 | this.widgetEditMode = data.widgetEditMode; |
261 | 267 | this.singlePageMode = data.singlePageMode; |
262 | 268 | |
263 | - this.dashboardCtx.dashboard = this.dashboard; | |
264 | - this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; | |
265 | 269 | this.dashboardCtx.aliasController = new AliasController(this.utils, |
266 | 270 | this.entityService, |
267 | 271 | () => this.dashboardCtx.stateController, |
... | ... | @@ -514,8 +518,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
514 | 518 | if ($event) { |
515 | 519 | $event.stopPropagation(); |
516 | 520 | } |
517 | - // TODO: | |
518 | - this.dialogService.todo(); | |
521 | + this.dialog.open<ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogData, | |
522 | + {[id: string]: DashboardState }>(ManageDashboardStatesDialogComponent, { | |
523 | + disableClose: true, | |
524 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
525 | + data: { | |
526 | + states: deepClone(this.dashboard.configuration.states) | |
527 | + } | |
528 | + }).afterClosed().subscribe((states) => { | |
529 | + if (states) { | |
530 | + this.updateStates(states); | |
531 | + } | |
532 | + }); | |
519 | 533 | } |
520 | 534 | |
521 | 535 | public manageDashboardLayouts($event: Event) { |
... | ... | @@ -541,6 +555,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
541 | 555 | this.updateLayouts(); |
542 | 556 | } |
543 | 557 | |
558 | + private updateStates(states: {[id: string]: DashboardState }) { | |
559 | + this.dashboard.configuration.states = states; | |
560 | + this.dashboardUtils.removeUnusedWidgets(this.dashboard); | |
561 | + let targetState = this.dashboardCtx.state; | |
562 | + if (!this.dashboard.configuration.states[targetState]) { | |
563 | + targetState = this.dashboardUtils.getRootStateId(this.dashboardConfiguration.states); | |
564 | + } | |
565 | + this.openDashboardState(targetState); | |
566 | + } | |
567 | + | |
544 | 568 | private importWidget($event: Event) { |
545 | 569 | if ($event) { |
546 | 570 | $event.stopPropagation(); |
... | ... | @@ -577,20 +601,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
577 | 601 | if (layoutsData) { |
578 | 602 | this.dashboardCtx.state = state; |
579 | 603 | this.dashboardCtx.aliasController.dashboardStateChanged(); |
580 | - let layoutVisibilityChanged = false; | |
581 | - for (const l of Object.keys(this.layouts)) { | |
582 | - const layout: DashboardPageLayout = this.layouts[l]; | |
583 | - let showLayout; | |
584 | - if (layoutsData[l]) { | |
585 | - showLayout = true; | |
586 | - } else { | |
587 | - showLayout = false; | |
588 | - } | |
589 | - if (layout.show !== showLayout) { | |
590 | - layout.show = showLayout; | |
591 | - layoutVisibilityChanged = !this.isMobile; | |
592 | - } | |
593 | - } | |
594 | 604 | this.isRightLayoutOpened = openRightLayout ? true : false; |
595 | 605 | this.updateLayouts(layoutsData); |
596 | 606 | } |
... | ... | @@ -603,9 +613,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
603 | 613 | for (const l of Object.keys(this.layouts)) { |
604 | 614 | const layout: DashboardPageLayout = this.layouts[l]; |
605 | 615 | if (layoutsData[l]) { |
616 | + layout.show = true; | |
606 | 617 | const layoutInfo: DashboardLayoutInfo = layoutsData[l]; |
607 | 618 | this.updateLayout(layout, layoutInfo); |
608 | 619 | } else { |
620 | + layout.show = false; | |
609 | 621 | this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); |
610 | 622 | } |
611 | 623 | } | ... | ... |
... | ... | @@ -30,7 +30,7 @@ export declare type DashboardPageScope = 'tenant' | 'customer'; |
30 | 30 | |
31 | 31 | export interface DashboardContext { |
32 | 32 | state: string; |
33 | - dashboard: Dashboard; | |
33 | + getDashboard: () => Dashboard; | |
34 | 34 | dashboardTimewindow: Timewindow; |
35 | 35 | aliasController: IAliasController; |
36 | 36 | stateController: IStateController; |
... | ... | @@ -79,7 +79,7 @@ export class LayoutWidgetsArray implements Iterable<Widget> { |
79 | 79 | |
80 | 80 | private loaded = false; |
81 | 81 | |
82 | - constructor(private dashboard: Dashboard) { | |
82 | + constructor(private dashboardCtx: DashboardContext) { | |
83 | 83 | } |
84 | 84 | |
85 | 85 | size() { |
... | ... | @@ -115,7 +115,7 @@ export class LayoutWidgetsArray implements Iterable<Widget> { |
115 | 115 | [Symbol.iterator](): Iterator<Widget> { |
116 | 116 | let pointer = 0; |
117 | 117 | const widgetIds = this.widgetIds; |
118 | - const dashboard = this.dashboard; | |
118 | + const dashboard = this.dashboardCtx.getDashboard(); | |
119 | 119 | return { |
120 | 120 | next(value?: any): IteratorResult<Widget> { |
121 | 121 | if (pointer < widgetIds.length) { |
... | ... | @@ -145,7 +145,7 @@ export class LayoutWidgetsArray implements Iterable<Widget> { |
145 | 145 | } |
146 | 146 | |
147 | 147 | private widgetById(widgetId: string): Widget { |
148 | - return this.dashboard.configuration.widgets[widgetId]; | |
148 | + return this.dashboardCtx.getDashboard().configuration.widgets[widgetId]; | |
149 | 149 | } |
150 | 150 | |
151 | 151 | } | ... | ... |
... | ... | @@ -34,6 +34,8 @@ import { AddWidgetDialogComponent } from './add-widget-dialog.component'; |
34 | 34 | import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component'; |
35 | 35 | import { SelectTargetLayoutDialogComponent } from './layout/select-target-layout-dialog.component'; |
36 | 36 | import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component'; |
37 | +import { ManageDashboardStatesDialogComponent } from './states/manage-dashboard-states-dialog.component'; | |
38 | +import { DashboardStateDialogComponent } from './states/dashboard-state-dialog.component'; | |
37 | 39 | |
38 | 40 | @NgModule({ |
39 | 41 | entryComponents: [ |
... | ... | @@ -44,7 +46,9 @@ import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.co |
44 | 46 | AddWidgetDialogComponent, |
45 | 47 | ManageDashboardLayoutsDialogComponent, |
46 | 48 | SelectTargetLayoutDialogComponent, |
47 | - DashboardSettingsDialogComponent | |
49 | + DashboardSettingsDialogComponent, | |
50 | + ManageDashboardStatesDialogComponent, | |
51 | + DashboardStateDialogComponent | |
48 | 52 | ], |
49 | 53 | declarations: [ |
50 | 54 | DashboardFormComponent, |
... | ... | @@ -59,7 +63,9 @@ import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.co |
59 | 63 | AddWidgetDialogComponent, |
60 | 64 | ManageDashboardLayoutsDialogComponent, |
61 | 65 | SelectTargetLayoutDialogComponent, |
62 | - DashboardSettingsDialogComponent | |
66 | + DashboardSettingsDialogComponent, | |
67 | + ManageDashboardStatesDialogComponent, | |
68 | + DashboardStateDialogComponent | |
63 | 69 | ], |
64 | 70 | imports: [ |
65 | 71 | CommonModule, | ... | ... |
... | ... | @@ -147,7 +147,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
147 | 147 | this.hotkeysService.add( |
148 | 148 | new Hotkey('ctrl+i', (event: KeyboardEvent) => { |
149 | 149 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
150 | - if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.dashboard, | |
150 | + if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), | |
151 | 151 | this.dashboardCtx.state, this.layoutCtx.id)) { |
152 | 152 | event.preventDefault(); |
153 | 153 | this.pasteWidgetReference(event); | ... | ... |
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 #stateForm="ngForm" [formGroup]="stateFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2 translate>{{ isAdd ? 'dashboard.add-state' : 'dashboard.edit-state' }}</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async" fxLayout="column"> | |
32 | + <mat-form-field class="mat-block"> | |
33 | + <mat-label translate>dashboard.state-name</mat-label> | |
34 | + <input required matInput formControlName="name"> | |
35 | + <mat-error *ngIf="stateFormGroup.get('name').hasError('required')"> | |
36 | + {{ 'dashboard.state-name-required' | translate }} | |
37 | + </mat-error> | |
38 | + </mat-form-field> | |
39 | + <mat-form-field class="mat-block"> | |
40 | + <mat-label translate>dashboard.state-id</mat-label> | |
41 | + <input required matInput formControlName="id"> | |
42 | + <mat-error *ngIf="stateFormGroup.get('id').hasError('required')"> | |
43 | + {{ 'dashboard.state-id-required' | translate }} | |
44 | + </mat-error> | |
45 | + <mat-error *ngIf="stateFormGroup.get('id').hasError('stateExists')"> | |
46 | + {{ 'dashboard.state-id-exists' | translate }} | |
47 | + </mat-error> | |
48 | + </mat-form-field> | |
49 | + <mat-checkbox formControlName="root"> | |
50 | + {{ 'dashboard.is-root-state' | translate }} | |
51 | + </mat-checkbox> | |
52 | + </fieldset> | |
53 | + </div> | |
54 | + <div mat-dialog-actions fxLayout="row"> | |
55 | + <span fxFlex></span> | |
56 | + <button mat-button mat-raised-button color="primary" | |
57 | + type="submit" | |
58 | + [disabled]="(isLoading$ | async) || stateFormGroup.invalid || !stateFormGroup.dirty"> | |
59 | + {{ (isAdd ? 'action.add' : 'action.save') | translate }} | |
60 | + </button> | |
61 | + <button mat-button color="primary" | |
62 | + style="margin-right: 20px;" | |
63 | + type="button" | |
64 | + [disabled]="(isLoading$ | async)" | |
65 | + (click)="cancel()" cdkFocusInitial> | |
66 | + {{ 'action.cancel' | translate }} | |
67 | + </button> | |
68 | + </div> | |
69 | +</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, Inject, OnInit, SkipSelf } from '@angular/core'; | |
18 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { | |
22 | + FormBuilder, | |
23 | + FormControl, | |
24 | + FormGroup, | |
25 | + FormGroupDirective, | |
26 | + NgForm, | |
27 | + ValidatorFn, | |
28 | + Validators | |
29 | +} from '@angular/forms'; | |
30 | +import { Router } from '@angular/router'; | |
31 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
32 | +import { DashboardState } from '@app/shared/models/dashboard.models'; | |
33 | +import { MatDialog } from '@angular/material/dialog'; | |
34 | +import { DashboardStateInfo } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; | |
35 | +import { TranslateService } from '@ngx-translate/core'; | |
36 | +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | |
37 | + | |
38 | +export interface DashboardStateDialogData { | |
39 | + states: {[id: string]: DashboardState }; | |
40 | + state: DashboardStateInfo; | |
41 | + isAdd: boolean; | |
42 | +} | |
43 | + | |
44 | +@Component({ | |
45 | + selector: 'tb-dashboard-state-dialog', | |
46 | + templateUrl: './dashboard-state-dialog.component.html', | |
47 | + providers: [{provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent}], | |
48 | + styleUrls: [] | |
49 | +}) | |
50 | +export class DashboardStateDialogComponent extends | |
51 | + DialogComponent<DashboardStateDialogComponent, DashboardStateInfo> | |
52 | + implements OnInit, ErrorStateMatcher { | |
53 | + | |
54 | + stateFormGroup: FormGroup; | |
55 | + | |
56 | + states: {[id: string]: DashboardState }; | |
57 | + state: DashboardStateInfo; | |
58 | + prevStateId: string; | |
59 | + | |
60 | + stateIdTouched: boolean; | |
61 | + | |
62 | + isAdd: boolean; | |
63 | + | |
64 | + submitted = false; | |
65 | + | |
66 | + constructor(protected store: Store<AppState>, | |
67 | + protected router: Router, | |
68 | + @Inject(MAT_DIALOG_DATA) public data: DashboardStateDialogData, | |
69 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
70 | + public dialogRef: MatDialogRef<DashboardStateDialogComponent, DashboardStateInfo>, | |
71 | + private fb: FormBuilder, | |
72 | + private translate: TranslateService, | |
73 | + private dashboardUtils: DashboardUtilsService, | |
74 | + private dialog: MatDialog) { | |
75 | + super(store, router, dialogRef); | |
76 | + | |
77 | + this.states = this.data.states; | |
78 | + this.isAdd = this.data.isAdd; | |
79 | + if (this.isAdd) { | |
80 | + this.state = {id: '', ...this.dashboardUtils.createDefaultState('', false)}; | |
81 | + this.prevStateId = ''; | |
82 | + } else { | |
83 | + this.state = this.data.state; | |
84 | + this.prevStateId = this.state.id; | |
85 | + } | |
86 | + | |
87 | + this.stateFormGroup = this.fb.group({ | |
88 | + name: [this.state.name, [Validators.required]], | |
89 | + id: [this.state.id, [Validators.required, this.validateDuplicateStateId()]], | |
90 | + root: [this.state.root, []], | |
91 | + }); | |
92 | + | |
93 | + this.stateFormGroup.get('name').valueChanges.subscribe((name: string) => { | |
94 | + this.checkStateName(name); | |
95 | + }); | |
96 | + | |
97 | + this.stateFormGroup.get('id').valueChanges.subscribe((id: string) => { | |
98 | + this.stateIdTouched = true; | |
99 | + }); | |
100 | + } | |
101 | + | |
102 | + private checkStateName(name: string) { | |
103 | + if (name && !this.stateIdTouched && this.isAdd) { | |
104 | + this.stateFormGroup.get('id').setValue( | |
105 | + name.toLowerCase().replace(/\W/g, '_'), | |
106 | + { emitEvent: false } | |
107 | + ); | |
108 | + } | |
109 | + } | |
110 | + | |
111 | + private validateDuplicateStateId(): ValidatorFn { | |
112 | + return (c: FormControl) => { | |
113 | + const newStateId: string = c.value; | |
114 | + if (newStateId) { | |
115 | + const existing = this.states[newStateId]; | |
116 | + if (existing && newStateId !== this.prevStateId) { | |
117 | + return { | |
118 | + stateExists: true | |
119 | + }; | |
120 | + } | |
121 | + } | |
122 | + return null; | |
123 | + }; | |
124 | + } | |
125 | + | |
126 | + ngOnInit(): void { | |
127 | + } | |
128 | + | |
129 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
130 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
131 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
132 | + return originalErrorState || customErrorState; | |
133 | + } | |
134 | + | |
135 | + cancel(): void { | |
136 | + this.dialogRef.close(null); | |
137 | + } | |
138 | + | |
139 | + save(): void { | |
140 | + this.submitted = true; | |
141 | + this.state = {...this.state, ...this.stateFormGroup.value}; | |
142 | + this.state.id = this.state.id.trim(); | |
143 | + this.dialogRef.close(this.state); | |
144 | + } | |
145 | +} | ... | ... |
... | ... | @@ -26,6 +26,7 @@ |
26 | 26 | } |
27 | 27 | |
28 | 28 | .state-entry { |
29 | + pointer-events: all; | |
29 | 30 | overflow: hidden; |
30 | 31 | font-size: 18px; |
31 | 32 | text-overflow: ellipsis; |
... | ... | @@ -35,7 +36,14 @@ |
35 | 36 | |
36 | 37 | mat-select { |
37 | 38 | margin: 0; |
39 | + } | |
40 | + } | |
41 | +} | |
38 | 42 | |
43 | +:host ::ng-deep { | |
44 | + mat-select { | |
45 | + .mat-select-value { | |
46 | + max-width: 200px; | |
39 | 47 | .mat-select-value-text { |
40 | 48 | font-size: 18px; |
41 | 49 | font-weight: 700; | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html
0 → 100644
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 #statesForm="ngForm" [formGroup]="statesFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2 translate>dashboard.manage-states</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async" fxLayout="column"> | |
32 | + <div class="manage-dashboard-states" fxLayout="column"> | |
33 | + <div class="tb-entity-table"> | |
34 | + <div fxLayout="column" class="tb-entity-table-content"> | |
35 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode"> | |
36 | + <div class="mat-toolbar-tools"> | |
37 | + <span class="tb-entity-table-title" translate>dashboard.states</span> | |
38 | + <span fxFlex></span> | |
39 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
40 | + type="button" | |
41 | + (click)="addState($event)" | |
42 | + matTooltip="{{ 'dashboard.add-state' | translate }}" | |
43 | + matTooltipPosition="above"> | |
44 | + <mat-icon>add</mat-icon> | |
45 | + </button> | |
46 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" | |
47 | + type="button" | |
48 | + matTooltip="{{ 'action.search' | translate }}" | |
49 | + matTooltipPosition="above"> | |
50 | + <mat-icon>search</mat-icon> | |
51 | + </button> | |
52 | + </div> | |
53 | + </mat-toolbar> | |
54 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> | |
55 | + <div class="mat-toolbar-tools"> | |
56 | + <button mat-button mat-icon-button | |
57 | + type="button" | |
58 | + matTooltip="{{ 'dashboard.search-states' | translate }}" | |
59 | + matTooltipPosition="above"> | |
60 | + <mat-icon>search</mat-icon> | |
61 | + </button> | |
62 | + <mat-form-field fxFlex> | |
63 | + <mat-label> </mat-label> | |
64 | + <input #searchInput matInput | |
65 | + [(ngModel)]="pageLink.textSearch" | |
66 | + [ngModelOptions]="{standalone: true}" | |
67 | + placeholder="{{ 'dashboard.search-states' | translate }}"/> | |
68 | + </mat-form-field> | |
69 | + <button mat-button mat-icon-button (click)="exitFilterMode()" | |
70 | + type="button" | |
71 | + matTooltip="{{ 'action.close' | translate }}" | |
72 | + matTooltipPosition="above"> | |
73 | + <mat-icon>close</mat-icon> | |
74 | + </button> | |
75 | + </div> | |
76 | + </mat-toolbar> | |
77 | + <div class="table-container"> | |
78 | + <mat-table [dataSource]="dataSource" | |
79 | + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> | |
80 | + <ng-container matColumnDef="name"> | |
81 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.state-name' | translate }} </mat-header-cell> | |
82 | + <mat-cell *matCellDef="let state"> | |
83 | + {{ state.name }} | |
84 | + </mat-cell> | |
85 | + </ng-container> | |
86 | + <ng-container matColumnDef="id"> | |
87 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.state-id' | translate }} </mat-header-cell> | |
88 | + <mat-cell *matCellDef="let state"> | |
89 | + {{ state.id }} | |
90 | + </mat-cell> | |
91 | + </ng-container> | |
92 | + <ng-container matColumnDef="root"> | |
93 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'dashboard.is-root-state' | translate }} </mat-header-cell> | |
94 | + <mat-cell *matCellDef="let state"> | |
95 | + <mat-icon class="material-icons mat-icon">{{state.root ? 'check_box' : 'check_box_outline_blank'}}</mat-icon> | |
96 | + </mat-cell> | |
97 | + </ng-container> | |
98 | + <ng-container matColumnDef="actions" stickyEnd> | |
99 | + <mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | |
100 | + </mat-header-cell> | |
101 | + <mat-cell *matCellDef="let state" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | |
102 | + <div fxFlex fxLayout="row"> | |
103 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | |
104 | + type="button" | |
105 | + matTooltip="{{ 'dashboard.edit-state' | translate }}" | |
106 | + matTooltipPosition="above" | |
107 | + (click)="editState($event, state)"> | |
108 | + <mat-icon>edit</mat-icon> | |
109 | + </button> | |
110 | + <button [fxShow]="!state.root" mat-button mat-icon-button [disabled]="isLoading$ | async" | |
111 | + type="button" | |
112 | + matTooltip="{{ 'dashboard.delete-state' | translate }}" | |
113 | + matTooltipPosition="above" | |
114 | + (click)="deleteState($event, state)"> | |
115 | + <mat-icon>delete</mat-icon> | |
116 | + </button> | |
117 | + </div> | |
118 | + </mat-cell> | |
119 | + </ng-container> | |
120 | + <mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> | |
121 | + <mat-row class="mat-row-select" | |
122 | + *matRowDef="let state; columns: displayedColumns;"></mat-row> | |
123 | + </mat-table> | |
124 | + <span [fxShow]="dataSource.isEmpty() | async" | |
125 | + fxLayoutAlign="center center" | |
126 | + class="no-data-found" translate>{{ 'dashboard.no-states-text' }}</span> | |
127 | + </div> | |
128 | + <mat-divider></mat-divider> | |
129 | + <mat-paginator [length]="dataSource.total() | async" | |
130 | + [pageIndex]="pageLink.page" | |
131 | + [pageSize]="pageLink.pageSize" | |
132 | + [pageSizeOptions]="[5, 10, 15]"></mat-paginator> | |
133 | + </div> | |
134 | + </div> | |
135 | + </div> | |
136 | + </fieldset> | |
137 | + </div> | |
138 | + <div mat-dialog-actions fxLayout="row"> | |
139 | + <span fxFlex></span> | |
140 | + <button mat-button mat-raised-button color="primary" | |
141 | + type="submit" | |
142 | + [disabled]="(isLoading$ | async) || statesFormGroup.invalid || !statesFormGroup.dirty"> | |
143 | + {{ 'action.save' | translate }} | |
144 | + </button> | |
145 | + <button mat-button color="primary" | |
146 | + style="margin-right: 20px;" | |
147 | + type="button" | |
148 | + [disabled]="(isLoading$ | async)" | |
149 | + (click)="cancel()" cdkFocusInitial> | |
150 | + {{ 'action.cancel' | translate }} | |
151 | + </button> | |
152 | + </div> | |
153 | +</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 { DashboardState } from '@shared/models/dashboard.models'; | |
18 | +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; | |
19 | +import { WidgetActionDescriptorInfo } from '@home/components/widget/action/manage-widget-actions.component.models'; | |
20 | +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; | |
21 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | |
22 | +import { PageLink } from '@shared/models/page/page-link'; | |
23 | +import { catchError, map, publishReplay, refCount } from 'rxjs/operators'; | |
24 | + | |
25 | +export interface DashboardStateInfo extends DashboardState { | |
26 | + id: string; | |
27 | +} | |
28 | + | |
29 | +export class DashboardStatesDatasource implements DataSource<DashboardStateInfo> { | |
30 | + | |
31 | + private statesSubject = new BehaviorSubject<DashboardStateInfo[]>([]); | |
32 | + private pageDataSubject = new BehaviorSubject<PageData<DashboardStateInfo>>(emptyPageData<DashboardStateInfo>()); | |
33 | + | |
34 | + public pageData$ = this.pageDataSubject.asObservable(); | |
35 | + | |
36 | + private allStates: Observable<Array<DashboardStateInfo>>; | |
37 | + | |
38 | + constructor(private states: {[id: string]: DashboardState }) { | |
39 | + } | |
40 | + | |
41 | + connect(collectionViewer: CollectionViewer): Observable<DashboardStateInfo[] | ReadonlyArray<DashboardStateInfo>> { | |
42 | + return this.statesSubject.asObservable(); | |
43 | + } | |
44 | + | |
45 | + disconnect(collectionViewer: CollectionViewer): void { | |
46 | + this.statesSubject.complete(); | |
47 | + this.pageDataSubject.complete(); | |
48 | + } | |
49 | + | |
50 | + loadStates(pageLink: PageLink, reload: boolean = false): Observable<PageData<DashboardStateInfo>> { | |
51 | + if (reload) { | |
52 | + this.allStates = null; | |
53 | + } | |
54 | + const result = new ReplaySubject<PageData<DashboardStateInfo>>(); | |
55 | + this.fetchStates(pageLink).pipe( | |
56 | + catchError(() => of(emptyPageData<DashboardStateInfo>())), | |
57 | + ).subscribe( | |
58 | + (pageData) => { | |
59 | + this.statesSubject.next(pageData.data); | |
60 | + this.pageDataSubject.next(pageData); | |
61 | + result.next(pageData); | |
62 | + } | |
63 | + ); | |
64 | + return result; | |
65 | + } | |
66 | + | |
67 | + fetchStates(pageLink: PageLink): Observable<PageData<DashboardStateInfo>> { | |
68 | + return this.getAllStates().pipe( | |
69 | + map((data) => pageLink.filterData(data)) | |
70 | + ); | |
71 | + } | |
72 | + | |
73 | + getAllStates(): Observable<Array<DashboardStateInfo>> { | |
74 | + if (!this.allStates) { | |
75 | + const states: DashboardStateInfo[] = []; | |
76 | + for (const id of Object.keys(this.states)) { | |
77 | + const state = this.states[id]; | |
78 | + states.push({id, ...state}); | |
79 | + } | |
80 | + this.allStates = of(states).pipe( | |
81 | + publishReplay(1), | |
82 | + refCount() | |
83 | + ); | |
84 | + } | |
85 | + return this.allStates; | |
86 | + } | |
87 | + | |
88 | + isEmpty(): Observable<boolean> { | |
89 | + return this.statesSubject.pipe( | |
90 | + map((states) => !states.length) | |
91 | + ); | |
92 | + } | |
93 | + | |
94 | + total(): Observable<number> { | |
95 | + return this.pageDataSubject.pipe( | |
96 | + map((pageData) => pageData.totalElements) | |
97 | + ); | |
98 | + } | |
99 | + | |
100 | +} | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss
0 → 100644
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 | + .manage-dashboard-states { | |
18 | + .tb-entity-table { | |
19 | + .tb-entity-table-content { | |
20 | + width: 100%; | |
21 | + height: 100%; | |
22 | + background: #fff; | |
23 | + | |
24 | + .tb-entity-table-title { | |
25 | + padding-right: 20px; | |
26 | + white-space: nowrap; | |
27 | + overflow: hidden; | |
28 | + text-overflow: ellipsis; | |
29 | + } | |
30 | + | |
31 | + .table-container { | |
32 | + overflow: auto; | |
33 | + } | |
34 | + } | |
35 | + } | |
36 | + } | |
37 | +} | |
38 | + | |
39 | +:host ::ng-deep { | |
40 | + .manage-dashboard-states { | |
41 | + .mat-sort-header-sorted .mat-sort-header-arrow { | |
42 | + opacity: 1 !important; | |
43 | + } | |
44 | + } | |
45 | +} | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts
0 → 100644
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 { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; | |
18 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; | |
22 | +import { Router } from '@angular/router'; | |
23 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
24 | +import { DashboardState } from '@app/shared/models/dashboard.models'; | |
25 | +import { MatDialog } from '@angular/material/dialog'; | |
26 | +import { PageLink } from '@shared/models/page/page-link'; | |
27 | +import { | |
28 | + WidgetActionDescriptorInfo, | |
29 | + WidgetActionsDatasource | |
30 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | |
31 | +import { | |
32 | + DashboardStateInfo, | |
33 | + DashboardStatesDatasource | |
34 | +} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; | |
35 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | |
36 | +import { MatPaginator } from '@angular/material/paginator'; | |
37 | +import { MatSort } from '@angular/material/sort'; | |
38 | +import { fromEvent, merge } from 'rxjs'; | |
39 | +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; | |
40 | +import { TranslateService } from '@ngx-translate/core'; | |
41 | +import { DialogService } from '@core/services/dialog.service'; | |
42 | +import { | |
43 | + WidgetActionDialogComponent, | |
44 | + WidgetActionDialogData | |
45 | +} from '@home/components/widget/action/widget-action-dialog.component'; | |
46 | +import { deepClone } from '@core/utils'; | |
47 | +import { | |
48 | + DashboardStateDialogComponent, | |
49 | + DashboardStateDialogData | |
50 | +} from '@home/pages/dashboard/states/dashboard-state-dialog.component'; | |
51 | + | |
52 | +export interface ManageDashboardStatesDialogData { | |
53 | + states: {[id: string]: DashboardState }; | |
54 | +} | |
55 | + | |
56 | +@Component({ | |
57 | + selector: 'tb-manage-dashboard-states-dialog', | |
58 | + templateUrl: './manage-dashboard-states-dialog.component.html', | |
59 | + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}], | |
60 | + styleUrls: ['./manage-dashboard-states-dialog.component.scss'] | |
61 | +}) | |
62 | +export class ManageDashboardStatesDialogComponent extends | |
63 | + DialogComponent<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }> | |
64 | + implements OnInit, ErrorStateMatcher, AfterViewInit { | |
65 | + | |
66 | + statesFormGroup: FormGroup; | |
67 | + | |
68 | + states: {[id: string]: DashboardState }; | |
69 | + | |
70 | + displayedColumns: string[]; | |
71 | + pageLink: PageLink; | |
72 | + textSearchMode = false; | |
73 | + dataSource: DashboardStatesDatasource; | |
74 | + | |
75 | + submitted = false; | |
76 | + | |
77 | + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; | |
78 | + | |
79 | + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; | |
80 | + @ViewChild(MatSort, {static: false}) sort: MatSort; | |
81 | + | |
82 | + constructor(protected store: Store<AppState>, | |
83 | + protected router: Router, | |
84 | + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData, | |
85 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
86 | + public dialogRef: MatDialogRef<ManageDashboardStatesDialogComponent, {[id: string]: DashboardState }>, | |
87 | + private fb: FormBuilder, | |
88 | + private translate: TranslateService, | |
89 | + private dialogs: DialogService, | |
90 | + private dialog: MatDialog) { | |
91 | + super(store, router, dialogRef); | |
92 | + | |
93 | + this.states = this.data.states; | |
94 | + this.statesFormGroup = this.fb.group({}); | |
95 | + | |
96 | + const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; | |
97 | + this.pageLink = new PageLink(5, 0, null, sortOrder); | |
98 | + this.displayedColumns = ['name', 'id', 'root', 'actions']; | |
99 | + this.dataSource = new DashboardStatesDatasource(this.states); | |
100 | + } | |
101 | + | |
102 | + ngOnInit(): void { | |
103 | + this.dataSource.loadStates(this.pageLink); | |
104 | + } | |
105 | + | |
106 | + ngAfterViewInit() { | |
107 | + fromEvent(this.searchInputField.nativeElement, 'keyup') | |
108 | + .pipe( | |
109 | + debounceTime(150), | |
110 | + distinctUntilChanged(), | |
111 | + tap(() => { | |
112 | + this.paginator.pageIndex = 0; | |
113 | + this.updateData(); | |
114 | + }) | |
115 | + ) | |
116 | + .subscribe(); | |
117 | + | |
118 | + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); | |
119 | + | |
120 | + merge(this.sort.sortChange, this.paginator.page) | |
121 | + .pipe( | |
122 | + tap(() => this.updateData()) | |
123 | + ) | |
124 | + .subscribe(); | |
125 | + } | |
126 | + | |
127 | + updateData(reload: boolean = false) { | |
128 | + this.pageLink.page = this.paginator.pageIndex; | |
129 | + this.pageLink.pageSize = this.paginator.pageSize; | |
130 | + this.pageLink.sortOrder.property = this.sort.active; | |
131 | + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; | |
132 | + this.dataSource.loadStates(this.pageLink, reload); | |
133 | + } | |
134 | + | |
135 | + addState($event: Event) { | |
136 | + this.openStateDialog($event); | |
137 | + } | |
138 | + | |
139 | + editState($event: Event, state: DashboardStateInfo) { | |
140 | + this.openStateDialog($event, state); | |
141 | + } | |
142 | + | |
143 | + deleteState($event: Event, state: DashboardStateInfo) { | |
144 | + if ($event) { | |
145 | + $event.stopPropagation(); | |
146 | + } | |
147 | + const title = this.translate.instant('dashboard.delete-state-title'); | |
148 | + const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name}); | |
149 | + this.dialogs.confirm(title, content, this.translate.instant('action.no'), | |
150 | + this.translate.instant('action.yes')).subscribe( | |
151 | + (res) => { | |
152 | + if (res) { | |
153 | + delete this.states[state.id]; | |
154 | + this.onStatesUpdated(); | |
155 | + } | |
156 | + } | |
157 | + ); | |
158 | + } | |
159 | + | |
160 | + enterFilterMode() { | |
161 | + this.textSearchMode = true; | |
162 | + this.pageLink.textSearch = ''; | |
163 | + setTimeout(() => { | |
164 | + this.searchInputField.nativeElement.focus(); | |
165 | + this.searchInputField.nativeElement.setSelectionRange(0, 0); | |
166 | + }, 10); | |
167 | + } | |
168 | + | |
169 | + exitFilterMode() { | |
170 | + this.textSearchMode = false; | |
171 | + this.pageLink.textSearch = null; | |
172 | + this.paginator.pageIndex = 0; | |
173 | + this.updateData(); | |
174 | + } | |
175 | + | |
176 | + openStateDialog($event: Event, state: DashboardStateInfo = null) { | |
177 | + if ($event) { | |
178 | + $event.stopPropagation(); | |
179 | + } | |
180 | + const isAdd = state === null; | |
181 | + let prevStateId = null; | |
182 | + if (!isAdd) { | |
183 | + prevStateId = state.id; | |
184 | + } | |
185 | + this.dialog.open<DashboardStateDialogComponent, DashboardStateDialogData, | |
186 | + DashboardStateInfo>(DashboardStateDialogComponent, { | |
187 | + disableClose: true, | |
188 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
189 | + data: { | |
190 | + isAdd, | |
191 | + states: this.states, | |
192 | + state: deepClone(state) | |
193 | + } | |
194 | + }).afterClosed().subscribe( | |
195 | + (res) => { | |
196 | + if (res) { | |
197 | + this.saveState(res, prevStateId); | |
198 | + } | |
199 | + } | |
200 | + ); | |
201 | + } | |
202 | + | |
203 | + saveState(state: DashboardStateInfo, prevStateId: string) { | |
204 | + const newState: DashboardState = { | |
205 | + name: state.name, | |
206 | + root: state.root, | |
207 | + layouts: state.layouts | |
208 | + }; | |
209 | + if (prevStateId) { | |
210 | + this.states[prevStateId] = newState; | |
211 | + } else { | |
212 | + this.states[state.id] = newState; | |
213 | + } | |
214 | + if (state.root) { | |
215 | + for (const id of Object.keys(this.states)) { | |
216 | + const otherState = this.states[id]; | |
217 | + if (id !== state.id) { | |
218 | + otherState.root = false; | |
219 | + } | |
220 | + } | |
221 | + } | |
222 | + this.onStatesUpdated(); | |
223 | + } | |
224 | + | |
225 | + private onStatesUpdated() { | |
226 | + this.statesFormGroup.markAsDirty(); | |
227 | + this.updateData(true); | |
228 | + } | |
229 | + | |
230 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
231 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
232 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
233 | + return originalErrorState || customErrorState; | |
234 | + } | |
235 | + | |
236 | + cancel(): void { | |
237 | + this.dialogRef.close(null); | |
238 | + } | |
239 | + | |
240 | + save(): void { | |
241 | + this.submitted = true; | |
242 | + this.dialogRef.close(this.states); | |
243 | + } | |
244 | +} | ... | ... |
... | ... | @@ -84,6 +84,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon |
84 | 84 | |
85 | 85 | currentState: string; |
86 | 86 | |
87 | + currentUrl: string; | |
88 | + | |
87 | 89 | private rxSubscriptions = new Array<Subscription>(); |
88 | 90 | |
89 | 91 | private inited = false; |
... | ... | @@ -94,12 +96,16 @@ export abstract class StateControllerComponent implements IStateControllerCompon |
94 | 96 | } |
95 | 97 | |
96 | 98 | ngOnInit(): void { |
99 | + this.currentUrl = this.router.url.split('?')[0]; | |
97 | 100 | this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => { |
98 | - const newState = paramMap.get('state'); | |
99 | - if (this.currentState !== newState) { | |
100 | - this.currentState = newState; | |
101 | - if (this.inited) { | |
102 | - this.onStateChanged(); | |
101 | + const newUrl = this.router.url.split('?')[0]; | |
102 | + if (this.currentUrl === newUrl) { | |
103 | + const newState = paramMap.get('state'); | |
104 | + if (this.currentState !== newState) { | |
105 | + this.currentState = newState; | |
106 | + if (this.inited) { | |
107 | + this.onStateChanged(); | |
108 | + } | |
103 | 109 | } |
104 | 110 | } |
105 | 111 | })); | ... | ... |
... | ... | @@ -557,6 +557,7 @@ |
557 | 557 | "edit-state": "Edit dashboard state", |
558 | 558 | "delete-state": "Delete dashboard state", |
559 | 559 | "add-state": "Add dashboard state", |
560 | + "no-states-text": "No states found", | |
560 | 561 | "state": "Dashboard state", |
561 | 562 | "state-name": "Name", |
562 | 563 | "state-name-required": "Dashboard state name is required.", | ... | ... |