Commit b60b3144a0854ec0cdf75fbb96492c2deefa42e9
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 | +} |
ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<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> |
ui-ngx/src/app/modules/home/pages/dashboard/states/default-state-controller.component.scss
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + 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 | +} |
ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | + | ||
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 | + |
ui-ngx/src/app/modules/home/pages/dashboard/states/entity-state-controller.component.scss
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2019 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +:host { | ||
17 | + .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; |