Commit de60fedfa3fba437e7268d6060546ad97af2f002

Authored by Igor Kulikov
1 parent f579add9

Manage dashboard states.

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
... ...
... ... @@ -148,8 +148,7 @@
148 148 #widgetComponent
149 149 [dashboardWidget]="widget"
150 150 [isEdit]="isEdit"
151   - [isMobile]="isMobileSize"
152   - [dashboard]="this">
  151 + [isMobile]="isMobileSize">
153 152 </tb-widget>
154 153 </div>
155 154 </div>
... ...
... ... @@ -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>;
... ...
... ... @@ -141,6 +141,7 @@
141 141 height: rightLayoutHeight(),
142 142 borderLeft: 'none'}"
143 143 disableClose="true"
  144 + [@.disabled]="!isMobile"
144 145 position="end"
145 146 [mode]="isMobile ? 'over' : 'side'"
146 147 [(opened)]="rightLayoutOpened">
... ...
... ... @@ -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 +}
... ...
... ... @@ -18,3 +18,11 @@
18 18 margin: 0;
19 19 }
20 20 }
  21 +
  22 +:host ::ng-deep {
  23 + mat-select.default-state-controller {
  24 + .mat-select-value {
  25 + max-width: 200px;
  26 + }
  27 + }
  28 +}
... ...
... ... @@ -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;
... ...
  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>&nbsp;</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 +}
... ...
  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 +}
... ...
  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.",
... ...