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 | 58 | updateEntityAliases(entityAliases: EntityAliases) { |
59 | 59 | } |
60 | 60 | |
61 | + dashboardStateChanged() { | |
62 | + } | |
63 | + | |
61 | 64 | } |
62 | 65 | |
63 | 66 | export class AliasController implements IAliasController { |
... | ... | @@ -111,4 +114,7 @@ export class AliasController implements IAliasController { |
111 | 114 | updateEntityAliases(entityAliases: EntityAliases) { |
112 | 115 | } |
113 | 116 | |
117 | + dashboardStateChanged() { | |
118 | + } | |
119 | + | |
114 | 120 | } | ... | ... |
... | ... | @@ -86,6 +86,7 @@ export interface IAliasController { |
86 | 86 | getEntityAliases(): EntityAliases; |
87 | 87 | updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo); |
88 | 88 | updateEntityAliases(entityAliases: EntityAliases); |
89 | + dashboardStateChanged(); | |
89 | 90 | [key: string]: any | null; |
90 | 91 | // TODO: |
91 | 92 | } |
... | ... | @@ -103,12 +104,19 @@ export interface StateParams { |
103 | 104 | } |
104 | 105 | |
105 | 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 | 122 | export interface SubscriptionInfo { | ... | ... |
... | ... | @@ -22,7 +22,9 @@ import { |
22 | 22 | DashboardLayout, |
23 | 23 | DashboardStateLayouts, |
24 | 24 | DashboardState, |
25 | - DashboardConfiguration | |
25 | + DashboardConfiguration, | |
26 | + DashboardLayoutInfo, | |
27 | + DashboardLayoutsInfo | |
26 | 28 | } from '@shared/models/dashboard.models'; |
27 | 29 | import { isUndefined, isDefined, isString } from '@core/utils'; |
28 | 30 | import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models'; |
... | ... | @@ -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 | 281 | private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, |
242 | 282 | datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, |
243 | 283 | targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | import { Inject, Injectable } from '@angular/core'; |
18 | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | 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 | 21 | import { WindowMessage } from '@shared/models/window-message.model'; |
22 | 22 | import { TranslateService } from '@ngx-translate/core'; |
23 | 23 | import { customTranslationsPrefix } from '@app/shared/models/constants'; |
... | ... | @@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; |
28 | 28 | import { materialColors } from '@app/shared/models/material.models'; |
29 | 29 | import { WidgetInfo } from '@home/models/widget-component.models'; |
30 | 30 | |
31 | +const varsRegex = /\$\{([^}]*)\}/g; | |
32 | + | |
31 | 33 | @Injectable({ |
32 | 34 | providedIn: 'root' |
33 | 35 | }) |
... | ... | @@ -144,6 +146,20 @@ export class UtilsService { |
144 | 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 | 163 | public guid(): string { |
148 | 164 | function s4(): string { |
149 | 165 | return Math.floor((1 + Math.random()) * 0x10000) | ... | ... |
... | ... | @@ -15,9 +15,12 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import {MissingTranslationHandler, MissingTranslationHandlerParams} from '@ngx-translate/core'; |
18 | +import { customTranslationsPrefix } from '@app/shared/models/constants'; | |
18 | 19 | |
19 | 20 | export class TbMissingTranslationHandler implements MissingTranslationHandler { |
20 | 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 | 26 | (contextmenu)="openDashboardContextMenu($event)"> |
27 | 27 | <div [ngClass]="dashboardClass" id="gridster-background" style="height: auto; min-height: 100%; display: inline;"> |
28 | 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 | 30 | <div tb-fullscreen [fullscreen]="widget.isFullscreen" (fullscreenChanged)="onWidgetFullscreenChanged($event, widget)" |
31 | 31 | fxLayout="column" |
32 | 32 | class="tb-widget" | ... | ... |
... | ... | @@ -37,16 +37,18 @@ import { |
37 | 37 | DashboardCallbacks, |
38 | 38 | DashboardWidget, |
39 | 39 | IDashboardComponent, |
40 | - WidgetsData | |
40 | + WidgetsData, | |
41 | + DashboardWidgets | |
41 | 42 | } from '../../models/dashboard-component.models'; |
42 | 43 | import { merge, Observable, ReplaySubject, Subject } from 'rxjs'; |
43 | 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 | 46 | import { DialogService } from '@core/services/dialog.service'; |
46 | 47 | import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; |
47 | 48 | import { BreakpointObserver } from '@angular/cdk/layout'; |
48 | 49 | import { MediaBreakpoints } from '@shared/models/constants'; |
49 | 50 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
51 | +import { Widget } from '@app/shared/models/widget.models'; | |
50 | 52 | |
51 | 53 | @Component({ |
52 | 54 | selector: 'tb-dashboard', |
... | ... | @@ -58,7 +60,10 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
58 | 60 | authUser: AuthUser; |
59 | 61 | |
60 | 62 | @Input() |
61 | - widgetsData: Observable<WidgetsData>; | |
63 | + widgets: Array<Widget>; | |
64 | + | |
65 | + @Input() | |
66 | + widgetLayouts: WidgetLayouts; | |
62 | 67 | |
63 | 68 | @Input() |
64 | 69 | callbacks: DashboardCallbacks; |
... | ... | @@ -125,8 +130,6 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
125 | 130 | |
126 | 131 | gridsterOpts: GridsterConfig; |
127 | 132 | |
128 | - dashboardLoading = true; | |
129 | - | |
130 | 133 | highlightedMode = false; |
131 | 134 | highlightedWidget: DashboardWidget = null; |
132 | 135 | selectedWidget: DashboardWidget = null; |
... | ... | @@ -138,9 +141,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
138 | 141 | |
139 | 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 | 148 | constructor(protected store: Store<AppState>, |
146 | 149 | private timeService: TimeService, |
... | ... | @@ -172,25 +175,26 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
172 | 175 | defaultItemRows: 6, |
173 | 176 | resizable: {enabled: this.isEdit}, |
174 | 177 | draggable: {enabled: this.isEdit}, |
175 | - itemChangeCallback: item => this.sortWidgets(this.widgets) | |
178 | + itemChangeCallback: item => this.dashboardWidgets.sortWidgets() | |
176 | 179 | }; |
177 | 180 | |
178 | 181 | this.updateMobileOpts(); |
179 | 182 | |
180 | - this.loadDashboard(); | |
181 | - | |
182 | 183 | this.breakpointObserver |
183 | 184 | .observe(MediaBreakpoints['gt-sm']).subscribe( |
184 | 185 | () => { |
185 | 186 | this.updateMobileOpts(); |
186 | 187 | } |
187 | 188 | ); |
189 | + | |
190 | + this.updateWidgets(); | |
188 | 191 | } |
189 | 192 | |
190 | 193 | ngOnChanges(changes: SimpleChanges): void { |
191 | 194 | let updateMobileOpts = false; |
192 | 195 | let updateLayoutOpts = false; |
193 | 196 | let updateEditingOpts = false; |
197 | + let updateWidgets = false; | |
194 | 198 | for (const propName of Object.keys(changes)) { |
195 | 199 | const change = changes[propName]; |
196 | 200 | if (!change.firstChange && change.currentValue !== change.previousValue) { |
... | ... | @@ -200,9 +204,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
200 | 204 | updateLayoutOpts = true; |
201 | 205 | } else if (propName === 'isEdit') { |
202 | 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 | 215 | if (updateMobileOpts) { |
207 | 216 | this.updateMobileOpts(); |
208 | 217 | } |
... | ... | @@ -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 | 234 | ngAfterViewInit(): void { | ... | ... |
... | ... | @@ -119,7 +119,7 @@ export class WidgetComponentService { |
119 | 119 | } else { |
120 | 120 | fetchQueue = new Array<Subject<WidgetInfo>>(); |
121 | 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 | 123 | (widgetType) => { |
124 | 124 | this.loadWidget(widgetType, bundleAlias, isSystem, widgetInfoSubject); |
125 | 125 | }, | ... | ... |
... | ... | @@ -43,6 +43,7 @@ export interface DashboardCallbacks { |
43 | 43 | export interface IDashboardComponent { |
44 | 44 | gridsterOpts: GridsterConfig; |
45 | 45 | gridster: GridsterComponent; |
46 | + dashboardWidgets: DashboardWidgets; | |
46 | 47 | mobileAutofillHeight: boolean; |
47 | 48 | isMobileSize: boolean; |
48 | 49 | autofillHeight: boolean; |
... | ... | @@ -54,6 +55,74 @@ export interface IDashboardComponent { |
54 | 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 | 126 | export class DashboardWidget implements GridsterItem { |
58 | 127 | |
59 | 128 | isFullscreen = false; | ... | ... |
... | ... | @@ -123,7 +123,7 @@ export const MissingWidgetType: WidgetInfo = { |
123 | 123 | sizeY: 6, |
124 | 124 | resources: [], |
125 | 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 | 127 | '</div>', |
128 | 128 | templateCss: '', |
129 | 129 | controllerScript: 'self.onInit = function() {}', | ... | ... |
... | ... | @@ -24,6 +24,9 @@ import {CustomersTableConfigResolver} from './customers-table-config.resolver'; |
24 | 24 | import {DevicesTableConfigResolver} from '@modules/home/pages/device/devices-table-config.resolver'; |
25 | 25 | import {AssetsTableConfigResolver} from '../asset/assets-table-config.resolver'; |
26 | 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 | 31 | const routes: Routes = [ |
29 | 32 | { |
... | ... | @@ -95,19 +98,42 @@ const routes: Routes = [ |
95 | 98 | }, |
96 | 99 | { |
97 | 100 | path: ':customerId/dashboards', |
98 | - component: EntitiesTableComponent, | |
99 | 101 | data: { |
100 | - auth: [Authority.TENANT_ADMIN], | |
101 | - title: 'customer.assets', | |
102 | - dashboardsType: 'customer', | |
103 | 102 | breadcrumb: { |
104 | 103 | label: 'customer.dashboards', |
105 | 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 | 85 | fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px"> |
86 | 86 | <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> |
87 | 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 | 113 | </div> |
90 | 114 | </div> |
91 | 115 | </tb-dashboard-toolbar> |
... | ... | @@ -110,7 +134,13 @@ |
110 | 134 | id="tb-main-layout" |
111 | 135 | [ngStyle]="{width: mainLayoutWidth(), |
112 | 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 | 144 | </div> |
115 | 145 | <mat-sidenav-container *ngIf="layouts.right.show" |
116 | 146 | id="tb-right-layout"> |
... | ... | @@ -123,14 +153,23 @@ |
123 | 153 | position="end" |
124 | 154 | [mode]="isMobile ? 'over' : 'side'" |
125 | 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 | 163 | </mat-sidenav> |
128 | 164 | </mat-sidenav-container> |
129 | 165 | </div> |
130 | 166 | <!--tb-details-sidenav TODO --> |
131 | 167 | <!--tb-details-sidenav TODO --> |
132 | 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 | 173 | <button *ngIf="(isTenantAdmin() || isSystemAdmin()) && !forceFullscreen" |
135 | 174 | mat-fab color="accent" class="tb-btn-footer" |
136 | 175 | [ngClass]="{'tb-hide': !isEdit || isAddingWidget}" | ... | ... |
... | ... | @@ -21,15 +21,21 @@ import { AppState } from '@core/core.state'; |
21 | 21 | import { ActivatedRoute, Router } from '@angular/router'; |
22 | 22 | import { UtilsService } from '@core/services/utils.service'; |
23 | 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 | 31 | import { WINDOW } from '@core/services/window.service'; |
26 | 32 | import { WindowMessage } from '@shared/models/window-message.model'; |
27 | 33 | import { deepClone, isDefined } from '@app/core/utils'; |
28 | 34 | import { |
29 | - DashboardContext, | |
35 | + DashboardContext, DashboardPageLayout, | |
30 | 36 | DashboardPageLayoutContext, |
31 | 37 | DashboardPageLayouts, |
32 | - DashboardPageScope | |
38 | + DashboardPageScope, IDashboardController | |
33 | 39 | } from './dashboard-page.models'; |
34 | 40 | import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; |
35 | 41 | import { MediaBreakpoints } from '@shared/models/constants'; |
... | ... | @@ -42,6 +48,9 @@ import { DialogService } from '@core/services/dialog.service'; |
42 | 48 | import { EntityService } from '@core/http/entity.service'; |
43 | 49 | import { AliasController } from '@core/api/alias-controller'; |
44 | 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 | 55 | @Component({ |
47 | 56 | selector: 'tb-dashboard-page', |
... | ... | @@ -49,7 +58,7 @@ import { Subscription } from 'rxjs'; |
49 | 58 | styleUrls: ['./dashboard-page.component.scss'], |
50 | 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 | 63 | authUser: AuthUser = getCurrentAuthUser(this.store); |
55 | 64 | |
... | ... | @@ -94,7 +103,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { |
94 | 103 | widgets: [], |
95 | 104 | widgetLayouts: {}, |
96 | 105 | gridSettings: {}, |
97 | - ignoreLoading: false | |
106 | + ignoreLoading: false, | |
107 | + ctrl: null | |
98 | 108 | } |
99 | 109 | }, |
100 | 110 | right: { |
... | ... | @@ -104,7 +114,8 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { |
104 | 114 | widgets: [], |
105 | 115 | widgetLayouts: {}, |
106 | 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 | 124 | dashboard: null, |
114 | 125 | dashboardTimewindow: null, |
115 | 126 | state: null, |
116 | - stateController: { | |
117 | - openRightLayout: this.openRightLayout.bind(this) | |
118 | - }, | |
127 | + stateController: null, | |
119 | 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 | 152 | private rxSubscriptions = new Array<Subscription>(); |
123 | 153 | |
124 | 154 | get toolbarOpened(): boolean { |
... | ... | @@ -140,6 +170,7 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { |
140 | 170 | private route: ActivatedRoute, |
141 | 171 | private router: Router, |
142 | 172 | private utils: UtilsService, |
173 | + private dashboardUtils: DashboardUtilsService, | |
143 | 174 | private authService: AuthService, |
144 | 175 | private entityService: EntityService, |
145 | 176 | private dialogService: DialogService) { |
... | ... | @@ -379,6 +410,38 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { |
379 | 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 | 445 | public currentDashboardIdChanged(dashboardId: string) { |
383 | 446 | if (!this.widgetEditMode) { |
384 | 447 | if (this.currentDashboardScope === 'customer' && this.authUser.authority === Authority.TENANT_ADMIN) { |
... | ... | @@ -397,6 +460,57 @@ export class DashboardPageComponent extends PageComponent implements OnDestroy { |
397 | 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 | 514 | private setEditMode(isEdit: boolean, revert: boolean) { |
401 | 515 | this.isEdit = isEdit; |
402 | 516 | if (this.isEdit) { | ... | ... |
... | ... | @@ -14,10 +14,11 @@ |
14 | 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 | 18 | import { Widget } from '@app/shared/models/widget.models'; |
19 | 19 | import { Timewindow } from '@shared/models/time/time.models'; |
20 | 20 | import { IAliasController, IStateController } from '@core/api/widget-api.models'; |
21 | +import { ILayoutController } from './layout/layout.models'; | |
21 | 22 | |
22 | 23 | export declare type DashboardPageScope = 'tenant' | 'customer'; |
23 | 24 | |
... | ... | @@ -29,11 +30,18 @@ export interface DashboardContext { |
29 | 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 | 39 | export interface DashboardPageLayoutContext { |
33 | 40 | id: DashboardLayoutId; |
34 | 41 | widgets: Array<Widget>; |
35 | - widgetLayouts: {[id: string]: WidgetLayout}; | |
42 | + widgetLayouts: WidgetLayouts; | |
36 | 43 | gridSettings: GridSettings; |
44 | + ctrl: ILayoutController; | |
37 | 45 | ignoreLoading: boolean; |
38 | 46 | } |
39 | 47 | |
... | ... | @@ -42,7 +50,5 @@ export interface DashboardPageLayout { |
42 | 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 | 26 | import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; |
27 | 27 | import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; |
28 | 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 | 32 | @NgModule({ |
31 | 33 | entryComponents: [ |
... | ... | @@ -40,13 +42,15 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component'; |
40 | 42 | ManageDashboardCustomersDialogComponent, |
41 | 43 | MakeDashboardPublicDialogComponent, |
42 | 44 | DashboardToolbarComponent, |
43 | - DashboardPageComponent | |
45 | + DashboardPageComponent, | |
46 | + DashboardLayoutComponent | |
44 | 47 | ], |
45 | 48 | imports: [ |
46 | 49 | CommonModule, |
47 | 50 | SharedModule, |
48 | 51 | HomeComponentsModule, |
49 | 52 | HomeDialogsModule, |
53 | + StatesControllerModule, | |
50 | 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 | 26 | import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
27 | 27 | import { WidgetService } from '@core/http/widget.service'; |
28 | 28 | import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; |
29 | -import { map } from 'rxjs/operators'; | |
29 | +import { map, share } from 'rxjs/operators'; | |
30 | 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 | 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 | 36 | export interface WidgetEditorData { |
35 | 37 | widgetType: WidgetType; |
... | ... | @@ -52,6 +54,69 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> { |
52 | 54 | } |
53 | 55 | |
54 | 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 | 120 | export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> { |
56 | 121 | |
57 | 122 | constructor(private widgetsService: WidgetService) { |
... | ... | @@ -137,6 +202,9 @@ export const routes: Routes = [ |
137 | 202 | data: { |
138 | 203 | auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], |
139 | 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 | 251 | providers: [ |
184 | 252 | WidgetsBundlesTableConfigResolver, |
185 | 253 | WidgetsBundleResolver, |
254 | + WidgetsTypesDataResolver, | |
186 | 255 | WidgetEditorDataResolver, |
187 | 256 | WidgetEditorAddDataResolver |
188 | 257 | ] | ... | ... |
... | ... | @@ -28,7 +28,8 @@ |
28 | 28 | class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> |
29 | 29 | </section> |
30 | 30 | <tb-dashboard [aliasController]="aliasController" |
31 | - [widgetsData]="widgetsData" | |
31 | + [widgets]="widgetsData.widgets" | |
32 | + [widgetLayouts]="widgetsData.widgetLayouts" | |
32 | 33 | [isEdit]="false" |
33 | 34 | [isEditActionEnabled]="true" |
34 | 35 | [isExportActionEnabled]="true" | ... | ... |
... | ... | @@ -55,7 +55,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { |
55 | 55 | |
56 | 56 | widgetsBundle: WidgetsBundle; |
57 | 57 | |
58 | - widgetTypes$: Observable<Array<Widget>>; | |
58 | + widgetsData: WidgetsData; | |
59 | 59 | |
60 | 60 | footerFabButtons: FooterFabButtons = { |
61 | 61 | fabTogglerName: 'widget.add-widget-type', |
... | ... | @@ -84,8 +84,6 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { |
84 | 84 | onRemoveWidget: this.removeWidgetType.bind(this) |
85 | 85 | }; |
86 | 86 | |
87 | - widgetsData: Observable<WidgetsData>; | |
88 | - | |
89 | 87 | aliasController: IAliasController = new DummyAliasController(); |
90 | 88 | |
91 | 89 | constructor(protected store: Store<AppState>, |
... | ... | @@ -98,70 +96,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { |
98 | 96 | |
99 | 97 | this.authUser = getCurrentAuthUser(store); |
100 | 98 | this.widgetsBundle = this.route.snapshot.data.widgetsBundle; |
99 | + this.widgetsData = this.route.snapshot.data.widgetsData; | |
101 | 100 | if (this.authUser.authority === Authority.TENANT_ADMIN) { |
102 | 101 | this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
103 | 102 | } else { |
104 | 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 | 107 | ngOnInit(): void { | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 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 | 19 | <div class="fab-container"> |
20 | 20 | <button [disabled]="isLoading$ | async" |
21 | 21 | mat-fab class="fab-toggler tb-btn-footer" | ... | ... |
... | ... | @@ -15,12 +15,14 @@ |
15 | 15 | */ |
16 | 16 | |
17 | 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 | 27 | .fab-container { |
26 | 28 | display: flex; |
... | ... | @@ -30,7 +32,6 @@ |
30 | 32 | display: flex; |
31 | 33 | flex-direction: column-reverse; |
32 | 34 | align-items: center; |
33 | - margin-bottom: 5px; | |
34 | 35 | |
35 | 36 | button { |
36 | 37 | margin-bottom: 17px; |
... | ... | @@ -39,6 +40,9 @@ |
39 | 40 | } |
40 | 41 | |
41 | 42 | .tb-btn-footer { |
43 | + &.fab-toggler { | |
44 | + margin-top: 0px; | |
45 | + } | |
42 | 46 | position: relative !important; |
43 | 47 | display: inline-block !important; |
44 | 48 | animation: tbMoveFromBottomFade .3s ease both; | ... | ... |
... | ... | @@ -19,6 +19,7 @@ import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
21 | 21 | import { speedDialFabAnimations } from '@shared/animations/speed-dial-fab.animations'; |
22 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
22 | 23 | |
23 | 24 | export interface FooterFabButton { |
24 | 25 | name: string; |
... | ... | @@ -43,6 +44,15 @@ export class FooterFabButtonsComponent extends PageComponent { |
43 | 44 | @Input() |
44 | 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 | 56 | buttons: Array<FooterFabButton> = []; |
47 | 57 | fabTogglerState = 'inactive'; |
48 | 58 | ... | ... |
... | ... | @@ -48,6 +48,10 @@ export interface GridSettings { |
48 | 48 | columns?: number; |
49 | 49 | margins?: [number, number]; |
50 | 50 | backgroundSizeMode?: string; |
51 | + backgroundImageUrl?: string; | |
52 | + autoFillHeight?: boolean; | |
53 | + mobileAutoFillHeight?: boolean; | |
54 | + mobileRowHeight?: number; | |
51 | 55 | [key: string]: any; |
52 | 56 | // TODO: |
53 | 57 | } |
... | ... | @@ -57,12 +61,17 @@ export interface DashboardLayout { |
57 | 61 | gridSettings: GridSettings; |
58 | 62 | } |
59 | 63 | |
64 | +export interface DashboardLayoutInfo { | |
65 | + widgets?: Array<Widget>; | |
66 | + widgetLayouts?: WidgetLayouts; | |
67 | + gridSettings?: GridSettings; | |
68 | +} | |
69 | + | |
60 | 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 | 76 | export interface DashboardState { |
68 | 77 | name: string; | ... | ... |