From b60b3144a0854ec0cdf75fbb96492c2deefa42e9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 20 Sep 2019 20:30:43 +0300 Subject: [PATCH] State Controllers and Dashboard Layouts. --- ui-ngx/src/app/core/api/alias-controller.ts | 6 + ui-ngx/src/app/core/api/widget-api.models.ts | 20 +- .../core/services/dashboard-utils.service.ts | 42 ++- ui-ngx/src/app/core/services/utils.service.ts | 18 +- .../translate/missing-translate-handler.ts | 5 +- .../dashboard/dashboard.component.html | 2 +- .../dashboard/dashboard.component.ts | 76 ++--- .../widget/widget-component.service.ts | 2 +- .../home/models/dashboard-component.models.ts | 69 ++++ .../home/models/widget-component.models.ts | 2 +- .../pages/customer/customer-routing.module.ts | 40 ++- .../dashboard/dashboard-page.component.html | 47 ++- .../dashboard/dashboard-page.component.ts | 132 +++++++- .../pages/dashboard/dashboard-page.models.ts | 18 +- .../home/pages/dashboard/dashboard.module.ts | 6 +- .../layout/dashboard-layout.component.html | 65 ++++ .../layout/dashboard-layout.component.scss | 23 ++ .../layout/dashboard-layout.component.ts | 80 +++++ .../pages/dashboard/layout/layout.models.ts | 20 ++ .../default-state-controller.component.html | 23 ++ .../default-state-controller.component.scss | 20 ++ .../default-state-controller.component.ts | 255 ++++++++++++++ .../entity-state-controller.component.html | 36 ++ .../entity-state-controller.component.scss | 45 +++ .../entity-state-controller.component.ts | 316 ++++++++++++++++++ .../states/state-controller.component.ts | 173 ++++++++++ .../states/state-controller.models.ts | 30 ++ .../states/states-component.directive.ts | 122 +++++++ .../states/states-controller.module.ts | 56 ++++ .../states/states-controller.service.ts | 64 ++++ .../widget/widget-library-routing.module.ts | 73 +++- .../widget/widget-library.component.html | 3 +- .../pages/widget/widget-library.component.ts | 64 +--- .../footer-fab-buttons.component.html | 2 +- .../footer-fab-buttons.component.scss | 18 +- .../footer-fab-buttons.component.ts | 10 + .../src/app/shared/models/dashboard.models.ts | 17 +- 37 files changed, 1830 insertions(+), 170 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 8f16ee5209..056efc83cf 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -58,6 +58,9 @@ export class DummyAliasController implements IAliasController { updateEntityAliases(entityAliases: EntityAliases) { } + dashboardStateChanged() { + } + } export class AliasController implements IAliasController { @@ -111,4 +114,7 @@ export class AliasController implements IAliasController { updateEntityAliases(entityAliases: EntityAliases) { } + dashboardStateChanged() { + } + } 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 9301857659..934315e678 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -86,6 +86,7 @@ export interface IAliasController { getEntityAliases(): EntityAliases; updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); updateEntityAliases(entityAliases: EntityAliases); + dashboardStateChanged(); [key: string]: any | null; // TODO: } @@ -103,12 +104,19 @@ export interface StateParams { } export interface IStateController { - getStateParams?: () => StateParams; - openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void; - updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void; - openRightLayout: () => void; - preserveState?: () => void; - // TODO: + getStateParams(): StateParams; + getStateParamsByStateId(stateId: string): StateParams; + openState(id: string, params?: StateParams, openRightLayout?: boolean): void; + updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; + resetState(): void; + openRightLayout(): void; + preserveState(): void; + cleanupPreservedStates(): void; + navigatePrevState(index: number): void; + getStateId(): string; + getStateIndex(): number; + getStateIdAtIndex(index: number): string; + getEntityId(entityParamName: string): EntityId; } export interface SubscriptionInfo { 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 30fdb3aded..cafeaccd2f 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -22,7 +22,9 @@ import { DashboardLayout, DashboardStateLayouts, DashboardState, - DashboardConfiguration + DashboardConfiguration, + DashboardLayoutInfo, + DashboardLayoutsInfo } from '@shared/models/dashboard.models'; import { isUndefined, isDefined, isString } from '@core/utils'; import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models'; @@ -238,6 +240,44 @@ export class DashboardUtilsService { }; } + public getRootStateId(states: {[id: string]: DashboardState }): string { + for (const stateId of Object.keys(states)) { + const state = states[stateId]; + if (state.root) { + return stateId; + } + } + return Object.keys(states)[0]; + } + + public getStateLayoutsData(dashboard: Dashboard, targetState: string): DashboardLayoutsInfo { + const dashboardConfiguration = dashboard.configuration; + const states = dashboardConfiguration.states; + const state = states[targetState]; + if (state) { + const allWidgets = dashboardConfiguration.widgets; + const result: DashboardLayoutsInfo = {}; + for (const l of Object.keys(state.layouts)) { + const layout: DashboardLayout = state.layouts[l]; + if (layout) { + result[l] = { + widgets: [], + widgetLayouts: {}, + gridSettings: {} + } as DashboardLayoutInfo; + for (const id of Object.keys(layout.widgets)) { + result[l].widgets.push(allWidgets[id]); + } + result[l].widgetLayouts = layout.widgets; + result[l].gridSettings = layout.gridSettings; + } + } + return result; + } else { + return null; + } + } + private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, datasourcesByAliasId: {[aliasId: string]: Array}, targetDevicesByAliasId: {[aliasId: string]: Array>}): DashboardConfiguration { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index b323baf2b5..1e33f84625 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -17,7 +17,7 @@ import { Inject, Injectable } from '@angular/core'; import { WINDOW } from '@core/services/window.service'; import { ExceptionData } from '@app/shared/models/error.models'; -import { isUndefined, isDefined } from '@core/utils'; +import { isUndefined, isDefined, deepClone } from '@core/utils'; import { WindowMessage } from '@shared/models/window-message.model'; import { TranslateService } from '@ngx-translate/core'; import { customTranslationsPrefix } from '@app/shared/models/constants'; @@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; import { materialColors } from '@app/shared/models/material.models'; import { WidgetInfo } from '@home/models/widget-component.models'; +const varsRegex = /\$\{([^}]*)\}/g; + @Injectable({ providedIn: 'root' }) @@ -144,6 +146,20 @@ export class UtilsService { return result; } + public insertVariable(pattern: string, name: string, value: any): string { + let result = deepClone(pattern); + let match = varsRegex.exec(pattern); + while (match !== null) { + const variable = match[0]; + const variableName = match[1]; + if (variableName === name) { + result = result.split(variable).join(value); + } + match = varsRegex.exec(pattern); + } + return result; + } + public guid(): string { function s4(): string { return Math.floor((1 + Math.random()) * 0x10000) diff --git a/ui-ngx/src/app/core/translate/missing-translate-handler.ts b/ui-ngx/src/app/core/translate/missing-translate-handler.ts index 71cf578b2b..08da9e71a7 100644 --- a/ui-ngx/src/app/core/translate/missing-translate-handler.ts +++ b/ui-ngx/src/app/core/translate/missing-translate-handler.ts @@ -15,9 +15,12 @@ /// import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core'; +import { customTranslationsPrefix } from '@app/shared/models/constants'; export class TbMissingTranslationHandler implements MissingTranslationHandler { handle(params: MissingTranslationHandlerParams) { - console.warn('Translation for ' + params.key + ' doesn\'t exist'); + if (params.key && !params.key.startsWith(customTranslationsPrefix)) { + console.warn('Translation for ' + params.key + ' doesn\'t exist'); + } } } 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 a352d2da46..467ce77fb5 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 @@ -26,7 +26,7 @@ (contextmenu)="openDashboardContextMenu($event)">
- +
; + widgets: Array; + + @Input() + widgetLayouts: WidgetLayouts; @Input() callbacks: DashboardCallbacks; @@ -125,8 +130,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo gridsterOpts: GridsterConfig; - dashboardLoading = true; - highlightedMode = false; highlightedWidget: DashboardWidget = null; selectedWidget: DashboardWidget = null; @@ -138,9 +141,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @ViewChildren(GridsterItemComponent) gridsterItems: QueryList; - widgets$: Observable>; + dashboardLoading = true; - widgets: Array; + dashboardWidgets = new DashboardWidgets(this); constructor(protected store: Store, private timeService: TimeService, @@ -172,25 +175,26 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo defaultItemRows: 6, resizable: {enabled: this.isEdit}, draggable: {enabled: this.isEdit}, - itemChangeCallback: item => this.sortWidgets(this.widgets) + itemChangeCallback: item => this.dashboardWidgets.sortWidgets() }; this.updateMobileOpts(); - this.loadDashboard(); - this.breakpointObserver .observe(MediaBreakpoints['gt-sm']).subscribe( () => { this.updateMobileOpts(); } ); + + this.updateWidgets(); } ngOnChanges(changes: SimpleChanges): void { let updateMobileOpts = false; let updateLayoutOpts = false; let updateEditingOpts = false; + let updateWidgets = false; for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { @@ -200,9 +204,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo updateLayoutOpts = true; } else if (propName === 'isEdit') { updateEditingOpts = true; + } else if (['widgets', 'widgetLayouts'].includes(propName)) { + updateWidgets = true; } } } + if (updateWidgets) { + this.updateWidgets(); + } if (updateMobileOpts) { this.updateMobileOpts(); } @@ -217,50 +226,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } } - loadDashboard() { - this.widgets$ = this.widgetsData.pipe( - map(widgetsData => { - const dashboardWidgets = new Array(); - let maxRows = this.gridsterOpts.maxRows; - widgetsData.widgets.forEach( - (widget) => { - let widgetLayout: WidgetLayout; - if (widgetsData.widgetLayouts && widget.id) { - widgetLayout = widgetsData.widgetLayouts[widget.id]; - } - const dashboardWidget = new DashboardWidget(this, widget, widgetLayout); - const bottom = dashboardWidget.y + dashboardWidget.rows; - maxRows = Math.max(maxRows, bottom); - dashboardWidgets.push(dashboardWidget); - } - ); - this.sortWidgets(dashboardWidgets); - this.gridsterOpts.maxRows = maxRows; - return dashboardWidgets; - }), - tap((widgets) => { - this.widgets = widgets; - this.dashboardLoading = false; - }) - ); - } - - reload() { - this.loadDashboard(); - } - - sortWidgets(widgets?: Array) { - if (widgets) { - widgets.sort((widget1, widget2) => { - const row1 = widget1.widgetOrder; - const row2 = widget2.widgetOrder; - let res = row1 - row2; - if (res === 0) { - res = widget1.x - widget2.x; - } - return res; - }); - } + private updateWidgets() { + this.dashboardWidgets.setWidgets(this.widgets, this.widgetLayouts); + this.dashboardLoading = false; } ngAfterViewInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index aa7682bf23..7271dff81a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -119,7 +119,7 @@ export class WidgetComponentService { } else { fetchQueue = new Array>(); this.widgetsInfoFetchQueue.set(key, fetchQueue); - this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( + this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, true, false).subscribe( (widgetType) => { this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); }, 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 d1be047ae4..66d5a340fd 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 @@ -43,6 +43,7 @@ export interface DashboardCallbacks { export interface IDashboardComponent { gridsterOpts: GridsterConfig; gridster: GridsterComponent; + dashboardWidgets: DashboardWidgets; mobileAutofillHeight: boolean; isMobileSize: boolean; autofillHeight: boolean; @@ -54,6 +55,74 @@ export interface IDashboardComponent { onResetTimewindow(): void; } +export class DashboardWidgets implements Iterable { + + dashboardWidgets: Array = []; + + [Symbol.iterator](): Iterator { + return this.dashboardWidgets[Symbol.iterator](); + } + + constructor(private dashboard: IDashboardComponent) { + } + + setWidgets(widgets: Array, widgetLayouts: WidgetLayouts) { + let maxRows = this.dashboard.gridsterOpts.maxRows; + this.dashboardWidgets.length = 0; + widgets.forEach((widget) => { + let widgetLayout: WidgetLayout; + if (widgetLayouts && widget.id) { + widgetLayout = widgetLayouts[widget.id]; + } + const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout); + const bottom = dashboardWidget.y + dashboardWidget.rows; + maxRows = Math.max(maxRows, bottom); + this.dashboardWidgets.push(dashboardWidget); + }); + this.sortWidgets(); + this.dashboard.gridsterOpts.maxRows = maxRows; + } + + addWidget(widget: Widget, widgetLayout: WidgetLayout) { + const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout); + let maxRows = this.dashboard.gridsterOpts.maxRows; + const bottom = dashboardWidget.y + dashboardWidget.rows; + maxRows = Math.max(maxRows, bottom); + this.dashboardWidgets.push(dashboardWidget); + this.sortWidgets(); + this.dashboard.gridsterOpts.maxRows = maxRows; + } + + removeWidget(widget: Widget): boolean { + const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widget === widget); + if (index > -1) { + this.dashboardWidgets.splice(index, 1); + let maxRows = this.dashboard.gridsterOpts.maxRows; + this.dashboardWidgets.forEach((dashboardWidget) => { + const bottom = dashboardWidget.y + dashboardWidget.rows; + maxRows = Math.max(maxRows, bottom); + }); + this.sortWidgets(); + this.dashboard.gridsterOpts.maxRows = maxRows; + return true; + } + return false; + } + + sortWidgets() { + this.dashboardWidgets.sort((widget1, widget2) => { + const row1 = widget1.widgetOrder; + const row2 = widget2.widgetOrder; + let res = row1 - row2; + if (res === 0) { + res = widget1.x - widget2.x; + } + return res; + }); + } + +} + export class DashboardWidget implements GridsterItem { isFullscreen = false; 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 35517d5209..5071cd8509 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 @@ -123,7 +123,7 @@ export const MissingWidgetType: WidgetInfo = { sizeY: 6, resources: [], templateHtml: '
' + - '
widget.widget-type-not-found
' + + '
' + '
', templateCss: '', controllerScript: 'self.onInit = function() {}', diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts index 10a0e0f4eb..2ee13e5160 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -24,6 +24,9 @@ import {CustomersTableConfigResolver} from './customers-table-config.resolver'; import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver'; import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver'; import {DashboardsTableConfigResolver} from '@modules/home/pages/dashboard/dashboards-table-config.resolver'; +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; const routes: Routes = [ { @@ -95,19 +98,42 @@ const routes: Routes = [ }, { path: ':customerId/dashboards', - component: EntitiesTableComponent, data: { - auth: [Authority.TENANT_ADMIN], - title: 'customer.assets', - dashboardsType: 'customer', breadcrumb: { label: 'customer.dashboards', icon: 'dashboard' } }, - resolve: { - entitiesTableConfig: DashboardsTableConfigResolver - } + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'customer.dashboards', + dashboardsType: 'customer' + }, + resolve: { + entitiesTableConfig: DashboardsTableConfigResolver + } + }, + { + path: ':dashboardId', + component: DashboardPageComponent, + data: { + breadcrumb: { + labelFunction: dashboardBreadcumbLabelFunction, + icon: 'dashboard' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + title: 'customer.dashboard', + widgetEditMode: false + }, + resolve: { + dashboard: DashboardResolver + } + } + ] } ] } 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 e2956320ba..65530c30a7 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 @@ -85,7 +85,31 @@ fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px"> - +
+ + +
+ +
@@ -110,7 +134,13 @@ id="tb-main-layout" [ngStyle]="{width: mainLayoutWidth(), height: mainLayoutHeight()}"> - TODO: MAIN LAYOUT tb-dashboard-layout + + @@ -123,14 +153,23 @@ position="end" [mode]="isMobile ? 'over' : 'side'" [(opened)]="rightLayoutOpened"> - TODO: RIGHT LAYOUT tb-dashboard-layout + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss new file mode 100644 index 0000000000..00358e9b31 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.scss @@ -0,0 +1,23 @@ +/** + * 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 { + button.tb-add-new-widget { + padding-right: 12px; + font-size: 24px; + border-style: dashed; + border-width: 2px; + } +} 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 new file mode 100644 index 0000000000..8c0ba8671b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts @@ -0,0 +1,80 @@ +/// +/// 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, OnDestroy, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core'; +import { StateControllerComponent } from '@home/pages/dashboard/states/state-controller.component'; +import { ILayoutController } from '@home/pages/dashboard/layout/layout.models'; +import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Widget } from '@shared/models/widget.models'; +import { WidgetLayouts } from '@shared/models/dashboard.models'; +import { GridsterComponent } from 'angular-gridster2'; +import { IDashboardComponent } from '@home/models/dashboard-component.models'; + +@Component({ + selector: 'tb-dashboard-layout', + templateUrl: './dashboard-layout.component.html', + styleUrls: ['./dashboard-layout.component.scss'] +}) +export class DashboardLayoutComponent extends PageComponent implements ILayoutController, OnInit, OnDestroy { + + layoutCtxValue: DashboardPageLayoutContext; + + @Input() + set layoutCtx(val: DashboardPageLayoutContext) { + this.layoutCtxValue = val; + if (this.layoutCtxValue) { + this.layoutCtxValue.ctrl = this; + } + } + get layoutCtx(): DashboardPageLayoutContext { + return this.layoutCtxValue; + } + + @Input() + dashboardCtx: DashboardContext; + + @Input() + isEdit: boolean; + + @Input() + isMobile: boolean; + + @Input() + widgetEditMode: boolean; + + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; + + constructor(protected store: Store, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + } + + reload() { + } + + setResizing(layoutVisibilityChanged: boolean) { + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts new file mode 100644 index 0000000000..86b224fe0a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts @@ -0,0 +1,20 @@ +/// +/// 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. +/// + +export interface ILayoutController { + reload(); + setResizing(layoutVisibilityChanged: boolean); +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html new file mode 100644 index 0000000000..b86d4bb530 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html @@ -0,0 +1,23 @@ + + + + {{getStateName(stateKv.key, stateKv.value)}} + + 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 new file mode 100644 index 0000000000..691099ef0b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss @@ -0,0 +1,20 @@ +/** + * 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 { + mat-select.default-state-controller { + margin: 0; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts new file mode 100644 index 0000000000..4a6b2bcdf7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.ts @@ -0,0 +1,255 @@ +/// +/// 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, + OnInit, + ViewEncapsulation, + Input, + OnDestroy, + OnChanges, + SimpleChanges, + NgZone +} from '@angular/core'; +import { IStateController, StateParams, StateObject } from '@core/api/widget-api.models'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, Subscription, of } from 'rxjs'; +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { IStateControllerComponent, StateControllerState } from './state-controller.models'; +import { StateControllerComponent } from './state-controller.component'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { UtilsService } from '@core/services/utils.service'; +import { base64toObj, objToBase64 } from '@app/core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-default-state-controller', + templateUrl: './default-state-controller.component.html', + styleUrls: ['./default-state-controller.component.scss'] +}) +export class DefaultStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy { + + constructor(protected router: Router, + protected route: ActivatedRoute, + protected statesControllerService: StatesControllerService, + private utils: UtilsService, + private entityService: EntityService, + private dashboardUtils: DashboardUtilsService) { + super(router, route, statesControllerService); + } + + ngOnInit(): void { + super.ngOnInit(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + protected init() { + if (this.preservedState) { + this.stateObject = this.preservedState; + setTimeout(() => { + this.gotoState(this.stateObject[0].id, true); + }, 1); + } else { + const initialState = this.currentState; + this.stateObject = this.parseState(initialState); + setTimeout(() => { + this.gotoState(this.stateObject[0].id, false); + }, 1); + } + } + + protected onMobileChanged() { + } + + protected onStateIdChanged() { + } + + protected onStatesChanged() { + } + + protected onStateChanged() { + this.stateObject = this.parseState(this.currentState); + this.gotoState(this.stateObject[0].id, true); + } + + protected stateControllerId(): string { + return 'default'; + } + + public getStateParams(): StateParams { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].params; + } else { + return {}; + } + } + + public openState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (this.states && this.states[id]) { + if (!params) { + params = {}; + } + const newState: StateObject = { + id, + params + }; + this.stateObject[0] = newState; + this.gotoState(this.stateObject[0].id, true, openRightLayout); + } + } + + public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (!id) { + id = this.getStateId(); + } + if (this.states && this.states[id]) { + if (!params) { + params = {}; + } + const newState: StateObject = { + id, + params + }; + this.stateObject[0] = newState; + this.gotoState(this.stateObject[0].id, true, openRightLayout); + } + } + + public getEntityId(entityParamName: string): EntityId { + return null; + } + + public getStateId(): string { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].id; + } else { + return ''; + } + } + + public getStateIdAtIndex(index: number): string { + if (this.stateObject && this.stateObject[index]) { + return this.stateObject[index].id; + } else { + return ''; + } + } + + public getStateIndex(): number { + if (this.stateObject && this.stateObject.length) { + return this.stateObject.length - 1; + } else { + return -1; + } + } + + public getStateParamsByStateId(stateId: string): StateParams { + const stateObj = this.getStateObjById(stateId); + if (stateObj) { + return stateObj.params; + } else { + return null; + } + } + + public navigatePrevState(index: number): void { + if (index < this.stateObject.length - 1) { + this.stateObject.splice(index + 1, this.stateObject.length - index - 1); + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + } + } + + public resetState(): void { + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + this.stateObject = [ { id: rootStateId, params: {} } ]; + this.gotoState(rootStateId, true); + } + + public getStateName(id: string, state: DashboardState): string { + return this.utils.customTranslation(state.name, id); + } + + public displayStateSelection(): boolean { + return this.states && Object.keys(this.states).length > 1; + } + + public selectedStateIdChanged() { + this.gotoState(this.stateObject[0].id, true); + } + + private parseState(stateBase64: string): StateControllerState { + let result: StateControllerState; + if (stateBase64) { + try { + result = base64toObj(stateBase64); + } catch (e) { + result = [ { id: null, params: {} } ]; + } + } + if (!result) { + result = []; + } + if (!result.length) { + result[0] = { id: null, params: {} }; + } else if (result.length > 1) { + const newResult = []; + newResult.push(result[result.length - 1]); + result = newResult; + } + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + if (!result[0].id) { + result[0].id = rootStateId; + } + if (!this.states[result[0].id]) { + result[0].id = rootStateId; + } + let i = result.length; + while (i--) { + if (!result[i].id || !this.states[result[i].id]) { + result.splice(i, 1); + } + } + return result; + } + + private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { + if (this.dashboardCtrl.dashboardCtx.state !== stateId) { + this.dashboardCtrl.openDashboardState(stateId, openRightLayout); + if (update) { + this.updateLocation(); + } + } + } + + private updateLocation() { + if (this.stateObject[0].id) { + const newState = objToBase64(this.stateObject); + this.updateStateParam(newState); + } + } + + private getStateObjById(id: string): StateObject { + return this.stateObject.find((stateObj) => stateObj.id === id); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html new file mode 100644 index 0000000000..e490340470 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html @@ -0,0 +1,36 @@ + + +
+
+ + {{getStateName(i)}} + > + +
+ + + {{getStateName(i)}} + + +
+ + + 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 new file mode 100644 index 0000000000..4cbec02a67 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.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 { + .entity-state-controller { + .state-divider { + padding-right: 15px; + padding-left: 15px; + overflow: hidden; + font-size: 18px; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + } + + .state-entry { + overflow: hidden; + font-size: 18px; + text-overflow: ellipsis; + white-space: nowrap; + outline: none; + } + + mat-select { + margin: 0; + + .mat-select-value-text { + font-size: 18px; + font-weight: 700; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts new file mode 100644 index 0000000000..4c87aaf2df --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.ts @@ -0,0 +1,316 @@ +/// +/// 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, + OnInit, + ViewEncapsulation, + Input, + OnDestroy, + OnChanges, + SimpleChanges, + NgZone +} from '@angular/core'; +import { IStateController, StateParams, StateObject } from '@core/api/widget-api.models'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Observable, Subscription, of } from 'rxjs'; +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { IStateControllerComponent, StateControllerState } from './state-controller.models'; +import { StateControllerComponent } from './state-controller.component'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { UtilsService } from '@core/services/utils.service'; +import { base64toObj, objToBase64 } from '@app/core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { EntityService } from '@core/http/entity.service'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'tb-entity-state-controller', + templateUrl: './entity-state-controller.component.html', + styleUrls: ['./entity-state-controller.component.scss'] +}) +export class EntityStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy { + + private selectedStateIndex = -1; + + constructor(protected router: Router, + protected route: ActivatedRoute, + protected statesControllerService: StatesControllerService, + private utils: UtilsService, + private entityService: EntityService, + private dashboardUtils: DashboardUtilsService) { + super(router, route, statesControllerService); + } + + ngOnInit(): void { + super.ngOnInit(); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + protected init() { + if (this.preservedState) { + this.stateObject = this.preservedState; + this.selectedStateIndex = this.stateObject.length - 1; + setTimeout(() => { + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + }, 1); + } else { + const initialState = this.currentState; + this.stateObject = this.parseState(initialState); + this.selectedStateIndex = this.stateObject.length - 1; + setTimeout(() => { + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false); + }, 1); + } + } + + protected onMobileChanged() { + } + + protected onStateIdChanged() { + } + + protected onStatesChanged() { + } + + protected onStateChanged() { + this.stateObject = this.parseState(this.currentState); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + } + + protected stateControllerId(): string { + return 'entity'; + } + + public getStateParams(): StateParams { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].params; + } else { + return {}; + } + } + + public openState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (this.states && this.states[id]) { + this.resolveEntity(params).subscribe( + (entityName) => { + params.entityName = entityName; + const newState: StateObject = { + id, + params + }; + this.stateObject.push(newState); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout); + } + ); + } + } + + public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void { + if (!id) { + id = this.getStateId(); + } + if (this.states && this.states[id]) { + this.resolveEntity(params).subscribe( + (entityName) => { + params.entityName = entityName; + const newState: StateObject = { + id, + params + }; + this.stateObject[this.stateObject.length - 1] = newState; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout); + } + ); + } + } + + public getEntityId(entityParamName: string): EntityId { + const stateParams = this.getStateParams(); + if (!entityParamName || !entityParamName.length) { + return stateParams.entityId; + } else if (stateParams[entityParamName]) { + return stateParams[entityParamName].entityId; + } + return null; + } + + public getStateId(): string { + if (this.stateObject && this.stateObject.length) { + return this.stateObject[this.stateObject.length - 1].id; + } else { + return ''; + } + } + + public getStateIdAtIndex(index: number): string { + if (this.stateObject && this.stateObject[index]) { + return this.stateObject[index].id; + } else { + return ''; + } + } + + public getStateIndex(): number { + if (this.stateObject && this.stateObject.length) { + return this.stateObject.length - 1; + } else { + return -1; + } + } + + public getStateParamsByStateId(stateId: string): StateParams { + const stateObj = this.getStateObjById(stateId); + if (stateObj) { + return stateObj.params; + } else { + return null; + } + } + + public navigatePrevState(index: number): void { + if (index < this.stateObject.length - 1) { + this.stateObject.splice(index + 1, this.stateObject.length - index - 1); + this.selectedStateIndex = this.stateObject.length - 1; + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); + } + } + + public resetState(): void { + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + this.stateObject = [ { id: rootStateId, params: {} } ]; + this.gotoState(rootStateId, true); + } + + public getStateName(index: number): string { + let result = ''; + if (this.stateObject[index]) { + let stateName = this.states[this.stateObject[index].id].name; + stateName = this.utils.customTranslation(stateName, stateName); + const params = this.stateObject[index].params; + const entityName = params && params.entityName ? params.entityName : ''; + result = this.utils.insertVariable(stateName, 'entityName', entityName); + for (const prop of Object.keys(params)) { + if (params[prop] && params[prop].entityName) { + result = this.utils.insertVariable(result, prop + ':entityName', params[prop].entityName); + } + } + } + return result; + } + + public selectedStateIndexChanged() { + this.navigatePrevState(this.selectedStateIndex); + } + + private parseState(stateBase64: string): StateControllerState { + let result: StateControllerState; + if (stateBase64) { + try { + result = base64toObj(stateBase64); + } catch (e) { + result = [ { id: null, params: {} } ]; + } + } + if (!result) { + result = []; + } + if (!result.length) { + result[0] = { id: null, params: {} }; + } + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + if (!result[0].id) { + result[0].id = rootStateId; + } + if (!this.states[result[0].id]) { + result[0].id = rootStateId; + } + let i = result.length; + while (i--) { + if (!result[i].id || !this.states[result[i].id]) { + result.splice(i, 1); + } + } + return result; + } + + private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { + this.dashboardCtrl.openDashboardState(stateId, openRightLayout); + if (update) { + this.updateLocation(); + } + } + + private updateLocation() { + if (this.stateObject[this.stateObject.length - 1].id) { + let newState; + if (this.isDefaultState()) { + newState = null; + } else { + newState = objToBase64(this.stateObject); + } + this.updateStateParam(newState); + } + } + + private isDefaultState(): boolean { + if (this.stateObject.length === 1) { + const state = this.stateObject[0]; + const rootStateId = this.dashboardUtils.getRootStateId(this.states); + if (state.id === rootStateId && (!state.params || this.isEmpty(state.params))) { + return true; + } + } + return false; + } + + private isEmpty(obj: any): boolean { + for (const key of Object.keys(obj)) { + return !Object.prototype.hasOwnProperty.call(obj, key); + } + return true; + } + + private resolveEntity(params: StateParams): Observable { + if (params && params.targetEntityParamName) { + params = params[params.targetEntityParamName]; + } + if (params && params.entityId && params.entityId.id && params.entityId.entityType) { + if (params.entityName && params.entityName.length) { + return of(params.entityName); + } else { + return this.entityService.getEntity(params.entityId.entityType as EntityType, + params.entityId.id, true, true).pipe( + map((entity) => entity.name) + ); + } + } else { + return of(''); + } + } + + private getStateObjById(id: string): StateObject { + return this.stateObject.find((stateObj) => stateObj.id === id); + } +} 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 new file mode 100644 index 0000000000..4ef1e83d83 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.component.ts @@ -0,0 +1,173 @@ +/// +/// 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 { IStateControllerComponent, StateControllerState } from '@home/pages/dashboard/states/state-controller.models'; +import { IDashboardController } from '../dashboard-page.models'; +import { DashboardState } from '@app/shared/models/dashboard.models'; +import { Subscription } from 'rxjs'; +import { OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router, Params } from '@angular/router'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { EntityId } from '@app/shared/models/id/entity-id'; +import { StateParams } from '@app/core/api/widget-api.models'; + +export abstract class StateControllerComponent implements IStateControllerComponent, OnInit, OnDestroy { + + stateObject: StateControllerState = []; + dashboardCtrl: IDashboardController; + preservedState: any; + + isMobileValue: boolean; + set isMobile(val: boolean) { + if (this.isMobileValue !== val) { + this.isMobileValue = val; + if (this.inited) { + this.onMobileChanged(); + } + } + } + get isMobile(): boolean { + return this.isMobileValue; + } + + stateValue: string; + set state(val: string) { + if (this.stateValue !== val) { + this.stateValue = val; + if (this.inited) { + this.onStateIdChanged(); + } + } + } + get state(): string { + return this.stateValue; + } + + dashboardIdValue: string; + set dashboardId(val: string) { + if (this.dashboardIdValue !== val) { + this.dashboardIdValue = val; + if (this.inited) { + this.init(); + } + } + } + get dashboardId(): string { + return this.dashboardIdValue; + } + + statesValue: { [id: string]: DashboardState }; + set states(val: { [id: string]: DashboardState }) { + if (this.statesValue !== val) { + this.statesValue = val; + if (this.inited) { + this.onStatesChanged(); + } + } + } + get states(): { [id: string]: DashboardState } { + return this.statesValue; + } + + currentState: string; + + private rxSubscriptions = new Array(); + + private inited = false; + + constructor(protected router: Router, + protected route: ActivatedRoute, + protected statesControllerService: StatesControllerService) { + } + + ngOnInit(): void { + 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(); + } + } + })); + this.init(); + this.inited = true; + } + + ngOnDestroy(): void { + this.rxSubscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + this.rxSubscriptions.length = 0; + } + + protected updateStateParam(newState: string) { + this.currentState = newState; + const queryParams: Params = { state: this.currentState }; + this.router.navigate( + [], + { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + } + + public openRightLayout(): void { + this.dashboardCtrl.openRightLayout(); + } + + public preserveState() { + this.statesControllerService.preserveStateControllerState(this.stateControllerId(), this.stateObject); + } + + public cleanupPreservedStates() { + this.statesControllerService.cleanupPreservedStates(); + } + + protected abstract init(); + + protected abstract onMobileChanged(); + + protected abstract onStateIdChanged(); + + protected abstract onStatesChanged(); + + protected abstract onStateChanged(); + + protected abstract stateControllerId(): string; + + public abstract getEntityId(entityParamName: string): EntityId; + + public abstract getStateId(): string; + + public abstract getStateIdAtIndex(index: number): string; + + public abstract getStateIndex(): number; + + public abstract getStateParams(): StateParams; + + public abstract getStateParamsByStateId(stateId: string): StateParams; + + public abstract navigatePrevState(index: number): void; + + public abstract openState(id: string, params?: StateParams, openRightLayout?: boolean): void; + + public abstract resetState(): void; + + public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; + +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts new file mode 100644 index 0000000000..2fa0fef100 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/state-controller.models.ts @@ -0,0 +1,30 @@ +/// +/// 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 { IStateController, StateObject } from '@core/api/widget-api.models'; +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models'; +import { DashboardState } from '@shared/models/dashboard.models'; + +export declare type StateControllerState = StateObject[]; + +export interface IStateControllerComponent extends IStateController { + state: string; + isMobile: boolean; + dashboardCtrl: IDashboardController; + states: {[id: string]: DashboardState }; + dashboardId: string; + preservedState: any; +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts new file mode 100644 index 0000000000..48970e36ae --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-component.directive.ts @@ -0,0 +1,122 @@ +/// +/// 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 { + ComponentRef, + Directive, + ElementRef, + Input, + OnChanges, + OnInit, + OnDestroy, + SimpleChanges, + ViewContainerRef, + ChangeDetectorRef +} from '@angular/core'; +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { DashboardState } from '@shared/models/dashboard.models'; +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models'; +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service'; +import { IStateController } from '@core/api/widget-api.models'; +import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models'; + +@Directive({ + selector: 'tb-states-component' +}) +export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges { + + @Input() + statesControllerId: string; + + @Input() + dashboardCtrl: IDashboardController; + + @Input() + dashboardId: string; + + @Input() + states: {[id: string]: DashboardState }; + + @Input() + state: string; + + @Input() + isMobile: boolean; + + stateControllerComponentRef: ComponentRef; + stateControllerComponent: IStateControllerComponent; + + constructor(private viewContainerRef: ViewContainerRef, + private statesControllerService: StatesControllerService) { + } + + ngOnInit(): void { + this.init(); + } + + ngOnDestroy(): void { + this.destroy(); + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (propName === 'statesControllerId') { + this.reInit(); + } else if (propName === 'states') { + this.stateControllerComponent.states = this.states; + } else if (propName === 'dashboardId') { + this.stateControllerComponent.dashboardId = this.dashboardId; + } else if (propName === 'isMobile') { + this.stateControllerComponent.isMobile = this.isMobile; + } else if (propName === 'state') { + this.stateControllerComponent.state = this.state; + } + } + } + } + + private reInit() { + this.destroy(); + this.init(); + } + + private init() { + this.viewContainerRef.clear(); + let stateControllerData = this.statesControllerService.getStateController(this.statesControllerId); + if (!stateControllerData) { + stateControllerData = this.statesControllerService.getStateController('default'); + } + const preservedState = this.statesControllerService.withdrawStateControllerState(this.statesControllerId); + const stateControllerFactory = stateControllerData.factory; + this.stateControllerComponentRef = this.viewContainerRef.createComponent(stateControllerFactory); + this.stateControllerComponent = this.stateControllerComponentRef.instance; + this.dashboardCtrl.dashboardCtx.stateController = this.stateControllerComponent; + this.stateControllerComponent.preservedState = preservedState; + this.stateControllerComponent.dashboardCtrl = this.dashboardCtrl; + this.stateControllerComponent.state = this.state; + this.stateControllerComponent.isMobile = this.isMobile; + this.stateControllerComponent.states = this.states; + this.stateControllerComponent.dashboardId = this.dashboardId; + } + + private destroy() { + if (this.stateControllerComponentRef) { + this.stateControllerComponentRef.destroy(); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts new file mode 100644 index 0000000000..55063eea2c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.module.ts @@ -0,0 +1,56 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { StatesControllerService } from './states-controller.service'; +import { EntityStateControllerComponent } from './entity-state-controller.component'; +import { StatesComponentDirective } from './states-component.directive'; +import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module'; +import { DefaultStateControllerComponent } from '@home/pages/dashboard/states/default-state-controller.component'; + +@NgModule({ + entryComponents: [ + DefaultStateControllerComponent, + EntityStateControllerComponent + ], + declarations: [ + StatesComponentDirective, + DefaultStateControllerComponent, + EntityStateControllerComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + HomeDialogsModule + ], + exports: [ + StatesComponentDirective + ], + providers: [ + StatesControllerService + ] +}) +export class StatesControllerModule { + + constructor(private statesControllerService: StatesControllerService) { + this.statesControllerService.registerStatesController('default', DefaultStateControllerComponent); + this.statesControllerService.registerStatesController('entity', EntityStateControllerComponent); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts new file mode 100644 index 0000000000..e99224342d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/states/states-controller.service.ts @@ -0,0 +1,64 @@ +/// +/// 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 { ComponentFactory, ComponentFactoryResolver, Injectable, Type } from '@angular/core'; +import { deepClone } from '@core/utils'; +import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models'; + +export interface StateControllerData { + factory: ComponentFactory; + state?: any; +} + +@Injectable() +export class StatesControllerService { + + statesControllers: {[stateControllerId: string]: StateControllerData} = {}; + + constructor(private componentFactoryResolver: ComponentFactoryResolver) { + } + + public registerStatesController(stateControllerId: string, stateControllerComponent: Type): void { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(stateControllerComponent); + this.statesControllers[stateControllerId] = { + factory: componentFactory + }; + } + + public getStateControllers(): {[stateControllerId: string]: StateControllerData} { + return this.statesControllers; + } + + public getStateController(stateControllerId: string): StateControllerData { + return this.statesControllers[stateControllerId]; + } + + public preserveStateControllerState(stateControllerId: string, state: any) { + this.statesControllers[stateControllerId].state = deepClone(state); + } + + public withdrawStateControllerState(stateControllerId: string): any { + const state = this.statesControllers[stateControllerId].state; + this.statesControllers[stateControllerId].state = null; + return state; + } + + public cleanupPreservedStates() { + for (const stateControllerId of Object.keys(this.statesControllers)) { + this.statesControllers[stateControllerId].state = null; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 5f452b346f..966c3feee7 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -26,10 +26,12 @@ import { Observable } from 'rxjs'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetService } from '@core/http/widget.service'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; -import { map } from 'rxjs/operators'; +import { map, share } from 'rxjs/operators'; import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; -import { widgetType, WidgetType } from '@app/shared/models/widget.models'; +import { Widget, widgetType, WidgetType } from '@app/shared/models/widget.models'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; +import { WidgetsData } from '@home/models/dashboard-component.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; export interface WidgetEditorData { widgetType: WidgetType; @@ -51,6 +53,69 @@ export class WidgetsBundleResolver implements Resolve { } } +@Injectable() +export class WidgetsTypesDataResolver implements Resolve { + + constructor(private widgetsService: WidgetService) { + } + + resolve(route: ActivatedRouteSnapshot): Observable { + const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle; + const bundleAlias = widgetsBundle.alias; + const isSystem = widgetsBundle.tenantId.id === NULL_UUID; + return this.widgetsService.getBundleWidgetTypes(bundleAlias, + isSystem).pipe( + map((types) => { + types = types.sort((a, b) => { + let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); + if (result === 0) { + result = b.createdTime - a.createdTime; + } + return result; + }); + const widgetTypes = new Array(types.length); + let top = 0; + const lastTop = [0, 0, 0]; + let col = 0; + let column = 0; + types.forEach((type) => { + const widgetTypeInfo = toWidgetInfo(type); + const sizeX = 8; + const sizeY = Math.floor(widgetTypeInfo.sizeY); + const widget: Widget = { + typeId: type.id, + isSystemType: isSystem, + bundleAlias, + typeAlias: widgetTypeInfo.alias, + type: widgetTypeInfo.type, + title: widgetTypeInfo.widgetName, + sizeX, + sizeY, + row: top, + col, + config: JSON.parse(widgetTypeInfo.defaultConfig) + }; + + widget.config.title = widgetTypeInfo.widgetName; + + widgetTypes.push(widget); + top += sizeY; + if (top > lastTop[column] + 10) { + lastTop[column] = top; + column++; + if (column > 2) { + column = 0; + } + top = lastTop[column]; + col = column * 8; + } + }); + return { widgets: widgetTypes }; + } + )); + } +} + @Injectable() export class WidgetEditorDataResolver implements Resolve { @@ -137,6 +202,9 @@ export const routes: Routes = [ data: { auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], title: 'widget.widget-library' + }, + resolve: { + widgetsData: WidgetsTypesDataResolver } }, { @@ -183,6 +251,7 @@ export const routes: Routes = [ providers: [ WidgetsBundlesTableConfigResolver, WidgetsBundleResolver, + WidgetsTypesDataResolver, WidgetEditorDataResolver, WidgetEditorAddDataResolver ] diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html index ebb93167ed..7d23708ed0 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html @@ -28,7 +28,8 @@ class="mat-headline tb-absolute-fill">widgets-bundle.empty >; + widgetsData: WidgetsData; footerFabButtons: FooterFabButtons = { fabTogglerName: 'widget.add-widget-type', @@ -84,8 +84,6 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { onRemoveWidget: this.removeWidgetType.bind(this) }; - widgetsData: Observable; - aliasController: IAliasController = new DummyAliasController(); constructor(protected store: Store, @@ -98,70 +96,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { this.authUser = getCurrentAuthUser(store); this.widgetsBundle = this.route.snapshot.data.widgetsBundle; + this.widgetsData = this.route.snapshot.data.widgetsData; if (this.authUser.authority === Authority.TENANT_ADMIN) { this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; } else { this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; } - this.loadWidgetTypes(); - this.widgetsData = this.widgetTypes$.pipe( - map(widgets => ({ widgets }))); - } - - loadWidgetTypes() { - const bundleAlias = this.widgetsBundle.alias; - const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; - this.widgetTypes$ = this.widgetService.getBundleWidgetTypes(bundleAlias, - isSystem).pipe( - map((types) => { - types = types.sort((a, b) => { - let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); - if (result === 0) { - result = b.createdTime - a.createdTime; - } - return result; - }); - const widgetTypes = new Array(types.length); - let top = 0; - const lastTop = [0, 0, 0]; - let col = 0; - let column = 0; - types.forEach((type) => { - const widgetTypeInfo = toWidgetInfo(type); - const sizeX = 8; - const sizeY = Math.floor(widgetTypeInfo.sizeY); - const widget: Widget = { - typeId: type.id, - isSystemType: isSystem, - bundleAlias, - typeAlias: widgetTypeInfo.alias, - type: widgetTypeInfo.type, - title: widgetTypeInfo.widgetName, - sizeX, - sizeY, - row: top, - col, - config: JSON.parse(widgetTypeInfo.defaultConfig) - }; - - widget.config.title = widgetTypeInfo.widgetName; - - widgetTypes.push(widget); - top += sizeY; - if (top > lastTop[column] + 10) { - lastTop[column] = top; - column++; - if (column > 2) { - column = 0; - } - top = lastTop[column]; - col = column * 8; - } - }); - return widgetTypes; - } - ), - share()); } ngOnInit(): void { diff --git a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html index 5d98611cc6..4e0eb00477 100644 --- a/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html +++ b/ui-ngx/src/app/shared/components/footer-fab-buttons.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -