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