Commit b60b3144a0854ec0cdf75fbb96492c2deefa42e9

Authored by Igor Kulikov
1 parent 700293d5

State Controllers and Dashboard Layouts.

Showing 37 changed files with 1830 additions and 170 deletions
@@ -58,6 +58,9 @@ export class DummyAliasController implements IAliasController { @@ -58,6 +58,9 @@ export class DummyAliasController implements IAliasController {
58 updateEntityAliases(entityAliases: EntityAliases) { 58 updateEntityAliases(entityAliases: EntityAliases) {
59 } 59 }
60 60
  61 + dashboardStateChanged() {
  62 + }
  63 +
61 } 64 }
62 65
63 export class AliasController implements IAliasController { 66 export class AliasController implements IAliasController {
@@ -111,4 +114,7 @@ export class AliasController implements IAliasController { @@ -111,4 +114,7 @@ export class AliasController implements IAliasController {
111 updateEntityAliases(entityAliases: EntityAliases) { 114 updateEntityAliases(entityAliases: EntityAliases) {
112 } 115 }
113 116
  117 + dashboardStateChanged() {
  118 + }
  119 +
114 } 120 }
@@ -86,6 +86,7 @@ export interface IAliasController { @@ -86,6 +86,7 @@ export interface IAliasController {
86 getEntityAliases(): EntityAliases; 86 getEntityAliases(): EntityAliases;
87 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); 87 updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo);
88 updateEntityAliases(entityAliases: EntityAliases); 88 updateEntityAliases(entityAliases: EntityAliases);
  89 + dashboardStateChanged();
89 [key: string]: any | null; 90 [key: string]: any | null;
90 // TODO: 91 // TODO:
91 } 92 }
@@ -103,12 +104,19 @@ export interface StateParams { @@ -103,12 +104,19 @@ export interface StateParams {
103 } 104 }
104 105
105 export interface IStateController { 106 export interface IStateController {
106 - getStateParams?: () => StateParams;  
107 - openState?: (id: string, params?: StateParams, openRightLayout?: boolean) => void;  
108 - updateState?: (id?: string, params?: StateParams, openRightLayout?: boolean) => void;  
109 - openRightLayout: () => void;  
110 - preserveState?: () => void;  
111 - // TODO: 107 + getStateParams(): StateParams;
  108 + getStateParamsByStateId(stateId: string): StateParams;
  109 + openState(id: string, params?: StateParams, openRightLayout?: boolean): void;
  110 + updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void;
  111 + resetState(): void;
  112 + openRightLayout(): void;
  113 + preserveState(): void;
  114 + cleanupPreservedStates(): void;
  115 + navigatePrevState(index: number): void;
  116 + getStateId(): string;
  117 + getStateIndex(): number;
  118 + getStateIdAtIndex(index: number): string;
  119 + getEntityId(entityParamName: string): EntityId;
112 } 120 }
113 121
114 export interface SubscriptionInfo { 122 export interface SubscriptionInfo {
@@ -22,7 +22,9 @@ import { @@ -22,7 +22,9 @@ import {
22 DashboardLayout, 22 DashboardLayout,
23 DashboardStateLayouts, 23 DashboardStateLayouts,
24 DashboardState, 24 DashboardState,
25 - DashboardConfiguration 25 + DashboardConfiguration,
  26 + DashboardLayoutInfo,
  27 + DashboardLayoutsInfo
26 } from '@shared/models/dashboard.models'; 28 } from '@shared/models/dashboard.models';
27 import { isUndefined, isDefined, isString } from '@core/utils'; 29 import { isUndefined, isDefined, isString } from '@core/utils';
28 import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models'; 30 import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models';
@@ -238,6 +240,44 @@ export class DashboardUtilsService { @@ -238,6 +240,44 @@ export class DashboardUtilsService {
238 }; 240 };
239 } 241 }
240 242
  243 + public getRootStateId(states: {[id: string]: DashboardState }): string {
  244 + for (const stateId of Object.keys(states)) {
  245 + const state = states[stateId];
  246 + if (state.root) {
  247 + return stateId;
  248 + }
  249 + }
  250 + return Object.keys(states)[0];
  251 + }
  252 +
  253 + public getStateLayoutsData(dashboard: Dashboard, targetState: string): DashboardLayoutsInfo {
  254 + const dashboardConfiguration = dashboard.configuration;
  255 + const states = dashboardConfiguration.states;
  256 + const state = states[targetState];
  257 + if (state) {
  258 + const allWidgets = dashboardConfiguration.widgets;
  259 + const result: DashboardLayoutsInfo = {};
  260 + for (const l of Object.keys(state.layouts)) {
  261 + const layout: DashboardLayout = state.layouts[l];
  262 + if (layout) {
  263 + result[l] = {
  264 + widgets: [],
  265 + widgetLayouts: {},
  266 + gridSettings: {}
  267 + } as DashboardLayoutInfo;
  268 + for (const id of Object.keys(layout.widgets)) {
  269 + result[l].widgets.push(allWidgets[id]);
  270 + }
  271 + result[l].widgetLayouts = layout.widgets;
  272 + result[l].gridSettings = layout.gridSettings;
  273 + }
  274 + }
  275 + return result;
  276 + } else {
  277 + return null;
  278 + }
  279 + }
  280 +
241 private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, 281 private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
242 datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, 282 datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
243 targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { 283 targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 import { Inject, Injectable } from '@angular/core'; 17 import { Inject, Injectable } from '@angular/core';
18 import { WINDOW } from '@core/services/window.service'; 18 import { WINDOW } from '@core/services/window.service';
19 import { ExceptionData } from '@app/shared/models/error.models'; 19 import { ExceptionData } from '@app/shared/models/error.models';
20 -import { isUndefined, isDefined } from '@core/utils'; 20 +import { isUndefined, isDefined, deepClone } from '@core/utils';
21 import { WindowMessage } from '@shared/models/window-message.model'; 21 import { WindowMessage } from '@shared/models/window-message.model';
22 import { TranslateService } from '@ngx-translate/core'; 22 import { TranslateService } from '@ngx-translate/core';
23 import { customTranslationsPrefix } from '@app/shared/models/constants'; 23 import { customTranslationsPrefix } from '@app/shared/models/constants';
@@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; @@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models';
28 import { materialColors } from '@app/shared/models/material.models'; 28 import { materialColors } from '@app/shared/models/material.models';
29 import { WidgetInfo } from '@home/models/widget-component.models'; 29 import { WidgetInfo } from '@home/models/widget-component.models';
30 30
  31 +const varsRegex = /\$\{([^}]*)\}/g;
  32 +
31 @Injectable({ 33 @Injectable({
32 providedIn: 'root' 34 providedIn: 'root'
33 }) 35 })
@@ -144,6 +146,20 @@ export class UtilsService { @@ -144,6 +146,20 @@ export class UtilsService {
144 return result; 146 return result;
145 } 147 }
146 148
  149 + public insertVariable(pattern: string, name: string, value: any): string {
  150 + let result = deepClone(pattern);
  151 + let match = varsRegex.exec(pattern);
  152 + while (match !== null) {
  153 + const variable = match[0];
  154 + const variableName = match[1];
  155 + if (variableName === name) {
  156 + result = result.split(variable).join(value);
  157 + }
  158 + match = varsRegex.exec(pattern);
  159 + }
  160 + return result;
  161 + }
  162 +
