Commit b3df9644e6586936e3a0c52ec475004a97c3fc41

Authored by Igor Kulikov
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 }
  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>
  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 +}
  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>
  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;