From de60fedfa3fba437e7268d6060546ad97af2f002 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 4 Nov 2019 15:47:36 +0200 Subject: [PATCH] Manage dashboard states. --- ui-ngx/src/app/core/api/widget-api.models.ts | 19 +- .../src/app/core/api/widget-subscription.ts | 36 +++ ui-ngx/src/app/core/http/dashboard.service.ts | 37 ++- .../core/services/dashboard-utils.service.ts | 2 +- ui-ngx/src/app/core/utils.ts | 24 ++ .../aliases-entity-select-panel.component.ts | 2 +- .../dashboard/dashboard.component.html | 3 +- .../dashboard/dashboard.component.ts | 6 +- .../components/widget/widget.component.ts | 143 ++++------ .../home/models/dashboard-component.models.ts | 2 +- .../home/models/widget-component.models.ts | 116 +++++++-- .../dashboard/dashboard-page.component.html | 1 + .../dashboard/dashboard-page.component.ts | 72 +++--- .../pages/dashboard/dashboard-page.models.ts | 8 +- .../home/pages/dashboard/dashboard.module.ts | 10 +- .../layout/dashboard-layout.component.ts | 2 +- .../dashboard-state-dialog.component.html | 69 +++++ .../dashboard-state-dialog.component.ts | 145 +++++++++++ .../default-state-controller.component.scss | 8 + .../entity-state-controller.component.scss | 8 + ...age-dashboard-states-dialog.component.html | 153 +++++++++++ ...ashboard-states-dialog.component.models.ts | 100 +++++++ ...age-dashboard-states-dialog.component.scss | 45 ++++ ...anage-dashboard-states-dialog.component.ts | 244 ++++++++++++++++++ .../states/state-controller.component.ts | 16 +- .../assets/locale/locale.constant-en_US.json | 1 + 26 files changed, 1077 insertions(+), 195 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index c364479290..6d5a05e4cc 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -39,6 +39,7 @@ import { EntityInfo } from '@app/shared/models/entity.models'; import { Type } from '@angular/core'; import { AssetService } from '@core/http/asset.service'; import { DialogService } from '@core/services/dialog.service'; +import { IDashboardComponent } from '@home/models/dashboard-component.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -148,7 +149,19 @@ export interface SubscriptionInfo { deviceIds?: Array; } -export interface WidgetSubscriptionContext { +export class WidgetSubscriptionContext { + + constructor(private dashboard: IDashboardComponent) {} + + get aliasController(): IAliasController { + return this.dashboard.aliasController; + } + + dashboardTimewindowApi: TimewindowFunctions = { + onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard), + onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard) + }; + timeService: TimeService; deviceService: DeviceService; alarmService: AlarmService; @@ -156,11 +169,7 @@ export interface WidgetSubscriptionContext { utils: UtilsService; raf: RafService; widgetUtils: IWidgetUtils; - dashboardTimewindowApi: TimewindowFunctions; getServerTimeDiff: () => Observable; - aliasController: IAliasController; - [key: string]: any; - // TODO: } export interface WidgetSubscriptionCallbacks { diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index e66cb58c24..bb9ecc560c 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -382,6 +382,13 @@ export class WidgetSubscription implements IWidgetSubscription { } onAliasesChanged(aliasIds: Array): boolean { + if (this.type === widgetType.rpc) { + return this.checkRpcTarget(aliasIds); + } else if (this.type === widgetType.alarm) { + return this.checkAlarmSource(aliasIds); + } else { + return this.checkSubscriptions(aliasIds); + } return false; } @@ -566,6 +573,35 @@ export class WidgetSubscription implements IWidgetSubscription { // TODO: } + private checkRpcTarget(aliasIds: Array): boolean { + if (aliasIds.indexOf(this.targetDeviceAliasId) > -1) { + return true; + } else { + return false; + } + } + + private checkAlarmSource(aliasIds: Array): boolean { + if (this.alarmSource && this.alarmSource.entityAliasId) { + return aliasIds.indexOf(this.alarmSource.entityAliasId) > -1; + } else { + return false; + } + } + + private checkSubscriptions(aliasIds: Array): boolean { + let subscriptionsChanged = false; + for (const listener of this.datasourceListeners) { + if (listener.datasource.entityAliasId) { + if (aliasIds.indexOf(listener.datasource.entityAliasId) > -1) { + subscriptionsChanged = true; + break; + } + } + } + return subscriptionsChanged; + } + destroy(): void { this.unsubscribe(); for (const cafId of Object.keys(this.cafs)) { diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index 9bd719663d..cff0385833 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -22,26 +22,29 @@ import {PageLink} from '@shared/models/page/page-link'; import {PageData} from '@shared/models/page/page-data'; import {Dashboard, DashboardInfo} from '@shared/models/dashboard.models'; import {WINDOW} from '@core/services/window.service'; -import { ActivationEnd, Router } from '@angular/router'; -import { filter } from 'rxjs/operators'; +import { ActivationEnd, NavigationEnd, Router } from '@angular/router'; +import { filter, map, publishReplay, refCount } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class DashboardService { - stDiffSubject: Subject; + stDiffObservable: Observable; + currentUrl: string; constructor( private http: HttpClient, private router: Router, @Inject(WINDOW) private window: Window ) { - this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( + this.currentUrl = this.router.url.split('?')[0]; + this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe( () => { - if (this.stDiffSubject) { - this.stDiffSubject.complete(); - this.stDiffSubject = null; + const newUrl = this.router.url.split('?')[0]; + if (this.currentUrl !== newUrl) { + this.stDiffObservable = null; + this.currentUrl = newUrl; } } ); @@ -139,24 +142,20 @@ export class DashboardService { } public getServerTimeDiff(): Observable { - if (this.stDiffSubject) { - return this.stDiffSubject.asObservable(); - } else { - this.stDiffSubject = new ReplaySubject(1); + if (!this.stDiffObservable) { const url = '/api/dashboard/serverTime'; const ct1 = Date.now(); - this.http.get(url, defaultHttpOptions(true)).subscribe( - (st) => { + this.stDiffObservable = this.http.get(url, defaultHttpOptions(true)).pipe( + map((st) => { const ct2 = Date.now(); const stDiff = Math.ceil(st - (ct1 + ct2) / 2); - this.stDiffSubject.next(stDiff); - }, - () => { - this.stDiffSubject.error(null); - } + return stDiff; + }), + publishReplay(1), + refCount() ); - return this.stDiffSubject.asObservable(); } + return this.stDiffObservable; } } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index d965912a12..976c36bf02 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -479,7 +479,7 @@ export class DashboardUtilsService { } } - private removeUnusedWidgets(dashboard: Dashboard) { + public removeUnusedWidgets(dashboard: Dashboard) { const dashboardConfiguration = dashboard.configuration; const states = dashboardConfiguration.states; const widgets = dashboardConfiguration.widgets; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 9960b5bbf4..a84ef56958 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -99,10 +99,34 @@ export function isNumber(value: any): boolean { return typeof value === 'number'; } +export function isNumeric(value: any): boolean { + return (value - parseFloat( value ) + 1) >= 0; +} + export function isString(value: any): boolean { return typeof value === 'string'; } +export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { + if (isDefined(value) && + value != null && isNumeric(value)) { + let formatted: string | number = Number(value); + if (isDefined(dec)) { + formatted = formatted.toFixed(dec); + } + if (!showZeroDecimals) { + formatted = (Number(formatted) * 1); + } + formatted = formatted.toString(); + if (isDefined(units) && units.length > 0) { + formatted += ' ' + units; + } + return formatted; + } else { + return value; + } +} + export function deleteNullProperties(obj: any) { if (isUndefined(obj) || obj == null) { return; diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts index f05f5bc69a..3494cfebea 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.ts @@ -52,7 +52,7 @@ export class AliasesEntitySelectPanelComponent { const resolvedEntities = this.entityAliasesInfo[aliasId].resolvedEntities; const selected = resolvedEntities.find((entity) => entity.id === selectedId); if (selected) { - this.data.aliasController.updateCurrentAliasEntity(aliasId, selected[0]); + this.data.aliasController.updateCurrentAliasEntity(aliasId, selected); } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index 73945b8602..8bb638fad7 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -148,8 +148,7 @@ #widgetComponent [dashboardWidget]="widget" [isEdit]="isEdit" - [isMobile]="isMobileSize" - [dashboard]="this"> + [isMobile]="isMobileSize"> diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 47ef73c300..a69331e670 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -180,6 +180,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo this.gridsterOpts = { gridType: 'scrollVertical', keepFixedHeightInMobile: true, + disableWarnings: false, + disableAutoPositionOnConflict: false, pushItems: false, swap: false, maxRows: 100, @@ -228,7 +230,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } ngDoCheck() { - this.dashboardWidgets.doCheck(); + if (!this.optionsChangeNotificationsPaused) { + this.dashboardWidgets.doCheck(); + } } ngOnChanges(changes: SimpleChanges): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index faadceceef..fc9a7b5eb6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -115,9 +115,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @Input() isMobile: boolean; - @Input() - dashboard: IDashboardComponent; - @Input() dashboardWidget: DashboardWidget; @@ -146,11 +143,12 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI subscriptionContext: WidgetSubscriptionContext; subscriptionInited = false; + destroyed = false; widgetSizeDetected = false; cafs: {[cafId: string]: CancelAnimationFrame} = {}; - onResizeListener = this.onResize.bind(this); + onResizeListener = null; private cssParser = new cssjs(); @@ -252,30 +250,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContext = this.dashboardWidget.widgetContext; this.widgetContext.servicesMap = ServicesMap; - this.widgetContext.inited = false; - this.widgetContext.hideTitlePanel = false; this.widgetContext.isEdit = this.isEdit; this.widgetContext.isMobile = this.isMobile; - this.widgetContext.dashboard = this.dashboard; - this.widgetContext.widgetConfig = this.widget.config; - this.widgetContext.settings = this.widget.config.settings; - this.widgetContext.units = this.widget.config.units || ''; - this.widgetContext.decimals = isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2; - this.widgetContext.subscriptions = {}; - this.widgetContext.defaultSubscription = null; - this.widgetContext.dashboardTimewindow = this.dashboard.dashboardTimewindow; - this.widgetContext.timewindowFunctions = { - onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { - if (this.widgetContext.defaultSubscription) { - this.widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); - } - }, - onResetTimewindow: () => { - if (this.widgetContext.defaultSubscription) { - this.widgetContext.defaultSubscription.onResetTimewindow(); - } - } - }; + this.widgetContext.subscriptionApi = { createSubscription: this.createSubscription.bind(this), createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this), @@ -287,33 +264,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } }; - this.widgetContext.controlApi = { - sendOneWayCommand: (method, params, timeout) => { - if (this.widgetContext.defaultSubscription) { - return this.widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout); - } else { - return of(null); - } - }, - sendTwoWayCommand: (method, params, timeout) => { - if (this.widgetContext.defaultSubscription) { - return this.widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout); - } else { - return of(null); - } - } - }; - this.widgetContext.utils = { - formatValue: this.formatValue.bind(this) - }; + this.widgetContext.actionsApi = { actionDescriptorsBySourceId, getActionDescriptors: this.getActionDescriptors.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this), elementClick: this.elementClick.bind(this) }; - this.widgetContext.stateController = this.dashboard.stateController; - this.widgetContext.aliasController = this.dashboard.aliasController; this.widgetContext.customHeaderActions = []; const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); @@ -333,21 +290,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContext.customHeaderActions.push(headerAction); }); - this.subscriptionContext = { - timeService: this.timeService, - deviceService: this.deviceService, - alarmService: this.alarmService, - datasourceService: this.datasourceService, - utils: this.utils, - raf: this.raf, - widgetUtils: this.widgetContext.utils, - dashboardTimewindowApi: { - onResetTimewindow: this.dashboard.onResetTimewindow.bind(this.dashboard), - onUpdateTimewindow: this.dashboard.onUpdateTimewindow.bind(this.dashboard) - }, - getServerTimeDiff: this.dashboardService.getServerTimeDiff.bind(this.dashboardService), - aliasController: this.dashboard.aliasController - }; + this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); + this.subscriptionContext.timeService = this.timeService; + this.subscriptionContext.deviceService = this.deviceService; + this.subscriptionContext.alarmService = this.alarmService; + this.subscriptionContext.datasourceService = this.datasourceService; + this.subscriptionContext.utils = this.utils; + this.subscriptionContext.raf = this.raf; + this.subscriptionContext.widgetUtils = this.widgetContext.utils; + this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( (widgetInfo) => { @@ -382,6 +333,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } ngOnDestroy(): void { + this.destroyed = true; this.rxSubscriptions.forEach((subscription) => { subscription.unsubscribe(); }); @@ -481,7 +433,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private onInit(skipSizeCheck?: boolean) { - if (!this.widgetContext.$containerParent) { + if (!this.widgetContext.$containerParent || this.destroyed) { return; } if (!skipSizeCheck) { @@ -565,17 +517,35 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private reInit() { + if (this.cafs.reinit) { + this.cafs.reinit(); + this.cafs.reinit = null; + } + this.cafs.reinit = this.raf.raf(() => { + this.reInitImpl(); + }); + } + + private reInitImpl() { this.onDestroy(); this.configureDynamicWidgetComponent(); if (!this.typeParameters.useCustomDatasources) { this.createDefaultSubscription().subscribe( () => { - this.subscriptionInited = true; - this.onInit(); + if (this.destroyed) { + this.onDestroy(); + } else { + this.subscriptionInited = true; + this.onInit(); + } }, () => { - this.subscriptionInited = true; - this.onInit(); + if (this.destroyed) { + this.onDestroy(); + } else { + this.subscriptionInited = true; + this.onInit(); + } } ); } else { @@ -588,7 +558,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const initSubject = new ReplaySubject(); - this.rxSubscriptions.push(this.dashboard.aliasController.entityAliasesChanged.subscribe( + this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe( (aliasIds) => { let subscriptionChanged = false; for (const id of Object.keys(this.widgetContext.subscriptions)) { @@ -601,7 +571,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } )); - this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( + this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( (dashboardTimewindow) => { for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; @@ -634,9 +604,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } private destroyDynamicWidgetComponent() { - if (this.widgetContext.$containerParent) { + if (this.widgetContext.$containerParent && this.onResizeListener) { // @ts-ignore removeResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); + this.onResizeListener = null; } if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); @@ -661,7 +632,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); - this.widgetContext.$container = $('> ng-component', containerElement); + // this.widgetContext.$container = $('> ng-component:not([id="container"])', containerElement); + this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); this.widgetContext.$container.css('display', 'block'); this.widgetContext.$container.css('user-select', 'none'); this.widgetContext.$container.attr('id', 'container'); @@ -672,13 +644,14 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); } + this.onResizeListener = this.onResize.bind(this); // @ts-ignore addResizeListener(this.widgetContext.$containerParent[0], this.onResizeListener); } private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable { const createSubscriptionSubject = new ReplaySubject(); - options.dashboardTimewindow = this.dashboard.dashboardTimewindow; + options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); subscription.init$.subscribe( () => { @@ -747,7 +720,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ? this.widget.config.useDashboardTimewindow : true; options.displayTimewindow = isDefined(this.widget.config.displayTimewindow) ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; - options.timeWindowConfig = options.useDashboardTimewindow ? this.dashboard.dashboardTimewindow : this.widget.config.timewindow; + options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; options.legendConfig = null; if (this.displayLegend) { options.legendConfig = this.legendConfig; @@ -875,30 +848,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI return createSubscriptionSubject.asObservable(); } - private isNumeric(value: any): boolean { - return (value - parseFloat( value ) + 1) >= 0; - } - - private formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { - if (isDefined(value) && - value != null && this.isNumeric(value)) { - let formatted: string | number = Number(value); - if (isDefined(dec)) { - formatted = formatted.toFixed(dec); - } - if (!showZeroDecimals) { - formatted = (Number(formatted) * 1); - } - formatted = formatted.toString(); - if (isDefined(units) && units.length > 0) { - formatted += ' ' + units; - } - return formatted; - } else { - return value; - } - } - private getActionDescriptors(actionSourceId: string): Array { let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId]; if (!result) { diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 267278a8a7..a03cbd4ea3 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -302,7 +302,7 @@ export class DashboardWidget implements GridsterItem { customHeaderActions: Array; widgetActions: Array; - widgetContext: WidgetContext = {}; + widgetContext = new WidgetContext(this.dashboard, this.widget); widgetId: string; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 764d26d4a0..4a9701f839 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -26,7 +26,8 @@ import { WidgetType, widgetType, WidgetTypeDescriptor, - WidgetTypeParameters + WidgetTypeParameters, + Widget } from '@shared/models/widget.models'; import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models'; import { @@ -34,10 +35,10 @@ import { IStateController, IWidgetSubscription, IWidgetUtils, - RpcApi, SubscriptionEntityInfo, + RpcApi, SubscriptionEntityInfo, SubscriptionInfo, TimewindowFunctions, WidgetActionsApi, - WidgetSubscriptionApi + WidgetSubscriptionApi, WidgetSubscriptionContext, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { ComponentFactory, Type } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; @@ -49,6 +50,9 @@ import { DeviceService } from '@core/http/device.service'; import { AssetService } from '@app/core/http/asset.service'; import { DialogService } from '@core/services/dialog.service'; import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; +import { isDefined, formatValue } from '@core/utils'; +import { Observable, of, ReplaySubject } from 'rxjs'; +import { WidgetSubscription } from '@core/api/widget-subscription'; export interface IWidgetAction { name: string; @@ -65,30 +69,89 @@ export interface WidgetAction extends IWidgetAction { show: boolean; } -export interface WidgetContext { - inited?: boolean; - $container?: JQuery; - $containerParent?: JQuery; - width?: number; - height?: number; - $scope?: IDynamicWidgetComponent; - isEdit?: boolean; - isMobile?: boolean; - dashboard?: IDashboardComponent; - widgetConfig?: WidgetConfig; - settings?: any; - units?: string; - decimals?: number; - subscriptions?: {[id: string]: IWidgetSubscription}; - defaultSubscription?: IWidgetSubscription; - dashboardTimewindow?: Timewindow; - timewindowFunctions?: TimewindowFunctions; +export class WidgetContext { + + constructor(public dashboard: IDashboardComponent, + private widget: Widget) {} + + get stateController(): IStateController { + return this.dashboard.stateController; + } + + get aliasController(): IAliasController { + return this.dashboard.aliasController; + } + + get dashboardTimewindow(): Timewindow { + return this.dashboard.dashboardTimewindow; + } + + get widgetConfig(): WidgetConfig { + return this.widget.config; + } + + get settings(): any { + return this.widget.config.settings; + } + + get units(): string { + return this.widget.config.units || ''; + } + + get decimals(): number { + return isDefined(this.widget.config.decimals) ? this.widget.config.decimals : 2; + } + + inited = false; + + subscriptions: {[id: string]: IWidgetSubscription} = {}; + defaultSubscription: IWidgetSubscription = null; + + timewindowFunctions: TimewindowFunctions = { + onUpdateTimewindow: (startTimeMs, endTimeMs, interval) => { + if (this.defaultSubscription) { + this.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs, interval); + } + }, + onResetTimewindow: () => { + if (this.defaultSubscription) { + this.defaultSubscription.onResetTimewindow(); + } + } + }; + + controlApi: RpcApi = { + sendOneWayCommand: (method, params, timeout) => { + if (this.defaultSubscription) { + return this.defaultSubscription.sendOneWayCommand(method, params, timeout); + } else { + return of(null); + } + }, + sendTwoWayCommand: (method, params, timeout) => { + if (this.defaultSubscription) { + return this.defaultSubscription.sendTwoWayCommand(method, params, timeout); + } else { + return of(null); + } + } + }; + + utils: IWidgetUtils = { + formatValue + }; + + $container: JQuery; + $containerParent: JQuery; + width: number; + height: number; + $scope: IDynamicWidgetComponent; + isEdit: boolean; + isMobile: boolean; + subscriptionApi?: WidgetSubscriptionApi; - controlApi?: RpcApi; - utils?: IWidgetUtils; + actionsApi?: WidgetActionsApi; - stateController?: IStateController; - aliasController?: IAliasController; activeEntityInfo?: SubscriptionEntityInfo; datasources?: Array; @@ -96,7 +159,8 @@ export interface WidgetContext { hiddenData?: Array<{data: DataSet}>; timeWindow?: WidgetTimewindow; - hideTitlePanel?: boolean; + hideTitlePanel = false; + widgetTitleTemplate?: string; widgetTitle?: string; customHeaderActions?: Array; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html index 0db11b7fdc..cf2f4e1c0f 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html @@ -141,6 +141,7 @@ height: rightLayoutHeight(), borderLeft: 'none'}" disableClose="true" + [@.disabled]="!isMobile" position="end" [mode]="isMobile ? 'over' : 'side'" [(opened)]="rightLayoutOpened"> diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts index b541f42ed3..29901bd857 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts @@ -26,7 +26,7 @@ import { DashboardConfiguration, DashboardLayoutId, DashboardLayoutInfo, - DashboardLayoutsInfo, + DashboardLayoutsInfo, DashboardState, DashboardStateLayouts, GridSettings, WidgetLayout } from '@app/shared/models/dashboard.models'; @@ -79,6 +79,10 @@ import { DashboardSettingsDialogComponent, DashboardSettingsDialogData } from '@home/pages/dashboard/dashboard-settings-dialog.component'; +import { + ManageDashboardStatesDialogComponent, + ManageDashboardStatesDialogData +} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component'; @Component({ selector: 'tb-dashboard-page', @@ -130,6 +134,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC addingLayoutCtx: DashboardPageLayoutContext; + + dashboardCtx: DashboardContext = { + getDashboard: () => this.dashboard, + dashboardTimewindow: null, + state: null, + stateController: null, + aliasController: null, + runChangeDetection: this.runChangeDetection.bind(this) + }; + layouts: DashboardPageLayouts = { main: { show: false, @@ -157,15 +171,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } }; - dashboardCtx: DashboardContext = { - dashboard: null, - dashboardTimewindow: null, - state: null, - stateController: null, - aliasController: null, - runChangeDetection: this.runChangeDetection.bind(this) - }; - addWidgetFabButtons: FooterFabButtons = { fabTogglerName: 'dashboard.add-widget', fabTogglerIcon: 'add', @@ -255,13 +260,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.dashboard = data.dashboard; this.dashboardConfiguration = this.dashboard.configuration; - this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard); - this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard); + this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; + this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); + this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); this.widgetEditMode = data.widgetEditMode; this.singlePageMode = data.singlePageMode; - this.dashboardCtx.dashboard = this.dashboard; - this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; this.dashboardCtx.aliasController = new AliasController(this.utils, this.entityService, () => this.dashboardCtx.stateController, @@ -514,8 +518,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC if ($event) { $event.stopPropagation(); } - // TODO: - this.dialogService.todo(); + this.dialog.open(ManageDashboardStatesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + states: deepClone(this.dashboard.configuration.states) + } + }).afterClosed().subscribe((states) => { + if (states) { + this.updateStates(states); + } + }); } public manageDashboardLayouts($event: Event) { @@ -541,6 +555,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.updateLayouts(); } + private updateStates(states: {[id: string]: DashboardState }) { + this.dashboard.configuration.states = states; + this.dashboardUtils.removeUnusedWidgets(this.dashboard); + let targetState = this.dashboardCtx.state; + if (!this.dashboard.configuration.states[targetState]) { + targetState = this.dashboardUtils.getRootStateId(this.dashboardConfiguration.states); + } + this.openDashboardState(targetState); + } + private importWidget($event: Event) { if ($event) { $event.stopPropagation(); @@ -577,20 +601,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC if (layoutsData) { this.dashboardCtx.state = state; this.dashboardCtx.aliasController.dashboardStateChanged(); - let layoutVisibilityChanged = false; - for (const l of Object.keys(this.layouts)) { - const layout: DashboardPageLayout = this.layouts[l]; - let showLayout; - if (layoutsData[l]) { - showLayout = true; - } else { - showLayout = false; - } - if (layout.show !== showLayout) { - layout.show = showLayout; - layoutVisibilityChanged = !this.isMobile; - } - } this.isRightLayoutOpened = openRightLayout ? true : false; this.updateLayouts(layoutsData); } @@ -603,9 +613,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC for (const l of Object.keys(this.layouts)) { const layout: DashboardPageLayout = this.layouts[l]; if (layoutsData[l]) { + layout.show = true; const layoutInfo: DashboardLayoutInfo = layoutsData[l]; this.updateLayout(layout, layoutInfo); } else { + layout.show = false; this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); } } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts index 278eb9241d..41f56ec592 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts @@ -30,7 +30,7 @@ export declare type DashboardPageScope = 'tenant' | 'customer'; export interface DashboardContext { state: string; - dashboard: Dashboard; + getDashboard: () => Dashboard; dashboardTimewindow: Timewindow; aliasController: IAliasController; stateController: IStateController; @@ -79,7 +79,7 @@ export class LayoutWidgetsArray implements Iterable { private loaded = false; - constructor(private dashboard: Dashboard) { + constructor(private dashboardCtx: DashboardContext) { } size() { @@ -115,7 +115,7 @@ export class LayoutWidgetsArray implements Iterable { [Symbol.iterator](): Iterator { let pointer = 0; const widgetIds = this.widgetIds; - const dashboard = this.dashboard; + const dashboard = this.dashboardCtx.getDashboard(); return { next(value?: any): IteratorResult { if (pointer < widgetIds.length) { @@ -145,7 +145,7 @@ export class LayoutWidgetsArray implements Iterable { } private widgetById(widgetId: string): Widget { - return this.dashboard.configuration.widgets[widgetId]; + return this.dashboardCtx.getDashboard().configuration.widgets[widgetId]; } } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts index c2df50b36a..406a107434 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts @@ -34,6 +34,8 @@ import { AddWidgetDialogComponent } from './add-widget-dialog.component'; import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component'; import { SelectTargetLayoutDialogComponent } from './layout/select-target-layout-dialog.component'; import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component'; +import { ManageDashboardStatesDialogComponent } from './states/manage-dashboard-states-dialog.component'; +import { DashboardStateDialogComponent } from './states/dashboard-state-dialog.component'; @NgModule({ entryComponents: [ @@ -44,7 +46,9 @@ import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.co AddWidgetDialogComponent, ManageDashboardLayoutsDialogComponent, SelectTargetLayoutDialogComponent, - DashboardSettingsDialogComponent + DashboardSettingsDialogComponent, + ManageDashboardStatesDialogComponent, + DashboardStateDialogComponent ], declarations: [ DashboardFormComponent, @@ -59,7 +63,9 @@ import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.co AddWidgetDialogComponent, ManageDashboardLayoutsDialogComponent, SelectTargetLayoutDialogComponent, - DashboardSettingsDialogComponent + DashboardSettingsDialogComponent, + ManageDashboardStatesDialogComponent, + DashboardStateDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts index 3c074f9f83..8e695060c1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts @@ -147,7 +147,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo this.hotkeysService.add( new Hotkey('ctrl+i', (event: KeyboardEvent) => { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { - if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.dashboard, + if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), this.dashboardCtx.state, this.layoutCtx.id)) { event.preventDefault(); this.pasteWidgetReference(event); diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html new file mode 100644 index 0000000000..c28a2890b8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.html @@ -0,0 +1,69 @@ + +
+ +

{{ isAdd ? 'dashboard.add-state' : 'dashboard.edit-state' }}

+ + +
+ + +
+
+ + dashboard.state-name + + + {{ 'dashboard.state-name-required' | translate }} + + + + dashboard.state-id + + + {{ 'dashboard.state-id-required' | translate }} + + + {{ 'dashboard.state-id-exists' | translate }} + + + + {{ 'dashboard.is-root-state' | translate }} + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts new file mode 100644 index 0000000000..973b3e40e3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/dashboard-state-dialog.component.ts @@ -0,0 +1,145 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidatorFn, + Validators +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { MatDialog } from '@angular/material/dialog'; +import { DashboardStateInfo } from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; + +export interface DashboardStateDialogData { + states: {[id: string]: DashboardState }; + state: DashboardStateInfo; + isAdd: boolean; +} + +@Component({ + selector: 'tb-dashboard-state-dialog', + templateUrl: './dashboard-state-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DashboardStateDialogComponent}], + styleUrls: [] +}) +export class DashboardStateDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher { + + stateFormGroup: FormGroup; + + states: {[id: string]: DashboardState }; + state: DashboardStateInfo; + prevStateId: string; + + stateIdTouched: boolean; + + isAdd: boolean; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardStateDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService, + private dashboardUtils: DashboardUtilsService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.states = this.data.states; + this.isAdd = this.data.isAdd; + if (this.isAdd) { + this.state = {id: '', ...this.dashboardUtils.createDefaultState('', false)}; + this.prevStateId = ''; + } else { + this.state = this.data.state; + this.prevStateId = this.state.id; + } + + this.stateFormGroup = this.fb.group({ + name: [this.state.name, [Validators.required]], + id: [this.state.id, [Validators.required, this.validateDuplicateStateId()]], + root: [this.state.root, []], + }); + + this.stateFormGroup.get('name').valueChanges.subscribe((name: string) => { + this.checkStateName(name); + }); + + this.stateFormGroup.get('id').valueChanges.subscribe((id: string) => { + this.stateIdTouched = true; + }); + } + + private checkStateName(name: string) { + if (name && !this.stateIdTouched && this.isAdd) { + this.stateFormGroup.get('id').setValue( + name.toLowerCase().replace(/\W/g, '_'), + { emitEvent: false } + ); + } + } + + private validateDuplicateStateId(): ValidatorFn { + return (c: FormControl) => { + const newStateId: string = c.value; + if (newStateId) { + const existing = this.states[newStateId]; + if (existing && newStateId !== this.prevStateId) { + return { + stateExists: true + }; + } + } + return null; + }; + } + + ngOnInit(): void { + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.state = {...this.state, ...this.stateFormGroup.value}; + this.state.id = this.state.id.trim(); + this.dialogRef.close(this.state); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss index 691099ef0b..275788b793 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss @@ -18,3 +18,11 @@ margin: 0; } } + +:host ::ng-deep { + mat-select.default-state-controller { + .mat-select-value { + max-width: 200px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss index 4cbec02a67..855edfe80c 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss @@ -26,6 +26,7 @@ } .state-entry { + pointer-events: all; overflow: hidden; font-size: 18px; text-overflow: ellipsis; @@ -35,7 +36,14 @@ mat-select { margin: 0; + } + } +} +:host ::ng-deep { + mat-select { + .mat-select-value { + max-width: 200px; .mat-select-value-text { font-size: 18px; font-weight: 700; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html new file mode 100644 index 0000000000..681ff8cd59 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.html @@ -0,0 +1,153 @@ + +
+ +

dashboard.manage-states

+ + +
+ + +
+
+
+
+
+ +
+ dashboard.states + + + +
+
+ +
+ + +   + + + +
+
+
+ + + {{ 'dashboard.state-name' | translate }} + + {{ state.name }} + + + + {{ 'dashboard.state-id' | translate }} + + {{ state.id }} + + + + {{ 'dashboard.is-root-state' | translate }} + + {{state.root ? 'check_box' : 'check_box_outline_blank'}} + + + + + + +
+ + +
+
+
+ + +
+ {{ 'dashboard.no-states-text' }} +
+ + +
+
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts new file mode 100644 index 0000000000..c33b85103a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.models.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { DashboardState } from '@shared/models/dashboard.models'; +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; +import { WidgetActionDescriptorInfo } from '@home/components/widget/action/manage-widget-actions.component.models'; +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; +import { catchError, map, publishReplay, refCount } from 'rxjs/operators'; + +export interface DashboardStateInfo extends DashboardState { + id: string; +} + +export class DashboardStatesDatasource implements DataSource { + + private statesSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public pageData$ = this.pageDataSubject.asObservable(); + + private allStates: Observable>; + + constructor(private states: {[id: string]: DashboardState }) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.statesSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.statesSubject.complete(); + this.pageDataSubject.complete(); + } + + loadStates(pageLink: PageLink, reload: boolean = false): Observable> { + if (reload) { + this.allStates = null; + } + const result = new ReplaySubject>(); + this.fetchStates(pageLink).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.statesSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + } + ); + return result; + } + + fetchStates(pageLink: PageLink): Observable> { + return this.getAllStates().pipe( + map((data) => pageLink.filterData(data)) + ); + } + + getAllStates(): Observable> { + if (!this.allStates) { + const states: DashboardStateInfo[] = []; + for (const id of Object.keys(this.states)) { + const state = this.states[id]; + states.push({id, ...state}); + } + this.allStates = of(states).pipe( + publishReplay(1), + refCount() + ); + } + return this.allStates; + } + + isEmpty(): Observable { + return this.statesSubject.pipe( + map((states) => !states.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss new file mode 100644 index 0000000000..68cb50ccc0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.scss @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .manage-dashboard-states { + .tb-entity-table { + .tb-entity-table-content { + width: 100%; + height: 100%; + background: #fff; + + .tb-entity-table-title { + padding-right: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .table-container { + overflow: auto; + } + } + } + } +} + +:host ::ng-deep { + .manage-dashboard-states { + .mat-sort-header-sorted .mat-sort-header-arrow { + opacity: 1 !important; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts new file mode 100644 index 0000000000..b20b47c662 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/manage-dashboard-states-dialog.component.ts @@ -0,0 +1,244 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@app/shared/components/dialog.component'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { MatDialog } from '@angular/material/dialog'; +import { PageLink } from '@shared/models/page/page-link'; +import { + WidgetActionDescriptorInfo, + WidgetActionsDatasource +} from '@home/components/widget/action/manage-widget-actions.component.models'; +import { + DashboardStateInfo, + DashboardStatesDatasource +} from '@home/pages/dashboard/states/manage-dashboard-states-dialog.component.models'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { fromEvent, merge } from 'rxjs'; +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '@core/services/dialog.service'; +import { + WidgetActionDialogComponent, + WidgetActionDialogData +} from '@home/components/widget/action/widget-action-dialog.component'; +import { deepClone } from '@core/utils'; +import { + DashboardStateDialogComponent, + DashboardStateDialogData +} from '@home/pages/dashboard/states/dashboard-state-dialog.component'; + +export interface ManageDashboardStatesDialogData { + states: {[id: string]: DashboardState }; +} + +@Component({ + selector: 'tb-manage-dashboard-states-dialog', + templateUrl: './manage-dashboard-states-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardStatesDialogComponent}], + styleUrls: ['./manage-dashboard-states-dialog.component.scss'] +}) +export class ManageDashboardStatesDialogComponent extends + DialogComponent + implements OnInit, ErrorStateMatcher, AfterViewInit { + + statesFormGroup: FormGroup; + + states: {[id: string]: DashboardState }; + + displayedColumns: string[]; + pageLink: PageLink; + textSearchMode = false; + dataSource: DashboardStatesDatasource; + + submitted = false; + + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; + + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; + @ViewChild(MatSort, {static: false}) sort: MatSort; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardStatesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + private translate: TranslateService, + private dialogs: DialogService, + private dialog: MatDialog) { + super(store, router, dialogRef); + + this.states = this.data.states; + this.statesFormGroup = this.fb.group({}); + + const sortOrder: SortOrder = { property: 'name', direction: Direction.ASC }; + this.pageLink = new PageLink(5, 0, null, sortOrder); + this.displayedColumns = ['name', 'id', 'root', 'actions']; + this.dataSource = new DashboardStatesDatasource(this.states); + } + + ngOnInit(): void { + this.dataSource.loadStates(this.pageLink); + } + + ngAfterViewInit() { + fromEvent(this.searchInputField.nativeElement, 'keyup') + .pipe( + debounceTime(150), + distinctUntilChanged(), + tap(() => { + this.paginator.pageIndex = 0; + this.updateData(); + }) + ) + .subscribe(); + + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + } + + updateData(reload: boolean = false) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.dataSource.loadStates(this.pageLink, reload); + } + + addState($event: Event) { + this.openStateDialog($event); + } + + editState($event: Event, state: DashboardStateInfo) { + this.openStateDialog($event, state); + } + + deleteState($event: Event, state: DashboardStateInfo) { + if ($event) { + $event.stopPropagation(); + } + const title = this.translate.instant('dashboard.delete-state-title'); + const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name}); + this.dialogs.confirm(title, content, this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe( + (res) => { + if (res) { + delete this.states[state.id]; + this.onStatesUpdated(); + } + } + ); + } + + enterFilterMode() { + this.textSearchMode = true; + this.pageLink.textSearch = ''; + setTimeout(() => { + this.searchInputField.nativeElement.focus(); + this.searchInputField.nativeElement.setSelectionRange(0, 0); + }, 10); + } + + exitFilterMode() { + this.textSearchMode = false; + this.pageLink.textSearch = null; + this.paginator.pageIndex = 0; + this.updateData(); + } + + openStateDialog($event: Event, state: DashboardStateInfo = null) { + if ($event) { + $event.stopPropagation(); + } + const isAdd = state === null; + let prevStateId = null; + if (!isAdd) { + prevStateId = state.id; + } + this.dialog.open(DashboardStateDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + states: this.states, + state: deepClone(state) + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.saveState(res, prevStateId); + } + } + ); + } + + saveState(state: DashboardStateInfo, prevStateId: string) { + const newState: DashboardState = { + name: state.name, + root: state.root, + layouts: state.layouts + }; + if (prevStateId) { + this.states[prevStateId] = newState; + } else { + this.states[state.id] = newState; + } + if (state.root) { + for (const id of Object.keys(this.states)) { + const otherState = this.states[id]; + if (id !== state.id) { + otherState.root = false; + } + } + } + this.onStatesUpdated(); + } + + private onStatesUpdated() { + this.statesFormGroup.markAsDirty(); + this.updateData(true); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.dialogRef.close(this.states); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts index 4ef1e83d83..cfe3e9bf8f 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts @@ -84,6 +84,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon currentState: string; + currentUrl: string; + private rxSubscriptions = new Array(); private inited = false; @@ -94,12 +96,16 @@ export abstract class StateControllerComponent implements IStateControllerCompon } ngOnInit(): void { + this.currentUrl = this.router.url.split('?')[0]; this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => { - const newState = paramMap.get('state'); - if (this.currentState !== newState) { - this.currentState = newState; - if (this.inited) { - this.onStateChanged(); + const newUrl = this.router.url.split('?')[0]; + if (this.currentUrl === newUrl) { + const newState = paramMap.get('state'); + if (this.currentState !== newState) { + this.currentState = newState; + if (this.inited) { + this.onStateChanged(); + } } } })); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 320a55def2..9f85270dfd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -557,6 +557,7 @@ "edit-state": "Edit dashboard state", "delete-state": "Delete dashboard state", "add-state": "Add dashboard state", + "no-states-text": "No states found", "state": "Dashboard state", "state-name": "Name", "state-name-required": "Dashboard state name is required.",