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,6 +1349,20 @@ | ||
1349 | "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz", | 1349 | "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz", |
1350 | "integrity": "sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw==" | 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 | "@mat-datetimepicker/core": { | 1366 | "@mat-datetimepicker/core": { |
1353 | "version": "2.0.1", | 1367 | "version": "2.0.1", |
1354 | "resolved": "https://registry.npmjs.org/@mat-datetimepicker/core/-/core-2.0.1.tgz", | 1368 | "resolved": "https://registry.npmjs.org/@mat-datetimepicker/core/-/core-2.0.1.tgz", |
@@ -1572,6 +1586,11 @@ | @@ -1572,6 +1586,11 @@ | ||
1572 | "@types/jquery": "*" | 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 | "@types/glob": { | 1594 | "@types/glob": { |
1576 | "version": "7.1.1", | 1595 | "version": "7.1.1", |
1577 | "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", | 1596 | "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", |
@@ -25,6 +25,8 @@ | @@ -25,6 +25,8 @@ | ||
25 | "@angular/router": "~8.2.11", | 25 | "@angular/router": "~8.2.11", |
26 | "@auth0/angular-jwt": "^3.0.0", | 26 | "@auth0/angular-jwt": "^3.0.0", |
27 | "@date-io/date-fns": "^1.3.11", | 27 | "@date-io/date-fns": "^1.3.11", |
28 | + "@flowjs/flow.js": "^2.13.2", | ||
29 | + "@flowjs/ngx-flow": "^0.4.3", | ||
28 | "@mat-datetimepicker/core": "^2.0.1", | 30 | "@mat-datetimepicker/core": "^2.0.1", |
29 | "@material-ui/core": "^4.5.1", | 31 | "@material-ui/core": "^4.5.1", |
30 | "@material-ui/icons": "^4.5.1", | 32 | "@material-ui/icons": "^4.5.1", |
@@ -38,7 +38,7 @@ import { FlexLayoutModule } from '@angular/flex-layout'; | @@ -38,7 +38,7 @@ import { FlexLayoutModule } from '@angular/flex-layout'; | ||
38 | import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; | 38 | import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; |
39 | import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; | 39 | import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component'; |
40 | import { WINDOW_PROVIDERS } from '@core/services/window.service'; | 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 | import { HotkeyModule } from 'angular2-hotkeys'; | 42 | import { HotkeyModule } from 'angular2-hotkeys'; |
43 | 43 | ||
44 | export function HttpLoaderFactory(http: HttpClient) { | 44 | export function HttpLoaderFactory(http: HttpClient) { |
@@ -24,7 +24,7 @@ import { | @@ -24,7 +24,7 @@ import { | ||
24 | DashboardState, | 24 | DashboardState, |
25 | DashboardConfiguration, | 25 | DashboardConfiguration, |
26 | DashboardLayoutInfo, | 26 | DashboardLayoutInfo, |
27 | - DashboardLayoutsInfo | 27 | + DashboardLayoutsInfo, DashboardLayoutId, WidgetLayout, GridSettings |
28 | } from '@shared/models/dashboard.models'; | 28 | } from '@shared/models/dashboard.models'; |
29 | import { isUndefined, isDefined, isString } from '@core/utils'; | 29 | import { isUndefined, isDefined, isString } from '@core/utils'; |
30 | import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models'; | 30 | import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models'; |
@@ -91,6 +91,7 @@ export class DashboardUtilsService { | @@ -91,6 +91,7 @@ export class DashboardUtilsService { | ||
91 | } else if (state.root) { | 91 | } else if (state.root) { |
92 | rootFound = true; | 92 | rootFound = true; |
93 | } | 93 | } |
94 | + this.validateAndUpdateState(state); | ||
94 | } | 95 | } |
95 | if (!rootFound) { | 96 | if (!rootFound) { |
96 | const firstStateId = Object.keys(states)[0]; | 97 | const firstStateId = Object.keys(states)[0]; |
@@ -216,13 +217,17 @@ export class DashboardUtilsService { | @@ -216,13 +217,17 @@ export class DashboardUtilsService { | ||
216 | public createDefaultLayoutData(): DashboardLayout { | 217 | public createDefaultLayoutData(): DashboardLayout { |
217 | return { | 218 | return { |
218 | widgets: {}, | 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,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 | public getRootStateId(states: {[id: string]: DashboardState }): string { | 307 | public getRootStateId(states: {[id: string]: DashboardState }): string { |
244 | for (const stateId of Object.keys(states)) { | 308 | for (const stateId of Object.keys(states)) { |
245 | const state = states[stateId]; | 309 | const state = states[stateId]; |
@@ -261,12 +325,12 @@ export class DashboardUtilsService { | @@ -261,12 +325,12 @@ export class DashboardUtilsService { | ||
261 | const layout: DashboardLayout = state.layouts[l]; | 325 | const layout: DashboardLayout = state.layouts[l]; |
262 | if (layout) { | 326 | if (layout) { |
263 | result[l] = { | 327 | result[l] = { |
264 | - widgets: [], | 328 | + widgetIds: [], |
265 | widgetLayouts: {}, | 329 | widgetLayouts: {}, |
266 | gridSettings: {} | 330 | gridSettings: {} |
267 | } as DashboardLayoutInfo; | 331 | } as DashboardLayoutInfo; |
268 | for (const id of Object.keys(layout.widgets)) { | 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 | result[l].widgetLayouts = layout.widgets; | 335 | result[l].widgetLayouts = layout.widgets; |
272 | result[l].gridSettings = layout.gridSettings; | 336 | result[l].gridSettings = layout.gridSettings; |
@@ -289,6 +353,154 @@ export class DashboardUtilsService { | @@ -289,6 +353,154 @@ export class DashboardUtilsService { | ||
289 | return widgetsArray; | 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 | private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, | 504 | private validateAndUpdateEntityAliases(configuration: DashboardConfiguration, |
293 | datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, | 505 | datasourcesByAliasId: {[aliasId: string]: Array<Datasource>}, |
294 | targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { | 506 | targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration { |
@@ -16,20 +16,347 @@ | @@ -16,20 +16,347 @@ | ||
16 | 16 | ||
17 | import { Injectable } from '@angular/core'; | 17 | import { Injectable } from '@angular/core'; |
18 | import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; | 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 | @Injectable({ | 53 | @Injectable({ |
21 | providedIn: 'root' | 54 | providedIn: 'root' |
22 | }) | 55 | }) |
23 | export class ItemBufferService { | 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 | public hasWidget(): boolean { | 115 | public hasWidget(): boolean { |
27 | - // TODO: | ||
28 | - return false; | 116 | + return this.storeHas(WIDGET_ITEM); |
29 | } | 117 | } |
30 | 118 | ||
31 | public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { | 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 | return false; | 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,7 +17,7 @@ | ||
17 | import { Inject, Injectable, NgZone } from '@angular/core'; | 17 | import { Inject, Injectable, NgZone } from '@angular/core'; |
18 | import { WINDOW } from '@core/services/window.service'; | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | import { ExceptionData } from '@app/shared/models/error.models'; | 19 | import { ExceptionData } from '@app/shared/models/error.models'; |
20 | -import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; | 20 | +import { deepClone, deleteNullProperties, guid, isDefined, isUndefined } from '@core/utils'; |
21 | import { WindowMessage } from '@shared/models/window-message.model'; | 21 | import { WindowMessage } from '@shared/models/window-message.model'; |
22 | import { TranslateService } from '@ngx-translate/core'; | 22 | import { TranslateService } from '@ngx-translate/core'; |
23 | import { customTranslationsPrefix } from '@app/shared/models/constants'; | 23 | import { customTranslationsPrefix } from '@app/shared/models/constants'; |
@@ -263,13 +263,7 @@ export class UtilsService { | @@ -263,13 +263,7 @@ export class UtilsService { | ||
263 | } | 263 | } |
264 | 264 | ||
265 | public guid(): string { | 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 | public validateDatasources(datasources: Array<Datasource>): Array<Datasource> { | 269 | public validateDatasources(datasources: Array<Datasource>): Array<Datasource> { |
@@ -353,3 +353,13 @@ export function deepClone<T>(target: T): T { | @@ -353,3 +353,13 @@ export function deepClone<T>(target: T): T { | ||
353 | } | 353 | } |
354 | return target; | 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,6 +17,7 @@ | ||
17 | --> | 17 | --> |
18 | <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" | 18 | <div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" |
19 | [ngStyle]="dashboardStyle" | 19 | [ngStyle]="dashboardStyle" |
20 | + [style.backgroundImage]="backgroundImage" | ||
20 | [fxShow]="(((isLoading$ | async) && !this.ignoreLoading) || this.dashboardLoading) && !isEdit"> | 21 | [fxShow]="(((isLoading$ | async) && !this.ignoreLoading) || this.dashboardLoading) && !isEdit"> |
21 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> | 22 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> |
22 | </mat-spinner> | 23 | </mat-spinner> |
@@ -114,7 +115,7 @@ | @@ -114,7 +115,7 @@ | ||
114 | </button> | 115 | </button> |
115 | <button mat-button mat-icon-button | 116 | <button mat-button mat-icon-button |
116 | [fxShow]="!isEdit && widget.enableFullscreen" | 117 | [fxShow]="!isEdit && widget.enableFullscreen" |
117 | - (click)="widget.isFullscreen = !widget.isFullscreen" | 118 | + (click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen" |
118 | matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | 119 | matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
119 | matTooltipPosition="above"> | 120 | matTooltipPosition="above"> |
120 | <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | 121 | <mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
@@ -15,13 +15,14 @@ | @@ -15,13 +15,14 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { | 17 | import { |
18 | - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, | 18 | + AfterViewInit, |
19 | Component, | 19 | Component, |
20 | DoCheck, | 20 | DoCheck, |
21 | Input, | 21 | Input, |
22 | IterableDiffers, | 22 | IterableDiffers, |
23 | - KeyValueDiffers, NgZone, | 23 | + NgZone, |
24 | OnChanges, | 24 | OnChanges, |
25 | + OnDestroy, | ||
25 | OnInit, | 26 | OnInit, |
26 | SimpleChanges, | 27 | SimpleChanges, |
27 | ViewChild | 28 | ViewChild |
@@ -33,35 +34,35 @@ import { AuthUser } from '@shared/models/user.model'; | @@ -33,35 +34,35 @@ import { AuthUser } from '@shared/models/user.model'; | ||
33 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | 34 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
34 | import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; | 35 | import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models'; |
35 | import { TimeService } from '@core/services/time.service'; | 36 | import { TimeService } from '@core/services/time.service'; |
36 | -import { GridsterComponent, GridsterConfig } from 'angular-gridster2'; | 37 | +import { GridsterComponent, GridsterComponentInterface, GridsterConfig } from 'angular-gridster2'; |
37 | import { | 38 | import { |
38 | DashboardCallbacks, | 39 | DashboardCallbacks, |
39 | DashboardWidget, | 40 | DashboardWidget, |
40 | DashboardWidgets, | 41 | DashboardWidgets, |
41 | - IDashboardComponent, | ||
42 | - WidgetPosition | 42 | + IDashboardComponent |
43 | } from '../../models/dashboard-component.models'; | 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 | import { DialogService } from '@core/services/dialog.service'; | 46 | import { DialogService } from '@core/services/dialog.service'; |
47 | import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; | 47 | import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; |
48 | import { BreakpointObserver } from '@angular/cdk/layout'; | 48 | import { BreakpointObserver } from '@angular/cdk/layout'; |
49 | import { MediaBreakpoints } from '@shared/models/constants'; | 49 | import { MediaBreakpoints } from '@shared/models/constants'; |
50 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; | 50 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
51 | -import { Widget } from '@app/shared/models/widget.models'; | 51 | +import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; |
52 | import { MatMenuTrigger } from '@angular/material'; | 52 | import { MatMenuTrigger } from '@angular/material'; |
53 | +import { SafeStyle } from '@angular/platform-browser'; | ||
53 | 54 | ||
54 | @Component({ | 55 | @Component({ |
55 | selector: 'tb-dashboard', | 56 | selector: 'tb-dashboard', |
56 | templateUrl: './dashboard.component.html', | 57 | templateUrl: './dashboard.component.html', |
57 | styleUrls: ['./dashboard.component.scss'] | 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 | authUser: AuthUser; | 62 | authUser: AuthUser; |
62 | 63 | ||
63 | @Input() | 64 | @Input() |
64 | - widgets: Array<Widget>; | 65 | + widgets: Iterable<Widget>; |
65 | 66 | ||
66 | @Input() | 67 | @Input() |
67 | widgetLayouts: WidgetLayouts; | 68 | widgetLayouts: WidgetLayouts; |
@@ -79,10 +80,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -79,10 +80,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
79 | columns: number; | 80 | columns: number; |
80 | 81 | ||
81 | @Input() | 82 | @Input() |
82 | - horizontalMargin: number; | ||
83 | - | ||
84 | - @Input() | ||
85 | - verticalMargin: number; | 83 | + margin: number; |
86 | 84 | ||
87 | @Input() | 85 | @Input() |
88 | isEdit: boolean; | 86 | isEdit: boolean; |
@@ -115,6 +113,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -115,6 +113,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
115 | dashboardStyle: {[klass: string]: any}; | 113 | dashboardStyle: {[klass: string]: any}; |
116 | 114 | ||
117 | @Input() | 115 | @Input() |
116 | + backgroundImage: SafeStyle | string; | ||
117 | + | ||
118 | + @Input() | ||
118 | dashboardClass: string; | 119 | dashboardClass: string; |
119 | 120 | ||
120 | @Input() | 121 | @Input() |
@@ -153,16 +154,20 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -153,16 +154,20 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
153 | dashboardWidgets = new DashboardWidgets(this, | 154 | dashboardWidgets = new DashboardWidgets(this, |
154 | this.differs.find([]).create<Widget>((index, item) => { | 155 | this.differs.find([]).create<Widget>((index, item) => { |
155 | return item; | 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 | constructor(protected store: Store<AppState>, | 166 | constructor(protected store: Store<AppState>, |
161 | private timeService: TimeService, | 167 | private timeService: TimeService, |
162 | private dialogService: DialogService, | 168 | private dialogService: DialogService, |
163 | private breakpointObserver: BreakpointObserver, | 169 | private breakpointObserver: BreakpointObserver, |
164 | private differs: IterableDiffers, | 170 | private differs: IterableDiffers, |
165 | - private kvDiffers: KeyValueDiffers, | ||
166 | private ngZone: NgZone) { | 171 | private ngZone: NgZone) { |
167 | super(store); | 172 | super(store); |
168 | this.authUser = getCurrentAuthUser(store); | 173 | this.authUser = getCurrentAuthUser(store); |
@@ -180,10 +185,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -180,10 +185,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
180 | maxRows: 100, | 185 | maxRows: 100, |
181 | minCols: this.columns ? this.columns : 24, | 186 | minCols: this.columns ? this.columns : 24, |
182 | outerMargin: true, | 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 | minItemCols: 1, | 189 | minItemCols: 1, |
188 | minItemRows: 1, | 190 | minItemRows: 1, |
189 | defaultItemCols: 8, | 191 | defaultItemCols: 8, |
@@ -198,7 +200,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -198,7 +200,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
198 | 200 | ||
199 | this.updateMobileOpts(); | 201 | this.updateMobileOpts(); |
200 | 202 | ||
201 | - this.breakpointObserver | 203 | + this.breakpointObserverSubscription = this.breakpointObserver |
202 | .observe(MediaBreakpoints['gt-sm']).subscribe( | 204 | .observe(MediaBreakpoints['gt-sm']).subscribe( |
203 | () => { | 205 | () => { |
204 | this.updateMobileOpts(); | 206 | this.updateMobileOpts(); |
@@ -209,6 +211,18 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -209,6 +211,18 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
209 | this.updateWidgets(); | 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 | ngDoCheck() { | 226 | ngDoCheck() { |
213 | this.dashboardWidgets.doCheck(); | 227 | this.dashboardWidgets.doCheck(); |
214 | } | 228 | } |
@@ -223,7 +237,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -223,7 +237,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
223 | if (!change.firstChange && change.currentValue !== change.previousValue) { | 237 | if (!change.firstChange && change.currentValue !== change.previousValue) { |
224 | if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { | 238 | if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) { |
225 | updateMobileOpts = true; | 239 | updateMobileOpts = true; |
226 | - } else if (['horizontalMargin', 'verticalMargin'].includes(propName)) { | 240 | + } else if (['margin', 'columns'].includes(propName)) { |
227 | updateLayoutOpts = true; | 241 | updateLayoutOpts = true; |
228 | } else if (propName === 'isEdit') { | 242 | } else if (propName === 'isEdit') { |
229 | updateEditingOpts = true; | 243 | updateEditingOpts = true; |
@@ -256,7 +270,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -256,7 +270,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
256 | this.dashboardLoading = false; | 270 | this.dashboardLoading = false; |
257 | } | 271 | } |
258 | 272 | ||
273 | + private updateWidgetLayouts() { | ||
274 | + this.dashboardWidgets.widgetLayoutsUpdated(); | ||
275 | + } | ||
276 | + | ||
259 | ngAfterViewInit(): void { | 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 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { | 283 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { |
@@ -305,7 +326,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -305,7 +326,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
305 | 326 | ||
306 | openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { | 327 | openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { |
307 | if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { | 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 | if (items && items.length) { | 330 | if (items && items.length) { |
310 | $event.preventDefault(); | 331 | $event.preventDefault(); |
311 | $event.stopPropagation(); | 332 | $event.stopPropagation(); |
@@ -324,13 +345,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -324,13 +345,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
324 | 345 | ||
325 | widgetMouseDown($event: Event, widget: DashboardWidget) { | 346 | widgetMouseDown($event: Event, widget: DashboardWidget) { |
326 | if (this.callbacks && this.callbacks.onWidgetMouseDown) { | 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 | widgetClicked($event: Event, widget: DashboardWidget) { | 352 | widgetClicked($event: Event, widget: DashboardWidget) { |
332 | if (this.callbacks && this.callbacks.onWidgetClicked) { | 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,7 +360,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
339 | $event.stopPropagation(); | 360 | $event.stopPropagation(); |
340 | } | 361 | } |
341 | if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) { | 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,7 +369,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
348 | $event.stopPropagation(); | 369 | $event.stopPropagation(); |
349 | } | 370 | } |
350 | if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) { | 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,19 +378,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
357 | $event.stopPropagation(); | 378 | $event.stopPropagation(); |
358 | } | 379 | } |
359 | if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { | 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 | if (highlighted) { | 387 | if (highlighted) { |
367 | this.scrollToWidget(highlighted, delay); | 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 | if (selected) { | 394 | if (selected) { |
374 | this.scrollToWidget(selected, delay); | 395 | this.scrollToWidget(selected, delay); |
375 | } | 396 | } |
@@ -385,15 +406,16 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -385,15 +406,16 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
385 | row: 0, | 406 | row: 0, |
386 | column: 0 | 407 | column: 0 |
387 | }; | 408 | }; |
388 | - const parentElement = this.gridster.el as HTMLElement; | 409 | + const parentElement = $(this.gridster.el); |
389 | let pageX = 0; | 410 | let pageX = 0; |
390 | let pageY = 0; | 411 | let pageY = 0; |
391 | if (event instanceof MouseEvent) { | 412 | if (event instanceof MouseEvent) { |
392 | pageX = event.pageX; | 413 | pageX = event.pageX; |
393 | pageY = event.pageY; | 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 | pos.row = this.gridster.pixelsToPositionY(y, Math.floor); | 419 | pos.row = this.gridster.pixelsToPositionY(y, Math.floor); |
398 | pos.column = this.gridster.pixelsToPositionX(x, Math.floor); | 420 | pos.column = this.gridster.pixelsToPositionX(x, Math.floor); |
399 | return pos; | 421 | return pos; |
@@ -434,26 +456,33 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -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 | this.isMobileSize = this.checkIsMobileSize(); | 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 | const mobileBreakPoint = this.isMobileSize ? 20000 : 0; | 467 | const mobileBreakPoint = this.isMobileSize ? 20000 : 0; |
440 | this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; | 468 | this.gridsterOpts.mobileBreakpoint = mobileBreakPoint; |
441 | - const rowSize = this.detectRowSize(this.isMobileSize); | 469 | + const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight); |
442 | if (this.gridsterOpts.fixedRowHeight !== rowSize) { | 470 | if (this.gridsterOpts.fixedRowHeight !== rowSize) { |
443 | this.gridsterOpts.fixedRowHeight = rowSize; | 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 | private updateLayoutOpts() { | 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 | private updateEditingOpts() { | 488 | private updateEditingOpts() { |
@@ -462,17 +491,42 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -462,17 +491,42 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
462 | } | 491 | } |
463 | 492 | ||
464 | public notifyGridsterOptionsChanged() { | 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 | let rowHeight = null; | 514 | let rowHeight = null; |
472 | - if (!this.isAutofillHeight()) { | 515 | + if (!autofillHeight) { |
473 | if (isMobile) { | 516 | if (isMobile) { |
474 | rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70; | 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 | return rowHeight; | 531 | return rowHeight; |
478 | } | 532 | } |
@@ -17,7 +17,7 @@ | @@ -17,7 +17,7 @@ | ||
17 | --> | 17 | --> |
18 | <header> | 18 | <header> |
19 | <mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}"> | 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 | <div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start"> | 21 | <div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start"> |
22 | <span class="tb-details-title">{{ headerTitle }}</span> | 22 | <span class="tb-details-title">{{ headerTitle }}</span> |
23 | <span class="tb-details-subtitle">{{ headerSubtitle }}</span> | 23 | <span class="tb-details-subtitle">{{ headerSubtitle }}</span> |
@@ -20,6 +20,9 @@ | @@ -20,6 +20,9 @@ | ||
20 | height: 100%; | 20 | height: 100%; |
21 | display: flex; | 21 | display: flex; |
22 | flex-direction: column; | 22 | flex-direction: column; |
23 | +} | ||
24 | + | ||
25 | +:host ::ng-deep { | ||
23 | .mat-toolbar-tools { | 26 | .mat-toolbar-tools { |
24 | height: 100%; | 27 | height: 100%; |
25 | min-height: 100px; | 28 | min-height: 100px; |
@@ -50,4 +53,9 @@ | @@ -50,4 +53,9 @@ | ||
50 | opacity: .8; | 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,12 +22,14 @@ | ||
22 | <span class="tb-entity-table-title" translate>widget-config.actions</span> | 22 | <span class="tb-entity-table-title" translate>widget-config.actions</span> |
23 | <span fxFlex></span> | 23 | <span fxFlex></span> |
24 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" | 24 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
25 | + type="button" | ||
25 | (click)="addAction($event)" | 26 | (click)="addAction($event)" |
26 | matTooltip="{{ 'widget-config.add-action' | translate }}" | 27 | matTooltip="{{ 'widget-config.add-action' | translate }}" |
27 | matTooltipPosition="above"> | 28 | matTooltipPosition="above"> |
28 | <mat-icon>add</mat-icon> | 29 | <mat-icon>add</mat-icon> |
29 | </button> | 30 | </button> |
30 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" | 31 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" |
32 | + type="button" | ||
31 | matTooltip="{{ 'action.search' | translate }}" | 33 | matTooltip="{{ 'action.search' | translate }}" |
32 | matTooltipPosition="above"> | 34 | matTooltipPosition="above"> |
33 | <mat-icon>search</mat-icon> | 35 | <mat-icon>search</mat-icon> |
@@ -37,6 +39,7 @@ | @@ -37,6 +39,7 @@ | ||
37 | <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> | 39 | <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> |
38 | <div class="mat-toolbar-tools"> | 40 | <div class="mat-toolbar-tools"> |
39 | <button mat-button mat-icon-button | 41 | <button mat-button mat-icon-button |
42 | + type="button" | ||
40 | matTooltip="{{ 'widget-config.search-actions' | translate }}" | 43 | matTooltip="{{ 'widget-config.search-actions' | translate }}" |
41 | matTooltipPosition="above"> | 44 | matTooltipPosition="above"> |
42 | <mat-icon>search</mat-icon> | 45 | <mat-icon>search</mat-icon> |
@@ -48,6 +51,7 @@ | @@ -48,6 +51,7 @@ | ||
48 | placeholder="{{ 'widget-config.search-actions' | translate }}"/> | 51 | placeholder="{{ 'widget-config.search-actions' | translate }}"/> |
49 | </mat-form-field> | 52 | </mat-form-field> |
50 | <button mat-button mat-icon-button (click)="exitFilterMode()" | 53 | <button mat-button mat-icon-button (click)="exitFilterMode()" |
54 | + type="button" | ||
51 | matTooltip="{{ 'action.close' | translate }}" | 55 | matTooltip="{{ 'action.close' | translate }}" |
52 | matTooltipPosition="above"> | 56 | matTooltipPosition="above"> |
53 | <mat-icon>close</mat-icon> | 57 | <mat-icon>close</mat-icon> |
@@ -87,12 +91,14 @@ | @@ -87,12 +91,14 @@ | ||
87 | <mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | 91 | <mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> |
88 | <div fxFlex fxLayout="row" fxLayoutAlign="end"> | 92 | <div fxFlex fxLayout="row" fxLayoutAlign="end"> |
89 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" | 93 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
94 | + type="button" | ||
90 | matTooltip="{{ 'widget-config.edit-action' | translate }}" | 95 | matTooltip="{{ 'widget-config.edit-action' | translate }}" |
91 | matTooltipPosition="above" | 96 | matTooltipPosition="above" |
92 | (click)="editAction($event, action)"> | 97 | (click)="editAction($event, action)"> |
93 | <mat-icon>edit</mat-icon> | 98 | <mat-icon>edit</mat-icon> |
94 | </button> | 99 | </button> |
95 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" | 100 | <button mat-button mat-icon-button [disabled]="isLoading$ | async" |
101 | + type="button" | ||
96 | matTooltip="{{ 'widget-config.delete-action' | translate }}" | 102 | matTooltip="{{ 'widget-config.delete-action' | translate }}" |
97 | matTooltipPosition="above" | 103 | matTooltipPosition="above" |
98 | (click)="deleteAction($event, action)"> | 104 | (click)="deleteAction($event, action)"> |
@@ -29,7 +29,7 @@ | @@ -29,7 +29,7 @@ | ||
29 | <div class="tb-color-preview" (click)="showColorPicker(key)" style="margin-right: 5px;"> | 29 | <div class="tb-color-preview" (click)="showColorPicker(key)" style="margin-right: 5px;"> |
30 | <div class="tb-color-result" [ngStyle]="{background: key.color}"></div> | 30 | <div class="tb-color-result" [ngStyle]="{background: key.color}"></div> |
31 | </div> | 31 | </div> |
32 | - <div fxLayout="row"> | 32 | + <div style="flex: 1; min-width: 0px;" fxLayout="row"> |
33 | <div class="tb-chip-label"> | 33 | <div class="tb-chip-label"> |
34 | <span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm"> | 34 | <span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm"> |
35 | <span *ngIf="key.type === dataKeyTypes.attribute" | 35 | <span *ngIf="key.type === dataKeyTypes.attribute" |
@@ -54,7 +54,9 @@ | @@ -54,7 +54,9 @@ | ||
54 | </ng-template> | 54 | </ng-template> |
55 | </div> | 55 | </div> |
56 | </div> | 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 | <mat-icon class="tb-mat-20">edit</mat-icon> | 60 | <mat-icon class="tb-mat-20">edit</mat-icon> |
59 | </button> | 61 | </button> |
60 | <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> | 62 | <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
@@ -17,6 +17,7 @@ | @@ -17,6 +17,7 @@ | ||
17 | :host { | 17 | :host { |
18 | .mat-chip.mat-standard-chip { | 18 | .mat-chip.mat-standard-chip { |
19 | .tb-attribute-chip { | 19 | .tb-attribute-chip { |
20 | + max-width: 100%; | ||
20 | color: rgb(66, 66, 66); | 21 | color: rgb(66, 66, 66); |
21 | font-weight: normal; | 22 | font-weight: normal; |
22 | font-size: 16px; | 23 | font-size: 16px; |
@@ -16,6 +16,7 @@ | @@ -16,6 +16,7 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" | 18 | <button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
19 | + type="button" | ||
19 | mat-button mat-raised-button color="primary" (click)="openEditMode($event)"> | 20 | mat-button mat-raised-button color="primary" (click)="openEditMode($event)"> |
20 | <mat-icon class="material-icons">toc</mat-icon> | 21 | <mat-icon class="material-icons">toc</mat-icon> |
21 | <span translate>legend.settings</span> | 22 | <span translate>legend.settings</span> |
@@ -140,6 +140,7 @@ | @@ -140,6 +140,7 @@ | ||
140 | </tb-data-keys> | 140 | </tb-data-keys> |
141 | </section> | 141 | </section> |
142 | <button [disabled]="isLoading$ | async" | 142 | <button [disabled]="isLoading$ | async" |
143 | + type="button" | ||
143 | mat-button mat-icon-button color="primary" | 144 | mat-button mat-icon-button color="primary" |
144 | style="min-width: 40px;" | 145 | style="min-width: 40px;" |
145 | (click)="removeDatasource($index)" | 146 | (click)="removeDatasource($index)" |
@@ -153,6 +154,7 @@ | @@ -153,6 +154,7 @@ | ||
153 | </ng-template> | 154 | </ng-template> |
154 | <div fxFlex fxLayout="row" fxLayoutAlign="start center"> | 155 | <div fxFlex fxLayout="row" fxLayoutAlign="start center"> |
155 | <button [disabled]="isLoading$ | async" | 156 | <button [disabled]="isLoading$ | async" |
157 | + type="button" | ||
156 | mat-button mat-raised-button color="primary" | 158 | mat-button mat-raised-button color="primary" |
157 | [fxShow]="modelValue?.typeParameters && | 159 | [fxShow]="modelValue?.typeParameters && |
158 | (modelValue?.typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < modelValue?.typeParameters.maxDatasources)" | 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,11 +996,11 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
996 | } | 996 | } |
997 | 997 | ||
998 | private elementClick($event: Event) { | 998 | private elementClick($event: Event) { |
999 | - $event.stopPropagation(); | ||
1000 | const e = ($event.target || $event.srcElement) as Element; | 999 | const e = ($event.target || $event.srcElement) as Element; |
1001 | if (e.id) { | 1000 | if (e.id) { |
1002 | const descriptors = this.getActionDescriptors('elementClick'); | 1001 | const descriptors = this.getActionDescriptors('elementClick'); |
1003 | if (descriptors.length) { | 1002 | if (descriptors.length) { |
1003 | + $event.stopPropagation(); | ||
1004 | descriptors.forEach((descriptor) => { | 1004 | descriptors.forEach((descriptor) => { |
1005 | if (descriptor.name === e.id) { | 1005 | if (descriptor.name === e.id) { |
1006 | const entityInfo = this.getActiveEntityInfo(); | 1006 | const entityInfo = this.getActiveEntityInfo(); |
@@ -15,12 +15,12 @@ | @@ -15,12 +15,12 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2'; | 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 | import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; | 19 | import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models'; |
20 | import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; | 20 | import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models'; |
21 | import { Timewindow } from '@shared/models/time/time.models'; | 21 | import { Timewindow } from '@shared/models/time/time.models'; |
22 | import { Observable, of, Subject } from 'rxjs'; | 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 | import { IterableDiffer, KeyValueDiffer } from '@angular/core'; | 24 | import { IterableDiffer, KeyValueDiffer } from '@angular/core'; |
25 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; | 25 | import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; |
26 | import * as deepEqual from 'deep-equal'; | 26 | import * as deepEqual from 'deep-equal'; |
@@ -46,18 +46,13 @@ export interface WidgetContextMenuItem extends ContextMenuItem { | @@ -46,18 +46,13 @@ export interface WidgetContextMenuItem extends ContextMenuItem { | ||
46 | } | 46 | } |
47 | 47 | ||
48 | export interface DashboardCallbacks { | 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 | prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>; | 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 | export interface IDashboardComponent { | 58 | export interface IDashboardComponent { |
@@ -74,11 +69,14 @@ export interface IDashboardComponent { | @@ -74,11 +69,14 @@ export interface IDashboardComponent { | ||
74 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; | 69 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void; |
75 | onResetTimewindow(): void; | 70 | onResetTimewindow(): void; |
76 | resetHighlight(): void; | 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 | getSelectedWidget(): Widget; | 74 | getSelectedWidget(): Widget; |
80 | getEventGridPosition(event: Event): WidgetPosition; | 75 | getEventGridPosition(event: Event): WidgetPosition; |
81 | notifyGridsterOptionsChanged(); | 76 | notifyGridsterOptionsChanged(); |
77 | + pauseChangeNotifications(); | ||
78 | + resumeChangeNotifications(); | ||
79 | + notifyLayoutUpdated(); | ||
82 | } | 80 | } |
83 | 81 | ||
84 | declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; | 82 | declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; |
@@ -86,7 +84,7 @@ declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; | @@ -86,7 +84,7 @@ declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; | ||
86 | interface DashboardWidgetUpdateRecord { | 84 | interface DashboardWidgetUpdateRecord { |
87 | widget?: Widget; | 85 | widget?: Widget; |
88 | widgetLayout?: WidgetLayout; | 86 | widgetLayout?: WidgetLayout; |
89 | - widgetIndex: number; | 87 | + widgetId: string; |
90 | operation: DashboardWidgetUpdateOperation; | 88 | operation: DashboardWidgetUpdateOperation; |
91 | } | 89 | } |
92 | 90 | ||
@@ -95,7 +93,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -95,7 +93,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
95 | highlightedMode = false; | 93 | highlightedMode = false; |
96 | 94 | ||
97 | dashboardWidgets: Array<DashboardWidget> = []; | 95 | dashboardWidgets: Array<DashboardWidget> = []; |
98 | - widgets: Array<Widget>; | 96 | + widgets: Iterable<Widget>; |
99 | widgetLayouts: WidgetLayouts; | 97 | widgetLayouts: WidgetLayouts; |
100 | 98 | ||
101 | [Symbol.iterator](): Iterator<DashboardWidget> { | 99 | [Symbol.iterator](): Iterator<DashboardWidget> { |
@@ -103,41 +101,30 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -103,41 +101,30 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
103 | } | 101 | } |
104 | 102 | ||
105 | constructor(private dashboard: IDashboardComponent, | 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 | doCheck() { | 107 | doCheck() { |
111 | const widgetChange = this.widgetsDiffer.diff(this.widgets); | 108 | const widgetChange = this.widgetsDiffer.diff(this.widgets); |
112 | if (widgetChange !== null) { | 109 | if (widgetChange !== null) { |
113 | 110 | ||
114 | - const layouts: WidgetLayouts = {}; | ||
115 | const updateRecords: Array<DashboardWidgetUpdateRecord> = []; | 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 | widgetChange.forEachAddedItem((added) => { | 113 | widgetChange.forEachAddedItem((added) => { |
127 | updateRecords.push({ | 114 | updateRecords.push({ |
128 | widget: added.item, | 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 | operation: 'add' | 118 | operation: 'add' |
132 | }); | 119 | }); |
133 | }); | 120 | }); |
134 | widgetChange.forEachRemovedItem((removed) => { | 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 | if (operation) { | 123 | if (operation) { |
137 | operation.operation = 'update'; | 124 | operation.operation = 'update'; |
138 | } else { | 125 | } else { |
139 | operation = { | 126 | operation = { |
140 | - widgetIndex: removed.previousIndex, | 127 | + widgetId: removed.item.id, |
141 | operation: 'remove' | 128 | operation: 'remove' |
142 | }; | 129 | }; |
143 | updateRecords.push(operation); | 130 | updateRecords.push(operation); |
@@ -147,21 +134,21 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -147,21 +134,21 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
147 | switch (record.operation) { | 134 | switch (record.operation) { |
148 | case 'add': | 135 | case 'add': |
149 | this.dashboardWidgets.push( | 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 | break; | 139 | break; |
153 | case 'remove': | 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 | if (index > -1) { | 142 | if (index > -1) { |
156 | this.dashboardWidgets.splice(index, 1); | 143 | this.dashboardWidgets.splice(index, 1); |
157 | } | 144 | } |
158 | break; | 145 | break; |
159 | case 'update': | 146 | case 'update': |
160 | - index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex); | 147 | + index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId); |
161 | if (index > -1) { | 148 | if (index > -1) { |
162 | const prevDashboardWidget = this.dashboardWidgets[index]; | 149 | const prevDashboardWidget = this.dashboardWidgets[index]; |
163 | if (!deepEqual(prevDashboardWidget.widget, record.widget)) { | 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 | this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; | 152 | this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted; |
166 | this.dashboardWidgets[index].selected = prevDashboardWidget.selected; | 153 | this.dashboardWidgets[index].selected = prevDashboardWidget.selected; |
167 | } else { | 154 | } else { |
@@ -178,14 +165,25 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -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 | this.highlightedMode = false; | 180 | this.highlightedMode = false; |
183 | this.widgets = widgets; | 181 | this.widgets = widgets; |
184 | this.widgetLayouts = widgetLayouts; | 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 | if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { | 187 | if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { |
190 | this.highlightedMode = true; | 188 | this.highlightedMode = true; |
191 | widget.highlighted = true; | 189 | widget.highlighted = true; |
@@ -200,8 +198,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -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 | if (widget && (!widget.selected)) { | 203 | if (widget && (!widget.selected)) { |
206 | widget.selected = true; | 204 | widget.selected = true; |
207 | this.dashboardWidgets.forEach((dashboardWidget) => { | 205 | this.dashboardWidgets.forEach((dashboardWidget) => { |
@@ -237,8 +235,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -237,8 +235,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
237 | return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.selected); | 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 | private updateRowsAndSort() { | 242 | private updateRowsAndSort() { |
@@ -306,6 +304,8 @@ export class DashboardWidget implements GridsterItem { | @@ -306,6 +304,8 @@ export class DashboardWidget implements GridsterItem { | ||
306 | 304 | ||
307 | widgetContext: WidgetContext = {}; | 305 | widgetContext: WidgetContext = {}; |
308 | 306 | ||
307 | + widgetId: string; | ||
308 | + | ||
309 | private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>(); | 309 | private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>(); |
310 | private gridsterItemComponentValue: GridsterItemComponentInterface; | 310 | private gridsterItemComponentValue: GridsterItemComponentInterface; |
311 | 311 | ||
@@ -318,8 +318,11 @@ export class DashboardWidget implements GridsterItem { | @@ -318,8 +318,11 @@ export class DashboardWidget implements GridsterItem { | ||
318 | constructor( | 318 | constructor( |
319 | private dashboard: IDashboardComponent, | 319 | private dashboard: IDashboardComponent, |
320 | public widget: Widget, | 320 | public widget: Widget, |
321 | - public widgetIndex: number, | ||
322 | public widgetLayout?: WidgetLayout) { | 321 | public widgetLayout?: WidgetLayout) { |
322 | + if (!widget.id) { | ||
323 | + widget.id = guid(); | ||
324 | + } | ||
325 | + this.widgetId = widget.id; | ||
323 | this.updateWidgetParams(); | 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,26 +132,13 @@ | ||
132 | </section> | 132 | </section> |
133 | <div class="tb-absolute-fill tb-dashboard-layouts" fxLayout="{{forceDashboardMobileMode ? 'column' : 'row'}}" | 133 | <div class="tb-absolute-fill tb-dashboard-layouts" fxLayout="{{forceDashboardMobileMode ? 'column' : 'row'}}" |
134 | [ngClass]="{ 'tb-padded' : !widgetEditMode && (isEdit || displayTitle()), 'tb-shrinked' : isEditingWidget }"> | 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 | disableClose="true" | 142 | disableClose="true" |
156 | position="end" | 143 | position="end" |
157 | [mode]="isMobile ? 'over' : 'side'" | 144 | [mode]="isMobile ? 'over' : 'side'" |
@@ -165,6 +152,19 @@ | @@ -165,6 +152,19 @@ | ||
165 | [widgetEditMode]="widgetEditMode"> | 152 | [widgetEditMode]="widgetEditMode"> |
166 | </tb-dashboard-layout> | 153 | </tb-dashboard-layout> |
167 | </mat-drawer> | 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 | </mat-drawer-container> | 168 | </mat-drawer-container> |
169 | </div> | 169 | </div> |
170 | <mat-drawer-container hasBackdrop="false" class="tb-widget-details-sidenav"> | 170 | <mat-drawer-container hasBackdrop="false" class="tb-widget-details-sidenav"> |
@@ -194,6 +194,37 @@ | @@ -194,6 +194,37 @@ | ||
194 | </tb-details-panel> | 194 | </tb-details-panel> |
195 | </mat-drawer> | 195 | </mat-drawer> |
196 | </mat-drawer-container> | 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 | <!--tb-details-sidenav TODO --> | 228 | <!--tb-details-sidenav TODO --> |
198 | <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> | 229 | <section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end"> |
199 | <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" | 230 | <tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode" |
@@ -136,6 +136,10 @@ div.tb-dashboard-page { | @@ -136,6 +136,10 @@ div.tb-dashboard-page { | ||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | + mat-drawer-container.tb-select-widget-sidenav { | ||
140 | + position: initial; | ||
141 | + } | ||
142 | + | ||
139 | section.tb-powered-by-footer { | 143 | section.tb-powered-by-footer { |
140 | position: absolute; | 144 | position: absolute; |
141 | right: 25px; | 145 | right: 25px; |
@@ -14,16 +14,7 @@ | @@ -14,16 +14,7 @@ | ||
14 | /// limitations under the License. | 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 | import { PageComponent } from '@shared/components/page.component'; | 18 | import { PageComponent } from '@shared/components/page.component'; |
28 | import { Store } from '@ngrx/store'; | 19 | import { Store } from '@ngrx/store'; |
29 | import { AppState } from '@core/core.state'; | 20 | import { AppState } from '@core/core.state'; |
@@ -33,47 +24,42 @@ import { AuthService } from '@core/auth/auth.service'; | @@ -33,47 +24,42 @@ import { AuthService } from '@core/auth/auth.service'; | ||
33 | import { | 24 | import { |
34 | Dashboard, | 25 | Dashboard, |
35 | DashboardConfiguration, | 26 | DashboardConfiguration, |
36 | - WidgetLayout, | 27 | + DashboardLayoutId, |
37 | DashboardLayoutInfo, | 28 | DashboardLayoutInfo, |
38 | - DashboardLayoutsInfo | 29 | + DashboardLayoutsInfo, |
30 | + DashboardStateLayouts, GridSettings, | ||
31 | + WidgetLayout | ||
39 | } from '@app/shared/models/dashboard.models'; | 32 | } from '@app/shared/models/dashboard.models'; |
40 | import { WINDOW } from '@core/services/window.service'; | 33 | import { WINDOW } from '@core/services/window.service'; |
41 | import { WindowMessage } from '@shared/models/window-message.model'; | 34 | import { WindowMessage } from '@shared/models/window-message.model'; |
42 | import { deepClone, isDefined } from '@app/core/utils'; | 35 | import { deepClone, isDefined } from '@app/core/utils'; |
43 | import { | 36 | import { |
44 | - DashboardContext, DashboardPageLayout, | 37 | + DashboardContext, |
38 | + DashboardPageLayout, | ||
45 | DashboardPageLayoutContext, | 39 | DashboardPageLayoutContext, |
46 | DashboardPageLayouts, | 40 | DashboardPageLayouts, |
47 | - DashboardPageScope, IDashboardController | 41 | + DashboardPageScope, |
42 | + IDashboardController, | ||
43 | + LayoutWidgetsArray | ||
48 | } from './dashboard-page.models'; | 44 | } from './dashboard-page.models'; |
49 | import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; | 45 | import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; |
50 | import { MediaBreakpoints } from '@shared/models/constants'; | 46 | import { MediaBreakpoints } from '@shared/models/constants'; |
51 | import { AuthUser } from '@shared/models/user.model'; | 47 | import { AuthUser } from '@shared/models/user.model'; |
52 | import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | 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 | import { environment as env } from '@env/environment'; | 50 | import { environment as env } from '@env/environment'; |
55 | import { Authority } from '@shared/models/authority.enum'; | 51 | import { Authority } from '@shared/models/authority.enum'; |
56 | import { DialogService } from '@core/services/dialog.service'; | 52 | import { DialogService } from '@core/services/dialog.service'; |
57 | import { EntityService } from '@core/http/entity.service'; | 53 | import { EntityService } from '@core/http/entity.service'; |
58 | import { AliasController } from '@core/api/alias-controller'; | 54 | import { AliasController } from '@core/api/alias-controller'; |
59 | -import { Observable, Subscription, of } from 'rxjs'; | 55 | +import { Observable, of, Subscription } from 'rxjs'; |
60 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; | 56 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; |
61 | -import { IStateController } from '@core/api/widget-api.models'; | ||
62 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | 57 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
63 | import { DashboardService } from '@core/http/dashboard.service'; | 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 | import { WidgetComponentService } from '../../components/widget/widget-component.service'; | 60 | import { WidgetComponentService } from '../../components/widget/widget-component.service'; |
70 | -import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; | 61 | +import { FormBuilder } from '@angular/forms'; |
71 | import { ItemBufferService } from '@core/services/item-buffer.service'; | 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 | import { MatDialog } from '@angular/material/dialog'; | 63 | import { MatDialog } from '@angular/material/dialog'; |
78 | import { | 64 | import { |
79 | EntityAliasesDialogComponent, | 65 | EntityAliasesDialogComponent, |
@@ -81,6 +67,18 @@ import { | @@ -81,6 +67,18 @@ import { | ||
81 | } from '@home/components/alias/entity-aliases-dialog.component'; | 67 | } from '@home/components/alias/entity-aliases-dialog.component'; |
82 | import { EntityAliases } from '@app/shared/models/alias.models'; | 68 | import { EntityAliases } from '@app/shared/models/alias.models'; |
83 | import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component'; | 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 | @Component({ | 83 | @Component({ |
86 | selector: 'tb-dashboard-page', | 84 | selector: 'tb-dashboard-page', |
@@ -109,6 +107,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -109,6 +107,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
109 | isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); | 107 | isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); |
110 | forceDashboardMobileMode = false; | 108 | forceDashboardMobileMode = false; |
111 | isAddingWidget = false; | 109 | isAddingWidget = false; |
110 | + widgetsBundle: WidgetsBundle = null; | ||
112 | 111 | ||
113 | isToolbarOpened = false; | 112 | isToolbarOpened = false; |
114 | isToolbarOpenedAnimate = false; | 113 | isToolbarOpenedAnimate = false; |
@@ -127,12 +126,14 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -127,12 +126,14 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
127 | currentCustomerId: string; | 126 | currentCustomerId: string; |
128 | currentDashboardScope: DashboardPageScope; | 127 | currentDashboardScope: DashboardPageScope; |
129 | 128 | ||
129 | + addingLayoutCtx: DashboardPageLayoutContext; | ||
130 | + | ||
130 | layouts: DashboardPageLayouts = { | 131 | layouts: DashboardPageLayouts = { |
131 | main: { | 132 | main: { |
132 | show: false, | 133 | show: false, |
133 | layoutCtx: { | 134 | layoutCtx: { |
134 | id: 'main', | 135 | id: 'main', |
135 | - widgets: [], | 136 | + widgets: null, |
136 | widgetLayouts: {}, | 137 | widgetLayouts: {}, |
137 | gridSettings: {}, | 138 | gridSettings: {}, |
138 | ignoreLoading: false, | 139 | ignoreLoading: false, |
@@ -144,7 +145,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -144,7 +145,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
144 | show: false, | 145 | show: false, |
145 | layoutCtx: { | 146 | layoutCtx: { |
146 | id: 'right', | 147 | id: 'right', |
147 | - widgets: [], | 148 | + widgets: null, |
148 | widgetLayouts: {}, | 149 | widgetLayouts: {}, |
149 | gridSettings: {}, | 150 | gridSettings: {}, |
150 | ignoreLoading: false, | 151 | ignoreLoading: false, |
@@ -216,6 +217,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -216,6 +217,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
216 | private itembuffer: ItemBufferService, | 217 | private itembuffer: ItemBufferService, |
217 | private fb: FormBuilder, | 218 | private fb: FormBuilder, |
218 | private dialog: MatDialog, | 219 | private dialog: MatDialog, |
220 | + private translate: TranslateService, | ||
219 | private ngZone: NgZone, | 221 | private ngZone: NgZone, |
220 | private cd: ChangeDetectorRef) { | 222 | private cd: ChangeDetectorRef) { |
221 | super(store); | 223 | super(store); |
@@ -251,6 +253,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -251,6 +253,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
251 | 253 | ||
252 | this.dashboard = data.dashboard; | 254 | this.dashboard = data.dashboard; |
253 | this.dashboardConfiguration = this.dashboard.configuration; | 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 | this.widgetEditMode = data.widgetEditMode; | 258 | this.widgetEditMode = data.widgetEditMode; |
255 | this.singlePageMode = data.singlePageMode; | 259 | this.singlePageMode = data.singlePageMode; |
256 | 260 | ||
@@ -282,6 +286,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -282,6 +286,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
282 | this.isEditingWidget = false; | 286 | this.isEditingWidget = false; |
283 | this.forceDashboardMobileMode = false; | 287 | this.forceDashboardMobileMode = false; |
284 | this.isAddingWidget = false; | 288 | this.isAddingWidget = false; |
289 | + this.widgetsBundle = null; | ||
285 | 290 | ||
286 | this.isToolbarOpened = false; | 291 | this.isToolbarOpened = false; |
287 | this.isToolbarOpenedAnimate = false; | 292 | this.isToolbarOpenedAnimate = false; |
@@ -475,8 +480,30 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -475,8 +480,30 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
475 | if ($event) { | 480 | if ($event) { |
476 | $event.stopPropagation(); | 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 | public manageDashboardStates($event: Event) { | 509 | public manageDashboardStates($event: Event) { |
@@ -491,8 +518,23 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -491,8 +518,23 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
491 | if ($event) { | 518 | if ($event) { |
492 | $event.stopPropagation(); | 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 | private importWidget($event: Event) { | 540 | private importWidget($event: Event) { |
@@ -526,7 +568,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -526,7 +568,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
526 | this.notifyDashboardUpdated(); | 568 | this.notifyDashboardUpdated(); |
527 | } | 569 | } |
528 | 570 | ||
529 | - public openDashboardState(state: string, openRightLayout: boolean) { | 571 | + public openDashboardState(state: string, openRightLayout?: boolean) { |
530 | const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); | 572 | const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); |
531 | if (layoutsData) { | 573 | if (layoutsData) { |
532 | this.dashboardCtx.state = state; | 574 | this.dashboardCtx.state = state; |
@@ -546,21 +588,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -546,21 +588,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
546 | } | 588 | } |
547 | } | 589 | } |
548 | this.isRightLayoutOpened = openRightLayout ? true : false; | 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 | for (const l of Object.keys(this.layouts)) { | 599 | for (const l of Object.keys(this.layouts)) { |
555 | const layout: DashboardPageLayout = this.layouts[l]; | 600 | const layout: DashboardPageLayout = this.layouts[l]; |
556 | if (layoutsData[l]) { | 601 | if (layoutsData[l]) { |
557 | const layoutInfo: DashboardLayoutInfo = layoutsData[l]; | 602 | const layoutInfo: DashboardLayoutInfo = layoutsData[l]; |
558 | - if (layout.layoutCtx.id === 'main') { | ||
559 | - layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged); | ||
560 | - } | ||
561 | this.updateLayout(layout, layoutInfo); | 603 | this.updateLayout(layout, layoutInfo); |
562 | } else { | 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,7 +611,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
569 | if (layoutInfo.gridSettings) { | 611 | if (layoutInfo.gridSettings) { |
570 | layout.layoutCtx.gridSettings = layoutInfo.gridSettings; | 612 | layout.layoutCtx.gridSettings = layoutInfo.gridSettings; |
571 | } | 613 | } |
572 | - layout.layoutCtx.widgets = layoutInfo.widgets; | 614 | + layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds); |
573 | layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; | 615 | layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts; |
574 | if (layout.show && layout.layoutCtx.ctrl) { | 616 | if (layout.show && layout.layoutCtx.ctrl) { |
575 | layout.layoutCtx.ctrl.reload(); | 617 | layout.layoutCtx.ctrl.reload(); |
@@ -594,6 +636,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -594,6 +636,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
594 | this.dashboardConfiguration = this.dashboard.configuration; | 636 | this.dashboardConfiguration = this.dashboard.configuration; |
595 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; | 637 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; |
596 | this.entityAliasesUpdated(); | 638 | this.entityAliasesUpdated(); |
639 | + this.updateLayouts(); | ||
597 | } else { | 640 | } else { |
598 | this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow; | 641 | this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow; |
599 | } | 642 | } |
@@ -617,7 +660,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -617,7 +660,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
617 | 660 | ||
618 | private notifyDashboardUpdated() { | 661 | private notifyDashboardUpdated() { |
619 | if (this.widgetEditMode) { | 662 | if (this.widgetEditMode) { |
620 | - const widget = this.layouts.main.layoutCtx.widgets[0]; | 663 | + const widget = this.layouts.main.layoutCtx.widgets.widgetByIndex(0); |
621 | const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; | 664 | const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; |
622 | widget.sizeX = layout.sizeX; | 665 | widget.sizeX = layout.sizeX; |
623 | widget.sizeY = layout.sizeY; | 666 | widget.sizeY = layout.sizeY; |
@@ -643,8 +686,86 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -643,8 +686,86 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
643 | if ($event) { | 686 | if ($event) { |
644 | $event.stopPropagation(); | 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 | onRevertWidgetEdit() { | 771 | onRevertWidgetEdit() { |
@@ -660,14 +781,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -660,14 +781,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
660 | const widget = deepClone(this.editingWidget); | 781 | const widget = deepClone(this.editingWidget); |
661 | const widgetLayout = deepClone(this.editingWidgetLayout); | 782 | const widgetLayout = deepClone(this.editingWidgetLayout); |
662 | const id = this.editingWidgetOriginal.id; | 783 | const id = this.editingWidgetOriginal.id; |
663 | - const index = this.editingLayoutCtx.widgets.indexOf(this.editingWidgetOriginal); | ||
664 | this.dashboardConfiguration.widgets[id] = widget; | 784 | this.dashboardConfiguration.widgets[id] = widget; |
665 | this.editingWidgetOriginal = widget; | 785 | this.editingWidgetOriginal = widget; |
666 | this.editingWidgetLayoutOriginal = widgetLayout; | 786 | this.editingWidgetLayoutOriginal = widgetLayout; |
667 | - this.editingLayoutCtx.widgets[index] = widget; | ||
668 | this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; | 787 | this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; |
669 | setTimeout(() => { | 788 | setTimeout(() => { |
670 | - this.editingLayoutCtx.ctrl.highlightWidget(index, 0); | 789 | + this.editingLayoutCtx.ctrl.highlightWidget(widget.id, 0); |
671 | }, 0); | 790 | }, 0); |
672 | } | 791 | } |
673 | 792 | ||
@@ -683,7 +802,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -683,7 +802,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
683 | this.forceDashboardMobileMode = false; | 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 | $event.stopPropagation(); | 806 | $event.stopPropagation(); |
688 | if (this.editingWidgetOriginal === widget) { | 807 | if (this.editingWidgetOriginal === widget) { |
689 | this.onEditWidgetClosed(); | 808 | this.onEditWidgetClosed(); |
@@ -701,52 +820,73 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -701,52 +820,73 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
701 | const delayOffset = transition ? 350 : 0; | 820 | const delayOffset = transition ? 350 : 0; |
702 | const delay = transition ? 400 : 300; | 821 | const delay = transition ? 400 : 300; |
703 | setTimeout(() => { | 822 | setTimeout(() => { |
704 | - layoutCtx.ctrl.highlightWidget(index, delay); | 823 | + layoutCtx.ctrl.highlightWidget(widget.id, delay); |
705 | }, delayOffset); | 824 | }, delayOffset); |
706 | } | 825 | } |
707 | } | 826 | } |
708 | } | 827 | } |
709 | 828 | ||
710 | copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | 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 | copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | 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 | pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { | 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 | pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { | 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 | removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { | 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 | $event.stopPropagation(); | 876 | $event.stopPropagation(); |
737 | // TODO: | 877 | // TODO: |
738 | this.dialogService.todo(); | 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 | if (this.isEditingWidget) { | 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 | if (this.isEdit && !this.isEditingWidget) { | 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,13 +935,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
795 | return dashboardContextActions; | 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 | const widgetContextActions: Array<WidgetContextMenuItem> = []; | 939 | const widgetContextActions: Array<WidgetContextMenuItem> = []; |
800 | if (this.isEdit && !this.isEditingWidget) { | 940 | if (this.isEdit && !this.isEditingWidget) { |
801 | widgetContextActions.push( | 941 | widgetContextActions.push( |
802 | { | 942 | { |
803 | action: (event, currentWidget) => { | 943 | action: (event, currentWidget) => { |
804 | - this.editWidget(event, layoutCtx, currentWidget, index); | 944 | + this.editWidget(event, layoutCtx, currentWidget); |
805 | }, | 945 | }, |
806 | enabled: true, | 946 | enabled: true, |
807 | value: 'action.edit', | 947 | value: 'action.edit', |
@@ -15,14 +15,13 @@ | @@ -15,14 +15,13 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard, WidgetLayouts } from '@app/shared/models/dashboard.models'; | 17 | import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard, WidgetLayouts } from '@app/shared/models/dashboard.models'; |
18 | -import { Widget } from '@app/shared/models/widget.models'; | 18 | +import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; |
19 | import { Timewindow } from '@shared/models/time/time.models'; | 19 | import { Timewindow } from '@shared/models/time/time.models'; |
20 | import { IAliasController, IStateController } from '@core/api/widget-api.models'; | 20 | import { IAliasController, IStateController } from '@core/api/widget-api.models'; |
21 | import { ILayoutController } from './layout/layout.models'; | 21 | import { ILayoutController } from './layout/layout.models'; |
22 | import { | 22 | import { |
23 | DashboardContextMenuItem, | 23 | DashboardContextMenuItem, |
24 | - WidgetContextMenuItem, | ||
25 | - WidgetPosition | 24 | + WidgetContextMenuItem |
26 | } from '@home/models/dashboard-component.models'; | 25 | } from '@home/models/dashboard-component.models'; |
27 | import { Observable } from 'rxjs'; | 26 | import { Observable } from 'rxjs'; |
28 | import { ChangeDetectorRef } from '@angular/core'; | 27 | import { ChangeDetectorRef } from '@angular/core'; |
@@ -43,13 +42,13 @@ export interface IDashboardController { | @@ -43,13 +42,13 @@ export interface IDashboardController { | ||
43 | openRightLayout(); | 42 | openRightLayout(); |
44 | openDashboardState(stateId: string, openRightLayout: boolean); | 43 | openDashboardState(stateId: string, openRightLayout: boolean); |
45 | addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); | 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 | removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | 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 | prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>; | 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 | copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | 52 | copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); |
54 | copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); | 53 | copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); |
55 | pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); | 54 | pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); |
@@ -58,7 +57,7 @@ export interface IDashboardController { | @@ -58,7 +57,7 @@ export interface IDashboardController { | ||
58 | 57 | ||
59 | export interface DashboardPageLayoutContext { | 58 | export interface DashboardPageLayoutContext { |
60 | id: DashboardLayoutId; | 59 | id: DashboardLayoutId; |
61 | - widgets: Array<Widget>; | 60 | + widgets: LayoutWidgetsArray; |
62 | widgetLayouts: WidgetLayouts; | 61 | widgetLayouts: WidgetLayouts; |
63 | gridSettings: GridSettings; | 62 | gridSettings: GridSettings; |
64 | ctrl: ILayoutController; | 63 | ctrl: ILayoutController; |
@@ -73,3 +72,69 @@ export interface DashboardPageLayout { | @@ -73,3 +72,69 @@ export interface DashboardPageLayout { | ||
73 | 72 | ||
74 | export declare type DashboardPageLayouts = {[key in DashboardLayoutId]: DashboardPageLayout}; | 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,13 +29,22 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component'; | ||
29 | import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; | 29 | import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; |
30 | import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; | 30 | import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; |
31 | import { EditWidgetComponent } from './edit-widget.component'; | 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 | @NgModule({ | 38 | @NgModule({ |
34 | entryComponents: [ | 39 | entryComponents: [ |
35 | DashboardFormComponent, | 40 | DashboardFormComponent, |
36 | DashboardTabsComponent, | 41 | DashboardTabsComponent, |
37 | ManageDashboardCustomersDialogComponent, | 42 | ManageDashboardCustomersDialogComponent, |
38 | - MakeDashboardPublicDialogComponent | 43 | + MakeDashboardPublicDialogComponent, |
44 | + AddWidgetDialogComponent, | ||
45 | + ManageDashboardLayoutsDialogComponent, | ||
46 | + SelectTargetLayoutDialogComponent, | ||
47 | + DashboardSettingsDialogComponent | ||
39 | ], | 48 | ], |
40 | declarations: [ | 49 | declarations: [ |
41 | DashboardFormComponent, | 50 | DashboardFormComponent, |
@@ -45,7 +54,12 @@ import { EditWidgetComponent } from './edit-widget.component'; | @@ -45,7 +54,12 @@ import { EditWidgetComponent } from './edit-widget.component'; | ||
45 | DashboardToolbarComponent, | 54 | DashboardToolbarComponent, |
46 | DashboardPageComponent, | 55 | DashboardPageComponent, |
47 | DashboardLayoutComponent, | 56 | DashboardLayoutComponent, |
48 | - EditWidgetComponent | 57 | + EditWidgetComponent, |
58 | + DashboardWidgetSelectComponent, | ||
59 | + AddWidgetDialogComponent, | ||
60 | + ManageDashboardLayoutsDialogComponent, | ||
61 | + SelectTargetLayoutDialogComponent, | ||
62 | + DashboardSettingsDialogComponent | ||
49 | ], | 63 | ], |
50 | imports: [ | 64 | imports: [ |
51 | CommonModule, | 65 | CommonModule, |
@@ -132,26 +132,4 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan | @@ -132,26 +132,4 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan | ||
132 | }; | 132 | }; |
133 | this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); | 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,14 +17,9 @@ | ||
17 | --> | 17 | --> |
18 | <hotkeys-cheatsheet></hotkeys-cheatsheet> | 18 | <hotkeys-cheatsheet></hotkeys-cheatsheet> |
19 | <div class="mat-content" style="position: relative; width: 100%; height: 100%;" | 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 | [ngStyle]="{'color': layoutCtx.gridSettings.color}" | 23 | [ngStyle]="{'color': layoutCtx.gridSettings.color}" |
29 | style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;" | 24 | style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;" |
30 | class="mat-headline tb-absolute-fill"> | 25 | class="mat-headline tb-absolute-fill"> |
@@ -37,18 +32,12 @@ | @@ -37,18 +32,12 @@ | ||
37 | {{ 'dashboard.add-widget' | translate }} | 32 | {{ 'dashboard.add-widget' | translate }} |
38 | </button> | 33 | </button> |
39 | </section> | 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 | [widgets]="layoutCtx.widgets" | 37 | [widgets]="layoutCtx.widgets" |
48 | [widgetLayouts]="layoutCtx.widgetLayouts" | 38 | [widgetLayouts]="layoutCtx.widgetLayouts" |
49 | [columns]="layoutCtx.gridSettings.columns" | 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 | [aliasController]="dashboardCtx.aliasController" | 41 | [aliasController]="dashboardCtx.aliasController" |
53 | [stateController]="dashboardCtx.stateController" | 42 | [stateController]="dashboardCtx.stateController" |
54 | [dashboardTimewindow]="dashboardCtx.dashboardTimewindow" | 43 | [dashboardTimewindow]="dashboardCtx.dashboardTimewindow" |
@@ -34,6 +34,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys'; | @@ -34,6 +34,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys'; | ||
34 | import { getCurrentIsLoading } from '@core/interceptors/load.selectors'; | 34 | import { getCurrentIsLoading } from '@core/interceptors/load.selectors'; |
35 | import { TranslateService } from '@ngx-translate/core'; | 35 | import { TranslateService } from '@ngx-translate/core'; |
36 | import { ItemBufferService } from '@app/core/services/item-buffer.service'; | 36 | import { ItemBufferService } from '@app/core/services/item-buffer.service'; |
37 | +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; | ||
37 | 38 | ||
38 | @Component({ | 39 | @Component({ |
39 | selector: 'tb-dashboard-layout', | 40 | selector: 'tb-dashboard-layout', |
@@ -43,12 +44,17 @@ import { ItemBufferService } from '@app/core/services/item-buffer.service'; | @@ -43,12 +44,17 @@ import { ItemBufferService } from '@app/core/services/item-buffer.service'; | ||
43 | export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy { | 44 | export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy { |
44 | 45 | ||
45 | layoutCtxValue: DashboardPageLayoutContext; | 46 | layoutCtxValue: DashboardPageLayoutContext; |
47 | + dashboardStyle: {[klass: string]: any} = null; | ||
48 | + backgroundImage: SafeStyle | string; | ||
46 | 49 | ||
47 | @Input() | 50 | @Input() |
48 | set layoutCtx(val: DashboardPageLayoutContext) { | 51 | set layoutCtx(val: DashboardPageLayoutContext) { |
49 | this.layoutCtxValue = val; | 52 | this.layoutCtxValue = val; |
50 | if (this.layoutCtxValue) { | 53 | if (this.layoutCtxValue) { |
51 | this.layoutCtxValue.ctrl = this; | 54 | this.layoutCtxValue.ctrl = this; |
55 | + if (this.dashboardStyle == null) { | ||
56 | + this.loadDashboardStyle(); | ||
57 | + } | ||
52 | } | 58 | } |
53 | } | 59 | } |
54 | get layoutCtx(): DashboardPageLayoutContext { | 60 | get layoutCtx(): DashboardPageLayoutContext { |
@@ -77,7 +83,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -77,7 +83,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
77 | constructor(protected store: Store<AppState>, | 83 | constructor(protected store: Store<AppState>, |
78 | private hotkeysService: HotkeysService, | 84 | private hotkeysService: HotkeysService, |
79 | private translate: TranslateService, | 85 | private translate: TranslateService, |
80 | - private itembuffer: ItemBufferService) { | 86 | + private itembuffer: ItemBufferService, |
87 | + private sanitizer: DomSanitizer) { | ||
81 | super(store); | 88 | super(store); |
82 | } | 89 | } |
83 | 90 | ||
@@ -165,54 +172,67 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -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 | resetHighlight() { | 194 | resetHighlight() { |
175 | this.dashboard.resetHighlight(); | 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 | addWidget($event: Event) { | 206 | addWidget($event: Event) { |
187 | this.layoutCtx.dashboardCtrl.addWidget($event, this.layoutCtx); | 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 | return this.layoutCtx.dashboardCtrl.removeWidget($event, this.layoutCtx, widget); | 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 | prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> { | 230 | prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> { |
211 | return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx); | 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 | copyWidget($event: Event, widget: Widget) { | 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,10 +19,9 @@ import { WidgetLayout } from '@shared/models/dashboard.models'; | ||
19 | 19 | ||
20 | export interface ILayoutController { | 20 | export interface ILayoutController { |
21 | reload(); | 21 | reload(); |
22 | - setResizing(layoutVisibilityChanged: boolean); | ||
23 | resetHighlight(); | 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 | pasteWidget($event: MouseEvent); | 25 | pasteWidget($event: MouseEvent); |
27 | pasteWidgetReference($event: MouseEvent); | 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,6 +16,7 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" | 18 | <button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" |
19 | + type="button" | ||
19 | mat-raised-button color="primary" (click)="openEditMode($event)"> | 20 | mat-raised-button color="primary" (click)="openEditMode($event)"> |
20 | <mat-icon class="material-icons">query_builder</mat-icon> | 21 | <mat-icon class="material-icons">query_builder</mat-icon> |
21 | <span>{{innerValue?.displayValue}}</span> | 22 | <span>{{innerValue?.displayValue}}</span> |
@@ -23,6 +24,7 @@ | @@ -23,6 +24,7 @@ | ||
23 | <section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" | 24 | <section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" |
24 | class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> | 25 | class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center"> |
25 | <button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" | 26 | <button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
27 | + type="button" | ||
26 | (click)="openEditMode($event)" | 28 | (click)="openEditMode($event)" |
27 | matTooltip="{{ 'timewindow.edit' | translate }}" | 29 | matTooltip="{{ 'timewindow.edit' | translate }}" |
28 | [matTooltipPosition]="tooltipPosition"> | 30 | [matTooltipPosition]="tooltipPosition"> |
@@ -35,6 +37,7 @@ | @@ -35,6 +37,7 @@ | ||
35 | {{innerValue?.displayValue}} | 37 | {{innerValue?.displayValue}} |
36 | </span> | 38 | </span> |
37 | <button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" | 39 | <button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32" |
40 | + type="button" | ||
38 | (click)="openEditMode($event)" | 41 | (click)="openEditMode($event)" |
39 | matTooltip="{{ 'timewindow.edit' | translate }}" | 42 | matTooltip="{{ 'timewindow.edit' | translate }}" |
40 | [matTooltipPosition]="tooltipPosition"> | 43 | [matTooltipPosition]="tooltipPosition"> |
@@ -24,10 +24,16 @@ | @@ -24,10 +24,16 @@ | ||
24 | panelClass="tb-widgets-bundle-select" | 24 | panelClass="tb-widgets-bundle-select" |
25 | placeholder="{{ 'widget.select-widgets-bundle' | translate }}" | 25 | placeholder="{{ 'widget.select-widgets-bundle' | translate }}" |
26 | (ngModelChange)="widgetsBundleChanged()"> | 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 | <mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle"> | 33 | <mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle"> |
28 | <div class="tb-bundle-item"> | 34 | <div class="tb-bundle-item"> |
29 | <span>{{widgetsBundle.title}}</span> | 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 | </div> | 37 | </div> |
32 | </mat-option> | 38 | </mat-option> |
33 | </mat-select> | 39 | </mat-select> |
@@ -20,8 +20,8 @@ tb-widgets-bundle-select { | @@ -20,8 +20,8 @@ tb-widgets-bundle-select { | ||
20 | } | 20 | } |
21 | 21 | ||
22 | .tb-bundle-item { | 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,28 +63,43 @@ tb-widgets-bundle-select, | ||
63 | 63 | ||
64 | mat-toolbar { | 64 | mat-toolbar { |
65 | tb-widgets-bundle-select { | 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,10 +132,13 @@ export interface EntityAliasFilter extends EntityFilters { | ||
132 | resolveMultiple?: boolean; | 132 | resolveMultiple?: boolean; |
133 | } | 133 | } |
134 | 134 | ||
135 | -export interface EntityAlias { | ||
136 | - id: string; | 135 | +export interface EntityAliasInfo { |
137 | alias: string; | 136 | alias: string; |
138 | filter: EntityAliasFilter; | 137 | filter: EntityAliasFilter; |
138 | +} | ||
139 | + | ||
140 | +export interface EntityAlias extends EntityAliasInfo { | ||
141 | + id: string; | ||
139 | [key: string]: any; | 142 | [key: string]: any; |
140 | } | 143 | } |
141 | 144 |
@@ -30,12 +30,12 @@ export interface DashboardInfo extends BaseData<DashboardId> { | @@ -30,12 +30,12 @@ export interface DashboardInfo extends BaseData<DashboardId> { | ||
30 | } | 30 | } |
31 | 31 | ||
32 | export interface WidgetLayout { | 32 | export interface WidgetLayout { |
33 | - sizeX: number; | ||
34 | - sizeY: number; | 33 | + sizeX?: number; |
34 | + sizeY?: number; | ||
35 | mobileHeight?: number; | 35 | mobileHeight?: number; |
36 | mobileOrder?: number; | 36 | mobileOrder?: number; |
37 | - col: number; | ||
38 | - row: number; | 37 | + col?: number; |
38 | + row?: number; | ||
39 | } | 39 | } |
40 | 40 | ||
41 | export interface WidgetLayouts { | 41 | export interface WidgetLayouts { |
@@ -46,14 +46,13 @@ export interface GridSettings { | @@ -46,14 +46,13 @@ export interface GridSettings { | ||
46 | backgroundColor?: string; | 46 | backgroundColor?: string; |
47 | color?: string; | 47 | color?: string; |
48 | columns?: number; | 48 | columns?: number; |
49 | - margins?: [number, number]; | 49 | + margin?: number; |
50 | backgroundSizeMode?: string; | 50 | backgroundSizeMode?: string; |
51 | backgroundImageUrl?: string; | 51 | backgroundImageUrl?: string; |
52 | autoFillHeight?: boolean; | 52 | autoFillHeight?: boolean; |
53 | mobileAutoFillHeight?: boolean; | 53 | mobileAutoFillHeight?: boolean; |
54 | mobileRowHeight?: number; | 54 | mobileRowHeight?: number; |
55 | [key: string]: any; | 55 | [key: string]: any; |
56 | - // TODO: | ||
57 | } | 56 | } |
58 | 57 | ||
59 | export interface DashboardLayout { | 58 | export interface DashboardLayout { |
@@ -62,7 +61,7 @@ export interface DashboardLayout { | @@ -62,7 +61,7 @@ export interface DashboardLayout { | ||
62 | } | 61 | } |
63 | 62 | ||
64 | export interface DashboardLayoutInfo { | 63 | export interface DashboardLayoutInfo { |
65 | - widgets?: Array<Widget>; | 64 | + widgetIds?: string[]; |
66 | widgetLayouts?: WidgetLayouts; | 65 | widgetLayouts?: WidgetLayouts; |
67 | gridSettings?: GridSettings; | 66 | gridSettings?: GridSettings; |
68 | } | 67 | } |
@@ -392,3 +392,13 @@ export interface JsonSettingsSchema { | @@ -392,3 +392,13 @@ export interface JsonSettingsSchema { | ||
392 | }; | 392 | }; |
393 | form?: any[]; | 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,6 +20,8 @@ import { FooterComponent } from './components/footer.component'; | ||
20 | import { LogoComponent } from './components/logo.component'; | 20 | import { LogoComponent } from './components/logo.component'; |
21 | import { TbSnackBarComponent, ToastDirective } from './components/toast.directive'; | 21 | import { TbSnackBarComponent, ToastDirective } from './components/toast.directive'; |
22 | import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component'; | 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 | import { | 26 | import { |
25 | MatAutocompleteModule, | 27 | MatAutocompleteModule, |
@@ -108,6 +110,7 @@ import { JsFuncComponent } from './components/js-func.component'; | @@ -108,6 +110,7 @@ import { JsFuncComponent } from './components/js-func.component'; | ||
108 | import { JsonFormComponent } from './components/json-form/json-form.component'; | 110 | import { JsonFormComponent } from './components/json-form/json-form.component'; |
109 | import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; | 111 | import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component'; |
110 | import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; | 112 | import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component'; |
113 | +import { ImageInputComponent } from './components/image-input.component'; | ||
111 | 114 | ||
112 | @NgModule({ | 115 | @NgModule({ |
113 | providers: [ | 116 | providers: [ |
@@ -115,7 +118,11 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | @@ -115,7 +118,11 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | ||
115 | MillisecondsToTimeStringPipe, | 118 | MillisecondsToTimeStringPipe, |
116 | EnumToArrayPipe, | 119 | EnumToArrayPipe, |
117 | HighlightPipe, | 120 | HighlightPipe, |
118 | - TruncatePipe | 121 | + TruncatePipe, |
122 | + { | ||
123 | + provide: FlowInjectionToken, | ||
124 | + useValue: Flow | ||
125 | + } | ||
119 | ], | 126 | ], |
120 | entryComponents: [ | 127 | entryComponents: [ |
121 | TbSnackBarComponent, | 128 | TbSnackBarComponent, |
@@ -173,6 +180,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | @@ -173,6 +180,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | ||
173 | ColorInputComponent, | 180 | ColorInputComponent, |
174 | MaterialIconSelectComponent, | 181 | MaterialIconSelectComponent, |
175 | JsonFormComponent, | 182 | JsonFormComponent, |
183 | + ImageInputComponent, | ||
176 | NospacePipe, | 184 | NospacePipe, |
177 | MillisecondsToTimeStringPipe, | 185 | MillisecondsToTimeStringPipe, |
178 | EnumToArrayPipe, | 186 | EnumToArrayPipe, |
@@ -222,7 +230,8 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | @@ -222,7 +230,8 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | ||
222 | OverlayModule, | 230 | OverlayModule, |
223 | ShareButtonsModule, | 231 | ShareButtonsModule, |
224 | HotkeyModule, | 232 | HotkeyModule, |
225 | - ColorPickerModule | 233 | + ColorPickerModule, |
234 | + NgxFlowModule | ||
226 | ], | 235 | ], |
227 | exports: [ | 236 | exports: [ |
228 | FooterComponent, | 237 | FooterComponent, |
@@ -308,6 +317,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | @@ -308,6 +317,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se | ||
308 | ColorInputComponent, | 317 | ColorInputComponent, |
309 | MaterialIconSelectComponent, | 318 | MaterialIconSelectComponent, |
310 | JsonFormComponent, | 319 | JsonFormComponent, |
320 | + ImageInputComponent, | ||
311 | NospacePipe, | 321 | NospacePipe, |
312 | MillisecondsToTimeStringPipe, | 322 | MillisecondsToTimeStringPipe, |
313 | EnumToArrayPipe, | 323 | EnumToArrayPipe, |
@@ -502,6 +502,9 @@ | @@ -502,6 +502,9 @@ | ||
502 | "min-columns-count-message": "Only 10 minimum column count is allowed.", | 502 | "min-columns-count-message": "Only 10 minimum column count is allowed.", |
503 | "max-columns-count-message": "Only 1000 maximum column count is allowed.", | 503 | "max-columns-count-message": "Only 1000 maximum column count is allowed.", |
504 | "widgets-margins": "Margin between widgets", | 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 | "horizontal-margin": "Horizontal margin", | 508 | "horizontal-margin": "Horizontal margin", |
506 | "horizontal-margin-required": "Horizontal margin value is required.", | 509 | "horizontal-margin-required": "Horizontal margin value is required.", |
507 | "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.", | 510 | "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.", |
@@ -27,3 +27,5 @@ $mat-gt-sm: "screen and (min-width: 960px)"; | @@ -27,3 +27,5 @@ $mat-gt-sm: "screen and (min-width: 960px)"; | ||
27 | $mat-gt-md: "screen and (min-width: 1280px)"; | 27 | $mat-gt-md: "screen and (min-width: 1280px)"; |
28 | $mat-gt-xmd: "screen and (min-width: 1600px)"; | 28 | $mat-gt-xmd: "screen and (min-width: 1600px)"; |
29 | $mat-gt-xl: "screen and (min-width: 1920px)"; | 29 | $mat-gt-xl: "screen and (min-width: 1920px)"; |
30 | + | ||
31 | +$primary-hue-3: rgb(207, 216, 220) !default; |
@@ -702,7 +702,6 @@ mat-label { | @@ -702,7 +702,6 @@ mat-label { | ||
702 | display: flex; | 702 | display: flex; |
703 | flex-direction: column; | 703 | flex-direction: column; |
704 | overflow: auto; | 704 | overflow: auto; |
705 | - height: 100%; | ||
706 | } | 705 | } |
707 | .mat-dialog-content { | 706 | .mat-dialog-content { |
708 | margin: 0; | 707 | margin: 0; |