147 public guid(): string { 163 public guid(): string {
148 function s4(): string { 164 function s4(): string {
149 return Math.floor((1 + Math.random()) * 0x10000) 165 return Math.floor((1 + Math.random()) * 0x10000)
@@ -15,9 +15,12 @@ @@ -15,9 +15,12 @@
15 /// 15 ///
16 16
17 import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core'; 17 import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core';
  18 +import { customTranslationsPrefix } from '@app/shared/models/constants';
18 19
19 export class TbMissingTranslationHandler implements MissingTranslationHandler { 20 export class TbMissingTranslationHandler implements MissingTranslationHandler {
20 handle(params: MissingTranslationHandlerParams) { 21 handle(params: MissingTranslationHandlerParams) {
21 - console.warn('Translation for ' + params.key + ' doesn\'t exist'); 22 + if (params.key && !params.key.startsWith(customTranslationsPrefix)) {
  23 + console.warn('Translation for ' + params.key + ' doesn\'t exist');
  24 + }
22 } 25 }
23 } 26 }
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 (contextmenu)="openDashboardContextMenu($event)"> 26 (contextmenu)="openDashboardContextMenu($event)">
27 <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> 27 <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;">
28 <gridster #gridster id="gridster-child" [options]="gridsterOpts"> 28 <gridster #gridster id="gridster-child" [options]="gridsterOpts">
29 - <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of widgets$ | async"> 29 + <gridster-item [item]="widget" class="tb-noselect" *ngFor="let widget of dashboardWidgets">
30 <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)" 30 <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)"
31 fxLayout="column" 31 fxLayout="column"
32 class="tb-widget" 32 class="tb-widget"
@@ -37,16 +37,18 @@ import { @@ -37,16 +37,18 @@ import {
37 DashboardCallbacks, 37 DashboardCallbacks,
38 DashboardWidget, 38 DashboardWidget,
39 IDashboardComponent, 39 IDashboardComponent,
40 - WidgetsData 40 + WidgetsData,
  41 + DashboardWidgets
41 } from '../../models/dashboard-component.models'; 42 } from '../../models/dashboard-component.models';
42 import { merge, Observable, ReplaySubject, Subject } from 'rxjs'; 43 import { merge, Observable, ReplaySubject, Subject } from 'rxjs';
43 import { map, share, tap } from 'rxjs/operators'; 44 import { map, share, tap } from 'rxjs/operators';
44 -import { WidgetLayout } from '@shared/models/dashboard.models'; 45 +import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
45 import { DialogService } from '@core/services/dialog.service'; 46 import { DialogService } from '@core/services/dialog.service';
46 import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; 47 import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
47 import { BreakpointObserver } from '@angular/cdk/layout'; 48 import { BreakpointObserver } from '@angular/cdk/layout';
48 import { MediaBreakpoints } from '@shared/models/constants'; 49 import { MediaBreakpoints } from '@shared/models/constants';
49 import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; 50 import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
  51 +import { Widget } from '@app/shared/models/widget.models';
50 52
51 @Component({ 53 @Component({
52 selector: 'tb-dashboard', 54 selector: 'tb-dashboard',
@@ -58,7 +60,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -58,7 +60,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
58 authUser: AuthUser; 60 authUser: AuthUser;
59 61
60 @Input() 62 @Input()
61 - widgetsData: Observable<WidgetsData>; 63 + widgets: Array<Widget>;
  64 +
  65 + @Input()
  66 + widgetLayouts: WidgetLayouts;
62 67
63 @Input() 68 @Input()
64 callbacks: DashboardCallbacks; 69 callbacks: DashboardCallbacks;
@@ -125,8 +130,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -125,8 +130,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
125 130
126 gridsterOpts: GridsterConfig; 131 gridsterOpts: GridsterConfig;
127 132
128 - dashboardLoading = true;  
129 -  
130 highlightedMode = false; 133 highlightedMode = false;
131 highlightedWidget: DashboardWidget = null; 134 highlightedWidget: DashboardWidget = null;
132 selectedWidget: DashboardWidget = null; 135 selectedWidget: DashboardWidget = null;
@@ -138,9 +141,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -138,9 +141,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
138 141
139 @ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>; 142 @ViewChildren(GridsterItemComponent) gridsterItems: QueryList<GridsterItemComponent>;
140 143
141 - widgets$: Observable<Array<DashboardWidget>>; 144 + dashboardLoading = true;
142 145
143 - widgets: Array<DashboardWidget>; 146 + dashboardWidgets = new DashboardWidgets(this);
144 147
145 constructor(protected store: Store<AppState>, 148 constructor(protected store: Store<AppState>,
146 private timeService: TimeService, 149 private timeService: TimeService,
@@ -172,25 +175,26 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -172,25 +175,26 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
172 defaultItemRows: 6, 175 defaultItemRows: 6,
173 resizable: {enabled: this.isEdit}, 176 resizable: {enabled: this.isEdit},
174 draggable: {enabled: this.isEdit}, 177 draggable: {enabled: this.isEdit},
175 - itemChangeCallback: item => this.sortWidgets(this.widgets) 178 + itemChangeCallback: item => this.dashboardWidgets.sortWidgets()
176 }; 179 };
177 180
178 this.updateMobileOpts(); 181 this.updateMobileOpts();
179 182
180 - this.loadDashboard();  
181 -  
182 this.breakpointObserver 183 this.breakpointObserver
183 .observe(MediaBreakpoints['gt-sm']).subscribe( 184 .observe(MediaBreakpoints['gt-sm']).subscribe(
184 () => { 185 () => {
185 this.updateMobileOpts(); 186 this.updateMobileOpts();
186 } 187 }
187 ); 188 );
  189 +
  190 + this.updateWidgets();
188 } 191 }
189 192
190 ngOnChanges(changes: SimpleChanges): void { 193 ngOnChanges(changes: SimpleChanges): void {
191 let updateMobileOpts = false; 194 let updateMobileOpts = false;
192 let updateLayoutOpts = false; 195 let updateLayoutOpts = false;
193 let updateEditingOpts = false; 196 let updateEditingOpts = false;
  197 + let updateWidgets = false;
194 for (const propName of Object.keys(changes)) { 198 for (const propName of Object.keys(changes)) {
195 const change = changes[propName]; 199 const change = changes[propName];
196 if (!change.firstChange && change.currentValue !== change.previousValue) { 200 if (!change.firstChange && change.currentValue !== change.previousValue) {
@@ -200,9 +204,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -200,9 +204,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
200 updateLayoutOpts = true; 204 updateLayoutOpts = true;
201 } else if (propName === 'isEdit') { 205 } else if (propName === 'isEdit') {
202 updateEditingOpts = true; 206 updateEditingOpts = true;
  207 + } else if (['widgets', 'widgetLayouts'].includes(propName)) {
  208 + updateWidgets = true;
203 } 209 }
204 } 210 }
205 } 211 }
  212 + if (updateWidgets) {
  213 + this.updateWidgets();
  214 + }
206 if (updateMobileOpts) { 215 if (updateMobileOpts) {
207 this.updateMobileOpts(); 216 this.updateMobileOpts();
208 } 217 }
@@ -217,50 +226,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @@ -217,50 +226,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
217 } 226 }
218 } 227 }
219 228
220 - loadDashboard() {  
221 - this.widgets$ = this.widgetsData.pipe(  
222 - map(widgetsData => {  
223 - const dashboardWidgets = new Array<DashboardWidget>();  
224 - let maxRows = this.gridsterOpts.maxRows;  
225 - widgetsData.widgets.forEach(  
226 - (widget) => {  
227 - let widgetLayout: WidgetLayout;  
228 - if (widgetsData.widgetLayouts && widget.id) {  
229 - widgetLayout = widgetsData.widgetLayouts[widget.id];  
230 - }  
231 - const dashboardWidget = new DashboardWidget(this, widget, widgetLayout);  
232 - const bottom = dashboardWidget.y + dashboardWidget.rows;  
233 - maxRows = Math.max(maxRows, bottom);  
234 - dashboardWidgets.push(dashboardWidget);  
235 - }  
236 - );  
237 - this.sortWidgets(dashboardWidgets);  
238 - this.gridsterOpts.maxRows = maxRows;  
239 - return dashboardWidgets;  
240 - }),  
241 - tap((widgets) => {  
242 - this.widgets = widgets;  
243 - this.dashboardLoading = false;  
244 - })  
245 - );  
246 - }  
247 -  
248 - reload() {  
249 - this.loadDashboard();  
250 - }  
251 -  
252 - sortWidgets(widgets?: Array<DashboardWidget>) {  
253 - if (widgets) {  
254 - widgets.sort((widget1, widget2) => {  
255 - const row1 = widget1.widgetOrder;  
256 - const row2 = widget2.widgetOrder;  
257 - let res = row1 - row2;  
258 - if (res === 0) {  
259 - res = widget1.x - widget2.x;  
260 - }  
261 - return res;  
262 - });  
263 - } 229 + private updateWidgets() {
  230 + this.dashboardWidgets.setWidgets(this.widgets, this.widgetLayouts);
  231 + this.dashboardLoading = false;
264 } 232 }
265 233
266 ngAfterViewInit(): void { 234 ngAfterViewInit(): void {
@@ -119,7 +119,7 @@ export class WidgetComponentService { @@ -119,7 +119,7 @@ export class WidgetComponentService {
119 } else { 119 } else {
120 fetchQueue = new Array<Subject<WidgetInfo>>(); 120 fetchQueue = new Array<Subject<WidgetInfo>>();
121 this.widgetsInfoFetchQueue.set(key, fetchQueue); 121 this.widgetsInfoFetchQueue.set(key, fetchQueue);
122 - this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem).subscribe( 122 + this.widgetService.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, true, false).subscribe(
123 (widgetType) => { 123 (widgetType) => {
124 this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); 124 this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject);
125 }, 125 },
@@ -43,6 +43,7 @@ export interface DashboardCallbacks { @@ -43,6 +43,7 @@ export interface DashboardCallbacks {
43 export interface IDashboardComponent { 43 export interface IDashboardComponent {
44 gridsterOpts: GridsterConfig; 44 gridsterOpts: GridsterConfig;
45 gridster: GridsterComponent; 45 gridster: GridsterComponent;
  46 + dashboardWidgets: DashboardWidgets;
46 mobileAutofillHeight: boolean; 47 mobileAutofillHeight: boolean;
47 isMobileSize: boolean; 48 isMobileSize: boolean;
48 autofillHeight: boolean; 49 autofillHeight: boolean;
@@ -54,6 +55,74 @@ export interface IDashboardComponent { @@ -54,6 +55,74 @@ export interface IDashboardComponent {
54 onResetTimewindow(): void; 55 onResetTimewindow(): void;
55 } 56 }
56 57
  58 +export class DashboardWidgets implements Iterable<DashboardWidget> {
  59 +
  60 + dashboardWidgets: Array<DashboardWidget> = [];
  61 +
  62 + [Symbol.iterator](): Iterator<DashboardWidget> {
  63 + return this.dashboardWidgets[Symbol.iterator]();
  64 + }
  65 +
  66 + constructor(private dashboard: IDashboardComponent) {
  67 + }
  68 +
  69 + setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
  70 + let maxRows = this.dashboard.gridsterOpts.maxRows;
  71 + this.dashboardWidgets.length = 0;
  72 + widgets.forEach((widget) => {
  73 + let widgetLayout: WidgetLayout;
  74 + if (widgetLayouts && widget.id) {
  75 + widgetLayout = widgetLayouts[widget.id];
  76 + }
  77 + const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
  78 + const bottom = dashboardWidget.y + dashboardWidget.rows;
  79 + maxRows = Math.max(maxRows, bottom);
  80 + this.dashboardWidgets.push(dashboardWidget);
  81 + });
  82 + this.sortWidgets();
  83 + this.dashboard.gridsterOpts.maxRows = maxRows;
  84 + }
  85 +
  86 + addWidget(widget: Widget, widgetLayout: WidgetLayout) {
  87 + const dashboardWidget = new DashboardWidget(this.dashboard, widget, widgetLayout);
  88 + let maxRows = this.dashboard.gridsterOpts.maxRows;
  89 + const bottom = dashboardWidget.y + dashboardWidget.rows;
  90 + maxRows = Math.max(maxRows, bottom);
  91 + this.dashboardWidgets.push(dashboardWidget);
  92 + this.sortWidgets();
  93 + this.dashboard.gridsterOpts.maxRows = maxRows;
  94 + }
  95 +
  96 + removeWidget(widget: Widget): boolean {
  97 + const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widget === widget);
  98 + if (index > -1) {
  99 + this.dashboardWidgets.splice(index, 1);
  100 + let maxRows = this.dashboard.gridsterOpts.maxRows;
  101 + this.dashboardWidgets.forEach((dashboardWidget) => {
  102 + const bottom = dashboardWidget.y + dashboardWidget.rows;
  103 + maxRows = Math.max(maxRows, bottom);
  104 + });
  105 + this.sortWidgets();
  106 + this.dashboard.gridsterOpts.maxRows = maxRows;
  107 + return true;
  108 + }
  109 + return false;
  110 + }
  111 +
  112 + sortWidgets() {
  113 + this.dashboardWidgets.sort((widget1, widget2) => {
  114 + const row1 = widget1.widgetOrder;
  115 + const row2 = widget2.widgetOrder;
  116 + let res = row1 - row2;
  117 + if (res === 0) {
  118 + res = widget1.x - widget2.x;
  119 + }
  120 + return res;
  121 + });
  122 + }
  123 +
  124 +}
  125 +
57 export class DashboardWidget implements GridsterItem { 126 export class DashboardWidget implements GridsterItem {
58 127
59 isFullscreen = false; 128 isFullscreen = false;
@@ -123,7 +123,7 @@ export const MissingWidgetType: WidgetInfo = { @@ -123,7 +123,7 @@ export const MissingWidgetType: WidgetInfo = {
123 sizeY: 6, 123 sizeY: 6,
124 resources: [], 124 resources: [],
125 templateHtml: '<div class="tb-widget-error-container">' + 125 templateHtml: '<div class="tb-widget-error-container">' +
126 - '<div translate class="tb-widget-error-msg">widget.widget-type-not-found</div>' + 126 + '<div class="tb-widget-error-msg" innerHTML="{{\'widget.widget-type-not-found\' | translate }}"></div>' +
127 '</div>', 127 '</div>',
128 templateCss: '', 128 templateCss: '',
129 controllerScript: 'self.onInit = function() {}', 129 controllerScript: 'self.onInit = function() {}',
@@ -24,6 +24,9 @@ import {CustomersTableConfigResolver} from './customers-table-config.resolver'; @@ -24,6 +24,9 @@ import {CustomersTableConfigResolver} from './customers-table-config.resolver';
24 import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver'; 24 import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver';
25 import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver'; 25 import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver';
26 import {DashboardsTableConfigResolver} from '@modules/home/pages/dashboard/dashboards-table-config.resolver'; 26 import {DashboardsTableConfigResolver} from '@modules/home/pages/dashboard/dashboards-table-config.resolver';
  27 +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
  28 +import { BreadCrumbConfig } from '@shared/components/breadcrumb';
  29 +import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module';
27 30
28 const routes: Routes = [ 31 const routes: Routes = [
29 { 32 {
@@ -95,19 +98,42 @@ const routes: Routes = [ @@ -95,19 +98,42 @@ const routes: Routes = [
95 }, 98 },
96 { 99 {
97 path: ':customerId/dashboards', 100 path: ':customerId/dashboards',
98 - component: EntitiesTableComponent,  
99 data: { 101 data: {
100 - auth: [Authority.TENANT_ADMIN],  
101 - title: 'customer.assets',  
102 - dashboardsType: 'customer',  
103 breadcrumb: { 102 breadcrumb: {
104 label: 'customer.dashboards', 103 label: 'customer.dashboards',
105 icon: 'dashboard' 104 icon: 'dashboard'
106 } 105 }
107 }, 106 },
108 - resolve: {  
109 - entitiesTableConfig: DashboardsTableConfigResolver  
110 - } 107 + children: [
  108 + {
  109 + path: '',
  110 + component: EntitiesTableComponent,
  111 + data: {
  112 + auth: [Authority.TENANT_ADMIN],
  113 + title: 'customer.dashboards',
  114 + dashboardsType: 'customer'
  115 + },
  116 + resolve: {
  117 + entitiesTableConfig: DashboardsTableConfigResolver
  118 + }
  119 + },
  120 + {
  121 + path: ':dashboardId',
  122 + component: DashboardPageComponent,
  123 + data: {
  124 + breadcrumb: {
  125 + labelFunction: dashboardBreadcumbLabelFunction,
  126 + icon: 'dashboard'
  127 + } as BreadCrumbConfig,
  128 + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
  129 + title: 'customer.dashboard',
  130 + widgetEditMode: false
  131 + },
  132 + resolve: {
  133 + dashboard: DashboardResolver
  134 + }
  135 + }
  136 + ]
111 } 137 }
112 ] 138 ]
113 } 139 }
@@ -85,7 +85,31 @@ @@ -85,7 +85,31 @@
85 fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px"> 85 fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
86 <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> 86 <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
87 </tb-user-menu> 87 </tb-user-menu>
88 - <!-- TODO --> 88 + <div [fxShow]="isEdit"
  89 + fxFlex.xs fxFlex.sm fxLayout="row"
  90 + fxLayoutAlign.gt-sm="start center"
  91 + fxLayoutAlign="space-between center" fxLayoutGap="12px">
  92 + <button mat-button mat-icon-button
  93 + matTooltip="{{'dashboard.manage-states' | translate}}"
  94 + matTooltipPosition="below"
  95 + (click)="manageDashboardStates($event)">
  96 + <mat-icon>layers</mat-icon>
  97 + </button>
  98 + <button mat-button mat-icon-button
  99 + matTooltip="{{'layout.manage' | translate}}"
  100 + matTooltipPosition="below"
  101 + (click)="manageDashboardLayouts($event)">
  102 + <mat-icon>view_compact</mat-icon>
  103 + </button>
  104 + </div>
  105 + <tb-states-component fxFlex.xs fxFlex.sm
  106 + [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
  107 + [dashboardCtrl]="this"
  108 + [dashboardId]="dashboard.id ? dashboard.id.id : ''"
  109 + [isMobile]="isMobile"
  110 + [state]="dashboardCtx.state"
  111 + [states]="dashboardConfiguration.states">
  112 + </tb-states-component>
89 </div> 113 </div>
90 </div> 114 </div>
91 </tb-dashboard-toolbar> 115 </tb-dashboard-toolbar>
@@ -110,7 +134,13 @@ @@ -110,7 +134,13 @@
110 id="tb-main-layout" 134 id="tb-main-layout"
111 [ngStyle]="{width: mainLayoutWidth(), 135 [ngStyle]="{width: mainLayoutWidth(),
112 height: mainLayoutHeight()}"> 136 height: mainLayoutHeight()}">
113 - TODO: MAIN LAYOUT tb-dashboard-layout 137 + <tb-dashboard-layout
  138 + [layoutCtx]="layouts.main.layoutCtx"
  139 + [dashboardCtx]="dashboardCtx"
  140 + [isEdit]="isEdit"
  141 + [isMobile]="forceDashboardMobileMode"
  142 + [widgetEditMode]="widgetEditMode">
  143 + </tb-dashboard-layout>
114 </div> 144 </div>
115 <mat-sidenav-container *ngIf="layouts.right.show" 145 <mat-sidenav-container *ngIf="layouts.right.show"
116 id="tb-right-layout"> 146 id="tb-right-layout">
@@ -123,14 +153,23 @@ @@ -123,14 +153,23 @@
123 position="end" 153 position="end"
124 [mode]="isMobile ? 'over' : 'side'" 154 [mode]="isMobile ? 'over' : 'side'"
125 [(opened)]="rightLayoutOpened"> 155 [(opened)]="rightLayoutOpened">
126 - TODO: RIGHT LAYOUT tb-dashboard-layout 156 + <tb-dashboard-layout style="height: 100%;"
  157 + [layoutCtx]="layouts.right.layoutCtx"
  158 + [dashboardCtx]="dashboardCtx"
  159 + [isEdit]="isEdit"
  160 + [isMobile]="forceDashboardMobileMode"
  161 + [widgetEditMode]="widgetEditMode">
  162 + </tb-dashboard-layout>
127 </mat-sidenav> 163 </mat-sidenav>
128 </mat-sidenav-container> 164 </mat-sidenav-container>
129 </div> 165 </div>
130 <!--tb-details-sidenav TODO --> 166 <!--tb-details-sidenav TODO -->
131 <!--tb-details-sidenav TODO --> 167 <!--tb-details-sidenav TODO -->
132 <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> 168 <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
133 - <!--md-fab-speed-dial TODO --> 169 + <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode"
  170 + relative
  171 + [footerFabButtons]="addWidgetFabButtons">
  172 + </tb-footer-fab-buttons>
134 <button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen" 173 <button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen"
135 mat-fab color="accent" class="tb-btn-footer" 174 mat-fab color="accent" class="tb-btn-footer"
136 [ngClass]="{'tb-hide': !isEdit || isAddingWidget}" 175 [ngClass]="{'tb-hide': !isEdit || isAddingWidget}"
@@ -21,15 +21,21 @@ import { AppState } from '@core/core.state'; @@ -21,15 +21,21 @@ import { AppState } from '@core/core.state';
21 import { ActivatedRoute, Router } from '@angular/router'; 21 import { ActivatedRoute, Router } from '@angular/router';
22 import { UtilsService } from '@core/services/utils.service'; 22 import { UtilsService } from '@core/services/utils.service';
23 import { AuthService } from '@core/auth/auth.service'; 23 import { AuthService } from '@core/auth/auth.service';
24 -import { Dashboard, DashboardConfiguration, WidgetLayout } from '@app/shared/models/dashboard.models'; 24 +import {
  25 + Dashboard,
  26 + DashboardConfiguration,
  27 + WidgetLayout,
  28 + DashboardLayoutInfo,
  29 + DashboardLayoutsInfo
  30 +} from '@app/shared/models/dashboard.models';
25 import { WINDOW } from '@core/services/window.service'; 31 import { WINDOW } from '@core/services/window.service';
26 import { WindowMessage } from '@shared/models/window-message.model'; 32 import { WindowMessage } from '@shared/models/window-message.model';
27 import { deepClone, isDefined } from '@app/core/utils'; 33 import { deepClone, isDefined } from '@app/core/utils';
28 import { 34 import {
29 - DashboardContext, 35 + DashboardContext, DashboardPageLayout,
30 DashboardPageLayoutContext, 36 DashboardPageLayoutContext,
31 DashboardPageLayouts, 37 DashboardPageLayouts,
32 - DashboardPageScope 38 + DashboardPageScope, IDashboardController
33 } from './dashboard-page.models'; 39 } from './dashboard-page.models';
34 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 40 import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
35 import { MediaBreakpoints } from '@shared/models/constants'; 41 import { MediaBreakpoints } from '@shared/models/constants';
@@ -42,6 +48,9 @@ import { DialogService } from '@core/services/dialog.service'; @@ -42,6 +48,9 @@ import { DialogService } from '@core/services/dialog.service';
42 import { EntityService } from '@core/http/entity.service'; 48 import { EntityService } from '@core/http/entity.service';
43 import { AliasController } from '@core/api/alias-controller'; 49 import { AliasController } from '@core/api/alias-controller';
44 import { Subscription } from 'rxjs'; 50 import { Subscription } from 'rxjs';
  51 +import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
  52 +import { IStateController } from '@core/api/widget-api.models';
  53 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
45 54
46 @Component({ 55 @Component({
47 selector: 'tb-dashboard-page', 56 selector: 'tb-dashboard-page',
@@ -49,7 +58,7 @@ import { Subscription } from 'rxjs'; @@ -49,7 +58,7 @@ import { Subscription } from 'rxjs';
49 styleUrls: ['./dashboard-page.component.scss'], 58 styleUrls: ['./dashboard-page.component.scss'],
50 encapsulation: ViewEncapsulation.None 59 encapsulation: ViewEncapsulation.None
51 }) 60 })
52 -export class DashboardPageComponent extends PageComponent implements OnDestroy { 61 +export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy {
53 62
54 authUser: AuthUser = getCurrentAuthUser(this.store); 63 authUser: AuthUser = getCurrentAuthUser(this.store);
55 64
@@ -94,7 +103,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { @@ -94,7 +103,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
94 widgets: [], 103 widgets: [],
95 widgetLayouts: {}, 104 widgetLayouts: {},
96 gridSettings: {}, 105 gridSettings: {},
97 - ignoreLoading: false 106 + ignoreLoading: false,
  107 + ctrl: null
98 } 108 }
99 }, 109 },
100 right: { 110 right: {
@@ -104,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { @@ -104,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
104 widgets: [], 114 widgets: [],
105 widgetLayouts: {}, 115 widgetLayouts: {},
106 gridSettings: {}, 116 gridSettings: {},
107 - ignoreLoading: false 117 + ignoreLoading: false,
  118 + ctrl: null
108 } 119 }
109 } 120 }
110 }; 121 };
@@ -113,12 +124,31 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { @@ -113,12 +124,31 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
113 dashboard: null, 124 dashboard: null,
114 dashboardTimewindow: null, 125 dashboardTimewindow: null,
115 state: null, 126 state: null,
116 - stateController: {  
117 - openRightLayout: this.openRightLayout.bind(this)  
118 - }, 127 + stateController: null,
119 aliasController: null 128 aliasController: null
120 }; 129 };
121 130
  131 + addWidgetFabButtons: FooterFabButtons = {
  132 + fabTogglerName: 'dashboard.add-widget',
  133 + fabTogglerIcon: 'add',
  134 + buttons: [
  135 + {
  136 + name: 'dashboard.create-new-widget',
  137 + icon: 'insert_drive_file',
  138 + onAction: ($event) => {
  139 + this.addWidget($event);
  140 + }
  141 + },
  142 + {
  143 + name: 'dashboard.import-widget',
  144 + icon: 'file_upload',
  145 + onAction: ($event) => {
  146 + this.importWidget($event);
  147 + }
  148 + }
  149 + ]
  150 + };
  151 +
122 private rxSubscriptions = new Array<Subscription>(); 152 private rxSubscriptions = new Array<Subscription>();
123 153
124 get toolbarOpened(): boolean { 154 get toolbarOpened(): boolean {
@@ -140,6 +170,7 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { @@ -140,6 +170,7 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
140 private route: ActivatedRoute, 170 private route: ActivatedRoute,
141 private router: Router, 171 private router: Router,
142 private utils: UtilsService, 172 private utils: UtilsService,
  173 + private dashboardUtils: DashboardUtilsService,
143 private authService: AuthService, 174 private authService: AuthService,
144 private entityService: EntityService, 175 private entityService: EntityService,
145 private dialogService: DialogService) { 176 private dialogService: DialogService) {
@@ -379,6 +410,38 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { @@ -379,6 +410,38 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
379 this.dialogService.todo(); 410 this.dialogService.todo();
380 } 411 }
381 412
  413 + public manageDashboardStates($event: Event) {
  414 + if ($event) {
  415 + $event.stopPropagation();
  416 + }
  417 + // TODO:
  418 + this.dialogService.todo();
  419 + }
  420 +
  421 + public manageDashboardLayouts($event: Event) {
  422 + if ($event) {
  423 + $event.stopPropagation();
  424 + }
  425 + // TODO:
  426 + this.dialogService.todo();
  427 + }
  428 +
  429 + private addWidget($event: Event) {
  430 + if ($event) {
  431 + $event.stopPropagation();
  432 + }
  433 + // TODO:
  434 + this.dialogService.todo();
  435 + }
  436 +
  437 + private importWidget($event: Event) {
  438 + if ($event) {
  439 + $event.stopPropagation();
  440 + }
  441 + // TODO:
  442 + this.dialogService.todo();
  443 + }
  444 +
382 public currentDashboardIdChanged(dashboardId: string) { 445 public currentDashboardIdChanged(dashboardId: string) {
383 if (!this.widgetEditMode) { 446 if (!this.widgetEditMode) {
384 if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) { 447 if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) {
@@ -397,6 +460,57 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { @@ -397,6 +460,57 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy {
397 this.setEditMode(!this.isEdit, true); 460 this.setEditMode(!this.isEdit, true);
398 } 461 }
399 462
  463 + public openDashboardState(state: string, openRightLayout: boolean) {
  464 + const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
  465 + if (layoutsData) {
  466 + this.dashboardCtx.state = state;
  467 + this.dashboardCtx.aliasController.dashboardStateChanged();
  468 + let layoutVisibilityChanged = false;
  469 + for (const l of Object.keys(this.layouts)) {
  470 + const layout: DashboardPageLayout = this.layouts[l];
  471 + let showLayout;
  472 + if (layoutsData[l]) {
  473 + showLayout = true;
  474 + } else {
  475 + showLayout = false;
  476 + }
  477 + if (layout.show !== showLayout) {
  478 + layout.show = showLayout;
  479 + layoutVisibilityChanged = !this.isMobile;
  480 + }
  481 + }
  482 + this.isRightLayoutOpened = openRightLayout ? true : false;
  483 + this.updateLayouts(layoutsData, layoutVisibilityChanged);
  484 + }
  485 + }
  486 +
  487 + private updateLayouts(layoutsData: DashboardLayoutsInfo, layoutVisibilityChanged: boolean) {
  488 + for (const l of Object.keys(this.layouts)) {
  489 + const layout: DashboardPageLayout = this.layouts[l];
  490 + if (layoutsData[l]) {
  491 + const layoutInfo: DashboardLayoutInfo = layoutsData[l];
  492 + if (layout.layoutCtx.id === 'main') {
  493 + layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged);
  494 + }
  495 + this.updateLayout(layout, layoutInfo);
  496 + } else {
  497 + this.updateLayout(layout, {widgets: [], widgetLayouts: {}, gridSettings: null});
  498 + }
  499 + }
  500 + }
  501 +
  502 + private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
  503 + if (layoutInfo.gridSettings) {
  504 + layout.layoutCtx.gridSettings = layoutInfo.gridSettings;
  505 + }
  506 + layout.layoutCtx.widgets = layoutInfo.widgets;
  507 + layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
  508 + if (layout.show && layout.layoutCtx.ctrl) {
  509 + layout.layoutCtx.ctrl.reload();
  510 + }
  511 + layout.layoutCtx.ignoreLoading = true;
  512 + }
  513 +
400 private setEditMode(isEdit: boolean, revert: boolean) { 514 private setEditMode(isEdit: boolean, revert: boolean) {
401 this.isEdit = isEdit; 515 this.isEdit = isEdit;
402 if (this.isEdit) { 516 if (this.isEdit) {
@@ -14,10 +14,11 @@ @@ -14,10 +14,11 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard } from '@app/shared/models/dashboard.models'; 17 +import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard, WidgetLayouts } from '@app/shared/models/dashboard.models';
18 import { Widget } from '@app/shared/models/widget.models'; 18 import { Widget } from '@app/shared/models/widget.models';
19 import { Timewindow } from '@shared/models/time/time.models'; 19 import { Timewindow } from '@shared/models/time/time.models';
20 import { IAliasController, IStateController } from '@core/api/widget-api.models'; 20 import { IAliasController, IStateController } from '@core/api/widget-api.models';
  21 +import { ILayoutController } from './layout/layout.models';
21 22
22 export declare type DashboardPageScope = 'tenant' | 'customer'; 23 export declare type DashboardPageScope = 'tenant' | 'customer';
23 24
@@ -29,11 +30,18 @@ export interface DashboardContext { @@ -29,11 +30,18 @@ export interface DashboardContext {
29 stateController: IStateController; 30 stateController: IStateController;
30 } 31 }
31 32
  33 +export interface IDashboardController {
  34 + dashboardCtx: DashboardContext;
  35 + openRightLayout();
  36 + openDashboardState(stateId: string, openRightLayout: boolean);
  37 +}
  38 +
32 export interface DashboardPageLayoutContext { 39 export interface DashboardPageLayoutContext {
33 id: DashboardLayoutId; 40 id: DashboardLayoutId;
34 widgets: Array<Widget>; 41 widgets: Array<Widget>;
35 - widgetLayouts: {[id: string]: WidgetLayout}; 42 + widgetLayouts: WidgetLayouts;
36 gridSettings: GridSettings; 43 gridSettings: GridSettings;
  44 + ctrl: ILayoutController;
37 ignoreLoading: boolean; 45 ignoreLoading: boolean;
38 } 46 }
39 47
@@ -42,7 +50,5 @@ export interface DashboardPageLayout { @@ -42,7 +50,5 @@ export interface DashboardPageLayout {
42 layoutCtx: DashboardPageLayoutContext; 50 layoutCtx: DashboardPageLayoutContext;
43 } 51 }
44 52
45 -export interface DashboardPageLayouts {  
46 - main: DashboardPageLayout;  
47 - right: DashboardPageLayout;  
48 -} 53 +export declare type DashboardPageLayouts = {[key in DashboardLayoutId]: DashboardPageLayout};
  54 +
@@ -26,6 +26,8 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m @@ -26,6 +26,8 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
26 import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; 26 import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component';
27 import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; 27 import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
28 import { DashboardToolbarComponent } from './dashboard-toolbar.component'; 28 import { DashboardToolbarComponent } from './dashboard-toolbar.component';
  29 +import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module';
  30 +import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
29 31
30 @NgModule({ 32 @NgModule({
31 entryComponents: [ 33 entryComponents: [
@@ -40,13 +42,15 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component'; @@ -40,13 +42,15 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component';
40 ManageDashboardCustomersDialogComponent, 42 ManageDashboardCustomersDialogComponent,
41 MakeDashboardPublicDialogComponent, 43 MakeDashboardPublicDialogComponent,
42 DashboardToolbarComponent, 44 DashboardToolbarComponent,
43 - DashboardPageComponent 45 + DashboardPageComponent,
  46 + DashboardLayoutComponent
44 ], 47 ],
45 imports: [ 48 imports: [
46 CommonModule, 49 CommonModule,
47 SharedModule, 50 SharedModule,
48 HomeComponentsModule, 51 HomeComponentsModule,
49 HomeDialogsModule, 52 HomeDialogsModule,
  53 + StatesControllerModule,
50 DashboardRoutingModule 54 DashboardRoutingModule
51 ] 55 ]
52 }) 56 })
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="mat-content" style="position: relative; width: 100%; height: 100%;"
  19 + [ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
  20 + 'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
  21 + 'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none',
  22 + 'background-repeat': 'no-repeat',
  23 + 'background-attachment': 'scroll',
  24 + 'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%',
  25 + 'background-position': '0% 0%'}">
  26 + <section *ngIf="layoutCtx.widgets.length === 0" fxLayoutAlign="center center"
  27 + [ngStyle]="{'color': layoutCtx.gridSettings.color}"
  28 + style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;"
  29 + class="mat-headline tb-absolute-fill">
  30 + <span *ngIf="!isEdit">
  31 + {{'dashboard.no-widgets' | translate}}
  32 + </span>
  33 + <button mat-button *ngIf="isEdit && !widgetEditMode" class="tb-add-new-widget"
  34 + (click)="addWidget($event)">
  35 + <mat-icon class="tb-mat-96">add</mat-icon>
  36 + {{ 'dashboard.add-widget' | translate }}
  37 + </button>
  38 + </section>
  39 + <tb-dashboard #dashboard [dashboardStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
  40 + 'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
  41 + 'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none',
  42 + 'background-repeat': 'no-repeat',
  43 + 'background-attachment': 'scroll',
  44 + 'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%',
  45 + 'background-position': '0% 0%'}"
  46 + [widgets]="layoutCtx.widgets"
  47 + [widgetLayouts]="layoutCtx.widgetLayouts"
  48 + [columns]="layoutCtx.gridSettings.columns"
  49 + [horizontalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[0] : 10"
  50 + [verticalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[1]: 10"
  51 + [aliasController]="dashboardCtx.aliasController"
  52 + [stateController]="dashboardCtx.stateController"
  53 + [dashboardTimewindow]="dashboardCtx.dashboardTimewindow"
  54 + [isEdit]="isEdit"
  55 + [autofillHeight]="layoutCtx.gridSettings.autoFillHeight && !isEdit"
  56 + [mobileAutofillHeight]="layoutCtx.gridSettings.mobileAutoFillHeight && !isEdit"
  57 + [mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
  58 + [isMobile]="isMobile"
  59 + [isMobileDisabled]="widgetEditMode"
  60 + [isEditActionEnabled]="isEdit"
  61 + [isExportActionEnabled]="isEdit && !widgetEditMode"
  62 + [isRemoveActionEnabled]="isEdit && !widgetEditMode"
  63 + [ignoreLoading]="layoutCtx.ignoreLoading">
  64 + </tb-dashboard>
  65 +</div>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + button.tb-add-new-widget {
  18 + padding-right: 12px;
  19 + font-size: 24px;
  20 + border-style: dashed;
  21 + border-width: 2px;
  22 + }
  23 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, OnDestroy, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core';
  18 +import { StateControllerComponent } from '@home/pages/dashboard/states/state-controller.component';
  19 +import { ILayoutController } from '@home/pages/dashboard/layout/layout.models';
  20 +import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models';
  21 +import { PageComponent } from '@shared/components/page.component';
  22 +import { Store } from '@ngrx/store';
  23 +import { AppState } from '@core/core.state';
  24 +import { Widget } from '@shared/models/widget.models';
  25 +import { WidgetLayouts } from '@shared/models/dashboard.models';
  26 +import { GridsterComponent } from 'angular-gridster2';
  27 +import { IDashboardComponent } from '@home/models/dashboard-component.models';
  28 +
  29 +@Component({
  30 + selector: 'tb-dashboard-layout',
  31 + templateUrl: './dashboard-layout.component.html',
  32 + styleUrls: ['./dashboard-layout.component.scss']
  33 +})
  34 +export class DashboardLayoutComponent extends PageComponent implements ILayoutController, OnInit, OnDestroy {
  35 +
  36 + layoutCtxValue: DashboardPageLayoutContext;
  37 +
  38 + @Input()
  39 + set layoutCtx(val: DashboardPageLayoutContext) {
  40 + this.layoutCtxValue = val;
  41 + if (this.layoutCtxValue) {
  42 + this.layoutCtxValue.ctrl = this;
  43 + }
  44 + }
  45 + get layoutCtx(): DashboardPageLayoutContext {
  46 + return this.layoutCtxValue;
  47 + }
  48 +
  49 + @Input()
  50 + dashboardCtx: DashboardContext;
  51 +
  52 + @Input()
  53 + isEdit: boolean;
  54 +
  55 + @Input()
  56 + isMobile: boolean;
  57 +
  58 + @Input()
  59 + widgetEditMode: boolean;
  60 +
  61 + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
  62 +
  63 + constructor(protected store: Store<AppState>,
  64 + private cd: ChangeDetectorRef) {
  65 + super(store);
  66 + }
  67 +
  68 + ngOnInit(): void {
  69 + }
  70 +
  71 + ngOnDestroy(): void {
  72 + }
  73 +
  74 + reload() {
  75 + }
  76 +
  77 + setResizing(layoutVisibilityChanged: boolean) {
  78 + }
  79 +
  80 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +export interface ILayoutController {
  18 + reload();
  19 + setResizing(layoutVisibilityChanged: boolean);
  20 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<mat-select class="default-state-controller" [fxShow]="displayStateSelection()"
  19 + [(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
  20 + <mat-option *ngFor="let stateKv of states | keyvalue" [value]="stateKv.key">
  21 + {{getStateName(stateKv.key, stateKv.value)}}
  22 + </mat-option>
  23 +</mat-select>
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + mat-select.default-state-controller {
  18 + margin: 0;
  19 + }
  20 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + OnInit,
  20 + ViewEncapsulation,
  21 + Input,
  22 + OnDestroy,
  23 + OnChanges,
  24 + SimpleChanges,
  25 + NgZone
  26 +} from '@angular/core';
  27 +import { IStateController, StateParams, StateObject } from '@core/api/widget-api.models';
  28 +import { ActivatedRoute, Router } from '@angular/router';
  29 +import { Observable, Subscription, of } from 'rxjs';
  30 +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
  31 +import { DashboardState } from '@shared/models/dashboard.models';
  32 +import { IStateControllerComponent, StateControllerState } from './state-controller.models';
  33 +import { StateControllerComponent } from './state-controller.component';
  34 +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
  35 +import { EntityId } from '@app/shared/models/id/entity-id';
  36 +import { UtilsService } from '@core/services/utils.service';
  37 +import { base64toObj, objToBase64 } from '@app/core/utils';
  38 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  39 +import { EntityService } from '@core/http/entity.service';
  40 +import { EntityType } from '@shared/models/entity-type.models';
  41 +import { map } from 'rxjs/operators';
  42 +
  43 +@Component({
  44 + selector: 'tb-default-state-controller',
  45 + templateUrl: './default-state-controller.component.html',
  46 + styleUrls: ['./default-state-controller.component.scss']
  47 +})
  48 +export class DefaultStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy {
  49 +
  50 + constructor(protected router: Router,
  51 + protected route: ActivatedRoute,
  52 + protected statesControllerService: StatesControllerService,
  53 + private utils: UtilsService,
  54 + private entityService: EntityService,
  55 + private dashboardUtils: DashboardUtilsService) {
  56 + super(router, route, statesControllerService);
  57 + }
  58 +
  59 + ngOnInit(): void {
  60 + super.ngOnInit();
  61 + }
  62 +
  63 + ngOnDestroy(): void {
  64 + super.ngOnDestroy();
  65 + }
  66 +
  67 + protected init() {
  68 + if (this.preservedState) {
  69 + this.stateObject = this.preservedState;
  70 + setTimeout(() => {
  71 + this.gotoState(this.stateObject[0].id, true);
  72 + }, 1);
  73 + } else {
  74 + const initialState = this.currentState;
  75 + this.stateObject = this.parseState(initialState);
  76 + setTimeout(() => {
  77 + this.gotoState(this.stateObject[0].id, false);
  78 + }, 1);
  79 + }
  80 + }
  81 +
  82 + protected onMobileChanged() {
  83 + }
  84 +
  85 + protected onStateIdChanged() {
  86 + }
  87 +
  88 + protected onStatesChanged() {
  89 + }
  90 +
  91 + protected onStateChanged() {
  92 + this.stateObject = this.parseState(this.currentState);
  93 + this.gotoState(this.stateObject[0].id, true);
  94 + }
  95 +
  96 + protected stateControllerId(): string {
  97 + return 'default';
  98 + }
  99 +
  100 + public getStateParams(): StateParams {
  101 + if (this.stateObject && this.stateObject.length) {
  102 + return this.stateObject[this.stateObject.length - 1].params;
  103 + } else {
  104 + return {};
  105 + }
  106 + }
  107 +
  108 + public openState(id: string, params?: StateParams, openRightLayout?: boolean): void {
  109 + if (this.states && this.states[id]) {
  110 + if (!params) {
  111 + params = {};
  112 + }
  113 + const newState: StateObject = {
  114 + id,
  115 + params
  116 + };
  117 + this.stateObject[0] = newState;
  118 + this.gotoState(this.stateObject[0].id, true, openRightLayout);
  119 + }
  120 + }
  121 +
  122 + public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void {
  123 + if (!id) {
  124 + id = this.getStateId();
  125 + }
  126 + if (this.states && this.states[id]) {
  127 + if (!params) {
  128 + params = {};
  129 + }
  130 + const newState: StateObject = {
  131 + id,
  132 + params
  133 + };
  134 + this.stateObject[0] = newState;
  135 + this.gotoState(this.stateObject[0].id, true, openRightLayout);
  136 + }
  137 + }
  138 +
  139 + public getEntityId(entityParamName: string): EntityId {
  140 + return null;
  141 + }
  142 +
  143 + public getStateId(): string {
  144 + if (this.stateObject && this.stateObject.length) {
  145 + return this.stateObject[this.stateObject.length - 1].id;
  146 + } else {
  147 + return '';
  148 + }
  149 + }
  150 +
  151 + public getStateIdAtIndex(index: number): string {
  152 + if (this.stateObject && this.stateObject[index]) {
  153 + return this.stateObject[index].id;
  154 + } else {
  155 + return '';
  156 + }
  157 + }
  158 +
  159 + public getStateIndex(): number {
  160 + if (this.stateObject && this.stateObject.length) {
  161 + return this.stateObject.length - 1;
  162 + } else {
  163 + return -1;
  164 + }
  165 + }
  166 +
  167 + public getStateParamsByStateId(stateId: string): StateParams {
  168 + const stateObj = this.getStateObjById(stateId);
  169 + if (stateObj) {
  170 + return stateObj.params;
  171 + } else {
  172 + return null;
  173 + }
  174 + }
  175 +
  176 + public navigatePrevState(index: number): void {
  177 + if (index < this.stateObject.length - 1) {
  178 + this.stateObject.splice(index + 1, this.stateObject.length - index - 1);
  179 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
  180 + }
  181 + }
  182 +
  183 + public resetState(): void {
  184 + const rootStateId = this.dashboardUtils.getRootStateId(this.states);
  185 + this.stateObject = [ { id: rootStateId, params: {} } ];
  186 + this.gotoState(rootStateId, true);
  187 + }
  188 +
  189 + public getStateName(id: string, state: DashboardState): string {
  190 + return this.utils.customTranslation(state.name, id);
  191 + }
  192 +
  193 + public displayStateSelection(): boolean {
  194 + return this.states && Object.keys(this.states).length > 1;
  195 + }
  196 +
  197 + public selectedStateIdChanged() {
  198 + this.gotoState(this.stateObject[0].id, true);
  199 + }
  200 +
  201 + private parseState(stateBase64: string): StateControllerState {
  202 + let result: StateControllerState;
  203 + if (stateBase64) {
  204 + try {
  205 + result = base64toObj(stateBase64);
  206 + } catch (e) {
  207 + result = [ { id: null, params: {} } ];
  208 + }
  209 + }
  210 + if (!result) {
  211 + result = [];
  212 + }
  213 + if (!result.length) {
  214 + result[0] = { id: null, params: {} };
  215 + } else if (result.length > 1) {
  216 + const newResult = [];
  217 + newResult.push(result[result.length - 1]);
  218 + result = newResult;
  219 + }
  220 + const rootStateId = this.dashboardUtils.getRootStateId(this.states);
  221 + if (!result[0].id) {
  222 + result[0].id = rootStateId;
  223 + }
  224 + if (!this.states[result[0].id]) {
  225 + result[0].id = rootStateId;
  226 + }
  227 + let i = result.length;
  228 + while (i--) {
  229 + if (!result[i].id || !this.states[result[i].id]) {
  230 + result.splice(i, 1);
  231 + }
  232 + }
  233 + return result;
  234 + }
  235 +
  236 + private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) {
  237 + if (this.dashboardCtrl.dashboardCtx.state !== stateId) {
  238 + this.dashboardCtrl.openDashboardState(stateId, openRightLayout);
  239 + if (update) {
  240 + this.updateLocation();
  241 + }
  242 + }
  243 + }
  244 +
  245 + private updateLocation() {
  246 + if (this.stateObject[0].id) {
  247 + const newState = objToBase64(this.stateObject);
  248 + this.updateStateParam(newState);
  249 + }
  250 + }
  251 +
  252 + private getStateObjById(id: string): StateObject {
  253 + return this.stateObject.find((stateObj) => stateObj.id === id);
  254 + }
  255 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<div class="entity-state-controller" fxLayout="row" fxLayoutAlign="start center">
  20 + <div *ngIf="!isMobile || stateObject.length === 1" fxLayout="row" fxLayoutAlign="start center">
  21 + <span *ngFor="let state of stateObject; index as i; last as isLast" class="state-entry" [ngStyle]="{fontWeight: isLast ? 'bold' : 'normal',
  22 + cursor: isLast ? 'default' : 'pointer'}" (click)="navigatePrevState(i)">
  23 + {{getStateName(i)}}
  24 + <span class='state-divider' [fxHide]="isLast"> > </span>
  25 + </span>
  26 + </div>
  27 + <mat-select *ngIf="isMobile && stateObject.length > 1"
  28 + [(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
  29 + <mat-option *ngFor="let state of stateObject; index as i" [value]="i">
  30 + {{getStateName(i)}}
  31 + </mat-option>
  32 + </mat-select>
  33 +</div>
  34 +
  35 +
  36 +
  1 +/**
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .entity-state-controller {
  18 + .state-divider {
  19 + padding-right: 15px;
  20 + padding-left: 15px;
  21 + overflow: hidden;
  22 + font-size: 18px;
  23 + text-overflow: ellipsis;
  24 + white-space: nowrap;
  25 + pointer-events: none;
  26 + }
  27 +
  28 + .state-entry {
  29 + overflow: hidden;
  30 + font-size: 18px;
  31 + text-overflow: ellipsis;
  32 + white-space: nowrap;
  33 + outline: none;
  34 + }
  35 +
  36 + mat-select {
  37 + margin: 0;
  38 +
  39 + .mat-select-value-text {
  40 + font-size: 18px;
  41 + font-weight: 700;
  42 + }
  43 + }
  44 + }
  45 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + OnInit,
  20 + ViewEncapsulation,
  21 + Input,
  22 + OnDestroy,
  23 + OnChanges,
  24 + SimpleChanges,
  25 + NgZone
  26 +} from '@angular/core';
  27 +import { IStateController, StateParams, StateObject } from '@core/api/widget-api.models';
  28 +import { ActivatedRoute, Router } from '@angular/router';
  29 +import { Observable, Subscription, of } from 'rxjs';
  30 +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
  31 +import { DashboardState } from '@shared/models/dashboard.models';
  32 +import { IStateControllerComponent, StateControllerState } from './state-controller.models';
  33 +import { StateControllerComponent } from './state-controller.component';
  34 +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
  35 +import { EntityId } from '@app/shared/models/id/entity-id';
  36 +import { UtilsService } from '@core/services/utils.service';
  37 +import { base64toObj, objToBase64 } from '@app/core/utils';
  38 +import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
  39 +import { EntityService } from '@core/http/entity.service';
  40 +import { EntityType } from '@shared/models/entity-type.models';
  41 +import { map } from 'rxjs/operators';
  42 +
  43 +@Component({
  44 + selector: 'tb-entity-state-controller',
  45 + templateUrl: './entity-state-controller.component.html',
  46 + styleUrls: ['./entity-state-controller.component.scss']
  47 +})
  48 +export class EntityStateControllerComponent extends StateControllerComponent implements OnInit, OnDestroy {
  49 +
  50 + private selectedStateIndex = -1;
  51 +
  52 + constructor(protected router: Router,
  53 + protected route: ActivatedRoute,
  54 + protected statesControllerService: StatesControllerService,
  55 + private utils: UtilsService,
  56 + private entityService: EntityService,
  57 + private dashboardUtils: DashboardUtilsService) {
  58 + super(router, route, statesControllerService);
  59 + }
  60 +
  61 + ngOnInit(): void {
  62 + super.ngOnInit();
  63 + }
  64 +
  65 + ngOnDestroy(): void {
  66 + super.ngOnDestroy();
  67 + }
  68 +
  69 + protected init() {
  70 + if (this.preservedState) {
  71 + this.stateObject = this.preservedState;
  72 + this.selectedStateIndex = this.stateObject.length - 1;
  73 + setTimeout(() => {
  74 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
  75 + }, 1);
  76 + } else {
  77 + const initialState = this.currentState;
  78 + this.stateObject = this.parseState(initialState);
  79 + this.selectedStateIndex = this.stateObject.length - 1;
  80 + setTimeout(() => {
  81 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false);
  82 + }, 1);
  83 + }
  84 + }
  85 +
  86 + protected onMobileChanged() {
  87 + }
  88 +
  89 + protected onStateIdChanged() {
  90 + }
  91 +
  92 + protected onStatesChanged() {
  93 + }
  94 +
  95 + protected onStateChanged() {
  96 + this.stateObject = this.parseState(this.currentState);
  97 + this.selectedStateIndex = this.stateObject.length - 1;
  98 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
  99 + }
  100 +
  101 + protected stateControllerId(): string {
  102 + return 'entity';
  103 + }
  104 +
  105 + public getStateParams(): StateParams {
  106 + if (this.stateObject && this.stateObject.length) {
  107 + return this.stateObject[this.stateObject.length - 1].params;
  108 + } else {
  109 + return {};
  110 + }
  111 + }
  112 +
  113 + public openState(id: string, params?: StateParams, openRightLayout?: boolean): void {
  114 + if (this.states && this.states[id]) {
  115 + this.resolveEntity(params).subscribe(
  116 + (entityName) => {
  117 + params.entityName = entityName;
  118 + const newState: StateObject = {
  119 + id,
  120 + params
  121 + };
  122 + this.stateObject.push(newState);
  123 + this.selectedStateIndex = this.stateObject.length - 1;
  124 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout);
  125 + }
  126 + );
  127 + }
  128 + }
  129 +
  130 + public updateState(id: string, params?: StateParams, openRightLayout?: boolean): void {
  131 + if (!id) {
  132 + id = this.getStateId();
  133 + }
  134 + if (this.states && this.states[id]) {
  135 + this.resolveEntity(params).subscribe(
  136 + (entityName) => {
  137 + params.entityName = entityName;
  138 + const newState: StateObject = {
  139 + id,
  140 + params
  141 + };
  142 + this.stateObject[this.stateObject.length - 1] = newState;
  143 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true, openRightLayout);
  144 + }
  145 + );
  146 + }
  147 + }
  148 +
  149 + public getEntityId(entityParamName: string): EntityId {
  150 + const stateParams = this.getStateParams();
  151 + if (!entityParamName || !entityParamName.length) {
  152 + return stateParams.entityId;
  153 + } else if (stateParams[entityParamName]) {
  154 + return stateParams[entityParamName].entityId;
  155 + }
  156 + return null;
  157 + }
  158 +
  159 + public getStateId(): string {
  160 + if (this.stateObject && this.stateObject.length) {
  161 + return this.stateObject[this.stateObject.length - 1].id;
  162 + } else {
  163 + return '';
  164 + }
  165 + }
  166 +
  167 + public getStateIdAtIndex(index: number): string {
  168 + if (this.stateObject && this.stateObject[index]) {
  169 + return this.stateObject[index].id;
  170 + } else {
  171 + return '';
  172 + }
  173 + }
  174 +
  175 + public getStateIndex(): number {
  176 + if (this.stateObject && this.stateObject.length) {
  177 + return this.stateObject.length - 1;
  178 + } else {
  179 + return -1;
  180 + }
  181 + }
  182 +
  183 + public getStateParamsByStateId(stateId: string): StateParams {
  184 + const stateObj = this.getStateObjById(stateId);
  185 + if (stateObj) {
  186 + return stateObj.params;
  187 + } else {
  188 + return null;
  189 + }
  190 + }
  191 +
  192 + public navigatePrevState(index: number): void {
  193 + if (index < this.stateObject.length - 1) {
  194 + this.stateObject.splice(index + 1, this.stateObject.length - index - 1);
  195 + this.selectedStateIndex = this.stateObject.length - 1;
  196 + this.gotoState(this.stateObject[this.stateObject.length - 1].id, true);
  197 + }
  198 + }
  199 +
  200 + public resetState(): void {
  201 + const rootStateId = this.dashboardUtils.getRootStateId(this.states);
  202 + this.stateObject = [ { id: rootStateId, params: {} } ];
  203 + this.gotoState(rootStateId, true);
  204 + }
  205 +
  206 + public getStateName(index: number): string {
  207 + let result = '';
  208 + if (this.stateObject[index]) {
  209 + let stateName = this.states[this.stateObject[index].id].name;
  210 + stateName = this.utils.customTranslation(stateName, stateName);
  211 + const params = this.stateObject[index].params;
  212 + const entityName = params && params.entityName ? params.entityName : '';
  213 + result = this.utils.insertVariable(stateName, 'entityName', entityName);
  214 + for (const prop of Object.keys(params)) {
  215 + if (params[prop] && params[prop].entityName) {
  216 + result = this.utils.insertVariable(result, prop + ':entityName', params[prop].entityName);
  217 + }
  218 + }
  219 + }
  220 + return result;
  221 + }
  222 +
  223 + public selectedStateIndexChanged() {
  224 + this.navigatePrevState(this.selectedStateIndex);
  225 + }
  226 +
  227 + private parseState(stateBase64: string): StateControllerState {
  228 + let result: StateControllerState;
  229 + if (stateBase64) {
  230 + try {
  231 + result = base64toObj(stateBase64);
  232 + } catch (e) {
  233 + result = [ { id: null, params: {} } ];
  234 + }
  235 + }
  236 + if (!result) {
  237 + result = [];
  238 + }
  239 + if (!result.length) {
  240 + result[0] = { id: null, params: {} };
  241 + }
  242 + const rootStateId = this.dashboardUtils.getRootStateId(this.states);
  243 + if (!result[0].id) {
  244 + result[0].id = rootStateId;
  245 + }
  246 + if (!this.states[result[0].id]) {
  247 + result[0].id = rootStateId;
  248 + }
  249 + let i = result.length;
  250 + while (i--) {
  251 + if (!result[i].id || !this.states[result[i].id]) {
  252 + result.splice(i, 1);
  253 + }
  254 + }
  255 + return result;
  256 + }
  257 +
  258 + private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) {
  259 + this.dashboardCtrl.openDashboardState(stateId, openRightLayout);
  260 + if (update) {
  261 + this.updateLocation();
  262 + }
  263 + }
  264 +
  265 + private updateLocation() {
  266 + if (this.stateObject[this.stateObject.length - 1].id) {
  267 + let newState;
  268 + if (this.isDefaultState()) {
  269 + newState = null;
  270 + } else {
  271 + newState = objToBase64(this.stateObject);
  272 + }
  273 + this.updateStateParam(newState);
  274 + }
  275 + }
  276 +
  277 + private isDefaultState(): boolean {
  278 + if (this.stateObject.length === 1) {
  279 + const state = this.stateObject[0];
  280 + const rootStateId = this.dashboardUtils.getRootStateId(this.states);
  281 + if (state.id === rootStateId && (!state.params || this.isEmpty(state.params))) {
  282 + return true;
  283 + }
  284 + }
  285 + return false;
  286 + }
  287 +
  288 + private isEmpty(obj: any): boolean {
  289 + for (const key of Object.keys(obj)) {
  290 + return !Object.prototype.hasOwnProperty.call(obj, key);
  291 + }
  292 + return true;
  293 + }
  294 +
  295 + private resolveEntity(params: StateParams): Observable<string> {
  296 + if (params && params.targetEntityParamName) {
  297 + params = params[params.targetEntityParamName];
  298 + }
  299 + if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
  300 + if (params.entityName && params.entityName.length) {
  301 + return of(params.entityName);
  302 + } else {
  303 + return this.entityService.getEntity(params.entityId.entityType as EntityType,
  304 + params.entityId.id, true, true).pipe(
  305 + map((entity) => entity.name)
  306 + );
  307 + }
  308 + } else {
  309 + return of('');
  310 + }
  311 + }
  312 +
  313 + private getStateObjById(id: string): StateObject {
  314 + return this.stateObject.find((stateObj) => stateObj.id === id);
  315 + }
  316 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { IStateControllerComponent, StateControllerState } from '@home/pages/dashboard/states/state-controller.models';
  18 +import { IDashboardController } from '../dashboard-page.models';
  19 +import { DashboardState } from '@app/shared/models/dashboard.models';
  20 +import { Subscription } from 'rxjs';
  21 +import { OnDestroy, OnInit } from '@angular/core';
  22 +import { ActivatedRoute, Router, Params } from '@angular/router';
  23 +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
  24 +import { EntityId } from '@app/shared/models/id/entity-id';
  25 +import { StateParams } from '@app/core/api/widget-api.models';
  26 +
  27 +export abstract class StateControllerComponent implements IStateControllerComponent, OnInit, OnDestroy {
  28 +
  29 + stateObject: StateControllerState = [];
  30 + dashboardCtrl: IDashboardController;
  31 + preservedState: any;
  32 +
  33 + isMobileValue: boolean;
  34 + set isMobile(val: boolean) {
  35 + if (this.isMobileValue !== val) {
  36 + this.isMobileValue = val;
  37 + if (this.inited) {
  38 + this.onMobileChanged();
  39 + }
  40 + }
  41 + }
  42 + get isMobile(): boolean {
  43 + return this.isMobileValue;
  44 + }
  45 +
  46 + stateValue: string;
  47 + set state(val: string) {
  48 + if (this.stateValue !== val) {
  49 + this.stateValue = val;
  50 + if (this.inited) {
  51 + this.onStateIdChanged();
  52 + }
  53 + }
  54 + }
  55 + get state(): string {
  56 + return this.stateValue;
  57 + }
  58 +
  59 + dashboardIdValue: string;
  60 + set dashboardId(val: string) {
  61 + if (this.dashboardIdValue !== val) {
  62 + this.dashboardIdValue = val;
  63 + if (this.inited) {
  64 + this.init();
  65 + }
  66 + }
  67 + }
  68 + get dashboardId(): string {
  69 + return this.dashboardIdValue;
  70 + }
  71 +
  72 + statesValue: { [id: string]: DashboardState };
  73 + set states(val: { [id: string]: DashboardState }) {
  74 + if (this.statesValue !== val) {
  75 + this.statesValue = val;
  76 + if (this.inited) {
  77 + this.onStatesChanged();
  78 + }
  79 + }
  80 + }
  81 + get states(): { [id: string]: DashboardState } {
  82 + return this.statesValue;
  83 + }
  84 +
  85 + currentState: string;
  86 +
  87 + private rxSubscriptions = new Array<Subscription>();
  88 +
  89 + private inited = false;
  90 +
  91 + constructor(protected router: Router,
  92 + protected route: ActivatedRoute,
  93 + protected statesControllerService: StatesControllerService) {
  94 + }
  95 +
  96 + ngOnInit(): void {
  97 + this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
  98 + const newState = paramMap.get('state');
  99 + if (this.currentState !== newState) {
  100 + this.currentState = newState;
  101 + if (this.inited) {
  102 + this.onStateChanged();
  103 + }
  104 + }
  105 + }));
  106 + this.init();
  107 + this.inited = true;
  108 + }
  109 +
  110 + ngOnDestroy(): void {
  111 + this.rxSubscriptions.forEach((subscription) => {
  112 + subscription.unsubscribe();
  113 + });
  114 + this.rxSubscriptions.length = 0;
  115 + }
  116 +
  117 + protected updateStateParam(newState: string) {
  118 + this.currentState = newState;
  119 + const queryParams: Params = { state: this.currentState };
  120 + this.router.navigate(
  121 + [],
  122 + {
  123 + relativeTo: this.route,
  124 + queryParams,
  125 + queryParamsHandling: 'merge',
  126 + });
  127 + }
  128 +
  129 + public openRightLayout(): void {
  130 + this.dashboardCtrl.openRightLayout();
  131 + }
  132 +
  133 + public preserveState() {
  134 + this.statesControllerService.preserveStateControllerState(this.stateControllerId(), this.stateObject);
  135 + }
  136 +
  137 + public cleanupPreservedStates() {
  138 + this.statesControllerService.cleanupPreservedStates();
  139 + }
  140 +
  141 + protected abstract init();
  142 +
  143 + protected abstract onMobileChanged();
  144 +
  145 + protected abstract onStateIdChanged();
  146 +
  147 + protected abstract onStatesChanged();
  148 +
  149 + protected abstract onStateChanged();
  150 +
  151 + protected abstract stateControllerId(): string;
  152 +
  153 + public abstract getEntityId(entityParamName: string): EntityId;
  154 +
  155 + public abstract getStateId(): string;
  156 +
  157 + public abstract getStateIdAtIndex(index: number): string;
  158 +
  159 + public abstract getStateIndex(): number;
  160 +
  161 + public abstract getStateParams(): StateParams;
  162 +
  163 + public abstract getStateParamsByStateId(stateId: string): StateParams;
  164 +
  165 + public abstract navigatePrevState(index: number): void;
  166 +
  167 + public abstract openState(id: string, params?: StateParams, openRightLayout?: boolean): void;
  168 +
  169 + public abstract resetState(): void;
  170 +
  171 + public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void;
  172 +
  173 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { IStateController, StateObject } from '@core/api/widget-api.models';
  18 +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
  19 +import { DashboardState } from '@shared/models/dashboard.models';
  20 +
  21 +export declare type StateControllerState = StateObject[];
  22 +
  23 +export interface IStateControllerComponent extends IStateController {
  24 + state: string;
  25 + isMobile: boolean;
  26 + dashboardCtrl: IDashboardController;
  27 + states: {[id: string]: DashboardState };
  28 + dashboardId: string;
  29 + preservedState: any;
  30 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + ComponentRef,
  19 + Directive,
  20 + ElementRef,
  21 + Input,
  22 + OnChanges,
  23 + OnInit,
  24 + OnDestroy,
  25 + SimpleChanges,
  26 + ViewContainerRef,
  27 + ChangeDetectorRef
  28 +} from '@angular/core';
  29 +import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component';
  30 +import { DashboardState } from '@shared/models/dashboard.models';
  31 +import { IDashboardController } from '@home/pages/dashboard/dashboard-page.models';
  32 +import { StatesControllerService } from '@home/pages/dashboard/states/states-controller.service';
  33 +import { IStateController } from '@core/api/widget-api.models';
  34 +import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models';
  35 +
  36 +@Directive({
  37 + selector: 'tb-states-component'
  38 +})
  39 +export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
  40 +
  41 + @Input()
  42 + statesControllerId: string;
  43 +
  44 + @Input()
  45 + dashboardCtrl: IDashboardController;
  46 +
  47 + @Input()
  48 + dashboardId: string;
  49 +
  50 + @Input()
  51 + states: {[id: string]: DashboardState };
  52 +
  53 + @Input()
  54 + state: string;
  55 +
  56 + @Input()
  57 + isMobile: boolean;
  58 +
  59 + stateControllerComponentRef: ComponentRef<IStateControllerComponent>;
  60 + stateControllerComponent: IStateControllerComponent;
  61 +
  62 + constructor(private viewContainerRef: ViewContainerRef,
  63 + private statesControllerService: StatesControllerService) {
  64 + }
  65 +
  66 + ngOnInit(): void {
  67 + this.init();
  68 + }
  69 +
  70 + ngOnDestroy(): void {
  71 + this.destroy();
  72 + }
  73 +
  74 + ngOnChanges(changes: SimpleChanges): void {
  75 + for (const propName of Object.keys(changes)) {
  76 + const change = changes[propName];
  77 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  78 + if (propName === 'statesControllerId') {
  79 + this.reInit();
  80 + } else if (propName === 'states') {
  81 + this.stateControllerComponent.states = this.states;
  82 + } else if (propName === 'dashboardId') {
  83 + this.stateControllerComponent.dashboardId = this.dashboardId;
  84 + } else if (propName === 'isMobile') {
  85 + this.stateControllerComponent.isMobile = this.isMobile;
  86 + } else if (propName === 'state') {
  87 + this.stateControllerComponent.state = this.state;
  88 + }
  89 + }
  90 + }
  91 + }
  92 +
  93 + private reInit() {
  94 + this.destroy();
  95 + this.init();
  96 + }
  97 +
  98 + private init() {
  99 + this.viewContainerRef.clear();
  100 + let stateControllerData = this.statesControllerService.getStateController(this.statesControllerId);
  101 + if (!stateControllerData) {
  102 + stateControllerData = this.statesControllerService.getStateController('default');
  103 + }
  104 + const preservedState = this.statesControllerService.withdrawStateControllerState(this.statesControllerId);
  105 + const stateControllerFactory = stateControllerData.factory;
  106 + this.stateControllerComponentRef = this.viewContainerRef.createComponent(stateControllerFactory);
  107 + this.stateControllerComponent = this.stateControllerComponentRef.instance;
  108 + this.dashboardCtrl.dashboardCtx.stateController = this.stateControllerComponent;
  109 + this.stateControllerComponent.preservedState = preservedState;
  110 + this.stateControllerComponent.dashboardCtrl = this.dashboardCtrl;
  111 + this.stateControllerComponent.state = this.state;
  112 + this.stateControllerComponent.isMobile = this.isMobile;
  113 + this.stateControllerComponent.states = this.states;
  114 + this.stateControllerComponent.dashboardId = this.dashboardId;
  115 + }
  116 +
  117 + private destroy() {
  118 + if (this.stateControllerComponentRef) {
  119 + this.stateControllerComponentRef.destroy();
  120 + }
  121 + }
  122 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { NgModule } from '@angular/core';
  18 +import { CommonModule } from '@angular/common';
  19 +import { SharedModule } from '@shared/shared.module';
  20 +import { HomeComponentsModule } from '@modules/home/components/home-components.module';
  21 +import { StatesControllerService } from './states-controller.service';
  22 +import { EntityStateControllerComponent } from './entity-state-controller.component';
  23 +import { StatesComponentDirective } from './states-component.directive';
  24 +import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module';
  25 +import { DefaultStateControllerComponent } from '@home/pages/dashboard/states/default-state-controller.component';
  26 +
  27 +@NgModule({
  28 + entryComponents: [
  29 + DefaultStateControllerComponent,
  30 + EntityStateControllerComponent
  31 + ],
  32 + declarations: [
  33 + StatesComponentDirective,
  34 + DefaultStateControllerComponent,
  35 + EntityStateControllerComponent
  36 + ],
  37 + imports: [
  38 + CommonModule,
  39 + SharedModule,
  40 + HomeComponentsModule,
  41 + HomeDialogsModule
  42 + ],
  43 + exports: [
  44 + StatesComponentDirective
  45 + ],
  46 + providers: [
  47 + StatesControllerService
  48 + ]
  49 +})
  50 +export class StatesControllerModule {
  51 +
  52 + constructor(private statesControllerService: StatesControllerService) {
  53 + this.statesControllerService.registerStatesController('default', DefaultStateControllerComponent);
  54 + this.statesControllerService.registerStatesController('entity', EntityStateControllerComponent);
  55 + }
  56 +}
  1 +///
  2 +/// Copyright © 2016-2019 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { ComponentFactory, ComponentFactoryResolver, Injectable, Type } from '@angular/core';
  18 +import { deepClone } from '@core/utils';
  19 +import { IStateControllerComponent } from '@home/pages/dashboard/states/state-controller.models';
  20 +
  21 +export interface StateControllerData {
  22 + factory: ComponentFactory<IStateControllerComponent>;
  23 + state?: any;
  24 +}
  25 +
  26 +@Injectable()
  27 +export class StatesControllerService {
  28 +
  29 + statesControllers: {[stateControllerId: string]: StateControllerData} = {};
  30 +
  31 + constructor(private componentFactoryResolver: ComponentFactoryResolver) {
  32 + }
  33 +
  34 + public registerStatesController(stateControllerId: string, stateControllerComponent: Type<IStateControllerComponent>): void {
  35 + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(stateControllerComponent);
  36 + this.statesControllers[stateControllerId] = {
  37 + factory: componentFactory
  38 + };
  39 + }
  40 +
  41 + public getStateControllers(): {[stateControllerId: string]: StateControllerData} {
  42 + return this.statesControllers;
  43 + }
  44 +
  45 + public getStateController(stateControllerId: string): StateControllerData {
  46 + return this.statesControllers[stateControllerId];
  47 + }
  48 +
  49 + public preserveStateControllerState(stateControllerId: string, state: any) {
  50 + this.statesControllers[stateControllerId].state = deepClone(state);
  51 + }
  52 +
  53 + public withdrawStateControllerState(stateControllerId: string): any {
  54 + const state = this.statesControllers[stateControllerId].state;
  55 + this.statesControllers[stateControllerId].state = null;
  56 + return state;
  57 + }
  58 +
  59 + public cleanupPreservedStates() {
  60 + for (const stateControllerId of Object.keys(this.statesControllers)) {
  61 + this.statesControllers[stateControllerId].state = null;
  62 + }
  63 + }
  64 +}
@@ -26,10 +26,12 @@ import { Observable } from 'rxjs'; @@ -26,10 +26,12 @@ import { Observable } from 'rxjs';
26 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 26 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
27 import { WidgetService } from '@core/http/widget.service'; 27 import { WidgetService } from '@core/http/widget.service';
28 import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; 28 import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
29 -import { map } from 'rxjs/operators'; 29 +import { map, share } from 'rxjs/operators';
30 import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; 30 import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
31 -import { widgetType, WidgetType } from '@app/shared/models/widget.models'; 31 +import { Widget, widgetType, WidgetType } from '@app/shared/models/widget.models';
32 import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; 32 import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
  33 +import { WidgetsData } from '@home/models/dashboard-component.models';
  34 +import { NULL_UUID } from '@shared/models/id/has-uuid';
33 35
34 export interface WidgetEditorData { 36 export interface WidgetEditorData {
35 widgetType: WidgetType; 37 widgetType: WidgetType;
@@ -52,6 +54,69 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { @@ -52,6 +54,69 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
52 } 54 }
53 55
54 @Injectable() 56 @Injectable()
  57 +export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
  58 +
  59 + constructor(private widgetsService: WidgetService) {
  60 + }
  61 +
  62 + resolve(route: ActivatedRouteSnapshot): Observable<WidgetsData> {
  63 + const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle;
  64 + const bundleAlias = widgetsBundle.alias;
  65 + const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
  66 + return this.widgetsService.getBundleWidgetTypes(bundleAlias,
  67 + isSystem).pipe(
  68 + map((types) => {
  69 + types = types.sort((a, b) => {
  70 + let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
  71 + if (result === 0) {
  72 + result = b.createdTime - a.createdTime;
  73 + }
  74 + return result;
  75 + });
  76 + const widgetTypes = new Array<Widget>(types.length);
  77 + let top = 0;
  78 + const lastTop = [0, 0, 0];
  79 + let col = 0;
  80 + let column = 0;
  81 + types.forEach((type) => {
  82 + const widgetTypeInfo = toWidgetInfo(type);
  83 + const sizeX = 8;
  84 + const sizeY = Math.floor(widgetTypeInfo.sizeY);
  85 + const widget: Widget = {
  86 + typeId: type.id,
  87 + isSystemType: isSystem,
  88 + bundleAlias,
  89 + typeAlias: widgetTypeInfo.alias,
  90 + type: widgetTypeInfo.type,
  91 + title: widgetTypeInfo.widgetName,
  92 + sizeX,
  93 + sizeY,
  94 + row: top,
  95 + col,
  96 + config: JSON.parse(widgetTypeInfo.defaultConfig)
  97 + };
  98 +
  99 + widget.config.title = widgetTypeInfo.widgetName;
  100 +
  101 + widgetTypes.push(widget);
  102 + top += sizeY;
  103 + if (top > lastTop[column] + 10) {
  104 + lastTop[column] = top;
  105 + column++;
  106 + if (column > 2) {
  107 + column = 0;
  108 + }
  109 + top = lastTop[column];
  110 + col = column * 8;
  111 + }
  112 + });
  113 + return { widgets: widgetTypes };
  114 + }
  115 + ));
  116 + }
  117 +}
  118 +
  119 +@Injectable()
55 export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> { 120 export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
56 121
57 constructor(private widgetsService: WidgetService) { 122 constructor(private widgetsService: WidgetService) {
@@ -137,6 +202,9 @@ export const routes: Routes = [ @@ -137,6 +202,9 @@ export const routes: Routes = [
137 data: { 202 data: {
138 auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], 203 auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
139 title: 'widget.widget-library' 204 title: 'widget.widget-library'
  205 + },
  206 + resolve: {
  207 + widgetsData: WidgetsTypesDataResolver
140 } 208 }
141 }, 209 },
142 { 210 {
@@ -183,6 +251,7 @@ export const routes: Routes = [ @@ -183,6 +251,7 @@ export const routes: Routes = [
183 providers: [ 251 providers: [
184 WidgetsBundlesTableConfigResolver, 252 WidgetsBundlesTableConfigResolver,
185 WidgetsBundleResolver, 253 WidgetsBundleResolver,
  254 + WidgetsTypesDataResolver,
186 WidgetEditorDataResolver, 255 WidgetEditorDataResolver,
187 WidgetEditorAddDataResolver 256 WidgetEditorAddDataResolver
188 ] 257 ]
@@ -28,7 +28,8 @@ @@ -28,7 +28,8 @@
28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> 28 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
29 </section> 29 </section>
30 <tb-dashboard [aliasController]="aliasController" 30 <tb-dashboard [aliasController]="aliasController"
31 - [widgetsData]="widgetsData" 31 + [widgets]="widgetsData.widgets"
  32 + [widgetLayouts]="widgetsData.widgetLayouts"
32 [isEdit]="false" 33 [isEdit]="false"
33 [isEditActionEnabled]="true" 34 [isEditActionEnabled]="true"
34 [isExportActionEnabled]="true" 35 [isExportActionEnabled]="true"
@@ -55,7 +55,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -55,7 +55,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
55 55
56 widgetsBundle: WidgetsBundle; 56 widgetsBundle: WidgetsBundle;
57 57
58 - widgetTypes$: Observable<Array<Widget>>; 58 + widgetsData: WidgetsData;
59 59
60 footerFabButtons: FooterFabButtons = { 60 footerFabButtons: FooterFabButtons = {
61 fabTogglerName: 'widget.add-widget-type', 61 fabTogglerName: 'widget.add-widget-type',
@@ -84,8 +84,6 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -84,8 +84,6 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
84 onRemoveWidget: this.removeWidgetType.bind(this) 84 onRemoveWidget: this.removeWidgetType.bind(this)
85 }; 85 };
86 86
87 - widgetsData: Observable<WidgetsData>;  
88 -  
89 aliasController: IAliasController = new DummyAliasController(); 87 aliasController: IAliasController = new DummyAliasController();
90 88
91 constructor(protected store: Store<AppState>, 89 constructor(protected store: Store<AppState>,
@@ -98,70 +96,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -98,70 +96,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
98 96
99 this.authUser = getCurrentAuthUser(store); 97 this.authUser = getCurrentAuthUser(store);
100 this.widgetsBundle = this.route.snapshot.data.widgetsBundle; 98 this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
  99 + this.widgetsData = this.route.snapshot.data.widgetsData;
101 if (this.authUser.authority === Authority.TENANT_ADMIN) { 100 if (this.authUser.authority === Authority.TENANT_ADMIN) {
102 this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; 101 this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
103 } else { 102 } else {
104 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; 103 this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
105 } 104 }
106 - this.loadWidgetTypes();  
107 - this.widgetsData = this.widgetTypes$.pipe(  
108 - map(widgets => ({ widgets })));  
109 - }  
110 -  
111 - loadWidgetTypes() {  
112 - const bundleAlias = this.widgetsBundle.alias;  
113 - const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;  
114 - this.widgetTypes$ = this.widgetService.getBundleWidgetTypes(bundleAlias,  
115 - isSystem).pipe(  
116 - map((types) => {  
117 - types = types.sort((a, b) => {  
118 - let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);  
119 - if (result === 0) {  
120 - result = b.createdTime - a.createdTime;  
121 - }  
122 - return result;  
123 - });  
124 - const widgetTypes = new Array<Widget>(types.length);  
125 - let top = 0;  
126 - const lastTop = [0, 0, 0];  
127 - let col = 0;  
128 - let column = 0;  
129 - types.forEach((type) => {  
130 - const widgetTypeInfo = toWidgetInfo(type);  
131 - const sizeX = 8;  
132 - const sizeY = Math.floor(widgetTypeInfo.sizeY);  
133 - const widget: Widget = {  
134 - typeId: type.id,  
135 - isSystemType: isSystem,  
136 - bundleAlias,  
137 - typeAlias: widgetTypeInfo.alias,  
138 - type: widgetTypeInfo.type,  
139 - title: widgetTypeInfo.widgetName,  
140 - sizeX,  
141 - sizeY,  
142 - row: top,  
143 - col,  
144 - config: JSON.parse(widgetTypeInfo.defaultConfig)  
145 - };  
146 -  
147 - widget.config.title = widgetTypeInfo.widgetName;  
148 -  
149 - widgetTypes.push(widget);  
150 - top += sizeY;  
151 - if (top > lastTop[column] + 10) {  
152 - lastTop[column] = top;  
153 - column++;  
154 - if (column > 2) {  
155 - column = 0;  
156 - }  
157 - top = lastTop[column];  
158 - col = column * 8;  
159 - }  
160 - });  
161 - return widgetTypes;  
162 - }  
163 - ),  
164 - share());  
165 } 105 }
166 106
167 ngOnInit(): void { 107 ngOnInit(): void {
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<section fxLayout="row" class="layout-wrap tb-footer-buttons"> 18 +<section fxLayout="row" class="layout-wrap tb-footer-fab-buttons" [ngClass]="{'relative-buttons': relative}">
19 <div class="fab-container"> 19 <div class="fab-container">
20 <button [disabled]="isLoading$ | async" 20 <button [disabled]="isLoading$ | async"
21 mat-fab class="fab-toggler tb-btn-footer" 21 mat-fab class="fab-toggler tb-btn-footer"
@@ -15,12 +15,14 @@ @@ -15,12 +15,14 @@
15 */ 15 */
16 16
17 :host { 17 :host {
18 - section.tb-footer-buttons {  
19 - position: fixed;  
20 - right: 20px;  
21 - bottom: 20px;  
22 - z-index: 30;  
23 - pointer-events: none; 18 + section.tb-footer-fab-buttons {
  19 + &:not(.relative-buttons) {
  20 + position: fixed;
  21 + right: 20px;
  22 + bottom: 20px;
  23 + z-index: 30;
  24 + pointer-events: none;
  25 + }
24 26
25 .fab-container { 27 .fab-container {
26 display: flex; 28 display: flex;
@@ -30,7 +32,6 @@ @@ -30,7 +32,6 @@
30 display: flex; 32 display: flex;
31 flex-direction: column-reverse; 33 flex-direction: column-reverse;
32 align-items: center; 34 align-items: center;
33 - margin-bottom: 5px;  
34 35
35 button { 36 button {
36 margin-bottom: 17px; 37 margin-bottom: 17px;
@@ -39,6 +40,9 @@ @@ -39,6 +40,9 @@
39 } 40 }
40 41
41 .tb-btn-footer { 42 .tb-btn-footer {
  43 + &.fab-toggler {
  44 + margin-top: 0px;
  45 + }
42 position: relative !important; 46 position: relative !important;
43 display: inline-block !important; 47 display: inline-block !important;
44 animation: tbMoveFromBottomFade .3s ease both; 48 animation: tbMoveFromBottomFade .3s ease both;
@@ -19,6 +19,7 @@ import { PageComponent } from '@shared/components/page.component'; @@ -19,6 +19,7 @@ import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 19 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 20 import { AppState } from '@core/core.state';
21 import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations'; 21 import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations';
  22 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
22 23
23 export interface FooterFabButton { 24 export interface FooterFabButton {
24 name: string; 25 name: string;
@@ -43,6 +44,15 @@ export class FooterFabButtonsComponent extends PageComponent { @@ -43,6 +44,15 @@ export class FooterFabButtonsComponent extends PageComponent {
43 @Input() 44 @Input()
44 footerFabButtons: FooterFabButtons; 45 footerFabButtons: FooterFabButtons;
45 46
  47 + private relativeValue: boolean;
  48 + get relative(): boolean {
  49 + return this.relativeValue;
  50 + }
  51 + @Input()
  52 + set relative(value: boolean) {
  53 + this.relativeValue = coerceBooleanProperty(value);
  54 + }
  55 +
46 buttons: Array<FooterFabButton> = []; 56 buttons: Array<FooterFabButton> = [];
47 fabTogglerState = 'inactive'; 57 fabTogglerState = 'inactive';
48 58
@@ -48,6 +48,10 @@ export interface GridSettings { @@ -48,6 +48,10 @@ export interface GridSettings {
48 columns?: number; 48 columns?: number;
49 margins?: [number, number]; 49 margins?: [number, number];
50 backgroundSizeMode?: string; 50 backgroundSizeMode?: string;
  51 + backgroundImageUrl?: string;
  52 + autoFillHeight?: boolean;
  53 + mobileAutoFillHeight?: boolean;
  54 + mobileRowHeight?: number;
51 [key: string]: any; 55 [key: string]: any;
52 // TODO: 56 // TODO:
53 } 57 }
@@ -57,12 +61,17 @@ export interface DashboardLayout { @@ -57,12 +61,17 @@ export interface DashboardLayout {
57 gridSettings: GridSettings; 61 gridSettings: GridSettings;
58 } 62 }
59 63
  64 +export interface DashboardLayoutInfo {
  65 + widgets?: Array<Widget>;
  66 + widgetLayouts?: WidgetLayouts;
  67 + gridSettings?: GridSettings;
  68 +}
  69 +
60 export declare type DashboardLayoutId = 'main' | 'right'; 70 export declare type DashboardLayoutId = 'main' | 'right';
61 71
62 -export interface DashboardStateLayouts {  
63 - main?: DashboardLayout;  
64 - right?: DashboardLayout;  
65 -} 72 +export declare type DashboardStateLayouts = {[key in DashboardLayoutId]?: DashboardLayout};
  73 +
  74 +export declare type DashboardLayoutsInfo = {[key in DashboardLayoutId]?: DashboardLayoutInfo};
66 75
67 export interface DashboardState { 76 export interface DashboardState {
68 name: string; 77 name: string;