Commit b3df9644e6586936e3a0c52ec475004a97c3fc41
1 parent
1c10ede1
Add widget dialog. Dashboard settings. Dashboard layout settings.
Showing
52 changed files
with
2611 additions
and
313 deletions
... | ... | @@ -1349,6 +1349,20 @@ |
1349 | 1349 | "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz", |
1350 | 1350 | "integrity": "sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw==" |
1351 | 1351 | }, |
1352 | + "@flowjs/flow.js": { | |
1353 | + "version": "2.13.2", | |
1354 | + "resolved": "https://registry.npmjs.org/@flowjs/flow.js/-/flow.js-2.13.2.tgz", | |
1355 | + "integrity": "sha512-N2uoQ+F8E/l3JiSoU/hIwUPEjCPDUvWeCJei0S5vA3guqSY8JtgIZacuhNC6B6TYY5cGWGR/qCOSR6v6S/K0aA==" | |
1356 | + }, | |
1357 | + "@flowjs/ngx-flow": { | |
1358 | + "version": "0.4.3", | |
1359 | + "resolved": "https://registry.npmjs.org/@flowjs/ngx-flow/-/ngx-flow-0.4.3.tgz", | |
1360 | + "integrity": "sha512-6k+jLebR1RAoSGt4NHtlVPaGdmGeVocQdgsRAov2OEXcKrAH48yd0FcZI2mNMqLd2zeFyeURKbklqpoCv4gIwg==", | |
1361 | + "requires": { | |
1362 | + "@types/flowjs": "2.13.1", | |
1363 | + "tslib": "^1.9.0" | |
1364 | + } | |
1365 | + }, | |
1352 | 1366 | "@mat-datetimepicker/core": { |
1353 | 1367 | "version": "2.0.1", |
1354 | 1368 | "resolved": "https://registry.npmjs.org/@mat-datetimepicker/core/-/core-2.0.1.tgz", |
... | ... | @@ -1572,6 +1586,11 @@ |
1572 | 1586 | "@types/jquery": "*" |
1573 | 1587 | } |
1574 | 1588 | }, |
1589 | + "@types/flowjs": { | |
1590 | + "version": "2.13.1", | |
1591 | + "resolved": "https://registry.npmjs.org/@types/flowjs/-/flowjs-2.13.1.tgz", | |
1592 | + "integrity": "sha512-cPuORQrWmJV7pmiSt1ApDOsQSooVka53Ugr3LB0MW/bsG/fDtOXSxsT5Aiej98VD3eCIZNyABfk3NBWU7CorsQ==" | |
1593 | + }, | |
1575 | 1594 | "@types/glob": { |
1576 | 1595 | "version": "7.1.1", |
1577 | 1596 | "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", | ... | ... |
... | ... | @@ -25,6 +25,8 @@ |
25 | 25 | "@angular/router": "~8.2.11", |
26 | 26 | "@auth0/angular-jwt": "^3.0.0", |
27 | 27 | "@date-io/date-fns": "^1.3.11", |
28 | + "@flowjs/flow.js": "^2.13.2", | |
29 | + "@flowjs/ngx-flow": "^0.4.3", | |
28 | 30 | "@mat-datetimepicker/core": "^2.0.1", |
29 | 31 | "@material-ui/core": "^4.5.1", |
30 | 32 | "@material-ui/icons": "^4.5.1", | ... | ... |
... | ... | @@ -38,7 +38,7 @@ import { FlexLayoutModule } from '@angular/flex-layout'; |
38 | 38 | import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; |
39 | 39 | import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; |
40 | 40 | import { WINDOW_PROVIDERS } from '@core/services/window.service'; |
41 | -import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component"; | |
41 | +import {TodoDialogComponent} from '@core/services/dialog/todo-dialog.component'; | |
42 | 42 | import { HotkeyModule } from 'angular2-hotkeys'; |
43 | 43 | |
44 | 44 | export function HttpLoaderFactory(http: HttpClient) { | ... | ... |
... | ... | @@ -24,7 +24,7 @@ import { |
24 | 24 | DashboardState, |
25 | 25 | DashboardConfiguration, |
26 | 26 | DashboardLayoutInfo, |
27 | - DashboardLayoutsInfo | |
27 | + DashboardLayoutsInfo, DashboardLayoutId, WidgetLayout, GridSettings | |
28 | 28 | } from '@shared/models/dashboard.models'; |
29 | 29 | import { isUndefined, isDefined, isString } from '@core/utils'; |
30 | 30 | import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models'; |
... | ... | @@ -91,6 +91,7 @@ export class DashboardUtilsService { |
91 | 91 | } else if (state.root) { |
92 | 92 | rootFound = true; |
93 | 93 | } |
94 | + this.validateAndUpdateState(state); | |
94 | 95 | } |
95 | 96 | if (!rootFound) { |
96 | 97 | const firstStateId = Object.keys(states)[0]; |
... | ... | @@ -216,13 +217,17 @@ export class DashboardUtilsService { |
216 | 217 | public createDefaultLayoutData(): DashboardLayout { |
217 | 218 | return { |
218 | 219 | widgets: {}, |
219 | - gridSettings: { | |
220 | - backgroundColor: '#eeeeee', | |
221 | - color: 'rgba(0,0,0,0.870588)', | |
222 | - columns: 24, | |
223 | - margins: [10, 10], | |
224 | - backgroundSizeMode: '100%' | |
225 | - } | |
220 | + gridSettings: this.createDefaultGridSettings() | |
221 | + }; | |
222 | + } | |
223 | + | |
224 | + private createDefaultGridSettings(): GridSettings { | |
225 | + return { | |
226 | + backgroundColor: '#eeeeee', | |
227 | + color: 'rgba(0,0,0,0.870588)', | |
228 | + columns: 24, | |
229 | + margin: 10, | |
230 | + backgroundSizeMode: '100%' | |
226 | 231 | }; |
227 | 232 | } |
228 | 233 | |
... | ... | @@ -240,6 +245,65 @@ export class DashboardUtilsService { |
240 | 245 | }; |
241 | 246 | } |
242 | 247 | |
248 | + private validateAndUpdateState(state: DashboardState) { | |
249 | + if (!state.layouts) { | |
250 | + state.layouts = this.createDefaultLayouts(); | |
251 | + } | |
252 | + for (const l of Object.keys(state.layouts)) { | |
253 | + const layout = state.layouts[l as DashboardLayoutId]; | |
254 | + this.validateAndUpdateLayout(layout); | |
255 | + } | |
256 | + } | |
257 | + | |
258 | + private validateAndUpdateLayout(layout: DashboardLayout) { | |
259 | + if (!layout.gridSettings) { | |
260 | + layout.gridSettings = this.createDefaultGridSettings(); | |
261 | + } | |
262 | + if (layout.gridSettings.margins && layout.gridSettings.margins.length === 2) { | |
263 | + layout.gridSettings.margin = layout.gridSettings.margins[0]; | |
264 | + delete layout.gridSettings.margins; | |
265 | + } | |
266 | + layout.gridSettings.margin = layout.gridSettings.margin || 10; | |
267 | + } | |
268 | + | |
269 | + public setLayouts(dashboard: Dashboard, targetState: string, newLayouts: DashboardStateLayouts) { | |
270 | + const dashboardConfiguration = dashboard.configuration; | |
271 | + const states = dashboardConfiguration.states; | |
272 | + const state = states[targetState]; | |
273 | + let addedCount = 0; | |
274 | + let removedCount = 0; | |
275 | + for (const l of Object.keys(state.layouts)) { | |
276 | + if (!newLayouts[l]) { | |
277 | + removedCount++; | |
278 | + } | |
279 | + } | |
280 | + for (const l of Object.keys(newLayouts)) { | |
281 | + if (!state.layouts[l]) { | |
282 | + addedCount++; | |
283 | + } | |
284 | + } | |
285 | + state.layouts = newLayouts; | |
286 | + const layoutsCount = Object.keys(state.layouts).length; | |
287 | + let newColumns; | |
288 | + if (addedCount) { | |
289 | + for (const l of Object.keys(state.layouts)) { | |
290 | + newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount; | |
291 | + if (newColumns > 0) { | |
292 | + state.layouts[l].gridSettings.columns = newColumns; | |
293 | + } | |
294 | + } | |
295 | + } | |
296 | + if (removedCount) { | |
297 | + for (const l of Object.keys(state.layouts)) { | |
298 | + newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount; | |
299 | + if (newColumns > 0) { | |
300 | + state.layouts[l].gridSettings.columns = newColumns; | |
301 | + } | |
302 | + } | |
303 | + } | |
304 | + this.removeUnusedWidgets(dashboard); | |
305 | + } | |
306 | + | |
243 | 307 | public getRootStateId(states: {[id: string]: DashboardState }): string { |
244 | 308 | for (const stateId of Object.keys(states)) { |
245 | 309 | const state = states[stateId]; |
... | ... | @@ -261,12 +325,12 @@ export class DashboardUtilsService { |
261 | 325 | const layout: DashboardLayout = state.layouts[l]; |
262 | 326 | if (layout) { |
263 | 327 | result[l] = { |
264 | - widgets: [], | |
328 | + widgetIds: [], | |
265 | 329 | widgetLayouts: {}, |
266 | 330 | gridSettings: {} |
267 | 331 | } as DashboardLayoutInfo; |
268 | 332 | for (const id of Object.keys(layout.widgets)) { |
269 | - result[l].widgets.push(allWidgets[id]); | |
333 | + result[l].widgetIds.push(id); | |
270 | 334 | } |
271 | 335 | result[l].widgetLayouts = layout.widgets; |
272 | 336 | result[l].gridSettings = layout.gridSettings; |
... | ... | @@ -289,6 +353,154 @@ export class DashboardUtilsService { |
289 | 353 | return widgetsArray; |
290 | 354 | } |
291 | 355 | |
356 | + public addWidgetToLayout(dashboard: Dashboard, | |
357 | + targetState: string, | |
358 | + targetLayout: DashboardLayoutId, | |
359 | + widget: Widget, | |
360 | + originalColumns?: number, | |
361 | + originalSize?: {sizeX: number, sizeY: number}, | |
362 | + row?: number, | |
363 | + column?: number): void { | |
364 | + const dashboardConfiguration = dashboard.configuration; | |
365 | + const states = dashboardConfiguration.states; | |
366 | + const state = states[targetState]; | |
367 | + const layout = state.layouts[targetLayout]; | |
368 | + const layoutCount = Object.keys(state.layouts).length; | |
369 | + if (!widget.id) { | |
370 | + widget.id = this.utils.guid(); | |
371 | + } | |
372 | + if (!dashboardConfiguration.widgets[widget.id]) { | |
373 | + dashboardConfiguration.widgets[widget.id] = widget; | |
374 | + } | |
375 | + const widgetLayout: WidgetLayout = { | |
376 | + sizeX: originalSize ? originalSize.sizeX : widget.sizeX, | |
377 | + sizeY: originalSize ? originalSize.sizeY : widget.sizeY, | |
378 | + mobileOrder: widget.config.mobileOrder, | |
379 | + mobileHeight: widget.config.mobileHeight | |
380 | + }; | |
381 | + if (isUndefined(originalColumns)) { | |
382 | + originalColumns = 24; | |
383 | + } | |
384 | + const gridSettings = layout.gridSettings; | |
385 | + let columns = 24; | |
386 | + if (gridSettings && gridSettings.columns) { | |
387 | + columns = gridSettings.columns; | |
388 | + } | |
389 | + columns = columns * layoutCount; | |
390 | + if (columns !== originalColumns) { | |
391 | + const ratio = columns / originalColumns; | |
392 | + widgetLayout.sizeX *= ratio; | |
393 | + widgetLayout.sizeY *= ratio; | |
394 | + } | |
395 | + | |
396 | + if (row > -1 && column > - 1) { | |
397 | + widgetLayout.row = row; | |
398 | + widgetLayout.col = column; | |
399 | + } else { | |
400 | + row = 0; | |
401 | + for (const w of Object.keys(layout.widgets)) { | |
402 | + const existingLayout = layout.widgets[w]; | |
403 | + const wRow = existingLayout.row ? existingLayout.row : 0; | |
404 | + const wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1; | |
405 | + const bottom = wRow + wSizeY; | |
406 | + row = Math.max(row, bottom); | |
407 | + } | |
408 | + widgetLayout.row = row; | |
409 | + widgetLayout.col = 0; | |
410 | + } | |
411 | + layout.widgets[widget.id] = widgetLayout; | |
412 | + } | |
413 | + | |
414 | + public removeWidgetFromLayout(dashboard: Dashboard, | |
415 | + targetState: string, | |
416 | + targetLayout: DashboardLayoutId, | |
417 | + widgetId: string) { | |
418 | + const dashboardConfiguration = dashboard.configuration; | |
419 | + const states = dashboardConfiguration.states; | |
420 | + const state = states[targetState]; | |
421 | + const layout = state.layouts[targetLayout]; | |
422 | + delete layout.widgets[widgetId]; | |
423 | + this.removeUnusedWidgets(dashboard); | |
424 | + } | |
425 | + | |
426 | + public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} { | |
427 | + const dashboardConfiguration = dashboard.configuration; | |
428 | + const states = dashboardConfiguration.states; | |
429 | + const stateKeys = Object.keys(states); | |
430 | + if (stateKeys.length === 1) { | |
431 | + const state = states[stateKeys[0]]; | |
432 | + const layouts = state.layouts; | |
433 | + const layoutKeys = Object.keys(layouts); | |
434 | + if (layoutKeys.length === 1) { | |
435 | + return { | |
436 | + state: stateKeys[0], | |
437 | + layout: layoutKeys[0] as DashboardLayoutId | |
438 | + }; | |
439 | + } | |
440 | + } | |
441 | + return null; | |
442 | + } | |
443 | + | |
444 | + public updateLayoutSettings(layout: DashboardLayout, gridSettings: GridSettings) { | |
445 | + const prevGridSettings = layout.gridSettings; | |
446 | + let prevColumns = prevGridSettings ? prevGridSettings.columns : 24; | |
447 | + if (!prevColumns) { | |
448 | + prevColumns = 24; | |
449 | + } | |
450 | + const columns = gridSettings.columns || 24; | |
451 | + const ratio = columns / prevColumns; | |
452 | + layout.gridSettings = gridSettings; | |
453 | + let maxRow = 0; | |
454 | + for (const w of Object.keys(layout.widgets)) { | |
455 | + const widget = layout.widgets[w]; | |
456 | + if (!widget.sizeX) { | |
457 | + widget.sizeX = 1; | |
458 | + } | |
459 | + if (!widget.sizeY) { | |
460 | + widget.sizeY = 1; | |
461 | + } | |
462 | + maxRow = Math.max(maxRow, widget.row + widget.sizeY); | |
463 | + } | |
464 | + const newMaxRow = Math.round(maxRow * ratio); | |
465 | + for (const w of Object.keys(layout.widgets)) { | |
466 | + const widget = layout.widgets[w]; | |
467 | + if (widget.row + widget.sizeY === maxRow) { | |
468 | + widget.row = Math.round(widget.row * ratio); | |
469 | + widget.sizeY = newMaxRow - widget.row; | |
470 | + } else { | |
471 | + widget.row = Math.round(widget.row * ratio); | |
472 | + widget.sizeY = Math.round(widget.sizeY * ratio); | |
473 | + } | |
474 | + widget.sizeX = Math.round(widget.sizeX * ratio); | |
475 | + widget.col = Math.round(widget.col * ratio); | |
476 | + if (widget.col + widget.sizeX > columns) { | |
477 | + widget.sizeX = columns - widget.col; | |
478 | + } | |
479 | + } | |
480 | + } | |
481 | + | |
482 | + private removeUnusedWidgets(dashboard: Dashboard) { | |
483 | + const dashboardConfiguration = dashboard.configuration; | |
484 | + const states = dashboardConfiguration.states; | |
485 | + const widgets = dashboardConfiguration.widgets; | |
486 | + for (const widgetId of Object.keys(widgets)) { | |
487 | + let found = false; | |
488 | + for (const s of Object.keys(states)) { | |
489 | + const state = states[s]; | |
490 | + for (const l of Object.keys(state.layouts)) { | |
491 | + const layout = state.layouts[l]; | |
492 | + if (layout.widgets[widgetId]) { | |
493 | + found = true; | |
494 | + break; | |
495 | + } | |
496 | + } | |
497 | + } | |
498 | + if (!found) { | |
499 | + delete dashboardConfiguration.widgets[widgetId]; | |
500 | + } | |
501 | + } | |
502 | + } | |
503 | + | |
292 | 504 | private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, |
293 | 505 | datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, |
294 | 506 | targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { | ... | ... |
... | ... | @@ -16,20 +16,347 @@ |
16 | 16 | |
17 | 17 | import { Injectable } from '@angular/core'; |
18 | 18 | import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; |
19 | +import { EntityAlias, EntityAliasFilter, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models'; | |
20 | +import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models'; | |
21 | +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | |
22 | +import { deepClone } from '@core/utils'; | |
23 | +import * as equal from 'deep-equal'; | |
24 | +import { UtilsService } from '@core/services/utils.service'; | |
25 | +import { Observable, of, throwError } from 'rxjs'; | |
26 | +import { map } from 'rxjs/operators'; | |
27 | + | |
28 | +const WIDGET_ITEM = 'widget_item'; | |
29 | +const WIDGET_REFERENCE = 'widget_reference'; | |
30 | +const RULE_NODES = 'rule_nodes'; | |
31 | + | |
32 | +export interface AliasesInfo { | |
33 | + datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo}; | |
34 | + targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo}; | |
35 | +} | |
36 | + | |
37 | +export interface WidgetItem { | |
38 | + widget: Widget; | |
39 | + aliasesInfo: AliasesInfo; | |
40 | + originalSize: WidgetSize; | |
41 | + originalColumns: number; | |
42 | +} | |
43 | + | |
44 | +export interface WidgetReference { | |
45 | + dashboardId: string; | |
46 | + sourceState: string; | |
47 | + sourceLayout: DashboardLayoutId; | |
48 | + widgetId: string; | |
49 | + originalSize: WidgetSize; | |
50 | + originalColumns: number; | |
51 | +} | |
19 | 52 | |
20 | 53 | @Injectable({ |
21 | 54 | providedIn: 'root' |
22 | 55 | }) |
23 | 56 | export class ItemBufferService { |
24 | - constructor() {} | |
57 | + | |
58 | + private namespace = 'tbBufferStore'; | |
59 | + private delimiter = '.'; | |
60 | + | |
61 | + constructor(private dashboardUtils: DashboardUtilsService, | |
62 | + private utils: UtilsService) {} | |
63 | + | |
64 | + public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { | |
65 | + const aliasesInfo: AliasesInfo = { | |
66 | + datasourceAliases: {}, | |
67 | + targetDeviceAliases: {} | |
68 | + }; | |
69 | + const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); | |
70 | + const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); | |
71 | + if (widget.config && dashboard.configuration | |
72 | + && dashboard.configuration.entityAliases) { | |
73 | + let entityAlias: EntityAlias; | |
74 | + if (widget.config.datasources) { | |
75 | + for (let i = 0; i < widget.config.datasources.length; i++) { | |
76 | + const datasource = widget.config.datasources[i]; | |
77 | + if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { | |
78 | + entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; | |
79 | + if (entityAlias) { | |
80 | + aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); | |
81 | + } | |
82 | + } | |
83 | + } | |
84 | + } | |
85 | + if (widget.config.targetDeviceAliasIds) { | |
86 | + for (let i = 0; i < widget.config.targetDeviceAliasIds.length; i++) { | |
87 | + const targetDeviceAliasId = widget.config.targetDeviceAliasIds[i]; | |
88 | + if (targetDeviceAliasId) { | |
89 | + entityAlias = dashboard.configuration.entityAliases[targetDeviceAliasId]; | |
90 | + if (entityAlias) { | |
91 | + aliasesInfo.targetDeviceAliases[i] = this.prepareAliasInfo(entityAlias); | |
92 | + } | |
93 | + } | |
94 | + } | |
95 | + } | |
96 | + } | |
97 | + return { | |
98 | + widget, | |
99 | + aliasesInfo, | |
100 | + originalSize, | |
101 | + originalColumns | |
102 | + }; | |
103 | + } | |
104 | + | |
105 | + public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { | |
106 | + const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); | |
107 | + this.storeSet(WIDGET_ITEM, JSON.stringify(widgetItem)); | |
108 | + } | |
109 | + | |
110 | + public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { | |
111 | + const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); | |
112 | + this.storeSet(WIDGET_REFERENCE, JSON.stringify(widgetReference)); | |
113 | + } | |
25 | 114 | |
26 | 115 | public hasWidget(): boolean { |
27 | - // TODO: | |
28 | - return false; | |
116 | + return this.storeHas(WIDGET_ITEM); | |
29 | 117 | } |
30 | 118 | |
31 | 119 | public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { |
32 | - // TODO: | |
120 | + const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE); | |
121 | + if (widgetReferenceJson) { | |
122 | + const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson); | |
123 | + if (widgetReference.dashboardId === dashboard.id.id) { | |
124 | + if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) | |
125 | + && dashboard.configuration.widgets[widgetReference.widgetId]) { | |
126 | + return true; | |
127 | + } | |
128 | + } | |
129 | + } | |
33 | 130 | return false; |
34 | 131 | } |
132 | + | |
133 | + public pasteWidget(targetDashboard: Dashboard, targetState: string, | |
134 | + targetLayout: DashboardLayoutId, position: WidgetPosition, | |
135 | + onAliasesUpdateFunction: () => void): Observable<Widget> { | |
136 | + const widgetItemJson = this.storeGet(WIDGET_ITEM); | |
137 | + if (widgetItemJson) { | |
138 | + const widgetItem: WidgetItem = JSON.parse(widgetItemJson); | |
139 | + const widget = widgetItem.widget; | |
140 | + const aliasesInfo = widgetItem.aliasesInfo; | |
141 | + const originalColumns = widgetItem.originalColumns; | |
142 | + const originalSize = widgetItem.originalSize; | |
143 | + let targetRow = -1; | |
144 | + let targetColumn = -1; | |
145 | + if (position) { | |
146 | + targetRow = position.row; | |
147 | + targetColumn = position.column; | |
148 | + } | |
149 | + widget.id = this.utils.guid(); | |
150 | + return this.addWidgetToDashboard(targetDashboard, targetState, | |
151 | + targetLayout, widget, aliasesInfo, | |
152 | + onAliasesUpdateFunction, originalColumns, | |
153 | + originalSize, targetRow, targetColumn).pipe( | |
154 | + map(() => widget) | |
155 | + ); | |
156 | + } else { | |
157 | + return throwError('Failed to read widget from buffer!'); | |
158 | + } | |
159 | + } | |
160 | + | |
161 | + public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, | |
162 | + targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> { | |
163 | + const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE); | |
164 | + if (widgetReferenceJson) { | |
165 | + const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson); | |
166 | + const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; | |
167 | + if (widget) { | |
168 | + const originalColumns = widgetReference.originalColumns; | |
169 | + const originalSize = widgetReference.originalSize; | |
170 | + let targetRow = -1; | |
171 | + let targetColumn = -1; | |
172 | + if (position) { | |
173 | + targetRow = position.row; | |
174 | + targetColumn = position.column; | |
175 | + } | |
176 | + return this.addWidgetToDashboard(targetDashboard, targetState, | |
177 | + targetLayout, widget, null, | |
178 | + null, originalColumns, | |
179 | + originalSize, targetRow, targetColumn).pipe( | |
180 | + map(() => widget) | |
181 | + ); | |
182 | + } else { | |
183 | + return throwError('Failed to read widget reference from buffer!'); | |
184 | + } | |
185 | + } else { | |
186 | + return throwError('Failed to read widget reference from buffer!'); | |
187 | + } | |
188 | + } | |
189 | + | |
190 | + public addWidgetToDashboard(dashboard: Dashboard, targetState: string, | |
191 | + targetLayout: DashboardLayoutId, widget: Widget, | |
192 | + aliasesInfo: AliasesInfo, | |
193 | + onAliasesUpdateFunction: () => void, | |
194 | + originalColumns: number, | |
195 | + originalSize: WidgetSize, | |
196 | + row: number, | |
197 | + column: number): Observable<Dashboard> { | |
198 | + let theDashboard: Dashboard; | |
199 | + if (dashboard) { | |
200 | + theDashboard = dashboard; | |
201 | + } else { | |
202 | + theDashboard = {}; | |
203 | + } | |
204 | + theDashboard = this.dashboardUtils.validateAndUpdateDashboard(theDashboard); | |
205 | + let callAliasUpdateFunction = false; | |
206 | + if (aliasesInfo) { | |
207 | + const newEntityAliases = this.updateAliases(theDashboard, widget, aliasesInfo); | |
208 | + const aliasesUpdated = !equal(newEntityAliases, theDashboard.configuration.entityAliases); | |
209 | + if (aliasesUpdated) { | |
210 | + theDashboard.configuration.entityAliases = newEntityAliases; | |
211 | + if (onAliasesUpdateFunction) { | |
212 | + callAliasUpdateFunction = true; | |
213 | + } | |
214 | + } | |
215 | + } | |
216 | + this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, | |
217 | + originalColumns, originalSize, row, column); | |
218 | + if (callAliasUpdateFunction) { | |
219 | + onAliasesUpdateFunction(); | |
220 | + } | |
221 | + return of(theDashboard); | |
222 | + } | |
223 | + | |
224 | + private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { | |
225 | + let originalColumns = 24; | |
226 | + let gridSettings = null; | |
227 | + const state = dashboard.configuration.states[sourceState]; | |
228 | + const layoutCount = Object.keys(state.layouts).length; | |
229 | + if (state) { | |
230 | + const layout = state.layouts[sourceLayout]; | |
231 | + if (layout) { | |
232 | + gridSettings = layout.gridSettings; | |
233 | + | |
234 | + } | |
235 | + } | |
236 | + if (gridSettings && | |
237 | + gridSettings.columns) { | |
238 | + originalColumns = gridSettings.columns; | |
239 | + } | |
240 | + originalColumns = originalColumns * layoutCount; | |
241 | + return originalColumns; | |
242 | + } | |
243 | + | |
244 | + private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetSize { | |
245 | + const layout = dashboard.configuration.states[sourceState].layouts[sourceLayout]; | |
246 | + const widgetLayout = layout.widgets[widget.id]; | |
247 | + return { | |
248 | + sizeX: widgetLayout.sizeX, | |
249 | + sizeY: widgetLayout.sizeY | |
250 | + }; | |
251 | + } | |
252 | + | |
253 | + private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo { | |
254 | + return { | |
255 | + alias: entityAlias.alias, | |
256 | + filter: entityAlias.filter | |
257 | + }; | |
258 | + } | |
259 | + | |
260 | + private prepareWidgetReference(dashboard: Dashboard, sourceState: string, | |
261 | + sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference { | |
262 | + const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); | |
263 | + const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); | |
264 | + return { | |
265 | + dashboardId: dashboard.id.id, | |
266 | + sourceState, | |
267 | + sourceLayout, | |
268 | + widgetId: widget.id, | |
269 | + originalSize, | |
270 | + originalColumns | |
271 | + }; | |
272 | + } | |
273 | + | |
274 | + private updateAliases(dashboard: Dashboard, widget: Widget, aliasesInfo: AliasesInfo): EntityAliases { | |
275 | + const entityAliases = deepClone(dashboard.configuration.entityAliases); | |
276 | + let aliasInfo: EntityAliasInfo; | |
277 | + let newAliasId: string; | |
278 | + for (const datasourceIndexStr of Object.keys(aliasesInfo.datasourceAliases)) { | |
279 | + const datasourceIndex = Number(datasourceIndexStr); | |
280 | + aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex]; | |
281 | + newAliasId = this.getEntityAliasId(entityAliases, aliasInfo); | |
282 | + widget.config.datasources[datasourceIndex].entityAliasId = newAliasId; | |
283 | + } | |
284 | + for (const targetDeviceAliasIndexStr of Object.keys(aliasesInfo.targetDeviceAliases)) { | |
285 | + const targetDeviceAliasIndex = Number(targetDeviceAliasIndexStr); | |
286 | + aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex]; | |
287 | + newAliasId = this.getEntityAliasId(entityAliases, aliasInfo); | |
288 | + widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId; | |
289 | + } | |
290 | + return entityAliases; | |
291 | + } | |
292 | + | |
293 | + private isEntityAliasEqual(alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean { | |
294 | + return equal(alias1.filter, alias2.filter); | |
295 | + } | |
296 | + | |
297 | + private getEntityAliasId(entityAliases: EntityAliases, aliasInfo: EntityAliasInfo): string { | |
298 | + let newAliasId: string; | |
299 | + for (const aliasId of Object.keys(entityAliases)) { | |
300 | + if (this.isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) { | |
301 | + newAliasId = aliasId; | |
302 | + break; | |
303 | + } | |
304 | + } | |
305 | + if (!newAliasId) { | |
306 | + const newAliasName = this.createEntityAliasName(entityAliases, aliasInfo.alias); | |
307 | + newAliasId = this.utils.guid(); | |
308 | + entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter}; | |
309 | + } | |
310 | + return newAliasId; | |
311 | + } | |
312 | + | |
313 | + private createEntityAliasName(entityAliases: EntityAliases, alias: string): string { | |
314 | + let c = 0; | |
315 | + let newAlias = alias; | |
316 | + let unique = false; | |
317 | + while (!unique) { | |
318 | + unique = true; | |
319 | + for (const entAliasId of Object.keys(entityAliases)) { | |
320 | + const entAlias = entityAliases[entAliasId]; | |
321 | + if (newAlias === entAlias.alias) { | |
322 | + c++; | |
323 | + newAlias = alias + c; | |
324 | + unique = false; | |
325 | + } | |
326 | + } | |
327 | + } | |
328 | + return newAlias; | |
329 | + } | |
330 | + | |
331 | + private storeSet(key: string, elem: any) { | |
332 | + localStorage.setItem(this.getNamespacedKey(key), JSON.stringify(elem)); | |
333 | + } | |
334 | + | |
335 | + private storeGet(key: string): any { | |
336 | + let obj = null; | |
337 | + const saved = localStorage.getItem(this.getNamespacedKey(key)); | |
338 | + try { | |
339 | + if (typeof saved === 'undefined' || saved === 'undefined') { | |
340 | + obj = undefined; | |
341 | + } else { | |
342 | + obj = JSON.parse(saved); | |
343 | + } | |
344 | + } catch (e) { | |
345 | + this.storeRemove(key); | |
346 | + } | |
347 | + return obj; | |
348 | + } | |
349 | + | |
350 | + private storeHas(key: string): boolean { | |
351 | + const saved = localStorage.getItem(this.getNamespacedKey(key)); | |
352 | + return typeof saved !== 'undefined' && saved !== 'undefined' && saved !== null; | |
353 | + } | |
354 | + | |
355 | + private storeRemove(key: string) { | |
356 | + localStorage.removeItem(this.getNamespacedKey(key)); | |
357 | + } | |
358 | + | |
359 | + private getNamespacedKey(key: string): string { | |
360 | + return [this.namespace, key].join(this.delimiter); | |
361 | + } | |
35 | 362 | } | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | import { Inject, Injectable, NgZone } from '@angular/core'; |
18 | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | 19 | import { ExceptionData } from '@app/shared/models/error.models'; |
20 | -import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; | |
20 | +import { deepClone, deleteNullProperties, guid, isDefined, isUndefined } 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'; |
... | ... | @@ -263,13 +263,7 @@ export class UtilsService { |
263 | 263 | } |
264 | 264 | |
265 | 265 | public guid(): string { |
266 | - function s4(): string { | |
267 | - return Math.floor((1 + Math.random()) * 0x10000) | |
268 | - .toString(16) | |
269 | - .substring(1); | |
270 | - } | |
271 | - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + | |
272 | - s4() + '-' + s4() + s4() + s4(); | |
266 | + return guid(); | |
273 | 267 | } |
274 | 268 | |
275 | 269 | public validateDatasources(datasources: Array<Datasource>): Array<Datasource> { | ... | ... |
... | ... | @@ -353,3 +353,13 @@ export function deepClone<T>(target: T): T { |
353 | 353 | } |
354 | 354 | return target; |
355 | 355 | } |
356 | + | |
357 | +export function guid(): string { | |
358 | + function s4(): string { | |
359 | + return Math.floor((1 + Math.random()) * 0x10000) | |
360 | + .toString(16) | |
361 | + .substring(1); | |
362 | + } | |
363 | + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + | |
364 | + s4() + '-' + s4() + s4() + s4(); | |
365 | +} | ... | ... |
... | ... | @@ -17,6 +17,7 @@ |
17 | 17 | --> |
18 | 18 | <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" |
19 | 19 | [ngStyle]="dashboardStyle" |
20 | + [style.backgroundImage]="backgroundImage" | |
20 | 21 | [fxShow]="(((isLoading$ | async) && !this.ignoreLoading) || this.dashboardLoading) && !isEdit"> |
21 | 22 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> |
22 | 23 | </mat-spinner> |
... | ... | @@ -114,7 +115,7 @@ |
114 | 115 | </button> |
115 | 116 | <button mat-button mat-icon-button |
116 | 117 | [fxShow]="!isEdit && widget.enableFullscreen" |
117 | - (click)="widget.isFullscreen = !widget.isFullscreen" | |
118 | + (click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen" | |
118 | 119 | matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
119 | 120 | matTooltipPosition="above"> |
120 | 121 | <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ... | ... |
... | ... | @@ -15,13 +15,14 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { |
18 | - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, | |
18 | + AfterViewInit, | |
19 | 19 | Component, |
20 | 20 | DoCheck, |
21 | 21 | Input, |
22 | 22 | IterableDiffers, |
23 | - KeyValueDiffers, NgZone, | |
23 | + NgZone, | |
24 | 24 | OnChanges, |
25 | + OnDestroy, | |
25 | 26 | OnInit, |
26 | 27 | SimpleChanges, |
27 | 28 | ViewChild |
... | ... | @@ -33,35 +34,35 @@ import { AuthUser } from '@shared/models/user.model'; |
33 | 34 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
34 | 35 | import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; |
35 | 36 | import { TimeService } from '@core/services/time.service'; |
36 | -import { GridsterComponent, GridsterConfig } from 'angular-gridster2'; | |
37 | +import { GridsterComponent, GridsterComponentInterface, GridsterConfig } from 'angular-gridster2'; | |
37 | 38 | import { |
38 | 39 | DashboardCallbacks, |
39 | 40 | DashboardWidget, |
40 | 41 | DashboardWidgets, |
41 | - IDashboardComponent, | |
42 | - WidgetPosition | |
42 | + IDashboardComponent | |
43 | 43 | } from '../../models/dashboard-component.models'; |
44 | -import { ReplaySubject, Subject } from 'rxjs'; | |
45 | -import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; | |
44 | +import { ReplaySubject, Subject, Subscription } from 'rxjs'; | |
45 | +import { WidgetLayouts } from '@shared/models/dashboard.models'; | |
46 | 46 | import { DialogService } from '@core/services/dialog.service'; |
47 | 47 | import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; |
48 | 48 | import { BreakpointObserver } from '@angular/cdk/layout'; |
49 | 49 | import { MediaBreakpoints } from '@shared/models/constants'; |
50 | 50 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
51 | -import { Widget } from '@app/shared/models/widget.models'; | |
51 | +import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; | |
52 | 52 | import { MatMenuTrigger } from '@angular/material'; |
53 | +import { SafeStyle } from '@angular/platform-browser'; | |
53 | 54 | |
54 | 55 | @Component({ |
55 | 56 | selector: 'tb-dashboard', |
56 | 57 | templateUrl: './dashboard.component.html', |
57 | 58 | styleUrls: ['./dashboard.component.scss'] |
58 | 59 | }) |
59 | -export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, AfterViewInit, OnChanges { | |
60 | +export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, OnDestroy, AfterViewInit, OnChanges { | |
60 | 61 | |
61 | 62 | authUser: AuthUser; |
62 | 63 | |
63 | 64 | @Input() |
64 | - widgets: Array<Widget>; | |
65 | + widgets: Iterable<Widget>; | |
65 | 66 | |
66 | 67 | @Input() |
67 | 68 | widgetLayouts: WidgetLayouts; |
... | ... | @@ -79,10 +80,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
79 | 80 | columns: number; |
80 | 81 | |
81 | 82 | @Input() |
82 | - horizontalMargin: number; | |
83 | - | |
84 | - @Input() | |
85 | - verticalMargin: number; | |
83 | + margin: number; | |
86 | 84 | |
87 | 85 | @Input() |
88 | 86 | isEdit: boolean; |
... | ... | @@ -115,6 +113,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
115 | 113 | dashboardStyle: {[klass: string]: any}; |
116 | 114 | |
117 | 115 | @Input() |
116 | + backgroundImage: SafeStyle | string; | |
117 | + | |
118 | + @Input() | |
118 | 119 | dashboardClass: string; |
119 | 120 | |
120 | 121 | @Input() |
... | ... | @@ -153,16 +154,20 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
153 | 154 | dashboardWidgets = new DashboardWidgets(this, |
154 | 155 | this.differs.find([]).create<Widget>((index, item) => { |
155 | 156 | return item; |
156 | - }), | |
157 | - this.kvDiffers.find([]).create<string, WidgetLayout>() | |
157 | + }) | |
158 | 158 | ); |
159 | 159 | |
160 | + breakpointObserverSubscription: Subscription; | |
161 | + | |
162 | + private optionsChangeNotificationsPaused = false; | |
163 | + | |
164 | + private gridsterResizeListener = null; | |
165 | + | |
160 | 166 | constructor(protected store: Store<AppState>, |
161 | 167 | private timeService: TimeService, |
162 | 168 | private dialogService: DialogService, |
163 | 169 | private breakpointObserver: BreakpointObserver, |
164 | 170 | private differs: IterableDiffers, |
165 | - private kvDiffers: KeyValueDiffers, | |
166 | 171 | private ngZone: NgZone) { |
167 | 172 | super(store); |
168 | 173 | this.authUser = getCurrentAuthUser(store); |
... | ... | @@ -180,10 +185,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
180 | 185 | maxRows: 100, |
181 | 186 | minCols: this.columns ? this.columns : 24, |
182 | 187 | outerMargin: true, |
183 | - outerMarginLeft: this.horizontalMargin ? this.horizontalMargin : 10, | |
184 | - outerMarginRight: this.horizontalMargin ? this.horizontalMargin : 10, | |
185 | - outerMarginTop: this.verticalMargin ? this.verticalMargin : 10, | |
186 | - outerMarginBottom: this.horizontalMargin ? this.horizontalMargin : 10, | |
188 | + margin: this.margin ? this.margin : 10, | |
187 | 189 | minItemCols: 1, |
188 | 190 | minItemRows: 1, |
189 | 191 | defaultItemCols: 8, |
... | ... | @@ -198,7 +200,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
198 | 200 | |
199 | 201 | this.updateMobileOpts(); |
200 | 202 | |
201 | - this.breakpointObserver | |
203 | + this.breakpointObserverSubscription = this.breakpointObserver | |
202 | 204 | .observe(MediaBreakpoints['gt-sm']).subscribe( |
203 | 205 | () => { |
204 | 206 | this.updateMobileOpts(); |
... | ... | @@ -209,6 +211,18 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
209 | 211 | this.updateWidgets(); |
210 | 212 | } |
211 | 213 | |
214 | + ngOnDestroy(): void { | |
215 | + super.ngOnDestroy(); | |
216 | + if (this.gridsterResizeListener) { | |
217 | + // @ts-ignore | |
218 | + removeResizeListener(this.gridster.el, this.gridsterResizeListener); | |
219 | + } | |
220 | + if (this.breakpointObserverSubscription) { | |
221 | + this.breakpointObserverSubscription.unsubscribe(); | |
222 | + } | |
223 | + this.gridster = null; | |
224 | + } | |
225 | + | |
212 | 226 | ngDoCheck() { |
213 | 227 | this.dashboardWidgets.doCheck(); |
214 | 228 | } |
... | ... | @@ -223,7 +237,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
223 | 237 | if (!change.firstChange && change.currentValue !== change.previousValue) { |
224 | 238 | if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { |
225 | 239 | updateMobileOpts = true; |
226 | - } else if (['horizontalMargin', 'verticalMargin'].includes(propName)) { | |
240 | + } else if (['margin', 'columns'].includes(propName)) { | |
227 | 241 | updateLayoutOpts = true; |
228 | 242 | } else if (propName === 'isEdit') { |
229 | 243 | updateEditingOpts = true; |
... | ... | @@ -256,7 +270,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
256 | 270 | this.dashboardLoading = false; |
257 | 271 | } |
258 | 272 | |
273 | + private updateWidgetLayouts() { | |
274 | + this.dashboardWidgets.widgetLayoutsUpdated(); | |
275 | + } | |
276 | + | |
259 | 277 | ngAfterViewInit(): void { |
278 | + this.gridsterResizeListener = this.onGridsterParentResize.bind(this); | |
279 | + // @ts-ignore | |
280 | + addResizeListener(this.gridster.el, this.gridsterResizeListener); | |
260 | 281 | } |
261 | 282 | |
262 | 283 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { |
... | ... | @@ -305,7 +326,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
305 | 326 | |
306 | 327 | openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { |
307 | 328 | if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { |
308 | - const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.widgetIndex); | |
329 | + const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget); | |
309 | 330 | if (items && items.length) { |
310 | 331 | $event.preventDefault(); |
311 | 332 | $event.stopPropagation(); |
... | ... | @@ -324,13 +345,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
324 | 345 | |
325 | 346 | widgetMouseDown($event: Event, widget: DashboardWidget) { |
326 | 347 | if (this.callbacks && this.callbacks.onWidgetMouseDown) { |
327 | - this.callbacks.onWidgetMouseDown($event, widget.widget, widget.widgetIndex); | |
348 | + this.callbacks.onWidgetMouseDown($event, widget.widget); | |
328 | 349 | } |
329 | 350 | } |
330 | 351 | |
331 | 352 | widgetClicked($event: Event, widget: DashboardWidget) { |
332 | 353 | if (this.callbacks && this.callbacks.onWidgetClicked) { |
333 | - this.callbacks.onWidgetClicked($event, widget.widget, widget.widgetIndex); | |
354 | + this.callbacks.onWidgetClicked($event, widget.widget); | |
334 | 355 | } |
335 | 356 | } |
336 | 357 | |
... | ... | @@ -339,7 +360,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
339 | 360 | $event.stopPropagation(); |
340 | 361 | } |
341 | 362 | if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { |
342 | - this.callbacks.onEditWidget($event, widget.widget, widget.widgetIndex); | |
363 | + this.callbacks.onEditWidget($event, widget.widget); | |
343 | 364 | } |
344 | 365 | } |
345 | 366 | |
... | ... | @@ -348,7 +369,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
348 | 369 | $event.stopPropagation(); |
349 | 370 | } |
350 | 371 | if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { |
351 | - this.callbacks.onExportWidget($event, widget.widget, widget.widgetIndex); | |
372 | + this.callbacks.onExportWidget($event, widget.widget); | |
352 | 373 | } |
353 | 374 | } |
354 | 375 | |
... | ... | @@ -357,19 +378,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
357 | 378 | $event.stopPropagation(); |
358 | 379 | } |
359 | 380 | if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { |
360 | - this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex); | |
381 | + this.callbacks.onRemoveWidget($event, widget.widget); | |
361 | 382 | } |
362 | 383 | } |
363 | 384 | |
364 | - highlightWidget(index: number, delay?: number) { | |
365 | - const highlighted = this.dashboardWidgets.highlightWidget(index); | |
385 | + highlightWidget(widgetId: string, delay?: number) { | |
386 | + const highlighted = this.dashboardWidgets.highlightWidget(widgetId); | |
366 | 387 | if (highlighted) { |
367 | 388 | this.scrollToWidget(highlighted, delay); |
368 | 389 | } |
369 | 390 | } |
370 | 391 | |
371 | - selectWidget(index: number, delay?: number) { | |
372 | - const selected = this.dashboardWidgets.selectWidget(index); | |
392 | + selectWidget(widgetId: string, delay?: number) { | |
393 | + const selected = this.dashboardWidgets.selectWidget(widgetId); | |
373 | 394 | if (selected) { |
374 | 395 | this.scrollToWidget(selected, delay); |
375 | 396 | } |
... | ... | @@ -385,15 +406,16 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
385 | 406 | row: 0, |
386 | 407 | column: 0 |
387 | 408 | }; |
388 | - const parentElement = this.gridster.el as HTMLElement; | |
409 | + const parentElement = $(this.gridster.el); | |
389 | 410 | let pageX = 0; |
390 | 411 | let pageY = 0; |
391 | 412 | if (event instanceof MouseEvent) { |
392 | 413 | pageX = event.pageX; |
393 | 414 | pageY = event.pageY; |
394 | 415 | } |
395 | - const x = pageX - parentElement.offsetLeft + parentElement.scrollLeft; | |
396 | - const y = pageY - parentElement.offsetTop + parentElement.scrollTop; | |
416 | + const offset = parentElement.offset(); | |
417 | + const x = pageX - offset.left + parentElement.scrollLeft(); | |
418 | + const y = pageY - offset.top + parentElement.scrollTop(); | |
397 | 419 | pos.row = this.gridster.pixelsToPositionY(y, Math.floor); |
398 | 420 | pos.column = this.gridster.pixelsToPositionX(x, Math.floor); |
399 | 421 | return pos; |
... | ... | @@ -434,26 +456,33 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
434 | 456 | }); |
435 | 457 | } |
436 | 458 | |
437 | - private updateMobileOpts() { | |
459 | + private updateMobileOpts(parentHeight?: number) { | |
438 | 460 | this.isMobileSize = this.checkIsMobileSize(); |
461 | + const autofillHeight = this.isAutofillHeight(); | |
462 | + if (autofillHeight) { | |
463 | + this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'fit'; | |
464 | + } else { | |
465 | + this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical'; | |
466 | + } | |
439 | 467 | const mobileBreakPoint = this.isMobileSize ? 20000 : 0; |
440 | 468 | this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; |
441 | - const rowSize = this.detectRowSize(this.isMobileSize); | |
469 | + const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight); | |
442 | 470 | if (this.gridsterOpts.fixedRowHeight !== rowSize) { |
443 | 471 | this.gridsterOpts.fixedRowHeight = rowSize; |
444 | 472 | } |
445 | - if (this.isAutofillHeight()) { | |
446 | - this.gridsterOpts.gridType = 'fit'; | |
447 | - } else { | |
448 | - this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical'; | |
473 | + } | |
474 | + | |
475 | + private onGridsterParentResize() { | |
476 | + const parentHeight = this.gridster.el.offsetHeight; | |
477 | + if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) { | |
478 | + this.updateMobileOpts(parentHeight); | |
479 | + this.notifyGridsterOptionsChanged(); | |
449 | 480 | } |
450 | 481 | } |
451 | 482 | |
452 | 483 | private updateLayoutOpts() { |
453 | - this.gridsterOpts.outerMarginLeft = this.horizontalMargin ? this.horizontalMargin : 10; | |
454 | - this.gridsterOpts.outerMarginRight = this.horizontalMargin ? this.horizontalMargin : 10; | |
455 | - this.gridsterOpts.outerMarginTop = this.verticalMargin ? this.verticalMargin : 10; | |
456 | - this.gridsterOpts.outerMarginBottom = this.horizontalMargin ? this.horizontalMargin : 10; | |
484 | + this.gridsterOpts.minCols = this.columns ? this.columns : 24; | |
485 | + this.gridsterOpts.margin = this.margin ? this.margin : 10; | |
457 | 486 | } |
458 | 487 | |
459 | 488 | private updateEditingOpts() { |
... | ... | @@ -462,17 +491,42 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
462 | 491 | } |
463 | 492 | |
464 | 493 | public notifyGridsterOptionsChanged() { |
465 | - if (this.gridster && this.gridster.options) { | |
466 | - this.gridster.optionsChanged(); | |
494 | + if (!this.optionsChangeNotificationsPaused) { | |
495 | + if (this.gridster && this.gridster.options) { | |
496 | + this.gridster.optionsChanged(); | |
497 | + } | |
467 | 498 | } |
468 | 499 | } |
469 | 500 | |
470 | - private detectRowSize(isMobile: boolean): number | null { | |
501 | + public pauseChangeNotifications() { | |
502 | + this.optionsChangeNotificationsPaused = true; | |
503 | + } | |
504 | + | |
505 | + public resumeChangeNotifications() { | |
506 | + this.optionsChangeNotificationsPaused = false; | |
507 | + } | |
508 | + | |
509 | + public notifyLayoutUpdated() { | |
510 | + this.updateWidgetLayouts(); | |
511 | + } | |
512 | + | |
513 | + private detectRowSize(isMobile: boolean, autofillHeight: boolean, parentHeight?: number): number | null { | |
471 | 514 | let rowHeight = null; |
472 | - if (!this.isAutofillHeight()) { | |
515 | + if (!autofillHeight) { | |
473 | 516 | if (isMobile) { |
474 | 517 | rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70; |
475 | 518 | } |
519 | + } else if (autofillHeight && isMobile) { | |
520 | + if (!parentHeight) { | |
521 | + parentHeight = this.gridster.el.offsetHeight; | |
522 | + } | |
523 | + if (parentHeight) { | |
524 | + let totalRows = 0; | |
525 | + for (const widget of this.dashboardWidgets.dashboardWidgets) { | |
526 | + totalRows += widget.rows; | |
527 | + } | |
528 | + rowHeight = (parentHeight - this.gridsterOpts.margin * (this.dashboardWidgets.dashboardWidgets.length + 2)) / totalRows; | |
529 | + } | |
476 | 530 | } |
477 | 531 | return rowHeight; |
478 | 532 | } | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | --> |
18 | 18 | <header> |
19 | 19 | <mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}"> |
20 | - <div fxFlex fxLayout="row" fxLayoutAlign="start center"> | |
20 | + <div fxFlex fxLayout="row" fxLayoutAlign="start center" style="height: 100%;"> | |
21 | 21 | <div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start"> |
22 | 22 | <span class="tb-details-title">{{ headerTitle }}</span> |
23 | 23 | <span class="tb-details-subtitle">{{ headerSubtitle }}</span> | ... | ... |
... | ... | @@ -20,6 +20,9 @@ |
20 | 20 | height: 100%; |
21 | 21 | display: flex; |
22 | 22 | flex-direction: column; |
23 | +} | |
24 | + | |
25 | +:host ::ng-deep { | |
23 | 26 | .mat-toolbar-tools { |
24 | 27 | height: 100%; |
25 | 28 | min-height: 100px; |
... | ... | @@ -50,4 +53,9 @@ |
50 | 53 | opacity: .8; |
51 | 54 | } |
52 | 55 | |
56 | + tb-dashboard { | |
57 | + .tb-dashboard-content { | |
58 | + background-color: $primary-hue-3 !important; | |
59 | + } | |
60 | + } | |
53 | 61 | } | ... | ... |
... | ... | @@ -22,12 +22,14 @@ |
22 | 22 | <span class="tb-entity-table-title" translate>widget-config.actions</span> |
23 | 23 | <span fxFlex></span> |
24 | 24 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
25 | + type="button" | |
25 | 26 | (click)="addAction($event)" |
26 | 27 | matTooltip="{{ 'widget-config.add-action' | translate }}" |
27 | 28 | matTooltipPosition="above"> |
28 | 29 | <mat-icon>add</mat-icon> |
29 | 30 | </button> |
30 | 31 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" |
32 | + type="button" | |
31 | 33 | matTooltip="{{ 'action.search' | translate }}" |
32 | 34 | matTooltipPosition="above"> |
33 | 35 | <mat-icon>search</mat-icon> |
... | ... | @@ -37,6 +39,7 @@ |
37 | 39 | <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> |
38 | 40 | <div class="mat-toolbar-tools"> |
39 | 41 | <button mat-button mat-icon-button |
42 | + type="button" | |
40 | 43 | matTooltip="{{ 'widget-config.search-actions' | translate }}" |
41 | 44 | matTooltipPosition="above"> |
42 | 45 | <mat-icon>search</mat-icon> |
... | ... | @@ -48,6 +51,7 @@ |
48 | 51 | placeholder="{{ 'widget-config.search-actions' | translate }}"/> |
49 | 52 | </mat-form-field> |
50 | 53 | <button mat-button mat-icon-button (click)="exitFilterMode()" |
54 | + type="button" | |
51 | 55 | matTooltip="{{ 'action.close' | translate }}" |
52 | 56 | matTooltipPosition="above"> |
53 | 57 | <mat-icon>close</mat-icon> |
... | ... | @@ -87,12 +91,14 @@ |
87 | 91 | <mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> |
88 | 92 | <div fxFlex fxLayout="row" fxLayoutAlign="end"> |
89 | 93 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
94 | + type="button" | |
90 | 95 | matTooltip="{{ 'widget-config.edit-action' | translate }}" |
91 | 96 | matTooltipPosition="above" |
92 | 97 | (click)="editAction($event, action)"> |
93 | 98 | <mat-icon>edit</mat-icon> |
94 | 99 | </button> |
95 | 100 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
101 | + type="button" | |
96 | 102 | matTooltip="{{ 'widget-config.delete-action' | translate }}" |
97 | 103 | matTooltipPosition="above" |
98 | 104 | (click)="deleteAction($event, action)"> | ... | ... |
... | ... | @@ -29,7 +29,7 @@ |
29 | 29 | <div class="tb-color-preview" (click)="showColorPicker(key)" style="margin-right: 5px;"> |
30 | 30 | <div class="tb-color-result" [ngStyle]="{background: key.color}"></div> |
31 | 31 | </div> |
32 | - <div fxLayout="row"> | |
32 | + <div style="flex: 1; min-width: 0px;" fxLayout="row"> | |
33 | 33 | <div class="tb-chip-label"> |
34 | 34 | <span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm"> |
35 | 35 | <span *ngIf="key.type === dataKeyTypes.attribute" |
... | ... | @@ -54,7 +54,9 @@ |
54 | 54 | </ng-template> |
55 | 55 | </div> |
56 | 56 | </div> |
57 | - <button *ngIf="!disabled" (click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32"> | |
57 | + <button *ngIf="!disabled" | |
58 | + type="button" | |
59 | + (click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32"> | |
58 | 60 | <mat-icon class="tb-mat-20">edit</mat-icon> |
59 | 61 | </button> |
60 | 62 | <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> | ... | ... |
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 18 | <button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
19 | + type="button" | |
19 | 20 | mat-button mat-raised-button color="primary" (click)="openEditMode($event)"> |
20 | 21 | <mat-icon class="material-icons">toc</mat-icon> |
21 | 22 | <span translate>legend.settings</span> | ... | ... |
... | ... | @@ -140,6 +140,7 @@ |
140 | 140 | </tb-data-keys> |
141 | 141 | </section> |
142 | 142 | <button [disabled]="isLoading$ | async" |
143 | + type="button" | |
143 | 144 | mat-button mat-icon-button color="primary" |
144 | 145 | style="min-width: 40px;" |
145 | 146 | (click)="removeDatasource($index)" |
... | ... | @@ -153,6 +154,7 @@ |
153 | 154 | </ng-template> |
154 | 155 | <div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
155 | 156 | <button [disabled]="isLoading$ | async" |
157 | + type="button" | |
156 | 158 | mat-button mat-raised-button color="primary" |
157 | 159 | [fxShow]="modelValue?.typeParameters && |
158 | 160 | (modelValue?.typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < modelValue?.typeParameters.maxDatasources)" | ... | ... |
... | ... | @@ -996,11 +996,11 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
996 | 996 | } |
997 | 997 | |
998 | 998 | private elementClick($event: Event) { |
999 | - $event.stopPropagation(); | |
1000 | 999 | const e = ($event.target || $event.srcElement) as Element; |
1001 | 1000 | if (e.id) { |
1002 | 1001 | const descriptors = this.getActionDescriptors('elementClick'); |
1003 | 1002 | if (descriptors.length) { |
1003 | + $event.stopPropagation(); | |
1004 | 1004 | descriptors.forEach((descriptor) => { |
1005 | 1005 | if (descriptor.name === e.id) { |
1006 | 1006 | const entityInfo = this.getActiveEntityInfo(); | ... | ... |
... | ... | @@ -15,12 +15,12 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; |
18 | -import { Widget, widgetType } from '@app/shared/models/widget.models'; | |
18 | +import { Widget, widgetType, WidgetPosition } from '@app/shared/models/widget.models'; | |
19 | 19 | import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; |
20 | 20 | import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; |
21 | 21 | import { Timewindow } from '@shared/models/time/time.models'; |
22 | 22 | import { Observable, of, Subject } from 'rxjs'; |
23 | -import { isDefined, isUndefined } from '@app/core/utils'; | |
23 | +import { guid, isDefined, isUndefined } from '@app/core/utils'; | |
24 | 24 | import { IterableDiffer, KeyValueDiffer } from '@angular/core'; |
25 | 25 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
26 | 26 | import * as deepEqual from 'deep-equal'; |
... | ... | @@ -46,18 +46,13 @@ export interface WidgetContextMenuItem extends ContextMenuItem { |
46 | 46 | } |
47 | 47 | |
48 | 48 | export interface DashboardCallbacks { |
49 | - onEditWidget?: ($event: Event, widget: Widget, index: number) => void; | |
50 | - onExportWidget?: ($event: Event, widget: Widget, index: number) => void; | |
51 | - onRemoveWidget?: ($event: Event, widget: Widget, index: number) => void; | |
52 | - onWidgetMouseDown?: ($event: Event, widget: Widget, index: number) => void; | |
53 | - onWidgetClicked?: ($event: Event, widget: Widget, index: number) => void; | |
49 | + onEditWidget?: ($event: Event, widget: Widget) => void; | |
50 | + onExportWidget?: ($event: Event, widget: Widget) => void; | |
51 | + onRemoveWidget?: ($event: Event, widget: Widget) => void; | |
52 | + onWidgetMouseDown?: ($event: Event, widget: Widget) => void; | |
53 | + onWidgetClicked?: ($event: Event, widget: Widget) => void; | |
54 | 54 | prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>; |
55 | - prepareWidgetContextMenu?: ($event: Event, widget: Widget, index: number) => Array<WidgetContextMenuItem>; | |
56 | -} | |
57 | - | |
58 | -export interface WidgetPosition { | |
59 | - row: number; | |
60 | - column: number; | |
55 | + prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array<WidgetContextMenuItem>; | |
61 | 56 | } |
62 | 57 | |
63 | 58 | export interface IDashboardComponent { |
... | ... | @@ -74,11 +69,14 @@ export interface IDashboardComponent { |
74 | 69 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; |
75 | 70 | onResetTimewindow(): void; |
76 | 71 | resetHighlight(): void; |
77 | - highlightWidget(index: number, delay?: number); | |
78 | - selectWidget(index: number, delay?: number); | |
72 | + highlightWidget(widgetId: string, delay?: number); | |
73 | + selectWidget(widgetId: string, delay?: number); | |
79 | 74 | getSelectedWidget(): Widget; |
80 | 75 | getEventGridPosition(event: Event): WidgetPosition; |
81 | 76 | notifyGridsterOptionsChanged(); |
77 | + pauseChangeNotifications(); | |
78 | + resumeChangeNotifications(); | |
79 | + notifyLayoutUpdated(); | |
82 | 80 | } |
83 | 81 | |
84 | 82 | declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; |
... | ... | @@ -86,7 +84,7 @@ declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; |
86 | 84 | interface DashboardWidgetUpdateRecord { |
87 | 85 | widget?: Widget; |
88 | 86 | widgetLayout?: WidgetLayout; |
89 | - widgetIndex: number; | |
87 | + widgetId: string; | |
90 | 88 | operation: DashboardWidgetUpdateOperation; |
91 | 89 | } |
92 | 90 | |
... | ... | @@ -95,7 +93,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
95 | 93 | highlightedMode = false; |
96 | 94 | |
97 | 95 | dashboardWidgets: Array<DashboardWidget> = []; |
98 | - widgets: Array<Widget>; | |
96 | + widgets: Iterable<Widget>; | |
99 | 97 | widgetLayouts: WidgetLayouts; |
100 | 98 | |
101 | 99 | [Symbol.iterator](): Iterator<DashboardWidget> { |
... | ... | @@ -103,41 +101,30 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
103 | 101 | } |
104 | 102 | |
105 | 103 | constructor(private dashboard: IDashboardComponent, |
106 | - private widgetsDiffer: IterableDiffer<Widget>, | |
107 | - private widgetLayoutsDiffer: KeyValueDiffer<string, WidgetLayout>) { | |
104 | + private widgetsDiffer: IterableDiffer<Widget>) { | |
108 | 105 | } |
109 | 106 | |
110 | 107 | doCheck() { |
111 | 108 | const widgetChange = this.widgetsDiffer.diff(this.widgets); |
112 | 109 | if (widgetChange !== null) { |
113 | 110 | |
114 | - const layouts: WidgetLayouts = {}; | |
115 | 111 | const updateRecords: Array<DashboardWidgetUpdateRecord> = []; |
116 | 112 | |
117 | - const widgetLayoutChange = this.widgetLayoutsDiffer.diff(this.widgetLayouts); | |
118 | - if (widgetLayoutChange !== null) { | |
119 | - widgetLayoutChange.forEachAddedItem((added) => { | |
120 | - layouts[added.key] = added.currentValue; | |
121 | - }); | |
122 | - widgetLayoutChange.forEachChangedItem((changed) => { | |
123 | - layouts[changed.key] = changed.currentValue; | |
124 | - }); | |
125 | - } | |
126 | 113 | widgetChange.forEachAddedItem((added) => { |
127 | 114 | updateRecords.push({ |
128 | 115 | widget: added.item, |
129 | - widgetLayout: layouts[added.item.id], | |
130 | - widgetIndex: added.currentIndex, | |
116 | + widgetId: added.item.id, | |
117 | + widgetLayout: this.widgetLayouts[added.item.id], | |
131 | 118 | operation: 'add' |
132 | 119 | }); |
133 | 120 | }); |
134 | 121 | widgetChange.forEachRemovedItem((removed) => { |
135 | - let operation = updateRecords.find((record) => record.widgetIndex === removed.previousIndex); | |
122 | + let operation = updateRecords.find((record) => record.widgetId === removed.item.id); | |
136 | 123 | if (operation) { |
137 | 124 | operation.operation = 'update'; |
138 | 125 | } else { |
139 | 126 | operation = { |
140 | - widgetIndex: removed.previousIndex, | |
127 | + widgetId: removed.item.id, | |
141 | 128 | operation: 'remove' |
142 | 129 | }; |
143 | 130 | updateRecords.push(operation); |
... | ... | @@ -147,21 +134,21 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
147 | 134 | switch (record.operation) { |
148 | 135 | case 'add': |
149 | 136 | this.dashboardWidgets.push( |
150 | - new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout) | |
137 | + new DashboardWidget(this.dashboard, record.widget, record.widgetLayout) | |
151 | 138 | ); |
152 | 139 | break; |
153 | 140 | case 'remove': |
154 | - let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex); | |
141 | + let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId); | |
155 | 142 | if (index > -1) { |
156 | 143 | this.dashboardWidgets.splice(index, 1); |
157 | 144 | } |
158 | 145 | break; |
159 | 146 | case 'update': |
160 | - index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex); | |
147 | + index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId); | |
161 | 148 | if (index > -1) { |
162 | 149 | const prevDashboardWidget = this.dashboardWidgets[index]; |
163 | 150 | if (!deepEqual(prevDashboardWidget.widget, record.widget)) { |
164 | - this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout); | |
151 | + this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout); | |
165 | 152 | this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; |
166 | 153 | this.dashboardWidgets[index].selected = prevDashboardWidget.selected; |
167 | 154 | } else { |
... | ... | @@ -178,14 +165,25 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
178 | 165 | } |
179 | 166 | } |
180 | 167 | |
181 | - setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) { | |
168 | + widgetLayoutsUpdated() { | |
169 | + for (const w of Object.keys(this.widgetLayouts)) { | |
170 | + const widgetLayout = this.widgetLayouts[w]; | |
171 | + const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === w); | |
172 | + if (index > -1) { | |
173 | + this.dashboardWidgets[index].widgetLayout = widgetLayout; | |
174 | + } | |
175 | + } | |
176 | + this.updateRowsAndSort(); | |
177 | + } | |
178 | + | |
179 | + setWidgets(widgets: Iterable<Widget>, widgetLayouts: WidgetLayouts) { | |
182 | 180 | this.highlightedMode = false; |
183 | 181 | this.widgets = widgets; |
184 | 182 | this.widgetLayouts = widgetLayouts; |
185 | 183 | } |
186 | 184 | |
187 | - highlightWidget(index: number): DashboardWidget { | |
188 | - const widget = this.findWidgetAtIndex(index); | |
185 | + highlightWidget(widgetId: string): DashboardWidget { | |
186 | + const widget = this.findWidgetById(widgetId); | |
189 | 187 | if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { |
190 | 188 | this.highlightedMode = true; |
191 | 189 | widget.highlighted = true; |
... | ... | @@ -200,8 +198,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
200 | 198 | } |
201 | 199 | } |
202 | 200 | |
203 | - selectWidget(index: number): DashboardWidget { | |
204 | - const widget = this.findWidgetAtIndex(index); | |
201 | + selectWidget(widgetId: string): DashboardWidget { | |
202 | + const widget = this.findWidgetById(widgetId); | |
205 | 203 | if (widget && (!widget.selected)) { |
206 | 204 | widget.selected = true; |
207 | 205 | this.dashboardWidgets.forEach((dashboardWidget) => { |
... | ... | @@ -237,8 +235,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { |
237 | 235 | return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.selected); |
238 | 236 | } |
239 | 237 | |
240 | - private findWidgetAtIndex(index: number): DashboardWidget { | |
241 | - return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetIndex === index); | |
238 | + private findWidgetById(widgetId: string): DashboardWidget { | |
239 | + return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetId === widgetId); | |
242 | 240 | } |
243 | 241 | |
244 | 242 | private updateRowsAndSort() { |
... | ... | @@ -306,6 +304,8 @@ export class DashboardWidget implements GridsterItem { |
306 | 304 | |
307 | 305 | widgetContext: WidgetContext = {}; |
308 | 306 | |
307 | + widgetId: string; | |
308 | + | |
309 | 309 | private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>(); |
310 | 310 | private gridsterItemComponentValue: GridsterItemComponentInterface; |
311 | 311 | |
... | ... | @@ -318,8 +318,11 @@ export class DashboardWidget implements GridsterItem { |
318 | 318 | constructor( |
319 | 319 | private dashboard: IDashboardComponent, |
320 | 320 | public widget: Widget, |
321 | - public widgetIndex: number, | |
322 | 321 | public widgetLayout?: WidgetLayout) { |
322 | + if (!widget.id) { | |
323 | + widget.id = guid(); | |
324 | + } | |
325 | + this.widgetId = widget.id; | |
323 | 326 | this.updateWidgetParams(); |
324 | 327 | } |
325 | 328 | ... | ... |
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 | +<form #widgetForm="ngForm" [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 900px;"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2 translate>widget.add</h2> | |
21 | + <span fxFlex></span> | |
22 | + <div [tb-help]="helpLinkIdForWidgetType()"></div> | |
23 | + <button mat-button mat-icon-button | |
24 | + (click)="cancel()" | |
25 | + type="button"> | |
26 | + <mat-icon class="material-icons">close</mat-icon> | |
27 | + </button> | |
28 | + </mat-toolbar> | |
29 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
30 | + </mat-progress-bar> | |
31 | + <div mat-dialog-content> | |
32 | + <fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;"> | |
33 | + <tb-widget-config | |
34 | + [aliasController]="aliasController" | |
35 | + [functionsOnly]="false" | |
36 | + [entityAliases]="dashboard.configuration.entityAliases" | |
37 | + [dashboardStates]="dashboard.configuration.states" | |
38 | + formControlName="widgetConfig"> | |
39 | + </tb-widget-config> | |
40 | + </fieldset> | |
41 | + </div> | |
42 | + <div mat-dialog-actions fxLayout="row"> | |
43 | + <span fxFlex></span> | |
44 | + <button mat-button mat-raised-button color="primary" | |
45 | + type="submit" | |
46 | + [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"> | |
47 | + {{ 'action.add' | translate }} | |
48 | + </button> | |
49 | + <button mat-button color="primary" | |
50 | + style="margin-right: 20px;" | |
51 | + type="button" | |
52 | + [disabled]="(isLoading$ | async)" | |
53 | + (click)="cancel()" cdkFocusInitial> | |
54 | + {{ 'action.cancel' | translate }} | |
55 | + </button> | |
56 | + </div> | |
57 | +</form> | ... | ... |
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, Inject, OnInit, SkipSelf } from '@angular/core'; | |
18 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; | |
22 | +import { Router } from '@angular/router'; | |
23 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
24 | +import { Widget, widgetTypesData } from '@shared/models/widget.models'; | |
25 | +import { UtilsService } from '@core/services/utils.service'; | |
26 | +import { TranslateService } from '@ngx-translate/core'; | |
27 | +import { EntityService } from '@core/http/entity.service'; | |
28 | +import { Dashboard } from '@app/shared/models/dashboard.models'; | |
29 | +import { IAliasController } from '@core/api/widget-api.models'; | |
30 | +import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; | |
31 | +import { isDefined, isString } from '@core/utils'; | |
32 | + | |
33 | +export interface AddWidgetDialogData { | |
34 | + dashboard: Dashboard; | |
35 | + aliasController: IAliasController; | |
36 | + widget: Widget; | |
37 | + widgetInfo: WidgetInfo; | |
38 | +} | |
39 | + | |
40 | +@Component({ | |
41 | + selector: 'tb-add-widget-dialog', | |
42 | + templateUrl: './add-widget-dialog.component.html', | |
43 | + providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}], | |
44 | + styleUrls: [] | |
45 | +}) | |
46 | +export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget> | |
47 | + implements OnInit, ErrorStateMatcher { | |
48 | + | |
49 | + widgetFormGroup: FormGroup; | |
50 | + | |
51 | + dashboard: Dashboard; | |
52 | + aliasController: IAliasController; | |
53 | + widget: Widget; | |
54 | + | |
55 | + submitted = false; | |
56 | + | |
57 | + constructor(protected store: Store<AppState>, | |
58 | + protected router: Router, | |
59 | + @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, | |
60 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
61 | + public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>, | |
62 | + private fb: FormBuilder, | |
63 | + private utils: UtilsService, | |
64 | + private translate: TranslateService, | |
65 | + private entityService: EntityService) { | |
66 | + super(store, router, dialogRef); | |
67 | + | |
68 | + this.dashboard = this.data.dashboard; | |
69 | + this.aliasController = this.data.aliasController; | |
70 | + this.widget = this.data.widget; | |
71 | + | |
72 | + const widgetInfo = this.data.widgetInfo; | |
73 | + | |
74 | + const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema; | |
75 | + const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; | |
76 | + const typeParameters = widgetInfo.typeParameters; | |
77 | + const actionSources = widgetInfo.actionSources; | |
78 | + const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; | |
79 | + let settingsSchema; | |
80 | + if (!rawSettingsSchema || rawSettingsSchema === '') { | |
81 | + settingsSchema = {}; | |
82 | + } else { | |
83 | + settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema; | |
84 | + } | |
85 | + let dataKeySettingsSchema; | |
86 | + if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') { | |
87 | + dataKeySettingsSchema = {}; | |
88 | + } else { | |
89 | + dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema; | |
90 | + } | |
91 | + const widgetConfig: WidgetConfigComponentData = { | |
92 | + config: this.widget.config, | |
93 | + layout: {}, | |
94 | + widgetType: this.widget.type, | |
95 | + typeParameters, | |
96 | + actionSources, | |
97 | + isDataEnabled, | |
98 | + settingsSchema, | |
99 | + dataKeySettingsSchema | |
100 | + }; | |
101 | + | |
102 | + this.widgetFormGroup = this.fb.group({ | |
103 | + widgetConfig: [widgetConfig, []] | |
104 | + } | |
105 | + ); | |
106 | + } | |
107 | + | |
108 | + ngOnInit(): void { | |
109 | + } | |
110 | + | |
111 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
112 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
113 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
114 | + return originalErrorState || customErrorState; | |
115 | + } | |
116 | + | |
117 | + helpLinkIdForWidgetType(): string { | |
118 | + let link = 'widgetsConfig'; | |
119 | + if (this.widget && this.widget.type) { | |
120 | + link = widgetTypesData.get(this.widget.type).configHelpLinkId; | |
121 | + } | |
122 | + return link; | |
123 | + } | |
124 | + | |
125 | + cancel(): void { | |
126 | + this.dialogRef.close(null); | |
127 | + } | |
128 | + | |
129 | + add(): void { | |
130 | + this.submitted = true; | |
131 | + const widgetConfig: WidgetConfigComponentData = this.widgetFormGroup.get('widgetConfig').value; | |
132 | + this.widget.config = widgetConfig.config; | |
133 | + this.widget.config.mobileOrder = widgetConfig.layout.mobileOrder; | |
134 | + this.widget.config.mobileHeight = widgetConfig.layout.mobileHeight; | |
135 | + this.dialogRef.close(this.widget); | |
136 | + } | |
137 | +} | ... | ... |
... | ... | @@ -132,26 +132,13 @@ |
132 | 132 | </section> |
133 | 133 | <div class="tb-absolute-fill tb-dashboard-layouts" fxLayout="{{forceDashboardMobileMode ? 'column' : 'row'}}" |
134 | 134 | [ngClass]="{ 'tb-padded' : !widgetEditMode && (isEdit || displayTitle()), 'tb-shrinked' : isEditingWidget }"> |
135 | - <div [fxShow]="layouts.main.show" | |
136 | - id="tb-main-layout" | |
137 | - [ngStyle]="{width: mainLayoutWidth(), | |
138 | - height: mainLayoutHeight()}"> | |
139 | - <tb-dashboard-layout | |
140 | - [layoutCtx]="layouts.main.layoutCtx" | |
141 | - [dashboardCtx]="dashboardCtx" | |
142 | - [isEdit]="isEdit" | |
143 | - [isEditingWidget]="isEditingWidget" | |
144 | - [isMobile]="forceDashboardMobileMode" | |
145 | - [widgetEditMode]="widgetEditMode"> | |
146 | - </tb-dashboard-layout> | |
147 | - </div> | |
148 | - <mat-drawer-container *ngIf="layouts.right.show" | |
149 | - id="tb-right-layout"> | |
150 | - <mat-drawer | |
151 | - [ngStyle]="{minWidth: rightLayoutWidth(), | |
152 | - maxWidth: rightLayoutWidth(), | |
153 | - height: rightLayoutHeight(), | |
154 | - zIndex: 25}" | |
135 | + <mat-drawer-container class="tb-absolute-fill"> | |
136 | + <mat-drawer *ngIf="layouts.right.show" | |
137 | + id="tb-right-layout" | |
138 | + [ngStyle]="{minWidth: rightLayoutWidth(), | |
139 | + maxWidth: rightLayoutWidth(), | |
140 | + height: rightLayoutHeight(), | |
141 | + borderLeft: 'none'}" | |
155 | 142 | disableClose="true" |
156 | 143 | position="end" |
157 | 144 | [mode]="isMobile ? 'over' : 'side'" |
... | ... | @@ -165,6 +152,19 @@ |
165 | 152 | [widgetEditMode]="widgetEditMode"> |
166 | 153 | </tb-dashboard-layout> |
167 | 154 | </mat-drawer> |
155 | + <mat-drawer-content [fxShow]="layouts.main.show" | |
156 | + id="tb-main-layout" | |
157 | + [ngStyle]="{width: mainLayoutWidth(), | |
158 | + height: mainLayoutHeight()}"> | |
159 | + <tb-dashboard-layout | |
160 | + [layoutCtx]="layouts.main.layoutCtx" | |
161 | + [dashboardCtx]="dashboardCtx" | |
162 | + [isEdit]="isEdit" | |
163 | + [isEditingWidget]="isEditingWidget" | |
164 | + [isMobile]="forceDashboardMobileMode" | |
165 | + [widgetEditMode]="widgetEditMode"> | |
166 | + </tb-dashboard-layout> | |
167 | + </mat-drawer-content> | |
168 | 168 | </mat-drawer-container> |
169 | 169 | </div> |
170 | 170 | <mat-drawer-container hasBackdrop="false" class="tb-widget-details-sidenav"> |
... | ... | @@ -194,6 +194,37 @@ |
194 | 194 | </tb-details-panel> |
195 | 195 | </mat-drawer> |
196 | 196 | </mat-drawer-container> |
197 | + <mat-drawer-container *ngIf="!widgetEditMode" hasBackdrop="false" class="tb-select-widget-sidenav"> | |
198 | + <mat-drawer class="tb-details-drawer" | |
199 | + [opened]="isAddingWidget" | |
200 | + mode="over" | |
201 | + position="end"> | |
202 | + <tb-details-panel *ngIf="isAddingWidget" fxFlex | |
203 | + headerTitle="{{'dashboard.select-widget-title' | translate}}" | |
204 | + headerHeightPx="120" | |
205 | + [isReadOnly]="true" | |
206 | + [isEdit]="false" | |
207 | + (closeDetails)="onAddWidgetClosed()"> | |
208 | + <div class="header-pane" *ngIf="isAddingWidget"> | |
209 | + <div fxLayout="row"> | |
210 | + <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span> | |
211 | + <tb-widgets-bundle-select fxFlexOffset="5" | |
212 | + fxFlex | |
213 | + required | |
214 | + [selectFirstBundle]="false" | |
215 | + [ngModel]="widgetsBundle" | |
216 | + (ngModelChange)="widgetsBundle = $event"> | |
217 | + </tb-widgets-bundle-select> | |
218 | + </div> | |
219 | + </div> | |
220 | + <tb-dashboard-widget-select *ngIf="isAddingWidget" | |
221 | + [aliasController]="dashboardCtx.aliasController" | |
222 | + [widgetsBundle]="widgetsBundle" | |
223 | + (widgetSelected)="addWidgetFromType($event)"> | |
224 | + </tb-dashboard-widget-select> | |
225 | + </tb-details-panel> | |
226 | + </mat-drawer> | |
227 | + </mat-drawer-container> | |
197 | 228 | <!--tb-details-sidenav TODO --> |
198 | 229 | <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> |
199 | 230 | <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" | ... | ... |
... | ... | @@ -14,16 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { | |
18 | - Component, | |
19 | - Inject, | |
20 | - OnDestroy, | |
21 | - OnInit, | |
22 | - ViewEncapsulation, | |
23 | - ViewChild, | |
24 | - NgZone, | |
25 | - ChangeDetectorRef, ChangeDetectionStrategy, ApplicationRef | |
26 | -} from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject, NgZone, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'; | |
27 | 18 | import { PageComponent } from '@shared/components/page.component'; |
28 | 19 | import { Store } from '@ngrx/store'; |
29 | 20 | import { AppState } from '@core/core.state'; |
... | ... | @@ -33,47 +24,42 @@ import { AuthService } from '@core/auth/auth.service'; |
33 | 24 | import { |
34 | 25 | Dashboard, |
35 | 26 | DashboardConfiguration, |
36 | - WidgetLayout, | |
27 | + DashboardLayoutId, | |
37 | 28 | DashboardLayoutInfo, |
38 | - DashboardLayoutsInfo | |
29 | + DashboardLayoutsInfo, | |
30 | + DashboardStateLayouts, GridSettings, | |
31 | + WidgetLayout | |
39 | 32 | } from '@app/shared/models/dashboard.models'; |
40 | 33 | import { WINDOW } from '@core/services/window.service'; |
41 | 34 | import { WindowMessage } from '@shared/models/window-message.model'; |
42 | 35 | import { deepClone, isDefined } from '@app/core/utils'; |
43 | 36 | import { |
44 | - DashboardContext, DashboardPageLayout, | |
37 | + DashboardContext, | |
38 | + DashboardPageLayout, | |
45 | 39 | DashboardPageLayoutContext, |
46 | 40 | DashboardPageLayouts, |
47 | - DashboardPageScope, IDashboardController | |
41 | + DashboardPageScope, | |
42 | + IDashboardController, | |
43 | + LayoutWidgetsArray | |
48 | 44 | } from './dashboard-page.models'; |
49 | 45 | import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; |
50 | 46 | import { MediaBreakpoints } from '@shared/models/constants'; |
51 | 47 | import { AuthUser } from '@shared/models/user.model'; |
52 | 48 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
53 | -import { Widget, widgetTypesData } from '@app/shared/models/widget.models'; | |
49 | +import { Widget, WidgetConfig, WidgetPosition, widgetTypesData } from '@app/shared/models/widget.models'; | |
54 | 50 | import { environment as env } from '@env/environment'; |
55 | 51 | import { Authority } from '@shared/models/authority.enum'; |
56 | 52 | import { DialogService } from '@core/services/dialog.service'; |
57 | 53 | import { EntityService } from '@core/http/entity.service'; |
58 | 54 | import { AliasController } from '@core/api/alias-controller'; |
59 | -import { Observable, Subscription, of } from 'rxjs'; | |
55 | +import { Observable, of, Subscription } from 'rxjs'; | |
60 | 56 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; |
61 | -import { IStateController } from '@core/api/widget-api.models'; | |
62 | 57 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
63 | 58 | import { DashboardService } from '@core/http/dashboard.service'; |
64 | -import { | |
65 | - WidgetContextMenuItem, | |
66 | - DashboardContextMenuItem, | |
67 | - IDashboardComponent, WidgetPosition | |
68 | -} from '../../models/dashboard-component.models'; | |
59 | +import { DashboardContextMenuItem, WidgetContextMenuItem } from '../../models/dashboard-component.models'; | |
69 | 60 | import { WidgetComponentService } from '../../components/widget/widget-component.service'; |
70 | -import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; | |
61 | +import { FormBuilder } from '@angular/forms'; | |
71 | 62 | import { ItemBufferService } from '@core/services/item-buffer.service'; |
72 | -import { | |
73 | - DeviceCredentialsDialogComponent, | |
74 | - DeviceCredentialsDialogData | |
75 | -} from '@home/pages/device/device-credentials-dialog.component'; | |
76 | -import { DeviceCredentials } from '@shared/models/device.models'; | |
77 | 63 | import { MatDialog } from '@angular/material/dialog'; |
78 | 64 | import { |
79 | 65 | EntityAliasesDialogComponent, |
... | ... | @@ -81,6 +67,18 @@ import { |
81 | 67 | } from '@home/components/alias/entity-aliases-dialog.component'; |
82 | 68 | import { EntityAliases } from '@app/shared/models/alias.models'; |
83 | 69 | import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component'; |
70 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | |
71 | +import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component'; | |
72 | +import { TranslateService } from '@ngx-translate/core'; | |
73 | +import { | |
74 | + ManageDashboardLayoutsDialogComponent, | |
75 | + ManageDashboardLayoutsDialogData | |
76 | +} from '@home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component'; | |
77 | +import { SelectTargetLayoutDialogComponent } from '@home/pages/dashboard/layout/select-target-layout-dialog.component'; | |
78 | +import { | |
79 | + DashboardSettingsDialogComponent, | |
80 | + DashboardSettingsDialogData | |
81 | +} from '@home/pages/dashboard/dashboard-settings-dialog.component'; | |
84 | 82 | |
85 | 83 | @Component({ |
86 | 84 | selector: 'tb-dashboard-page', |
... | ... | @@ -109,6 +107,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
109 | 107 | isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); |
110 | 108 | forceDashboardMobileMode = false; |
111 | 109 | isAddingWidget = false; |
110 | + widgetsBundle: WidgetsBundle = null; | |
112 | 111 | |
113 | 112 | isToolbarOpened = false; |
114 | 113 | isToolbarOpenedAnimate = false; |
... | ... | @@ -127,12 +126,14 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
127 | 126 | currentCustomerId: string; |
128 | 127 | currentDashboardScope: DashboardPageScope; |
129 | 128 | |
129 | + addingLayoutCtx: DashboardPageLayoutContext; | |
130 | + | |
130 | 131 | layouts: DashboardPageLayouts = { |
131 | 132 | main: { |
132 | 133 | show: false, |
133 | 134 | layoutCtx: { |
134 | 135 | id: 'main', |
135 | - widgets: [], | |
136 | + widgets: null, | |
136 | 137 | widgetLayouts: {}, |
137 | 138 | gridSettings: {}, |
138 | 139 | ignoreLoading: false, |
... | ... | @@ -144,7 +145,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
144 | 145 | show: false, |
145 | 146 | layoutCtx: { |
146 | 147 | id: 'right', |
147 | - widgets: [], | |
148 | + widgets: null, | |
148 | 149 | widgetLayouts: {}, |
149 | 150 | gridSettings: {}, |
150 | 151 | ignoreLoading: false, |
... | ... | @@ -216,6 +217,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
216 | 217 | private itembuffer: ItemBufferService, |
217 | 218 | private fb: FormBuilder, |
218 | 219 | private dialog: MatDialog, |
220 | + private translate: TranslateService, | |
219 | 221 | private ngZone: NgZone, |
220 | 222 | private cd: ChangeDetectorRef) { |
221 | 223 | super(store); |
... | ... | @@ -251,6 +253,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
251 | 253 | |
252 | 254 | this.dashboard = data.dashboard; |
253 | 255 | this.dashboardConfiguration = this.dashboard.configuration; |
256 | + this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard); | |
257 | + this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard); | |
254 | 258 | this.widgetEditMode = data.widgetEditMode; |
255 | 259 | this.singlePageMode = data.singlePageMode; |
256 | 260 | |
... | ... | @@ -282,6 +286,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
282 | 286 | this.isEditingWidget = false; |
283 | 287 | this.forceDashboardMobileMode = false; |
284 | 288 | this.isAddingWidget = false; |
289 | + this.widgetsBundle = null; | |
285 | 290 | |
286 | 291 | this.isToolbarOpened = false; |
287 | 292 | this.isToolbarOpenedAnimate = false; |
... | ... | @@ -475,8 +480,30 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
475 | 480 | if ($event) { |
476 | 481 | $event.stopPropagation(); |
477 | 482 | } |
478 | - // TODO: | |
479 | - this.dialogService.todo(); | |
483 | + let gridSettings: GridSettings = null; | |
484 | + const layoutKeys = this.dashboardUtils.isSingleLayoutDashboard(this.dashboard); | |
485 | + if (layoutKeys) { | |
486 | + gridSettings = deepClone(this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings); | |
487 | + } | |
488 | + this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData, | |
489 | + DashboardSettingsDialogData>(DashboardSettingsDialogComponent, { | |
490 | + disableClose: true, | |
491 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
492 | + data: { | |
493 | + settings: deepClone(this.dashboard.configuration.settings), | |
494 | + gridSettings | |
495 | + } | |
496 | + }).afterClosed().subscribe((data) => { | |
497 | + if (data) { | |
498 | + this.dashboard.configuration.settings = data.settings; | |
499 | + const newGridSettings = data.gridSettings; | |
500 | + if (newGridSettings) { | |
501 | + const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; | |
502 | + this.dashboardUtils.updateLayoutSettings(layout, newGridSettings); | |
503 | + this.updateLayouts(); | |
504 | + } | |
505 | + } | |
506 | + }); | |
480 | 507 | } |
481 | 508 | |
482 | 509 | public manageDashboardStates($event: Event) { |
... | ... | @@ -491,8 +518,23 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
491 | 518 | if ($event) { |
492 | 519 | $event.stopPropagation(); |
493 | 520 | } |
494 | - // TODO: | |
495 | - this.dialogService.todo(); | |
521 | + this.dialog.open<ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogData, | |
522 | + DashboardStateLayouts>(ManageDashboardLayoutsDialogComponent, { | |
523 | + disableClose: true, | |
524 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
525 | + data: { | |
526 | + layouts: deepClone(this.dashboard.configuration.states[this.dashboardCtx.state].layouts) | |
527 | + } | |
528 | + }).afterClosed().subscribe((layouts) => { | |
529 | + if (layouts) { | |
530 | + this.updateDashboardLayouts(layouts); | |
531 | + } | |
532 | + }); | |
533 | + } | |
534 | + | |
535 | + private updateDashboardLayouts(newLayouts: DashboardStateLayouts) { | |
536 | + this.dashboardUtils.setLayouts(this.dashboard, this.dashboardCtx.state, newLayouts); | |
537 | + this.updateLayouts(); | |
496 | 538 | } |
497 | 539 | |
498 | 540 | private importWidget($event: Event) { |
... | ... | @@ -526,7 +568,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
526 | 568 | this.notifyDashboardUpdated(); |
527 | 569 | } |
528 | 570 | |
529 | - public openDashboardState(state: string, openRightLayout: boolean) { | |
571 | + public openDashboardState(state: string, openRightLayout?: boolean) { | |
530 | 572 | const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); |
531 | 573 | if (layoutsData) { |
532 | 574 | this.dashboardCtx.state = state; |
... | ... | @@ -546,21 +588,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
546 | 588 | } |
547 | 589 | } |
548 | 590 | this.isRightLayoutOpened = openRightLayout ? true : false; |
549 | - this.updateLayouts(layoutsData, layoutVisibilityChanged); | |
591 | + this.updateLayouts(layoutsData); | |
550 | 592 | } |
551 | 593 | } |
552 | 594 | |
553 | - private updateLayouts(layoutsData: DashboardLayoutsInfo, layoutVisibilityChanged: boolean) { | |
595 | + private updateLayouts(layoutsData?: DashboardLayoutsInfo) { | |
596 | + if (!layoutsData) { | |
597 | + layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, this.dashboardCtx.state); | |
598 | + } | |
554 | 599 | for (const l of Object.keys(this.layouts)) { |
555 | 600 | const layout: DashboardPageLayout = this.layouts[l]; |
556 | 601 | if (layoutsData[l]) { |
557 | 602 | const layoutInfo: DashboardLayoutInfo = layoutsData[l]; |
558 | - if (layout.layoutCtx.id === 'main') { | |
559 | - layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged); | |
560 | - } | |
561 | 603 | this.updateLayout(layout, layoutInfo); |
562 | 604 | } else { |
563 | - this.updateLayout(layout, {widgets: [], widgetLayouts: {}, gridSettings: null}); | |
605 | + this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); | |
564 | 606 | } |
565 | 607 | } |
566 | 608 | } |
... | ... | @@ -569,7 +611,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
569 | 611 | if (layoutInfo.gridSettings) { |
570 | 612 | layout.layoutCtx.gridSettings = layoutInfo.gridSettings; |
571 | 613 | } |
572 | - layout.layoutCtx.widgets = layoutInfo.widgets; | |
614 | + layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); | |
573 | 615 | layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; |
574 | 616 | if (layout.show && layout.layoutCtx.ctrl) { |
575 | 617 | layout.layoutCtx.ctrl.reload(); |
... | ... | @@ -594,6 +636,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
594 | 636 | this.dashboardConfiguration = this.dashboard.configuration; |
595 | 637 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; |
596 | 638 | this.entityAliasesUpdated(); |
639 | + this.updateLayouts(); | |
597 | 640 | } else { |
598 | 641 | this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow; |
599 | 642 | } |
... | ... | @@ -617,7 +660,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
617 | 660 | |
618 | 661 | private notifyDashboardUpdated() { |
619 | 662 | if (this.widgetEditMode) { |
620 | - const widget = this.layouts.main.layoutCtx.widgets[0]; | |
663 | + const widget = this.layouts.main.layoutCtx.widgets.widgetByIndex(0); | |
621 | 664 | const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; |
622 | 665 | widget.sizeX = layout.sizeX; |
623 | 666 | widget.sizeY = layout.sizeY; |
... | ... | @@ -643,8 +686,86 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
643 | 686 | if ($event) { |
644 | 687 | $event.stopPropagation(); |
645 | 688 | } |
646 | - // TODO: | |
647 | - this.dialogService.todo(); | |
689 | + this.isAddingWidget = true; | |
690 | + this.addingLayoutCtx = layoutCtx; | |
691 | + } | |
692 | + | |
693 | + onAddWidgetClosed() { | |
694 | + this.isAddingWidget = false; | |
695 | + } | |
696 | + | |
697 | + private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) { | |
698 | + this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget); | |
699 | + this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id); | |
700 | + } | |
701 | + | |
702 | + private selectTargetLayout(): Observable<DashboardLayoutId> { | |
703 | + const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts; | |
704 | + const layoutIds = Object.keys(layouts); | |
705 | + if (layoutIds.length > 1) { | |
706 | + return this.dialog.open<SelectTargetLayoutDialogComponent, any, | |
707 | + DashboardLayoutId>(SelectTargetLayoutDialogComponent, { | |
708 | + disableClose: true, | |
709 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] | |
710 | + }).afterClosed(); | |
711 | + } else { | |
712 | + return of(layoutIds[0] as DashboardLayoutId); | |
713 | + } | |
714 | + } | |
715 | + | |
716 | + private addWidgetToDashboard(widget: Widget) { | |
717 | + if (this.addingLayoutCtx) { | |
718 | + this.addWidgetToLayout(widget, this.addingLayoutCtx.id); | |
719 | + this.addingLayoutCtx = null; | |
720 | + } else { | |
721 | + this.selectTargetLayout().subscribe((layoutId) => { | |
722 | + if (layoutId) { | |
723 | + this.addWidgetToLayout(widget, layoutId); | |
724 | + } | |
725 | + }); | |
726 | + } | |
727 | + } | |
728 | + | |
729 | + addWidgetFromType(widget: Widget) { | |
730 | + this.onAddWidgetClosed(); | |
731 | + this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe( | |
732 | + (widgetTypeInfo) => { | |
733 | + const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig); | |
734 | + config.title = 'New ' + widgetTypeInfo.widgetName; | |
735 | + config.datasources = []; | |
736 | + const newWidget: Widget = { | |
737 | + isSystemType: widget.isSystemType, | |
738 | + bundleAlias: widget.bundleAlias, | |
739 | + typeAlias: widgetTypeInfo.alias, | |
740 | + type: widgetTypeInfo.type, | |
741 | + title: 'New widget', | |
742 | + sizeX: widgetTypeInfo.sizeX, | |
743 | + sizeY: widgetTypeInfo.sizeY, | |
744 | + config, | |
745 | + row: 0, | |
746 | + col: 0 | |
747 | + }; | |
748 | + if (widgetTypeInfo.typeParameters.useCustomDatasources) { | |
749 | + this.addWidgetToDashboard(newWidget); | |
750 | + } else { | |
751 | + this.dialog.open<AddWidgetDialogComponent, AddWidgetDialogData, | |
752 | + Widget>(AddWidgetDialogComponent, { | |
753 | + disableClose: true, | |
754 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
755 | + data: { | |
756 | + dashboard: this.dashboard, | |
757 | + aliasController: this.dashboardCtx.aliasController, | |
758 | + widget: newWidget, | |
759 | + widgetInfo: widgetTypeInfo | |
760 | + } | |
761 | + }).afterClosed().subscribe((addedWidget) => { | |
762 | + if (addedWidget) { | |
763 | + this.addWidgetToDashboard(addedWidget); | |
764 | + } | |
765 | + }); | |
766 | + } | |
767 | + } | |
768 | + ); | |
648 | 769 | } |
649 | 770 | |
650 | 771 | onRevertWidgetEdit() { |
... | ... | @@ -660,14 +781,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
660 | 781 | const widget = deepClone(this.editingWidget); |
661 | 782 | const widgetLayout = deepClone(this.editingWidgetLayout); |
662 | 783 | const id = this.editingWidgetOriginal.id; |
663 | - const index = this.editingLayoutCtx.widgets.indexOf(this.editingWidgetOriginal); | |
664 | 784 | this.dashboardConfiguration.widgets[id] = widget; |
665 | 785 | this.editingWidgetOriginal = widget; |
666 | 786 | this.editingWidgetLayoutOriginal = widgetLayout; |
667 | - this.editingLayoutCtx.widgets[index] = widget; | |
668 | 787 | this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; |
669 | 788 | setTimeout(() => { |
670 | - this.editingLayoutCtx.ctrl.highlightWidget(index, 0); | |
789 | + this.editingLayoutCtx.ctrl.highlightWidget(widget.id, 0); | |
671 | 790 | }, 0); |
672 | 791 | } |
673 | 792 | |
... | ... | @@ -683,7 +802,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
683 | 802 | this.forceDashboardMobileMode = false; |
684 | 803 | } |
685 | 804 | |
686 | - editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) { | |
805 | + editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | |
687 | 806 | $event.stopPropagation(); |
688 | 807 | if (this.editingWidgetOriginal === widget) { |
689 | 808 | this.onEditWidgetClosed(); |
... | ... | @@ -701,52 +820,73 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
701 | 820 | const delayOffset = transition ? 350 : 0; |
702 | 821 | const delay = transition ? 400 : 300; |
703 | 822 | setTimeout(() => { |
704 | - layoutCtx.ctrl.highlightWidget(index, delay); | |
823 | + layoutCtx.ctrl.highlightWidget(widget.id, delay); | |
705 | 824 | }, delayOffset); |
706 | 825 | } |
707 | 826 | } |
708 | 827 | } |
709 | 828 | |
710 | 829 | copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { |
711 | - // TODO: | |
712 | - this.dialogService.todo(); | |
830 | + this.itembuffer.copyWidget(this.dashboard, | |
831 | + this.dashboardCtx.state, layoutCtx.id, widget); | |
713 | 832 | } |
714 | 833 | |
715 | 834 | copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { |
716 | - // TODO: | |
717 | - this.dialogService.todo(); | |
835 | + this.itembuffer.copyWidgetReference(this.dashboard, | |
836 | + this.dashboardCtx.state, layoutCtx.id, widget); | |
718 | 837 | } |
719 | 838 | |
720 | 839 | pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { |
721 | - // TODO: | |
722 | - this.dialogService.todo(); | |
840 | + this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, | |
841 | + pos, this.entityAliasesUpdated.bind(this)).subscribe( | |
842 | + (widget) => { | |
843 | + layoutCtx.widgets.addWidgetId(widget.id); | |
844 | + }); | |
723 | 845 | } |
724 | 846 | |
725 | 847 | pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { |
726 | - // TODO: | |
727 | - this.dialogService.todo(); | |
848 | + this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, | |
849 | + pos).subscribe( | |
850 | + (widget) => { | |
851 | + layoutCtx.widgets.addWidgetId(widget.id); | |
852 | + }); | |
728 | 853 | } |
729 | 854 | |
730 | 855 | removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { |
731 | - // TODO: | |
732 | - this.dialogService.todo(); | |
856 | + let title = widget.config.title; | |
857 | + if (!title || title.length === 0) { | |
858 | + title = this.widgetComponentService.getInstantWidgetInfo(widget).widgetName; | |
859 | + } | |
860 | + const confirmTitle = this.translate.instant('widget.remove-widget-title', {widgetTitle: title}); | |
861 | + const confirmContent = this.translate.instant('widget.remove-widget-text'); | |
862 | + this.dialogService.confirm(confirmTitle, | |
863 | + confirmContent, | |
864 | + this.translate.instant('action.no'), | |
865 | + this.translate.instant('action.yes'), | |
866 | + ).subscribe((res) => { | |
867 | + if (res) { | |
868 | + if (layoutCtx.widgets.removeWidgetId(widget.id)) { | |
869 | + this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget.id); | |
870 | + } | |
871 | + } | |
872 | + }); | |
733 | 873 | } |
734 | 874 | |
735 | - exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) { | |
875 | + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | |
736 | 876 | $event.stopPropagation(); |
737 | 877 | // TODO: |
738 | 878 | this.dialogService.todo(); |
739 | 879 | } |
740 | 880 | |
741 | - widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) { | |
881 | + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | |
742 | 882 | if (this.isEditingWidget) { |
743 | - this.editWidget($event, layoutCtx, widget, index); | |
883 | + this.editWidget($event, layoutCtx, widget); | |
744 | 884 | } |
745 | 885 | } |
746 | 886 | |
747 | - widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) { | |
887 | + widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | |
748 | 888 | if (this.isEdit && !this.isEditingWidget) { |
749 | - layoutCtx.ctrl.selectWidget(index, 0); | |
889 | + layoutCtx.ctrl.selectWidget(widget.id, 0); | |
750 | 890 | } |
751 | 891 | } |
752 | 892 | |
... | ... | @@ -795,13 +935,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
795 | 935 | return dashboardContextActions; |
796 | 936 | } |
797 | 937 | |
798 | - prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem> { | |
938 | + prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem> { | |
799 | 939 | const widgetContextActions: Array<WidgetContextMenuItem> = []; |
800 | 940 | if (this.isEdit && !this.isEditingWidget) { |
801 | 941 | widgetContextActions.push( |
802 | 942 | { |
803 | 943 | action: (event, currentWidget) => { |
804 | - this.editWidget(event, layoutCtx, currentWidget, index); | |
944 | + this.editWidget(event, layoutCtx, currentWidget); | |
805 | 945 | }, |
806 | 946 | enabled: true, |
807 | 947 | value: 'action.edit', | ... | ... |
... | ... | @@ -15,14 +15,13 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard, WidgetLayouts } from '@app/shared/models/dashboard.models'; |
18 | -import { Widget } from '@app/shared/models/widget.models'; | |
18 | +import { Widget, WidgetPosition } 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 | 21 | import { ILayoutController } from './layout/layout.models'; |
22 | 22 | import { |
23 | 23 | DashboardContextMenuItem, |
24 | - WidgetContextMenuItem, | |
25 | - WidgetPosition | |
24 | + WidgetContextMenuItem | |
26 | 25 | } from '@home/models/dashboard-component.models'; |
27 | 26 | import { Observable } from 'rxjs'; |
28 | 27 | import { ChangeDetectorRef } from '@angular/core'; |
... | ... | @@ -43,13 +42,13 @@ export interface IDashboardController { |
43 | 42 | openRightLayout(); |
44 | 43 | openDashboardState(stateId: string, openRightLayout: boolean); |
45 | 44 | addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); |
46 | - editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number); | |
47 | - exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number); | |
45 | + editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | |
46 | + exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | |
48 | 47 | removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); |
49 | - widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number); | |
50 | - widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number); | |
48 | + widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | |
49 | + widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | |
51 | 50 | prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>; |
52 | - prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem>; | |
51 | + prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem>; | |
53 | 52 | copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); |
54 | 53 | copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); |
55 | 54 | pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); |
... | ... | @@ -58,7 +57,7 @@ export interface IDashboardController { |
58 | 57 | |
59 | 58 | export interface DashboardPageLayoutContext { |
60 | 59 | id: DashboardLayoutId; |
61 | - widgets: Array<Widget>; | |
60 | + widgets: LayoutWidgetsArray; | |
62 | 61 | widgetLayouts: WidgetLayouts; |
63 | 62 | gridSettings: GridSettings; |
64 | 63 | ctrl: ILayoutController; |
... | ... | @@ -73,3 +72,69 @@ export interface DashboardPageLayout { |
73 | 72 | |
74 | 73 | export declare type DashboardPageLayouts = {[key in DashboardLayoutId]: DashboardPageLayout}; |
75 | 74 | |
75 | +export class LayoutWidgetsArray implements Iterable<Widget> { | |
76 | + | |
77 | + private widgetIds: string[] = []; | |
78 | + private pointer = 0; | |
79 | + | |
80 | + constructor(private dashboard: Dashboard) { | |
81 | + } | |
82 | + | |
83 | + size() { | |
84 | + return this.widgetIds.length; | |
85 | + } | |
86 | + | |
87 | + setWidgetIds(widgetIds: string[]) { | |
88 | + this.widgetIds = widgetIds; | |
89 | + } | |
90 | + | |
91 | + addWidgetId(widgetId: string) { | |
92 | + this.widgetIds.push(widgetId); | |
93 | + } | |
94 | + | |
95 | + removeWidgetId(widgetId: string): boolean { | |
96 | + const index = this.widgetIds.indexOf(widgetId); | |
97 | + if (index > -1) { | |
98 | + this.widgetIds.splice(index, 1); | |
99 | + return true; | |
100 | + } | |
101 | + return false; | |
102 | + } | |
103 | + | |
104 | + [Symbol.iterator](): Iterator<Widget> { | |
105 | + let pointer = 0; | |
106 | + const widgetIds = this.widgetIds; | |
107 | + const dashboard = this.dashboard; | |
108 | + return { | |
109 | + next(value?: any): IteratorResult<Widget> { | |
110 | + if (pointer < widgetIds.length) { | |
111 | + const widgetId = widgetIds[pointer++]; | |
112 | + const widget = dashboard.configuration.widgets[widgetId]; | |
113 | + return { | |
114 | + done: false, | |
115 | + value: widget | |
116 | + }; | |
117 | + } else { | |
118 | + return { | |
119 | + done: true, | |
120 | + value: null | |
121 | + }; | |
122 | + } | |
123 | + } | |
124 | + }; | |
125 | + } | |
126 | + | |
127 | + public widgetByIndex(index: number): Widget { | |
128 | + const widgetId = this.widgetIds[index]; | |
129 | + if (widgetId) { | |
130 | + return this.widgetById(widgetId); | |
131 | + } else { | |
132 | + return null; | |
133 | + } | |
134 | + } | |
135 | + | |
136 | + private widgetById(widgetId: string): Widget { | |
137 | + return this.dashboard.configuration.widgets[widgetId]; | |
138 | + } | |
139 | + | |
140 | +} | ... | ... |
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 | +<form #settingsForm="ngForm" (ngSubmit)="save()"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2 translate>{{settings ? 'dashboard.settings' : 'layout.settings'}}</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async"> | |
32 | + <div *ngIf="settings" [formGroup]="settingsFormGroup"> | |
33 | + <mat-form-field class="mat-block"> | |
34 | + <mat-label translate>dashboard.state-controller</mat-label> | |
35 | + <mat-select required matInput formControlName="stateControllerId"> | |
36 | + <mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId"> | |
37 | + {{stateControllerId}} | |
38 | + </mat-option> | |
39 | + </mat-select> | |
40 | + </mat-form-field> | |
41 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
42 | + <mat-checkbox fxFlex formControlName="toolbarAlwaysOpen"> | |
43 | + {{ 'dashboard.toolbar-always-open' | translate }} | |
44 | + </mat-checkbox> | |
45 | + <mat-checkbox fxFlex formControlName="showTitle"> | |
46 | + {{ 'dashboard.display-title' | translate }} | |
47 | + </mat-checkbox> | |
48 | + <tb-color-input fxFlex | |
49 | + label="{{'dashboard.title-color' | translate}}" | |
50 | + icon="format_color_fill" | |
51 | + openOnInput | |
52 | + formControlName="titleColor"> | |
53 | + </tb-color-input> | |
54 | + </div> | |
55 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
56 | + <mat-checkbox fxFlex formControlName="showDashboardsSelect"> | |
57 | + {{ 'dashboard.display-dashboards-selection' | translate }} | |
58 | + </mat-checkbox> | |
59 | + <mat-checkbox fxFlex formControlName="showEntitiesSelect"> | |
60 | + {{ 'dashboard.display-entities-selection' | translate }} | |
61 | + </mat-checkbox> | |
62 | + <mat-checkbox fxFlex formControlName="showDashboardTimewindow"> | |
63 | + {{ 'dashboard.display-dashboard-timewindow' | translate }} | |
64 | + </mat-checkbox> | |
65 | + <mat-checkbox fxFlex formControlName="showDashboardExport"> | |
66 | + {{ 'dashboard.display-dashboard-export' | translate }} | |
67 | + </mat-checkbox> | |
68 | + </div> | |
69 | + </div> | |
70 | + <div *ngIf="gridSettings" [formGroup]="gridSettingsFormGroup"> | |
71 | + <tb-color-input fxFlex | |
72 | + label="{{'layout.color' | translate}}" | |
73 | + icon="format_color_fill" | |
74 | + openOnInput | |
75 | + formControlName="color"> | |
76 | + </tb-color-input> | |
77 | + <mat-form-field class="mat-block"> | |
78 | + <mat-label translate>dashboard.columns-count</mat-label> | |
79 | + <input matInput formControlName="columns" type="number" step="any" min="10" | |
80 | + max="1000" required> | |
81 | + <mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('required')"> | |
82 | + {{ 'dashboard.columns-count-required' | translate }} | |
83 | + </mat-error> | |
84 | + <mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('min')"> | |
85 | + {{ 'dashboard.min-columns-count-message' | translate }} | |
86 | + </mat-error> | |
87 | + <mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('max')"> | |
88 | + {{ 'dashboard.max-columns-count-message' | translate }} | |
89 | + </mat-error> | |
90 | + </mat-form-field> | |
91 | + <mat-form-field fxFlex class="mat-block"> | |
92 | + <mat-label translate>dashboard.widgets-margins</mat-label> | |
93 | + <input matInput formControlName="margin" type="number" step="any" min="0" | |
94 | + max="50" required> | |
95 | + <mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('required')"> | |
96 | + {{ 'dashboard.margin-required' | translate }} | |
97 | + </mat-error> | |
98 | + <mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('min')"> | |
99 | + {{ 'dashboard.min-margin-message' | translate }} | |
100 | + </mat-error> | |
101 | + <mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('max')"> | |
102 | + {{ 'dashboard.max-margin-message' | translate }} | |
103 | + </mat-error> | |
104 | + </mat-form-field> | |
105 | + <mat-checkbox fxFlex formControlName="autoFillHeight" style="display: block; padding-bottom: 12px;"> | |
106 | + {{ 'dashboard.autofill-height' | translate }} | |
107 | + </mat-checkbox> | |
108 | + <tb-color-input fxFlex | |
109 | + label="{{'dashboard.background-color' | translate}}" | |
110 | + icon="format_color_fill" | |
111 | + openOnInput | |
112 | + formControlName="backgroundColor"> | |
113 | + </tb-color-input> | |
114 | + <tb-image-input fxFlex | |
115 | + label="{{'dashboard.background-image' | translate}}" | |
116 | + formControlName="backgroundImageUrl"> | |
117 | + </tb-image-input> | |
118 | + <mat-form-field class="mat-block"> | |
119 | + <mat-label translate>dashboard.background-size-mode</mat-label> | |
120 | + <mat-select matInput formControlName="backgroundSizeMode"> | |
121 | + <mat-option value="100%">Fit width</mat-option> | |
122 | + <mat-option value="auto 100%">Fit height</mat-option> | |
123 | + <mat-option value="cover">Cover</mat-option> | |
124 | + <mat-option value="contain">Contain</mat-option> | |
125 | + <mat-option value="auto">Original size</mat-option> | |
126 | + </mat-select> | |
127 | + </mat-form-field> | |
128 | + <small translate>dashboard.mobile-layout</small> | |
129 | + <div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
130 | + <mat-checkbox fxFlex formControlName="mobileAutoFillHeight"> | |
131 | + {{ 'dashboard.autofill-height' | translate }} | |
132 | + </mat-checkbox> | |
133 | + <mat-form-field fxFlex class="mat-block"> | |
134 | + <mat-label translate>dashboard.mobile-row-height</mat-label> | |
135 | + <input matInput formControlName="mobileRowHeight" type="number" step="any" min="5" | |
136 | + max="200" required> | |
137 | + <mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')"> | |
138 | + {{ 'dashboard.mobile-row-height-required' | translate }} | |
139 | + </mat-error> | |
140 | + <mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')"> | |
141 | + {{ 'dashboard.min-mobile-row-height-message' | translate }} | |
142 | + </mat-error> | |
143 | + <mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')"> | |
144 | + {{ 'dashboard.max-mobile-row-height-message' | translate }} | |
145 | + </mat-error> | |
146 | + </mat-form-field> | |
147 | + </div> | |
148 | + </div> | |
149 | + </fieldset> | |
150 | + </div> | |
151 | + <div mat-dialog-actions fxLayout="row"> | |
152 | + <span fxFlex></span> | |
153 | + <button mat-button mat-raised-button color="primary" | |
154 | + type="submit" | |
155 | + [disabled]="(isLoading$ | async) || settingsFormGroup.invalid || gridSettingsFormGroup.invalid | |
156 | + || (!settingsFormGroup.dirty && !gridSettingsFormGroup.dirty)"> | |
157 | + {{ 'action.save' | translate }} | |
158 | + </button> | |
159 | + <button mat-button color="primary" | |
160 | + style="margin-right: 20px;" | |
161 | + type="button" | |
162 | + [disabled]="(isLoading$ | async)" | |
163 | + (click)="cancel()" cdkFocusInitial> | |
164 | + {{ 'action.cancel' | translate }} | |
165 | + </button> | |
166 | + </div> | |
167 | +</form> | ... | ... |
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, Inject, OnInit, SkipSelf } from '@angular/core'; | |
18 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; | |
22 | +import { Router } from '@angular/router'; | |
23 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
24 | +import { UtilsService } from '@core/services/utils.service'; | |
25 | +import { TranslateService } from '@ngx-translate/core'; | |
26 | +import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models'; | |
27 | +import { isUndefined } from '@core/utils'; | |
28 | +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | |
29 | +import { StatesControllerService } from './states/states-controller.service'; | |
30 | + | |
31 | +export interface DashboardSettingsDialogData { | |
32 | + settings?: DashboardSettings; | |
33 | + gridSettings?: GridSettings; | |
34 | +} | |
35 | + | |
36 | +@Component({ | |
37 | + selector: 'tb-dashboard-settings-dialog', | |
38 | + templateUrl: './dashboard-settings-dialog.component.html', | |
39 | + providers: [{provide: ErrorStateMatcher, useExisting: DashboardSettingsDialogComponent}], | |
40 | + styleUrls: [] | |
41 | +}) | |
42 | +export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData> | |
43 | + implements OnInit, ErrorStateMatcher { | |
44 | + | |
45 | + settings: DashboardSettings; | |
46 | + gridSettings: GridSettings; | |
47 | + | |
48 | + settingsFormGroup: FormGroup; | |
49 | + gridSettingsFormGroup: FormGroup; | |
50 | + | |
51 | + stateControllerIds: string[]; | |
52 | + | |
53 | + submitted = false; | |
54 | + | |
55 | + constructor(protected store: Store<AppState>, | |
56 | + protected router: Router, | |
57 | + @Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData, | |
58 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
59 | + public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>, | |
60 | + private fb: FormBuilder, | |
61 | + private utils: UtilsService, | |
62 | + private dashboardUtils: DashboardUtilsService, | |
63 | + private translate: TranslateService, | |
64 | + private statesControllerService: StatesControllerService) { | |
65 | + super(store, router, dialogRef); | |
66 | + | |
67 | + this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers()); | |
68 | + | |
69 | + this.settings = this.data.settings; | |
70 | + this.gridSettings = this.data.gridSettings; | |
71 | + | |
72 | + if (this.settings) { | |
73 | + this.settingsFormGroup = this.fb.group({ | |
74 | + stateControllerId: [isUndefined(this.settings.stateControllerId) ? 'entity' : this.settings.stateControllerId, []], | |
75 | + toolbarAlwaysOpen: [isUndefined(this.settings.toolbarAlwaysOpen) ? true : this.settings.toolbarAlwaysOpen, []], | |
76 | + showTitle: [isUndefined(this.settings.showTitle) ? true : this.settings.showTitle, []], | |
77 | + titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []], | |
78 | + showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []], | |
79 | + showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []], | |
80 | + showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []], | |
81 | + showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []] | |
82 | + }); | |
83 | + this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe( | |
84 | + (stateControllerId: StateControllerId) => { | |
85 | + if (stateControllerId !== 'default') { | |
86 | + this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true); | |
87 | + } | |
88 | + } | |
89 | + ); | |
90 | + } else { | |
91 | + this.settingsFormGroup = this.fb.group({}); | |
92 | + } | |
93 | + | |
94 | + if (this.gridSettings) { | |
95 | + this.gridSettingsFormGroup = this.fb.group({ | |
96 | + color: [this.gridSettings.color || 'rgba(0,0,0,0.870588)', []], | |
97 | + columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1000)]], | |
98 | + margin: [this.gridSettings.margin || 10, [Validators.required, Validators.min(0), Validators.max(50)]], | |
99 | + autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []], | |
100 | + backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []], | |
101 | + backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []], | |
102 | + backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []], | |
103 | + mobileAutoFillHeight: [isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight, []], | |
104 | + mobileRowHeight: [isUndefined(this.gridSettings.mobileRowHeight) ? 70 : this.gridSettings.mobileRowHeight, | |
105 | + [Validators.required, Validators.min(5), Validators.max(200)]] | |
106 | + }); | |
107 | + } else { | |
108 | + this.gridSettingsFormGroup = this.fb.group({}); | |
109 | + } | |
110 | + } | |
111 | + | |
112 | + ngOnInit(): void { | |
113 | + } | |
114 | + | |
115 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
116 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
117 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
118 | + return originalErrorState || customErrorState; | |
119 | + } | |
120 | + | |
121 | + cancel(): void { | |
122 | + this.dialogRef.close(null); | |
123 | + } | |
124 | + | |
125 | + save(): void { | |
126 | + this.submitted = true; | |
127 | + let settings: DashboardSettings = null; | |
128 | + let gridSettings: GridSettings = null; | |
129 | + if (this.settings) { | |
130 | + settings = {...this.settings, ...this.settingsFormGroup.value}; | |
131 | + } | |
132 | + if (this.gridSettings) { | |
133 | + gridSettings = {...this.gridSettings, ...this.gridSettingsFormGroup.value}; | |
134 | + } | |
135 | + this.dialogRef.close({settings, gridSettings}); | |
136 | + } | |
137 | +} | ... | ... |
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> | |
19 | + <mat-tab-group *ngIf="hasWidgetTypes()" class="tb-absolute-fill" fxFlex> | |
20 | + <mat-tab *ngIf="timeseriesWidgetTypes.length" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> | |
21 | + <tb-dashboard [aliasController]="aliasController" | |
22 | + [widgets]="timeseriesWidgetTypes" | |
23 | + [widgetLayouts]="{}" | |
24 | + [isEdit]="false" | |
25 | + [isMobile]="true" | |
26 | + [isEditActionEnabled]="false" | |
27 | + [isExportActionEnabled]="false" | |
28 | + [isRemoveActionEnabled]="false" | |
29 | + [callbacks]="callbacks"></tb-dashboard> | |
30 | + </mat-tab> | |
31 | + <mat-tab *ngIf="latestWidgetTypes.length" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}"> | |
32 | + <tb-dashboard [aliasController]="aliasController" | |
33 | + [widgets]="latestWidgetTypes" | |
34 | + [widgetLayouts]="{}" | |
35 | + [isEdit]="false" | |
36 | + [isMobile]="true" | |
37 | + [isEditActionEnabled]="false" | |
38 | + [isExportActionEnabled]="false" | |
39 | + [isRemoveActionEnabled]="false" | |
40 | + [callbacks]="callbacks"></tb-dashboard> | |
41 | + </mat-tab> | |
42 | + <mat-tab *ngIf="rpcWidgetTypes.length" style="height: 100%;" label="{{ 'widget.rpc' | translate }}"> | |
43 | + <tb-dashboard [aliasController]="aliasController" | |
44 | + [widgets]="rpcWidgetTypes" | |
45 | + [widgetLayouts]="{}" | |
46 | + [isEdit]="false" | |
47 | + [isMobile]="true" | |
48 | + [isEditActionEnabled]="false" | |
49 | + [isExportActionEnabled]="false" | |
50 | + [isRemoveActionEnabled]="false" | |
51 | + [callbacks]="callbacks"></tb-dashboard> | |
52 | + </mat-tab> | |
53 | + <mat-tab *ngIf="alarmWidgetTypes.length" style="height: 100%;" label="{{ 'widget.alarm' | translate }}"> | |
54 | + <tb-dashboard [aliasController]="aliasController" | |
55 | + [widgets]="alarmWidgetTypes" | |
56 | + [widgetLayouts]="{}" | |
57 | + [isEdit]="false" | |
58 | + [isMobile]="true" | |
59 | + [isEditActionEnabled]="false" | |
60 | + [isExportActionEnabled]="false" | |
61 | + [isRemoveActionEnabled]="false" | |
62 | + [callbacks]="callbacks"></tb-dashboard> | |
63 | + </mat-tab> | |
64 | + <mat-tab *ngIf="staticWidgetTypes.length" style="height: 100%;" label="{{ 'widget.static' | translate }}"> | |
65 | + <tb-dashboard [aliasController]="aliasController" | |
66 | + [widgets]="staticWidgetTypes" | |
67 | + [widgetLayouts]="{}" | |
68 | + [isEdit]="false" | |
69 | + [isMobile]="true" | |
70 | + [isEditActionEnabled]="false" | |
71 | + [isExportActionEnabled]="false" | |
72 | + [isRemoveActionEnabled]="false" | |
73 | + [callbacks]="callbacks"></tb-dashboard> | |
74 | + </mat-tab> | |
75 | + </mat-tab-group> | |
76 | + <span translate *ngIf="widgetsBundle && !hasWidgetTypes()" | |
77 | + style="text-transform: uppercase; display: flex;" | |
78 | + fxLayoutAlign="center center" | |
79 | + class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> | |
80 | + <span translate *ngIf="!widgetsBundle" | |
81 | + style="text-transform: uppercase; display: flex;" | |
82 | + fxLayoutAlign="center center" | |
83 | + class="mat-headline tb-absolute-fill">widget.select-widgets-bundle</span> | |
84 | +</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 ::ng-deep { | |
17 | + .mat-tab-group { | |
18 | + .mat-tab-body-wrapper { | |
19 | + height: 100%; | |
20 | + } | |
21 | + } | |
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 { | |
18 | + Component, | |
19 | + OnDestroy, | |
20 | + OnInit, | |
21 | + ViewEncapsulation, | |
22 | + Input, | |
23 | + Output, | |
24 | + EventEmitter, | |
25 | + OnChanges, | |
26 | + SimpleChanges | |
27 | +} from '@angular/core'; | |
28 | +import { PageComponent } from '@shared/components/page.component'; | |
29 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | |
30 | +import { IAliasController } from '@core/api/widget-api.models'; | |
31 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | |
32 | +import { WidgetService } from '@core/http/widget.service'; | |
33 | +import { widgetType, Widget } from '@shared/models/widget.models'; | |
34 | +import { toWidgetInfo } from '@home/models/widget-component.models'; | |
35 | +import { DashboardCallbacks } from '../../models/dashboard-component.models'; | |
36 | + | |
37 | +@Component({ | |
38 | + selector: 'tb-dashboard-widget-select', | |
39 | + templateUrl: './dashboard-widget-select.component.html', | |
40 | + styleUrls: ['./dashboard-widget-select.component.scss'] | |
41 | +}) | |
42 | +export class DashboardWidgetSelectComponent implements OnInit, OnChanges { | |
43 | + | |
44 | + @Input() | |
45 | + widgetsBundle: WidgetsBundle; | |
46 | + | |
47 | + @Input() | |
48 | + aliasController: IAliasController; | |
49 | + | |
50 | + @Output() | |
51 | + widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); | |
52 | + | |
53 | + timeseriesWidgetTypes: Array<Widget> = []; | |
54 | + latestWidgetTypes: Array<Widget> = []; | |
55 | + rpcWidgetTypes: Array<Widget> = []; | |
56 | + alarmWidgetTypes: Array<Widget> = []; | |
57 | + staticWidgetTypes: Array<Widget> = []; | |
58 | + | |
59 | + callbacks: DashboardCallbacks = { | |
60 | + onWidgetClicked: this.onWidgetClicked.bind(this) | |
61 | + }; | |
62 | + | |
63 | + constructor(private widgetsService: WidgetService) { | |
64 | + } | |
65 | + | |
66 | + ngOnInit(): void { | |
67 | + } | |
68 | + | |
69 | + ngOnChanges(changes: SimpleChanges): void { | |
70 | + for (const propName of Object.keys(changes)) { | |
71 | + const change = changes[propName]; | |
72 | + if (change.currentValue !== change.previousValue && change.currentValue) { | |
73 | + if (propName === 'widgetsBundle') { | |
74 | + this.loadLibrary(); | |
75 | + } | |
76 | + } | |
77 | + } | |
78 | + } | |
79 | + | |
80 | + private loadLibrary() { | |
81 | + this.timeseriesWidgetTypes.length = 0; | |
82 | + this.latestWidgetTypes.length = 0; | |
83 | + this.rpcWidgetTypes.length = 0; | |
84 | + this.alarmWidgetTypes.length = 0; | |
85 | + this.staticWidgetTypes.length = 0; | |
86 | + const bundleAlias = this.widgetsBundle.alias; | |
87 | + const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; | |
88 | + this.widgetsService.getBundleWidgetTypes(bundleAlias, | |
89 | + isSystem).subscribe( | |
90 | + (types) => { | |
91 | + types = types.sort((a, b) => b.createdTime - a.createdTime); | |
92 | + let top = 0; | |
93 | + types.forEach((type) => { | |
94 | + const widgetTypeInfo = toWidgetInfo(type); | |
95 | + const widget: Widget = { | |
96 | + typeId: type.id, | |
97 | + isSystemType: isSystem, | |
98 | + bundleAlias, | |
99 | + typeAlias: widgetTypeInfo.alias, | |
100 | + type: widgetTypeInfo.type, | |
101 | + title: widgetTypeInfo.widgetName, | |
102 | + sizeX: widgetTypeInfo.sizeX, | |
103 | + sizeY: widgetTypeInfo.sizeY, | |
104 | + row: top, | |
105 | + col: 0, | |
106 | + config: JSON.parse(widgetTypeInfo.defaultConfig) | |
107 | + }; | |
108 | + widget.config.title = widgetTypeInfo.widgetName; | |
109 | + switch (widgetTypeInfo.type) { | |
110 | + case widgetType.timeseries: | |
111 | + this.timeseriesWidgetTypes.push(widget); | |
112 | + break; | |
113 | + case widgetType.latest: | |
114 | + this.latestWidgetTypes.push(widget); | |
115 | + break; | |
116 | + case widgetType.rpc: | |
117 | + this.rpcWidgetTypes.push(widget); | |
118 | + break; | |
119 | + case widgetType.alarm: | |
120 | + this.alarmWidgetTypes.push(widget); | |
121 | + break; | |
122 | + case widgetType.static: | |
123 | + this.staticWidgetTypes.push(widget); | |
124 | + break; | |
125 | + } | |
126 | + top += widget.sizeY; | |
127 | + }); | |
128 | + } | |
129 | + ); | |
130 | + } | |
131 | + | |
132 | + hasWidgetTypes() { | |
133 | + return this.timeseriesWidgetTypes.length > 0 || | |
134 | + this.latestWidgetTypes.length > 0 || | |
135 | + this.rpcWidgetTypes.length > 0 || | |
136 | + this.alarmWidgetTypes.length > 0 || | |
137 | + this.staticWidgetTypes.length > 0; | |
138 | + } | |
139 | + | |
140 | + private onWidgetClicked($event: Event, widget: Widget, index: number): void { | |
141 | + this.widgetSelected.emit(widget); | |
142 | + } | |
143 | + | |
144 | +} | ... | ... |
... | ... | @@ -29,13 +29,22 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component'; |
29 | 29 | import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; |
30 | 30 | import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; |
31 | 31 | import { EditWidgetComponent } from './edit-widget.component'; |
32 | +import { DashboardWidgetSelectComponent } from './dashboard-widget-select.component'; | |
33 | +import { AddWidgetDialogComponent } from './add-widget-dialog.component'; | |
34 | +import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component'; | |
35 | +import { SelectTargetLayoutDialogComponent } from './layout/select-target-layout-dialog.component'; | |
36 | +import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component'; | |
32 | 37 | |
33 | 38 | @NgModule({ |
34 | 39 | entryComponents: [ |
35 | 40 | DashboardFormComponent, |
36 | 41 | DashboardTabsComponent, |
37 | 42 | ManageDashboardCustomersDialogComponent, |
38 | - MakeDashboardPublicDialogComponent | |
43 | + MakeDashboardPublicDialogComponent, | |
44 | + AddWidgetDialogComponent, | |
45 | + ManageDashboardLayoutsDialogComponent, | |
46 | + SelectTargetLayoutDialogComponent, | |
47 | + DashboardSettingsDialogComponent | |
39 | 48 | ], |
40 | 49 | declarations: [ |
41 | 50 | DashboardFormComponent, |
... | ... | @@ -45,7 +54,12 @@ import { EditWidgetComponent } from './edit-widget.component'; |
45 | 54 | DashboardToolbarComponent, |
46 | 55 | DashboardPageComponent, |
47 | 56 | DashboardLayoutComponent, |
48 | - EditWidgetComponent | |
57 | + EditWidgetComponent, | |
58 | + DashboardWidgetSelectComponent, | |
59 | + AddWidgetDialogComponent, | |
60 | + ManageDashboardLayoutsDialogComponent, | |
61 | + SelectTargetLayoutDialogComponent, | |
62 | + DashboardSettingsDialogComponent | |
49 | 63 | ], |
50 | 64 | imports: [ |
51 | 65 | CommonModule, | ... | ... |
... | ... | @@ -132,26 +132,4 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan |
132 | 132 | }; |
133 | 133 | this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); |
134 | 134 | } |
135 | - | |
136 | - private createEntityAlias(alias: string, allowedEntityTypes: Array<EntityType>): Observable<EntityAlias> { | |
137 | - const singleEntityAlias: EntityAlias = {id: null, alias, filter: {resolveMultiple: false}}; | |
138 | - return this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData, | |
139 | - EntityAlias>(EntityAliasDialogComponent, { | |
140 | - disableClose: true, | |
141 | - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
142 | - data: { | |
143 | - isAdd: true, | |
144 | - allowedEntityTypes, | |
145 | - entityAliases: this.dashboard.configuration.entityAliases, | |
146 | - alias: singleEntityAlias | |
147 | - } | |
148 | - }).afterClosed().pipe( | |
149 | - tap((entityAlias) => { | |
150 | - if (entityAlias) { | |
151 | - this.dashboard.configuration.entityAliases[entityAlias.id] = entityAlias; | |
152 | - this.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); | |
153 | - } | |
154 | - }) | |
155 | - ); | |
156 | - } | |
157 | 135 | } | ... | ... |
... | ... | @@ -17,14 +17,9 @@ |
17 | 17 | --> |
18 | 18 | <hotkeys-cheatsheet></hotkeys-cheatsheet> |
19 | 19 | <div class="mat-content" style="position: relative; width: 100%; height: 100%;" |
20 | - [ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor, | |
21 | - 'background-image': layoutCtx.gridSettings.backgroundImageUrl ? | |
22 | - 'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none', | |
23 | - 'background-repeat': 'no-repeat', | |
24 | - 'background-attachment': 'scroll', | |
25 | - 'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%', | |
26 | - 'background-position': '0% 0%'}"> | |
27 | - <section *ngIf="layoutCtx.widgets.length === 0" fxLayoutAlign="center center" | |
20 | + [style.backgroundImage]="backgroundImage" | |
21 | + [ngStyle]="dashboardStyle"> | |
22 | + <section *ngIf="layoutCtx.widgets.size() === 0" fxLayoutAlign="center center" | |
28 | 23 | [ngStyle]="{'color': layoutCtx.gridSettings.color}" |
29 | 24 | style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;" |
30 | 25 | class="mat-headline tb-absolute-fill"> |
... | ... | @@ -37,18 +32,12 @@ |
37 | 32 | {{ 'dashboard.add-widget' | translate }} |
38 | 33 | </button> |
39 | 34 | </section> |
40 | - <tb-dashboard #dashboard [dashboardStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor, | |
41 | - 'background-image': layoutCtx.gridSettings.backgroundImageUrl ? | |
42 | - 'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none', | |
43 | - 'background-repeat': 'no-repeat', | |
44 | - 'background-attachment': 'scroll', | |
45 | - 'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%', | |
46 | - 'background-position': '0% 0%'}" | |
35 | + <tb-dashboard #dashboard [dashboardStyle]="dashboardStyle" | |
36 | + [backgroundImage]="backgroundImage" | |
47 | 37 | [widgets]="layoutCtx.widgets" |
48 | 38 | [widgetLayouts]="layoutCtx.widgetLayouts" |
49 | 39 | [columns]="layoutCtx.gridSettings.columns" |
50 | - [horizontalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[0] : 10" | |
51 | - [verticalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[1]: 10" | |
40 | + [margin]="layoutCtx.gridSettings.margin" | |
52 | 41 | [aliasController]="dashboardCtx.aliasController" |
53 | 42 | [stateController]="dashboardCtx.stateController" |
54 | 43 | [dashboardTimewindow]="dashboardCtx.dashboardTimewindow" | ... | ... |
... | ... | @@ -34,6 +34,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys'; |
34 | 34 | import { getCurrentIsLoading } from '@core/interceptors/load.selectors'; |
35 | 35 | import { TranslateService } from '@ngx-translate/core'; |
36 | 36 | import { ItemBufferService } from '@app/core/services/item-buffer.service'; |
37 | +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; | |
37 | 38 | |
38 | 39 | @Component({ |
39 | 40 | selector: 'tb-dashboard-layout', |
... | ... | @@ -43,12 +44,17 @@ import { ItemBufferService } from '@app/core/services/item-buffer.service'; |
43 | 44 | export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy { |
44 | 45 | |
45 | 46 | layoutCtxValue: DashboardPageLayoutContext; |
47 | + dashboardStyle: {[klass: string]: any} = null; | |
48 | + backgroundImage: SafeStyle | string; | |
46 | 49 | |
47 | 50 | @Input() |
48 | 51 | set layoutCtx(val: DashboardPageLayoutContext) { |
49 | 52 | this.layoutCtxValue = val; |
50 | 53 | if (this.layoutCtxValue) { |
51 | 54 | this.layoutCtxValue.ctrl = this; |
55 | + if (this.dashboardStyle == null) { | |
56 | + this.loadDashboardStyle(); | |
57 | + } | |
52 | 58 | } |
53 | 59 | } |
54 | 60 | get layoutCtx(): DashboardPageLayoutContext { |
... | ... | @@ -77,7 +83,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
77 | 83 | constructor(protected store: Store<AppState>, |
78 | 84 | private hotkeysService: HotkeysService, |
79 | 85 | private translate: TranslateService, |
80 | - private itembuffer: ItemBufferService) { | |
86 | + private itembuffer: ItemBufferService, | |
87 | + private sanitizer: DomSanitizer) { | |
81 | 88 | super(store); |
82 | 89 | } |
83 | 90 | |
... | ... | @@ -165,54 +172,67 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
165 | 172 | ); |
166 | 173 | } |
167 | 174 | |
168 | - reload() { | |
175 | + private loadDashboardStyle() { | |
176 | + this.dashboardStyle = {'background-color': this.layoutCtx.gridSettings.backgroundColor, | |
177 | + 'background-repeat': 'no-repeat', | |
178 | + 'background-attachment': 'scroll', | |
179 | + 'background-size': this.layoutCtx.gridSettings.backgroundSizeMode || '100%', | |
180 | + 'background-position': '0% 0%'}; | |
181 | + this.backgroundImage = this.layoutCtx.gridSettings.backgroundImageUrl ? | |
182 | + this.sanitizer.bypassSecurityTrustStyle('url(' + this.layoutCtx.gridSettings.backgroundImageUrl + ')') : 'none'; | |
169 | 183 | } |
170 | 184 | |
171 | - setResizing(layoutVisibilityChanged: boolean) { | |
185 | + reload() { | |
186 | + this.loadDashboardStyle(); | |
187 | + this.dashboard.pauseChangeNotifications(); | |
188 | + setTimeout(() => { | |
189 | + this.dashboard.resumeChangeNotifications(); | |
190 | + this.dashboard.notifyLayoutUpdated(); | |
191 | + }, 0); | |
172 | 192 | } |
173 | 193 | |
174 | 194 | resetHighlight() { |
175 | 195 | this.dashboard.resetHighlight(); |
176 | 196 | } |
177 | 197 | |
178 | - highlightWidget(index: number, delay?: number) { | |
179 | - this.dashboard.highlightWidget(index, delay); | |
198 | + highlightWidget(widgetId: string, delay?: number) { | |
199 | + this.dashboard.highlightWidget(widgetId, delay); | |
180 | 200 | } |
181 | 201 | |
182 | - selectWidget(index: number, delay?: number) { | |
183 | - this.dashboard.selectWidget(index, delay); | |
202 | + selectWidget(widgetId: string, delay?: number) { | |
203 | + this.dashboard.selectWidget(widgetId, delay); | |
184 | 204 | } |
185 | 205 | |
186 | 206 | addWidget($event: Event) { |
187 | 207 | this.layoutCtx.dashboardCtrl.addWidget($event, this.layoutCtx); |
188 | 208 | } |
189 | 209 | |
190 | - onEditWidget($event: Event, widget: Widget, index: number): void { | |
191 | - this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget, index); | |
210 | + onEditWidget($event: Event, widget: Widget): void { | |
211 | + this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget); | |
192 | 212 | } |
193 | 213 | |
194 | - onExportWidget($event: Event, widget: Widget, index: number): void { | |
195 | - this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, index); | |
214 | + onExportWidget($event: Event, widget: Widget): void { | |
215 | + this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget); | |
196 | 216 | } |
197 | 217 | |
198 | - onRemoveWidget($event: Event, widget: Widget, index: number): void { | |
218 | + onRemoveWidget($event: Event, widget: Widget): void { | |
199 | 219 | return this.layoutCtx.dashboardCtrl.removeWidget($event, this.layoutCtx, widget); |
200 | 220 | } |
201 | 221 | |
202 | - onWidgetMouseDown($event: Event, widget: Widget, index: number): void { | |
203 | - this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget, index); | |
222 | + onWidgetMouseDown($event: Event, widget: Widget): void { | |
223 | + this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget); | |
204 | 224 | } |
205 | 225 | |
206 | - onWidgetClicked($event: Event, widget: Widget, index: number): void { | |
207 | - this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget, index); | |
226 | + onWidgetClicked($event: Event, widget: Widget): void { | |
227 | + this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget); | |
208 | 228 | } |
209 | 229 | |
210 | 230 | prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> { |
211 | 231 | return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx); |
212 | 232 | } |
213 | 233 | |
214 | - prepareWidgetContextMenu($event: Event, widget: Widget, index: number): Array<WidgetContextMenuItem> { | |
215 | - return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, index); | |
234 | + prepareWidgetContextMenu($event: Event, widget: Widget): Array<WidgetContextMenuItem> { | |
235 | + return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget); | |
216 | 236 | } |
217 | 237 | |
218 | 238 | copyWidget($event: Event, widget: Widget) { | ... | ... |
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 ::ng-deep { | |
17 | + button.tb-layout-button { | |
18 | + width: 100%; | |
19 | + max-width: 240px; | |
20 | + height: 100%; | |
21 | + .mat-button-wrapper { | |
22 | + padding: 40px; | |
23 | + line-height: 18px; | |
24 | + span { | |
25 | + font-size: 18px; | |
26 | + font-weight: 400; | |
27 | + white-space: normal; | |
28 | + } | |
29 | + } | |
30 | + } | |
31 | +} | ... | ... |
... | ... | @@ -19,10 +19,9 @@ import { WidgetLayout } from '@shared/models/dashboard.models'; |
19 | 19 | |
20 | 20 | export interface ILayoutController { |
21 | 21 | reload(); |
22 | - setResizing(layoutVisibilityChanged: boolean); | |
23 | 22 | resetHighlight(); |
24 | - highlightWidget(index: number, delay?: number); | |
25 | - selectWidget(index: number, delay?: number); | |
23 | + highlightWidget(widgetId: string, delay?: number); | |
24 | + selectWidget(widgetId: string, delay?: number); | |
26 | 25 | pasteWidget($event: MouseEvent); |
27 | 26 | pasteWidgetReference($event: MouseEvent); |
28 | 27 | } | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.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 | +<form #widgetForm="ngForm" [formGroup]="layoutsFormGroup" (ngSubmit)="save()"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2 translate>layout.manage</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px"> | |
32 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
33 | + <mat-checkbox fxFlex formControlName="main"> | |
34 | + {{ 'layout.main' | translate }} | |
35 | + </mat-checkbox> | |
36 | + <mat-checkbox fxFlex formControlName="right"> | |
37 | + {{ 'layout.right' | translate }} | |
38 | + </mat-checkbox> | |
39 | + </div> | |
40 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
41 | + <button fxFlex fxLayout="column" | |
42 | + type="button" mat-button mat-raised-button color="primary" | |
43 | + class="tb-layout-button" | |
44 | + (click)="openLayoutSettings('main')"> | |
45 | + <span translate>layout.main</span> | |
46 | + </button> | |
47 | + <button fxFlex fxLayout="column" [fxShow]="layoutsFormGroup.get('right').value" | |
48 | + type="button" mat-button mat-raised-button color="primary" | |
49 | + class="tb-layout-button" | |
50 | + (click)="openLayoutSettings('right')"> | |
51 | + <span translate>layout.right</span> | |
52 | + </button> | |
53 | + </div> | |
54 | + </fieldset> | |
55 | + </div> | |
56 | + <div mat-dialog-actions fxLayout="row"> | |
57 | + <span fxFlex></span> | |
58 | + <button mat-button mat-raised-button color="primary" | |
59 | + type="submit" | |
60 | + [disabled]="(isLoading$ | async) || layoutsFormGroup.invalid || !layoutsFormGroup.dirty"> | |
61 | + {{ 'action.save' | translate }} | |
62 | + </button> | |
63 | + <button mat-button color="primary" | |
64 | + style="margin-right: 20px;" | |
65 | + type="button" | |
66 | + [disabled]="(isLoading$ | async)" | |
67 | + (click)="cancel()" cdkFocusInitial> | |
68 | + {{ 'action.cancel' | translate }} | |
69 | + </button> | |
70 | + </div> | |
71 | +</form> | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.ts
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 | + | |
17 | +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; | |
18 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms'; | |
22 | +import { Router } from '@angular/router'; | |
23 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
24 | +import { Widget, widgetTypesData } from '@shared/models/widget.models'; | |
25 | +import { UtilsService } from '@core/services/utils.service'; | |
26 | +import { TranslateService } from '@ngx-translate/core'; | |
27 | +import { EntityService } from '@core/http/entity.service'; | |
28 | +import { Dashboard, DashboardLayoutId, DashboardStateLayouts } from '@app/shared/models/dashboard.models'; | |
29 | +import { IAliasController } from '@core/api/widget-api.models'; | |
30 | +import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; | |
31 | +import { deepClone, isDefined, isString } from '@core/utils'; | |
32 | +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | |
33 | +import { MatDialog } from '@angular/material/dialog'; | |
34 | +import { | |
35 | + DashboardSettingsDialogComponent, | |
36 | + DashboardSettingsDialogData | |
37 | +} from '@home/pages/dashboard/dashboard-settings-dialog.component'; | |
38 | + | |
39 | +export interface ManageDashboardLayoutsDialogData { | |
40 | + layouts: DashboardStateLayouts; | |
41 | +} | |
42 | + | |
43 | +@Component({ | |
44 | + selector: 'tb-manage-dashboard-layouts-dialog', | |
45 | + templateUrl: './manage-dashboard-layouts-dialog.component.html', | |
46 | + providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardLayoutsDialogComponent}], | |
47 | + styleUrls: ['./layout-button.scss'] | |
48 | +}) | |
49 | +export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts> | |
50 | + implements OnInit, ErrorStateMatcher { | |
51 | + | |
52 | + layoutsFormGroup: FormGroup; | |
53 | + | |
54 | + layouts: DashboardStateLayouts; | |
55 | + | |
56 | + submitted = false; | |
57 | + | |
58 | + constructor(protected store: Store<AppState>, | |
59 | + protected router: Router, | |
60 | + @Inject(MAT_DIALOG_DATA) public data: ManageDashboardLayoutsDialogData, | |
61 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
62 | + public dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>, | |
63 | + private fb: FormBuilder, | |
64 | + private utils: UtilsService, | |
65 | + private dashboardUtils: DashboardUtilsService, | |
66 | + private translate: TranslateService, | |
67 | + private dialog: MatDialog) { | |
68 | + super(store, router, dialogRef); | |
69 | + | |
70 | + this.layouts = this.data.layouts; | |
71 | + this.layoutsFormGroup = this.fb.group({ | |
72 | + main: [{value: isDefined(this.layouts.main), disabled: true}, []], | |
73 | + right: [isDefined(this.layouts.right), []], | |
74 | + } | |
75 | + ); | |
76 | + for (const l of Object.keys(this.layoutsFormGroup.controls)) { | |
77 | + const control = this.layoutsFormGroup.controls[l]; | |
78 | + if (!this.layouts[l]) { | |
79 | + this.layouts[l] = this.dashboardUtils.createDefaultLayoutData(); | |
80 | + } | |
81 | + } | |
82 | + } | |
83 | + | |
84 | + ngOnInit(): void { | |
85 | + } | |
86 | + | |
87 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
88 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
89 | + const customErrorState = !!(control && control.invalid && this.submitted); | |
90 | + return originalErrorState || customErrorState; | |
91 | + } | |
92 | + | |
93 | + openLayoutSettings(layoutId: DashboardLayoutId) { | |
94 | + const gridSettings = deepClone(this.layouts[layoutId].gridSettings); | |
95 | + this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData, | |
96 | + DashboardSettingsDialogData>(DashboardSettingsDialogComponent, { | |
97 | + disableClose: true, | |
98 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
99 | + data: { | |
100 | + settings: null, | |
101 | + gridSettings | |
102 | + } | |
103 | + }).afterClosed().subscribe((data) => { | |
104 | + if (data && data.gridSettings) { | |
105 | + this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings); | |
106 | + this.layoutsFormGroup.markAsDirty(); | |
107 | + } | |
108 | + }); | |
109 | + } | |
110 | + | |
111 | + cancel(): void { | |
112 | + this.dialogRef.close(null); | |
113 | + } | |
114 | + | |
115 | + save(): void { | |
116 | + this.submitted = true; | |
117 | + for (const l of Object.keys(this.layoutsFormGroup.controls)) { | |
118 | + const control = this.layoutsFormGroup.controls[l]; | |
119 | + if (!control.value) { | |
120 | + delete this.layouts[l]; | |
121 | + } | |
122 | + } | |
123 | + this.dialogRef.close(this.layouts); | |
124 | + } | |
125 | +} | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/layout/select-target-layout-dialog.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 | +<form #widgetForm="ngForm"> | |
19 | + <mat-toolbar fxLayout="row" color="primary"> | |
20 | + <h2 translate>layout.select</h2> | |
21 | + <span fxFlex></span> | |
22 | + <button mat-button mat-icon-button | |
23 | + (click)="cancel()" | |
24 | + type="button"> | |
25 | + <mat-icon class="material-icons">close</mat-icon> | |
26 | + </button> | |
27 | + </mat-toolbar> | |
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
29 | + </mat-progress-bar> | |
30 | + <div mat-dialog-content> | |
31 | + <fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px"> | |
32 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | |
33 | + <button fxFlex fxLayout="column" | |
34 | + type="button" mat-button mat-raised-button color="primary" | |
35 | + class="tb-layout-button" | |
36 | + (click)="selectLayout('main')"> | |
37 | + <span translate>layout.main</span> | |
38 | + </button> | |
39 | + <button fxFlex fxLayout="column" | |
40 | + type="button" mat-button mat-raised-button color="primary" | |
41 | + class="tb-layout-button" | |
42 | + (click)="selectLayout('right')"> | |
43 | + <span translate>layout.right</span> | |
44 | + </button> | |
45 | + </div> | |
46 | + </fieldset> | |
47 | + </div> | |
48 | +</form> | ... | ... |
ui-ngx/src/app/modules/home/pages/dashboard/layout/select-target-layout-dialog.component.ts
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 | + | |
17 | +import { Component, OnInit } from '@angular/core'; | |
18 | +import { MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { Router } from '@angular/router'; | |
22 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
23 | +import { DashboardLayoutId } from '@app/shared/models/dashboard.models'; | |
24 | + | |
25 | +@Component({ | |
26 | + selector: 'tb-select-target-layout-dialog', | |
27 | + templateUrl: './select-target-layout-dialog.component.html', | |
28 | + styleUrls: ['./layout-button.scss'] | |
29 | +}) | |
30 | +export class SelectTargetLayoutDialogComponent extends DialogComponent<SelectTargetLayoutDialogComponent, DashboardLayoutId> | |
31 | + implements OnInit { | |
32 | + | |
33 | + constructor(protected store: Store<AppState>, | |
34 | + protected router: Router, | |
35 | + public dialogRef: MatDialogRef<SelectTargetLayoutDialogComponent, DashboardLayoutId>) { | |
36 | + super(store, router, dialogRef); | |
37 | + } | |
38 | + | |
39 | + ngOnInit(): void { | |
40 | + } | |
41 | + | |
42 | + selectLayout(layoutId: DashboardLayoutId) { | |
43 | + this.dialogRef.close(layoutId); | |
44 | + } | |
45 | + | |
46 | + cancel(): void { | |
47 | + this.dialogRef.close(null); | |
48 | + } | |
49 | + | |
50 | +} | ... | ... |
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="tb-container"> | |
19 | + <label class="tb-title">{{label}}</label> | |
20 | + <ng-container #flow="flow" | |
21 | + [flowConfig]="{singleFile: true}"> | |
22 | + <div class="tb-image-select-container"> | |
23 | + <div class="tb-image-preview-container"> | |
24 | + <div *ngIf="!safeImageUrl" translate>dashboard.no-image</div> | |
25 | + <img *ngIf="safeImageUrl" class="tb-image-preview" [src]="safeImageUrl" /> | |
26 | + </div> | |
27 | + <div class="tb-image-clear-container"> | |
28 | + <button mat-button mat-icon-button color="primary" | |
29 | + type="button" | |
30 | + (click)="clearImage()" | |
31 | + class="tb-image-clear-btn" | |
32 | + matTooltip="{{ 'action.remove' | translate }}" | |
33 | + matTooltipPosition="above"> | |
34 | + <mat-icon>close</mat-icon> | |
35 | + </button> | |
36 | + </div> | |
37 | + <div class="drop-area tb-flow-drop" | |
38 | + flowDrop | |
39 | + [flow]="flow.flowJs"> | |
40 | + <label for="select" translate>dashboard.drop-image</label> | |
41 | + <input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="select"> | |
42 | + </div> | |
43 | + </div> | |
44 | + </ng-container> | |
45 | +</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 | +@import "../../../scss/constants"; | |
17 | + | |
18 | +$previewSize: 100px !default; | |
19 | + | |
20 | +:host { | |
21 | + | |
22 | + .tb-container { | |
23 | + margin-top: 0px; | |
24 | + label.tb-title { | |
25 | + display: block; | |
26 | + padding-bottom: 8px; | |
27 | + } | |
28 | + } | |
29 | + | |
30 | + .tb-image-select-container { | |
31 | + position: relative; | |
32 | + width: 100%; | |
33 | + height: $previewSize; | |
34 | + } | |
35 | + | |
36 | + .tb-image-preview { | |
37 | + width: auto; | |
38 | + max-width: $previewSize; | |
39 | + height: auto; | |
40 | + max-height: $previewSize; | |
41 | + } | |
42 | + | |
43 | + .tb-image-preview-container { | |
44 | + position: relative; | |
45 | + float: left; | |
46 | + width: $previewSize; | |
47 | + height: $previewSize; | |
48 | + margin-right: 12px; | |
49 | + vertical-align: top; | |
50 | + border: solid 1px; | |
51 | + | |
52 | + div { | |
53 | + width: 100%; | |
54 | + font-size: 18px; | |
55 | + text-align: center; | |
56 | + } | |
57 | + | |
58 | + div, | |
59 | + .tb-image-preview { | |
60 | + position: absolute; | |
61 | + top: 50%; | |
62 | + left: 50%; | |
63 | + transform: translate(-50%, -50%); | |
64 | + } | |
65 | + } | |
66 | + | |
67 | + .tb-image-clear-container { | |
68 | + position: relative; | |
69 | + float: right; | |
70 | + width: 48px; | |
71 | + height: $previewSize; | |
72 | + } | |
73 | + | |
74 | + .tb-image-clear-btn { | |
75 | + position: absolute !important; | |
76 | + top: 50%; | |
77 | + transform: translate(0%, -50%) !important; | |
78 | + } | |
79 | + | |
80 | + .file-input { | |
81 | + display: none; | |
82 | + } | |
83 | + | |
84 | + .tb-flow-drop { | |
85 | + position: relative; | |
86 | + height: $previewSize; | |
87 | + overflow: hidden; | |
88 | + border: dashed 2px; | |
89 | + | |
90 | + label { | |
91 | + display: flex; | |
92 | + flex-direction: column; | |
93 | + justify-content: center; | |
94 | + width: 100%; | |
95 | + height: 100%; | |
96 | + font-size: 16px; | |
97 | + text-align: center; | |
98 | + | |
99 | + @media #{$mat-gt-sm} { | |
100 | + font-size: 24px; | |
101 | + } | |
102 | + } | |
103 | + } | |
104 | +} | ... | ... |
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, ElementRef, forwardRef, Input, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; | |
18 | +import { PageComponent } from '@shared/components/page.component'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { DataKey, DatasourceType } from '@shared/models/widget.models'; | |
22 | +import { | |
23 | + ControlValueAccessor, | |
24 | + FormBuilder, | |
25 | + FormControl, | |
26 | + FormGroup, | |
27 | + NG_VALIDATORS, | |
28 | + NG_VALUE_ACCESSOR, | |
29 | + Validator, | |
30 | + Validators | |
31 | +} from '@angular/forms'; | |
32 | +import { UtilsService } from '@core/services/utils.service'; | |
33 | +import { TranslateService } from '@ngx-translate/core'; | |
34 | +import { MatDialog } from '@angular/material/dialog'; | |
35 | +import { EntityService } from '@core/http/entity.service'; | |
36 | +import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; | |
37 | +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; | |
38 | +import { Observable, of, Subscription } from 'rxjs'; | |
39 | +import { map, mergeMap, tap } from 'rxjs/operators'; | |
40 | +import { alarmFields } from '@shared/models/alarm.models'; | |
41 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
42 | +import { DialogService } from '@core/services/dialog.service'; | |
43 | +import { FlowDirective } from '@flowjs/ngx-flow'; | |
44 | +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; | |
45 | + | |
46 | +@Component({ | |
47 | + selector: 'tb-image-input', | |
48 | + templateUrl: './image-input.component.html', | |
49 | + styleUrls: ['./image-input.component.scss'], | |
50 | + providers: [ | |
51 | + { | |
52 | + provide: NG_VALUE_ACCESSOR, | |
53 | + useExisting: forwardRef(() => ImageInputComponent), | |
54 | + multi: true | |
55 | + } | |
56 | + ] | |
57 | +}) | |
58 | +export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { | |
59 | + | |
60 | + @Input() | |
61 | + label: string; | |
62 | + | |
63 | + private requiredValue: boolean; | |
64 | + get required(): boolean { | |
65 | + return this.requiredValue; | |
66 | + } | |
67 | + @Input() | |
68 | + set required(value: boolean) { | |
69 | + const newVal = coerceBooleanProperty(value); | |
70 | + if (this.requiredValue !== newVal) { | |
71 | + this.requiredValue = newVal; | |
72 | + } | |
73 | + } | |
74 | + | |
75 | + @Input() | |
76 | + disabled: boolean; | |
77 | + | |
78 | + imageUrl: string; | |
79 | + safeImageUrl: SafeUrl; | |
80 | + | |
81 | + @ViewChild('flow', {static: true}) | |
82 | + flow: FlowDirective; | |
83 | + | |
84 | + autoUploadSubscription: Subscription; | |
85 | + | |
86 | + private propagateChange = null; | |
87 | + | |
88 | + constructor(protected store: Store<AppState>, | |
89 | + private sanitizer: DomSanitizer) { | |
90 | + super(store); | |
91 | + } | |
92 | + | |
93 | + ngAfterViewInit() { | |
94 | + this.autoUploadSubscription = this.flow.events$.subscribe(event => { | |
95 | + if (event.type === 'fileAdded') { | |
96 | + const file = (event.event[0] as flowjs.FlowFile).file; | |
97 | + const reader = new FileReader(); | |
98 | + reader.onload = (loadEvent) => { | |
99 | + if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { | |
100 | + this.imageUrl = reader.result; | |
101 | + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); | |
102 | + this.updateModel(); | |
103 | + } | |
104 | + }; | |
105 | + reader.readAsDataURL(file); | |
106 | + } | |
107 | + }); | |
108 | + } | |
109 | + | |
110 | + ngOnDestroy() { | |
111 | + this.autoUploadSubscription.unsubscribe(); | |
112 | + } | |
113 | + | |
114 | + registerOnChange(fn: any): void { | |
115 | + this.propagateChange = fn; | |
116 | + } | |
117 | + | |
118 | + registerOnTouched(fn: any): void { | |
119 | + } | |
120 | + | |
121 | + setDisabledState(isDisabled: boolean): void { | |
122 | + this.disabled = isDisabled; | |
123 | + } | |
124 | + | |
125 | + writeValue(value: string): void { | |
126 | + this.imageUrl = value; | |
127 | + if (this.imageUrl) { | |
128 | + this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl); | |
129 | + } else { | |
130 | + this.safeImageUrl = null; | |
131 | + } | |
132 | + } | |
133 | + | |
134 | + private updateModel() { | |
135 | + this.propagateChange(this.imageUrl); | |
136 | + } | |
137 | + | |
138 | + clearImage() { | |
139 | + this.imageUrl = null; | |
140 | + this.safeImageUrl = null; | |
141 | + this.updateModel(); | |
142 | + } | |
143 | +} | ... | ... |
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 18 | <button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
19 | + type="button" | |
19 | 20 | mat-raised-button color="primary" (click)="openEditMode($event)"> |
20 | 21 | <mat-icon class="material-icons">query_builder</mat-icon> |
21 | 22 | <span>{{innerValue?.displayValue}}</span> |
... | ... | @@ -23,6 +24,7 @@ |
23 | 24 | <section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" |
24 | 25 | class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> |
25 | 26 | <button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
27 | + type="button" | |
26 | 28 | (click)="openEditMode($event)" |
27 | 29 | matTooltip="{{ 'timewindow.edit' | translate }}" |
28 | 30 | [matTooltipPosition]="tooltipPosition"> |
... | ... | @@ -35,6 +37,7 @@ |
35 | 37 | {{innerValue?.displayValue}} |
36 | 38 | </span> |
37 | 39 | <button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
40 | + type="button" | |
38 | 41 | (click)="openEditMode($event)" |
39 | 42 | matTooltip="{{ 'timewindow.edit' | translate }}" |
40 | 43 | [matTooltipPosition]="tooltipPosition"> | ... | ... |
... | ... | @@ -24,10 +24,16 @@ |
24 | 24 | panelClass="tb-widgets-bundle-select" |
25 | 25 | placeholder="{{ 'widget.select-widgets-bundle' | translate }}" |
26 | 26 | (ngModelChange)="widgetsBundleChanged()"> |
27 | + <mat-select-trigger> | |
28 | + <div class="tb-bundle-item"> | |
29 | + <span>{{widgetsBundle?.title}}</span> | |
30 | + <span translate class="tb-bundle-system" *ngIf="isSystem(widgetsBundle)">widgets-bundle.system</span> | |
31 | + </div> | |
32 | + </mat-select-trigger> | |
27 | 33 | <mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle"> |
28 | 34 | <div class="tb-bundle-item"> |
29 | 35 | <span>{{widgetsBundle.title}}</span> |
30 | - <span translate class="tb-bundle-system" *ngIf="isSystem(item)">widgets-bundle.system</span> | |
36 | + <span translate class="tb-bundle-system" *ngIf="isSystem(widgetsBundle)">widgets-bundle.system</span> | |
31 | 37 | </div> |
32 | 38 | </mat-option> |
33 | 39 | </mat-select> | ... | ... |
... | ... | @@ -20,8 +20,8 @@ tb-widgets-bundle-select { |
20 | 20 | } |
21 | 21 | |
22 | 22 | .tb-bundle-item { |
23 | - height: 24px; | |
24 | - line-height: 24px; | |
23 | + height: 26px; | |
24 | + line-height: 26px; | |
25 | 25 | } |
26 | 26 | } |
27 | 27 | |
... | ... | @@ -63,28 +63,43 @@ tb-widgets-bundle-select, |
63 | 63 | |
64 | 64 | mat-toolbar { |
65 | 65 | tb-widgets-bundle-select { |
66 | - mat-select { | |
67 | - background: rgba(255, 255, 255, .2); | |
68 | - padding: 5px 20px; | |
66 | + .mat-form-field-wrapper { | |
67 | + padding-bottom: 0 !important; | |
68 | + .mat-form-field-infix { | |
69 | + background: rgba(255, 255, 255, .2); | |
70 | + padding: 5px 20px !important; | |
71 | + border: none; | |
69 | 72 | |
70 | - .mat-select-value-text { | |
71 | - font-size: 1.2rem; | |
72 | - color: #fff; | |
73 | + mat-select { | |
74 | + .mat-select-value-text { | |
75 | + font-size: 1.2rem; | |
76 | + color: #fff; | |
73 | 77 | |
74 | - span:first-child::after { | |
75 | - color: #fff; | |
78 | + span:first-child::after { | |
79 | + color: #fff; | |
80 | + } | |
81 | + } | |
82 | + | |
83 | + .mat-select-value { | |
84 | + vertical-align: middle; | |
85 | + min-height: 30px; | |
86 | + height: 30px; | |
87 | + padding: 2px 2px 1px; | |
88 | + .mat-select-placeholder { | |
89 | + color: #fff; | |
90 | + opacity: .8; | |
91 | + } | |
92 | + } | |
76 | 93 | } |
77 | - } | |
78 | 94 | |
79 | - .mat-select-value.mat-select-placeholder { | |
80 | - color: #fff; | |
81 | - opacity: .8; | |
95 | + .mat-select.mat-select-invalid { | |
96 | + .mat-select-arrow { | |
97 | + color: #fff !important; | |
98 | + } | |
99 | + } | |
82 | 100 | } |
83 | - } | |
84 | - | |
85 | - mat-select.ng-invalid.ng-touched { | |
86 | - .mat-select-value-text { | |
87 | - color: #fff !important; | |
101 | + .mat-form-field-underline { | |
102 | + display: none; | |
88 | 103 | } |
89 | 104 | } |
90 | 105 | } | ... | ... |
... | ... | @@ -132,10 +132,13 @@ export interface EntityAliasFilter extends EntityFilters { |
132 | 132 | resolveMultiple?: boolean; |
133 | 133 | } |
134 | 134 | |
135 | -export interface EntityAlias { | |
136 | - id: string; | |
135 | +export interface EntityAliasInfo { | |
137 | 136 | alias: string; |
138 | 137 | filter: EntityAliasFilter; |
138 | +} | |
139 | + | |
140 | +export interface EntityAlias extends EntityAliasInfo { | |
141 | + id: string; | |
139 | 142 | [key: string]: any; |
140 | 143 | } |
141 | 144 | ... | ... |
... | ... | @@ -30,12 +30,12 @@ export interface DashboardInfo extends BaseData<DashboardId> { |
30 | 30 | } |
31 | 31 | |
32 | 32 | export interface WidgetLayout { |
33 | - sizeX: number; | |
34 | - sizeY: number; | |
33 | + sizeX?: number; | |
34 | + sizeY?: number; | |
35 | 35 | mobileHeight?: number; |
36 | 36 | mobileOrder?: number; |
37 | - col: number; | |
38 | - row: number; | |
37 | + col?: number; | |
38 | + row?: number; | |
39 | 39 | } |
40 | 40 | |
41 | 41 | export interface WidgetLayouts { |
... | ... | @@ -46,14 +46,13 @@ export interface GridSettings { |
46 | 46 | backgroundColor?: string; |
47 | 47 | color?: string; |
48 | 48 | columns?: number; |
49 | - margins?: [number, number]; | |
49 | + margin?: number; | |
50 | 50 | backgroundSizeMode?: string; |
51 | 51 | backgroundImageUrl?: string; |
52 | 52 | autoFillHeight?: boolean; |
53 | 53 | mobileAutoFillHeight?: boolean; |
54 | 54 | mobileRowHeight?: number; |
55 | 55 | [key: string]: any; |
56 | - // TODO: | |
57 | 56 | } |
58 | 57 | |
59 | 58 | export interface DashboardLayout { |
... | ... | @@ -62,7 +61,7 @@ export interface DashboardLayout { |
62 | 61 | } |
63 | 62 | |
64 | 63 | export interface DashboardLayoutInfo { |
65 | - widgets?: Array<Widget>; | |
64 | + widgetIds?: string[]; | |
66 | 65 | widgetLayouts?: WidgetLayouts; |
67 | 66 | gridSettings?: GridSettings; |
68 | 67 | } | ... | ... |
... | ... | @@ -392,3 +392,13 @@ export interface JsonSettingsSchema { |
392 | 392 | }; |
393 | 393 | form?: any[]; |
394 | 394 | } |
395 | + | |
396 | +export interface WidgetPosition { | |
397 | + row: number; | |
398 | + column: number; | |
399 | +} | |
400 | + | |
401 | +export interface WidgetSize { | |
402 | + sizeX: number; | |
403 | + sizeY: number; | |
404 | +} | ... | ... |
... | ... | @@ -20,6 +20,8 @@ import { FooterComponent } from './components/footer.component'; |
20 | 20 | import { LogoComponent } from './components/logo.component'; |
21 | 21 | import { TbSnackBarComponent, ToastDirective } from './components/toast.directive'; |
22 | 22 | import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component'; |
23 | +import { NgxFlowModule, FlowInjectionToken } from '@flowjs/ngx-flow'; | |
24 | +import Flow from '@flowjs/flow.js'; | |
23 | 25 | |
24 | 26 | import { |
25 | 27 | MatAutocompleteModule, |
... | ... | @@ -108,6 +110,7 @@ import { JsFuncComponent } from './components/js-func.component'; |
108 | 110 | import { JsonFormComponent } from './components/json-form/json-form.component'; |
109 | 111 | import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; |
110 | 112 | import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; |
113 | +import { ImageInputComponent } from './components/image-input.component'; | |
111 | 114 | |
112 | 115 | @NgModule({ |
113 | 116 | providers: [ |
... | ... | @@ -115,7 +118,11 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se |
115 | 118 | MillisecondsToTimeStringPipe, |
116 | 119 | EnumToArrayPipe, |
117 | 120 | HighlightPipe, |
118 | - TruncatePipe | |
121 | + TruncatePipe, | |
122 | + { | |
123 | + provide: FlowInjectionToken, | |
124 | + useValue: Flow | |
125 | + } | |
119 | 126 | ], |
120 | 127 | entryComponents: [ |
121 | 128 | TbSnackBarComponent, |
... | ... | @@ -173,6 +180,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se |
173 | 180 | ColorInputComponent, |
174 | 181 | MaterialIconSelectComponent, |
175 | 182 | JsonFormComponent, |
183 | + ImageInputComponent, | |
176 | 184 | NospacePipe, |
177 | 185 | MillisecondsToTimeStringPipe, |
178 | 186 | EnumToArrayPipe, |
... | ... | @@ -222,7 +230,8 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se |
222 | 230 | OverlayModule, |
223 | 231 | ShareButtonsModule, |
224 | 232 | HotkeyModule, |
225 | - ColorPickerModule | |
233 | + ColorPickerModule, | |
234 | + NgxFlowModule | |
226 | 235 | ], |
227 | 236 | exports: [ |
228 | 237 | FooterComponent, |
... | ... | @@ -308,6 +317,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se |
308 | 317 | ColorInputComponent, |
309 | 318 | MaterialIconSelectComponent, |
310 | 319 | JsonFormComponent, |
320 | + ImageInputComponent, | |
311 | 321 | NospacePipe, |
312 | 322 | MillisecondsToTimeStringPipe, |
313 | 323 | EnumToArrayPipe, | ... | ... |
... | ... | @@ -502,6 +502,9 @@ |
502 | 502 | "min-columns-count-message": "Only 10 minimum column count is allowed.", |
503 | 503 | "max-columns-count-message": "Only 1000 maximum column count is allowed.", |
504 | 504 | "widgets-margins": "Margin between widgets", |
505 | + "margin-required": "Margin value is required.", | |
506 | + "min-margin-message": "Only 0 is allowed as minimum margin value.", | |
507 | + "max-margin-message": "Only 50 is allowed as maximum margin value.", | |
505 | 508 | "horizontal-margin": "Horizontal margin", |
506 | 509 | "horizontal-margin-required": "Horizontal margin value is required.", |
507 | 510 | "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.", | ... | ... |