Browse Source

Add widget dialog. Dashboard settings. Dashboard layout settings.

pull/2150/head
Igor Kulikov 7 years ago
parent
commit
b3df9644e6
  1. 19
      ui-ngx/package-lock.json
  2. 2
      ui-ngx/package.json
  3. 2
      ui-ngx/src/app/core/core.module.ts
  4. 232
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  5. 335
      ui-ngx/src/app/core/services/item-buffer.service.ts
  6. 10
      ui-ngx/src/app/core/services/utils.service.ts
  7. 10
      ui-ngx/src/app/core/utils.ts
  8. 3
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  9. 154
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  10. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  11. 8
      ui-ngx/src/app/modules/home/components/details-panel.component.scss
  12. 6
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  13. 6
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  14. 1
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss
  15. 1
      ui-ngx/src/app/modules/home/components/widget/legend-config.component.html
  16. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  17. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  18. 93
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  19. 57
      ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html
  20. 137
      ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts
  21. 71
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html
  22. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss
  23. 274
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts
  24. 83
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts
  25. 167
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html
  26. 137
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts
  27. 84
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html
  28. 23
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.scss
  29. 144
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts
  30. 18
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts
  31. 22
      ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts
  32. 23
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html
  33. 56
      ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts
  34. 31
      ui-ngx/src/app/modules/home/pages/dashboard/layout/layout-button.scss
  35. 5
      ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts
  36. 71
      ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html
  37. 125
      ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.ts
  38. 48
      ui-ngx/src/app/modules/home/pages/dashboard/layout/select-target-layout-dialog.component.html
  39. 50
      ui-ngx/src/app/modules/home/pages/dashboard/layout/select-target-layout-dialog.component.ts
  40. 45
      ui-ngx/src/app/shared/components/image-input.component.html
  41. 104
      ui-ngx/src/app/shared/components/image-input.component.scss
  42. 143
      ui-ngx/src/app/shared/components/image-input.component.ts
  43. 3
      ui-ngx/src/app/shared/components/time/timewindow.component.html
  44. 8
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.html
  45. 53
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss
  46. 7
      ui-ngx/src/app/shared/models/alias.models.ts
  47. 13
      ui-ngx/src/app/shared/models/dashboard.models.ts
  48. 10
      ui-ngx/src/app/shared/models/widget.models.ts
  49. 14
      ui-ngx/src/app/shared/shared.module.ts
  50. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  51. 2
      ui-ngx/src/scss/constants.scss
  52. 1
      ui-ngx/src/styles.scss

19
ui-ngx/package-lock.json

@ -1349,6 +1349,20 @@
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.3.tgz",
"integrity": "sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw=="
},
"@flowjs/flow.js": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/@flowjs/flow.js/-/flow.js-2.13.2.tgz",
"integrity": "sha512-N2uoQ+F8E/l3JiSoU/hIwUPEjCPDUvWeCJei0S5vA3guqSY8JtgIZacuhNC6B6TYY5cGWGR/qCOSR6v6S/K0aA=="
},
"@flowjs/ngx-flow": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@flowjs/ngx-flow/-/ngx-flow-0.4.3.tgz",
"integrity": "sha512-6k+jLebR1RAoSGt4NHtlVPaGdmGeVocQdgsRAov2OEXcKrAH48yd0FcZI2mNMqLd2zeFyeURKbklqpoCv4gIwg==",
"requires": {
"@types/flowjs": "2.13.1",
"tslib": "^1.9.0"
}
},
"@mat-datetimepicker/core": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@mat-datetimepicker/core/-/core-2.0.1.tgz",
@ -1572,6 +1586,11 @@
"@types/jquery": "*"
}
},
"@types/flowjs": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@types/flowjs/-/flowjs-2.13.1.tgz",
"integrity": "sha512-cPuORQrWmJV7pmiSt1ApDOsQSooVka53Ugr3LB0MW/bsG/fDtOXSxsT5Aiej98VD3eCIZNyABfk3NBWU7CorsQ=="
},
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",

2
ui-ngx/package.json

@ -25,6 +25,8 @@
"@angular/router": "~8.2.11",
"@auth0/angular-jwt": "^3.0.0",
"@date-io/date-fns": "^1.3.11",
"@flowjs/flow.js": "^2.13.2",
"@flowjs/ngx-flow": "^0.4.3",
"@mat-datetimepicker/core": "^2.0.1",
"@material-ui/core": "^4.5.1",
"@material-ui/icons": "^4.5.1",

2
ui-ngx/src/app/core/core.module.ts

@ -38,7 +38,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler';
import { AlertDialogComponent } from '@core/services/dialog/alert-dialog.component';
import { WINDOW_PROVIDERS } from '@core/services/window.service';
import {TodoDialogComponent} from "@core/services/dialog/todo-dialog.component";
import {TodoDialogComponent} from '@core/services/dialog/todo-dialog.component';
import { HotkeyModule } from 'angular2-hotkeys';
export function HttpLoaderFactory(http: HttpClient) {

232
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -24,7 +24,7 @@ import {
DashboardState,
DashboardConfiguration,
DashboardLayoutInfo,
DashboardLayoutsInfo
DashboardLayoutsInfo, DashboardLayoutId, WidgetLayout, GridSettings
} from '@shared/models/dashboard.models';
import { isUndefined, isDefined, isString } from '@core/utils';
import { DatasourceType, Widget, Datasource } from '@app/shared/models/widget.models';
@ -91,6 +91,7 @@ export class DashboardUtilsService {
} else if (state.root) {
rootFound = true;
}
this.validateAndUpdateState(state);
}
if (!rootFound) {
const firstStateId = Object.keys(states)[0];
@ -216,13 +217,17 @@ export class DashboardUtilsService {
public createDefaultLayoutData(): DashboardLayout {
return {
widgets: {},
gridSettings: {
backgroundColor: '#eeeeee',
color: 'rgba(0,0,0,0.870588)',
columns: 24,
margins: [10, 10],
backgroundSizeMode: '100%'
}
gridSettings: this.createDefaultGridSettings()
};
}
private createDefaultGridSettings(): GridSettings {
return {
backgroundColor: '#eeeeee',
color: 'rgba(0,0,0,0.870588)',
columns: 24,
margin: 10,
backgroundSizeMode: '100%'
};
}
@ -240,6 +245,65 @@ export class DashboardUtilsService {
};
}
private validateAndUpdateState(state: DashboardState) {
if (!state.layouts) {
state.layouts = this.createDefaultLayouts();
}
for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l as DashboardLayoutId];
this.validateAndUpdateLayout(layout);
}
}
private validateAndUpdateLayout(layout: DashboardLayout) {
if (!layout.gridSettings) {
layout.gridSettings = this.createDefaultGridSettings();
}
if (layout.gridSettings.margins && layout.gridSettings.margins.length === 2) {
layout.gridSettings.margin = layout.gridSettings.margins[0];
delete layout.gridSettings.margins;
}
layout.gridSettings.margin = layout.gridSettings.margin || 10;
}
public setLayouts(dashboard: Dashboard, targetState: string, newLayouts: DashboardStateLayouts) {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[targetState];
let addedCount = 0;
let removedCount = 0;
for (const l of Object.keys(state.layouts)) {
if (!newLayouts[l]) {
removedCount++;
}
}
for (const l of Object.keys(newLayouts)) {
if (!state.layouts[l]) {
addedCount++;
}
}
state.layouts = newLayouts;
const layoutsCount = Object.keys(state.layouts).length;
let newColumns;
if (addedCount) {
for (const l of Object.keys(state.layouts)) {
newColumns = state.layouts[l].gridSettings.columns * (layoutsCount - addedCount) / layoutsCount;
if (newColumns > 0) {
state.layouts[l].gridSettings.columns = newColumns;
}
}
}
if (removedCount) {
for (const l of Object.keys(state.layouts)) {
newColumns = state.layouts[l].gridSettings.columns * (layoutsCount + removedCount) / layoutsCount;
if (newColumns > 0) {
state.layouts[l].gridSettings.columns = newColumns;
}
}
}
this.removeUnusedWidgets(dashboard);
}
public getRootStateId(states: {[id: string]: DashboardState }): string {
for (const stateId of Object.keys(states)) {
const state = states[stateId];
@ -261,12 +325,12 @@ export class DashboardUtilsService {
const layout: DashboardLayout = state.layouts[l];
if (layout) {
result[l] = {
widgets: [],
widgetIds: [],
widgetLayouts: {},
gridSettings: {}
} as DashboardLayoutInfo;
for (const id of Object.keys(layout.widgets)) {
result[l].widgets.push(allWidgets[id]);
result[l].widgetIds.push(id);
}
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
@ -289,6 +353,154 @@ export class DashboardUtilsService {
return widgetsArray;
}
public addWidgetToLayout(dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
widget: Widget,
originalColumns?: number,
originalSize?: {sizeX: number, sizeY: number},
row?: number,
column?: number): void {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[targetState];
const layout = state.layouts[targetLayout];
const layoutCount = Object.keys(state.layouts).length;
if (!widget.id) {
widget.id = this.utils.guid();
}
if (!dashboardConfiguration.widgets[widget.id]) {
dashboardConfiguration.widgets[widget.id] = widget;
}
const widgetLayout: WidgetLayout = {
sizeX: originalSize ? originalSize.sizeX : widget.sizeX,
sizeY: originalSize ? originalSize.sizeY : widget.sizeY,
mobileOrder: widget.config.mobileOrder,
mobileHeight: widget.config.mobileHeight
};
if (isUndefined(originalColumns)) {
originalColumns = 24;
}
const gridSettings = layout.gridSettings;
let columns = 24;
if (gridSettings && gridSettings.columns) {
columns = gridSettings.columns;
}
columns = columns * layoutCount;
if (columns !== originalColumns) {
const ratio = columns / originalColumns;
widgetLayout.sizeX *= ratio;
widgetLayout.sizeY *= ratio;
}
if (row > -1 && column > - 1) {
widgetLayout.row = row;
widgetLayout.col = column;
} else {
row = 0;
for (const w of Object.keys(layout.widgets)) {
const existingLayout = layout.widgets[w];
const wRow = existingLayout.row ? existingLayout.row : 0;
const wSizeY = existingLayout.sizeY ? existingLayout.sizeY : 1;
const bottom = wRow + wSizeY;
row = Math.max(row, bottom);
}
widgetLayout.row = row;
widgetLayout.col = 0;
}
layout.widgets[widget.id] = widgetLayout;
}
public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
widgetId: string) {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[targetState];
const layout = state.layouts[targetLayout];
delete layout.widgets[widgetId];
this.removeUnusedWidgets(dashboard);
}
public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const stateKeys = Object.keys(states);
if (stateKeys.length === 1) {
const state = states[stateKeys[0]];
const layouts = state.layouts;
const layoutKeys = Object.keys(layouts);
if (layoutKeys.length === 1) {
return {
state: stateKeys[0],
layout: layoutKeys[0] as DashboardLayoutId
};
}
}
return null;
}
public updateLayoutSettings(layout: DashboardLayout, gridSettings: GridSettings) {
const prevGridSettings = layout.gridSettings;
let prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
if (!prevColumns) {
prevColumns = 24;
}
const columns = gridSettings.columns || 24;
const ratio = columns / prevColumns;
layout.gridSettings = gridSettings;
let maxRow = 0;
for (const w of Object.keys(layout.widgets)) {
const widget = layout.widgets[w];
if (!widget.sizeX) {
widget.sizeX = 1;
}
if (!widget.sizeY) {
widget.sizeY = 1;
}
maxRow = Math.max(maxRow, widget.row + widget.sizeY);
}
const newMaxRow = Math.round(maxRow * ratio);
for (const w of Object.keys(layout.widgets)) {
const widget = layout.widgets[w];
if (widget.row + widget.sizeY === maxRow) {
widget.row = Math.round(widget.row * ratio);
widget.sizeY = newMaxRow - widget.row;
} else {
widget.row = Math.round(widget.row * ratio);
widget.sizeY = Math.round(widget.sizeY * ratio);
}
widget.sizeX = Math.round(widget.sizeX * ratio);
widget.col = Math.round(widget.col * ratio);
if (widget.col + widget.sizeX > columns) {
widget.sizeX = columns - widget.col;
}
}
}
private removeUnusedWidgets(dashboard: Dashboard) {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const widgets = dashboardConfiguration.widgets;
for (const widgetId of Object.keys(widgets)) {
let found = false;
for (const s of Object.keys(states)) {
const state = states[s];
for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l];
if (layout.widgets[widgetId]) {
found = true;
break;
}
}
}
if (!found) {
delete dashboardConfiguration.widgets[widgetId];
}
}
}
private validateAndUpdateEntityAliases(configuration: DashboardConfiguration,
datasourcesByAliasId: {[aliasId: string]: Array<Datasource>},
targetDevicesByAliasId: {[aliasId: string]: Array<Array<string>>}): DashboardConfiguration {

335
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -16,20 +16,347 @@
import { Injectable } from '@angular/core';
import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models';
import { EntityAlias, EntityAliasFilter, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models';
import { DatasourceType, Widget, WidgetPosition, WidgetSize } from '@shared/models/widget.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { deepClone } from '@core/utils';
import * as equal from 'deep-equal';
import { UtilsService } from '@core/services/utils.service';
import { Observable, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference';
const RULE_NODES = 'rule_nodes';
export interface AliasesInfo {
datasourceAliases: {[datasourceIndex: number]: EntityAliasInfo};
targetDeviceAliases: {[targetDeviceAliasIndex: number]: EntityAliasInfo};
}
export interface WidgetItem {
widget: Widget;
aliasesInfo: AliasesInfo;
originalSize: WidgetSize;
originalColumns: number;
}
export interface WidgetReference {
dashboardId: string;
sourceState: string;
sourceLayout: DashboardLayoutId;
widgetId: string;
originalSize: WidgetSize;
originalColumns: number;
}
@Injectable({
providedIn: 'root'
})
export class ItemBufferService {
constructor() {}
private namespace = 'tbBufferStore';
private delimiter = '.';
constructor(private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) {}
public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem {
const aliasesInfo: AliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
};
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) {
let entityAlias: EntityAlias;
if (widget.config.datasources) {
for (let i = 0; i < widget.config.datasources.length; i++) {
const datasource = widget.config.datasources[i];
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) {
entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
if (entityAlias) {
aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias);
}
}
}
}
if (widget.config.targetDeviceAliasIds) {
for (let i = 0; i < widget.config.targetDeviceAliasIds.length; i++) {
const targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
if (targetDeviceAliasId) {
entityAlias = dashboard.configuration.entityAliases[targetDeviceAliasId];
if (entityAlias) {
aliasesInfo.targetDeviceAliases[i] = this.prepareAliasInfo(entityAlias);
}
}
}
}
}
return {
widget,
aliasesInfo,
originalSize,
originalColumns
};
}
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
this.storeSet(WIDGET_ITEM, JSON.stringify(widgetItem));
}
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
this.storeSet(WIDGET_REFERENCE, JSON.stringify(widgetReference));
}
public hasWidget(): boolean {
// TODO:
return false;
return this.storeHas(WIDGET_ITEM);
}
public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean {
// TODO:
const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE);
if (widgetReferenceJson) {
const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson);
if (widgetReference.dashboardId === dashboard.id.id) {
if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout)
&& dashboard.configuration.widgets[widgetReference.widgetId]) {
return true;
}
}
}
return false;
}
public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition,
onAliasesUpdateFunction: () => void): Observable<Widget> {
const widgetItemJson = this.storeGet(WIDGET_ITEM);
if (widgetItemJson) {
const widgetItem: WidgetItem = JSON.parse(widgetItemJson);
const widget = widgetItem.widget;
const aliasesInfo = widgetItem.aliasesInfo;
const originalColumns = widgetItem.originalColumns;
const originalSize = widgetItem.originalSize;
let targetRow = -1;
let targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
widget.id = this.utils.guid();
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo,
onAliasesUpdateFunction, originalColumns,
originalSize, targetRow, targetColumn).pipe(
map(() => widget)
);
} else {
return throwError('Failed to read widget from buffer!');
}
}
public pasteWidgetReference(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> {
const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE);
if (widgetReferenceJson) {
const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson);
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
if (widget) {
const originalColumns = widgetReference.originalColumns;
const originalSize = widgetReference.originalSize;
let targetRow = -1;
let targetColumn = -1;
if (position) {
targetRow = position.row;
targetColumn = position.column;
}
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null,
null, originalColumns,
originalSize, targetRow, targetColumn).pipe(
map(() => widget)
);
} else {
return throwError('Failed to read widget reference from buffer!');
}
} else {
return throwError('Failed to read widget reference from buffer!');
}
}
public addWidgetToDashboard(dashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, widget: Widget,
aliasesInfo: AliasesInfo,
onAliasesUpdateFunction: () => void,
originalColumns: number,
originalSize: WidgetSize,
row: number,
column: number): Observable<Dashboard> {
let theDashboard: Dashboard;
if (dashboard) {
theDashboard = dashboard;
} else {
theDashboard = {};
}
theDashboard = this.dashboardUtils.validateAndUpdateDashboard(theDashboard);
let callAliasUpdateFunction = false;
if (aliasesInfo) {
const newEntityAliases = this.updateAliases(theDashboard, widget, aliasesInfo);
const aliasesUpdated = !equal(newEntityAliases, theDashboard.configuration.entityAliases);
if (aliasesUpdated) {
theDashboard.configuration.entityAliases = newEntityAliases;
if (onAliasesUpdateFunction) {
callAliasUpdateFunction = true;
}
}
}
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column);
if (callAliasUpdateFunction) {
onAliasesUpdateFunction();
}
return of(theDashboard);
}
private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number {
let originalColumns = 24;
let gridSettings = null;
const state = dashboard.configuration.states[sourceState];
const layoutCount = Object.keys(state.layouts).length;
if (state) {
const layout = state.layouts[sourceLayout];
if (layout) {
gridSettings = layout.gridSettings;
}
}
if (gridSettings &&
gridSettings.columns) {
originalColumns = gridSettings.columns;
}
originalColumns = originalColumns * layoutCount;
return originalColumns;
}
private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetSize {
const layout = dashboard.configuration.states[sourceState].layouts[sourceLayout];
const widgetLayout = layout.widgets[widget.id];
return {
sizeX: widgetLayout.sizeX,
sizeY: widgetLayout.sizeY
};
}
private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo {
return {
alias: entityAlias.alias,
filter: entityAlias.filter
};
}
private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
return {
dashboardId: dashboard.id.id,
sourceState,
sourceLayout,
widgetId: widget.id,
originalSize,
originalColumns
};
}
private updateAliases(dashboard: Dashboard, widget: Widget, aliasesInfo: AliasesInfo): EntityAliases {
const entityAliases = deepClone(dashboard.configuration.entityAliases);
let aliasInfo: EntityAliasInfo;
let newAliasId: string;
for (const datasourceIndexStr of Object.keys(aliasesInfo.datasourceAliases)) {
const datasourceIndex = Number(datasourceIndexStr);
aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
newAliasId = this.getEntityAliasId(entityAliases, aliasInfo);
widget.config.datasources[datasourceIndex].entityAliasId = newAliasId;
}
for (const targetDeviceAliasIndexStr of Object.keys(aliasesInfo.targetDeviceAliases)) {
const targetDeviceAliasIndex = Number(targetDeviceAliasIndexStr);
aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex];
newAliasId = this.getEntityAliasId(entityAliases, aliasInfo);
widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
}
return entityAliases;
}
private isEntityAliasEqual(alias1: EntityAliasInfo, alias2: EntityAliasInfo): boolean {
return equal(alias1.filter, alias2.filter);
}
private getEntityAliasId(entityAliases: EntityAliases, aliasInfo: EntityAliasInfo): string {
let newAliasId: string;
for (const aliasId of Object.keys(entityAliases)) {
if (this.isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) {
newAliasId = aliasId;
break;
}
}
if (!newAliasId) {
const newAliasName = this.createEntityAliasName(entityAliases, aliasInfo.alias);
newAliasId = this.utils.guid();
entityAliases[newAliasId] = {id: newAliasId, alias: newAliasName, filter: aliasInfo.filter};
}
return newAliasId;
}
private createEntityAliasName(entityAliases: EntityAliases, alias: string): string {
let c = 0;
let newAlias = alias;
let unique = false;
while (!unique) {
unique = true;
for (const entAliasId of Object.keys(entityAliases)) {
const entAlias = entityAliases[entAliasId];
if (newAlias === entAlias.alias) {
c++;
newAlias = alias + c;
unique = false;
}
}
}
return newAlias;
}
private storeSet(key: string, elem: any) {
localStorage.setItem(this.getNamespacedKey(key), JSON.stringify(elem));
}
private storeGet(key: string): any {
let obj = null;
const saved = localStorage.getItem(this.getNamespacedKey(key));
try {
if (typeof saved === 'undefined' || saved === 'undefined') {
obj = undefined;
} else {
obj = JSON.parse(saved);
}
} catch (e) {
this.storeRemove(key);
}
return obj;
}
private storeHas(key: string): boolean {
const saved = localStorage.getItem(this.getNamespacedKey(key));
return typeof saved !== 'undefined' && saved !== 'undefined' && saved !== null;
}
private storeRemove(key: string) {
localStorage.removeItem(this.getNamespacedKey(key));
}
private getNamespacedKey(key: string): string {
return [this.namespace, key].join(this.delimiter);
}
}

10
ui-ngx/src/app/core/services/utils.service.ts

@ -17,7 +17,7 @@
import { Inject, Injectable, NgZone } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models';
import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils';
import { deepClone, deleteNullProperties, guid, isDefined, isUndefined } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix } from '@app/shared/models/constants';
@ -263,13 +263,7 @@ export class UtilsService {
}
public guid(): string {
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
return guid();
}
public validateDatasources(datasources: Array<Datasource>): Array<Datasource> {

10
ui-ngx/src/app/core/utils.ts

@ -353,3 +353,13 @@ export function deepClone<T>(target: T): T {
}
return target;
}
export function guid(): string {
function s4(): string {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}

3
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -17,6 +17,7 @@
-->
<div fxFlex fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center"
[ngStyle]="dashboardStyle"
[style.backgroundImage]="backgroundImage"
[fxShow]="(((isLoading$ | async) && !this.ignoreLoading) || this.dashboardLoading) && !isEdit">
<mat-spinner color="warn" mode="indeterminate" diameter="100">
</mat-spinner>
@ -114,7 +115,7 @@
</button>
<button mat-button mat-icon-button
[fxShow]="!isEdit && widget.enableFullscreen"
(click)="widget.isFullscreen = !widget.isFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen"
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>

154
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -15,13 +15,14 @@
///
import {
AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef,
AfterViewInit,
Component,
DoCheck,
Input,
IterableDiffers,
KeyValueDiffers, NgZone,
NgZone,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild
@ -33,35 +34,35 @@ import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Timewindow, toHistoryTimewindow } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service';
import { GridsterComponent, GridsterConfig } from 'angular-gridster2';
import { GridsterComponent, GridsterComponentInterface, GridsterConfig } from 'angular-gridster2';
import {
DashboardCallbacks,
DashboardWidget,
DashboardWidgets,
IDashboardComponent,
WidgetPosition
IDashboardComponent
} from '../../models/dashboard-component.models';
import { ReplaySubject, Subject } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { Widget } from '@app/shared/models/widget.models';
import { Widget, WidgetPosition } from '@app/shared/models/widget.models';
import { MatMenuTrigger } from '@angular/material';
import { SafeStyle } from '@angular/platform-browser';
@Component({
selector: 'tb-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, AfterViewInit, OnChanges {
export class DashboardComponent extends PageComponent implements IDashboardComponent, DoCheck, OnInit, OnDestroy, AfterViewInit, OnChanges {
authUser: AuthUser;
@Input()
widgets: Array<Widget>;
widgets: Iterable<Widget>;
@Input()
widgetLayouts: WidgetLayouts;
@ -79,10 +80,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
columns: number;
@Input()
horizontalMargin: number;
@Input()
verticalMargin: number;
margin: number;
@Input()
isEdit: boolean;
@ -114,6 +112,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
dashboardStyle: {[klass: string]: any};
@Input()
backgroundImage: SafeStyle | string;
@Input()
dashboardClass: string;
@ -153,16 +154,20 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((index, item) => {
return item;
}),
this.kvDiffers.find([]).create<string, WidgetLayout>()
})
);
breakpointObserverSubscription: Subscription;
private optionsChangeNotificationsPaused = false;
private gridsterResizeListener = null;
constructor(protected store: Store<AppState>,
private timeService: TimeService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers,
private ngZone: NgZone) {
super(store);
this.authUser = getCurrentAuthUser(store);
@ -180,10 +185,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
maxRows: 100,
minCols: this.columns ? this.columns : 24,
outerMargin: true,
outerMarginLeft: this.horizontalMargin ? this.horizontalMargin : 10,
outerMarginRight: this.horizontalMargin ? this.horizontalMargin : 10,
outerMarginTop: this.verticalMargin ? this.verticalMargin : 10,
outerMarginBottom: this.horizontalMargin ? this.horizontalMargin : 10,
margin: this.margin ? this.margin : 10,
minItemCols: 1,
minItemRows: 1,
defaultItemCols: 8,
@ -198,7 +200,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.updateMobileOpts();
this.breakpointObserver
this.breakpointObserverSubscription = this.breakpointObserver
.observe(MediaBreakpoints['gt-sm']).subscribe(
() => {
this.updateMobileOpts();
@ -209,6 +211,18 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.updateWidgets();
}
ngOnDestroy(): void {
super.ngOnDestroy();
if (this.gridsterResizeListener) {
// @ts-ignore
removeResizeListener(this.gridster.el, this.gridsterResizeListener);
}
if (this.breakpointObserverSubscription) {
this.breakpointObserverSubscription.unsubscribe();
}
this.gridster = null;
}
ngDoCheck() {
this.dashboardWidgets.doCheck();
}
@ -223,7 +237,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['isMobile', 'isMobileDisabled', 'autofillHeight', 'mobileAutofillHeight', 'mobileRowHeight'].includes(propName)) {
updateMobileOpts = true;
} else if (['horizontalMargin', 'verticalMargin'].includes(propName)) {
} else if (['margin', 'columns'].includes(propName)) {
updateLayoutOpts = true;
} else if (propName === 'isEdit') {
updateEditingOpts = true;
@ -256,7 +270,14 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
this.dashboardLoading = false;
}
private updateWidgetLayouts() {
this.dashboardWidgets.widgetLayoutsUpdated();
}
ngAfterViewInit(): void {
this.gridsterResizeListener = this.onGridsterParentResize.bind(this);
// @ts-ignore
addResizeListener(this.gridster.el, this.gridsterResizeListener);
}
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void {
@ -305,7 +326,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.widgetIndex);
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget);
if (items && items.length) {
$event.preventDefault();
$event.stopPropagation();
@ -324,13 +345,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetMouseDown($event: Event, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.onWidgetMouseDown) {
this.callbacks.onWidgetMouseDown($event, widget.widget, widget.widgetIndex);
this.callbacks.onWidgetMouseDown($event, widget.widget);
}
}
widgetClicked($event: Event, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.onWidgetClicked) {
this.callbacks.onWidgetClicked($event, widget.widget, widget.widgetIndex);
this.callbacks.onWidgetClicked($event, widget.widget);
}
}
@ -339,7 +360,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation();
}
if (this.isEditActionEnabled && this.callbacks && this.callbacks.onEditWidget) {
this.callbacks.onEditWidget($event, widget.widget, widget.widgetIndex);
this.callbacks.onEditWidget($event, widget.widget);
}
}
@ -348,7 +369,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation();
}
if (this.isExportActionEnabled && this.callbacks && this.callbacks.onExportWidget) {
this.callbacks.onExportWidget($event, widget.widget, widget.widgetIndex);
this.callbacks.onExportWidget($event, widget.widget);
}
}
@ -357,19 +378,19 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
$event.stopPropagation();
}
if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) {
this.callbacks.onRemoveWidget($event, widget.widget, widget.widgetIndex);
this.callbacks.onRemoveWidget($event, widget.widget);
}
}
highlightWidget(index: number, delay?: number) {
const highlighted = this.dashboardWidgets.highlightWidget(index);
highlightWidget(widgetId: string, delay?: number) {
const highlighted = this.dashboardWidgets.highlightWidget(widgetId);
if (highlighted) {
this.scrollToWidget(highlighted, delay);
}
}
selectWidget(index: number, delay?: number) {
const selected = this.dashboardWidgets.selectWidget(index);
selectWidget(widgetId: string, delay?: number) {
const selected = this.dashboardWidgets.selectWidget(widgetId);
if (selected) {
this.scrollToWidget(selected, delay);
}
@ -385,15 +406,16 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
row: 0,
column: 0
};
const parentElement = this.gridster.el as HTMLElement;
const parentElement = $(this.gridster.el);
let pageX = 0;
let pageY = 0;
if (event instanceof MouseEvent) {
pageX = event.pageX;
pageY = event.pageY;
}
const x = pageX - parentElement.offsetLeft + parentElement.scrollLeft;
const y = pageY - parentElement.offsetTop + parentElement.scrollTop;
const offset = parentElement.offset();
const x = pageX - offset.left + parentElement.scrollLeft();
const y = pageY - offset.top + parentElement.scrollTop();
pos.row = this.gridster.pixelsToPositionY(y, Math.floor);
pos.column = this.gridster.pixelsToPositionX(x, Math.floor);
return pos;
@ -434,26 +456,33 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
});
}
private updateMobileOpts() {
private updateMobileOpts(parentHeight?: number) {
this.isMobileSize = this.checkIsMobileSize();
const autofillHeight = this.isAutofillHeight();
if (autofillHeight) {
this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'fit';
} else {
this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical';
}
const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
const rowSize = this.detectRowSize(this.isMobileSize);
const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight);
if (this.gridsterOpts.fixedRowHeight !== rowSize) {
this.gridsterOpts.fixedRowHeight = rowSize;
}
if (this.isAutofillHeight()) {
this.gridsterOpts.gridType = 'fit';
} else {
this.gridsterOpts.gridType = this.isMobileSize ? 'fixed' : 'scrollVertical';
}
private onGridsterParentResize() {
const parentHeight = this.gridster.el.offsetHeight;
if (this.isMobileSize && this.mobileAutofillHeight && parentHeight) {
this.updateMobileOpts(parentHeight);
this.notifyGridsterOptionsChanged();
}
}
private updateLayoutOpts() {
this.gridsterOpts.outerMarginLeft = this.horizontalMargin ? this.horizontalMargin : 10;
this.gridsterOpts.outerMarginRight = this.horizontalMargin ? this.horizontalMargin : 10;
this.gridsterOpts.outerMarginTop = this.verticalMargin ? this.verticalMargin : 10;
this.gridsterOpts.outerMarginBottom = this.horizontalMargin ? this.horizontalMargin : 10;
this.gridsterOpts.minCols = this.columns ? this.columns : 24;
this.gridsterOpts.margin = this.margin ? this.margin : 10;
}
private updateEditingOpts() {
@ -462,17 +491,42 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
}
public notifyGridsterOptionsChanged() {
if (this.gridster && this.gridster.options) {
this.gridster.optionsChanged();
if (!this.optionsChangeNotificationsPaused) {
if (this.gridster && this.gridster.options) {
this.gridster.optionsChanged();
}
}
}
private detectRowSize(isMobile: boolean): number | null {
public pauseChangeNotifications() {
this.optionsChangeNotificationsPaused = true;
}
public resumeChangeNotifications() {
this.optionsChangeNotificationsPaused = false;
}
public notifyLayoutUpdated() {
this.updateWidgetLayouts();
}
private detectRowSize(isMobile: boolean, autofillHeight: boolean, parentHeight?: number): number | null {
let rowHeight = null;
if (!this.isAutofillHeight()) {
if (!autofillHeight) {
if (isMobile) {
rowHeight = isDefined(this.mobileRowHeight) ? this.mobileRowHeight : 70;
}
} else if (autofillHeight && isMobile) {
if (!parentHeight) {
parentHeight = this.gridster.el.offsetHeight;
}
if (parentHeight) {
let totalRows = 0;
for (const widget of this.dashboardWidgets.dashboardWidgets) {
totalRows += widget.rows;
}
rowHeight = (parentHeight - this.gridsterOpts.margin * (this.dashboardWidgets.dashboardWidgets.length + 2)) / totalRows;
}
}
return rowHeight;
}

2
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -17,7 +17,7 @@
-->
<header>
<mat-toolbar color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div fxFlex fxLayout="row" fxLayoutAlign="start center" style="height: 100%;">
<div class="mat-toolbar-tools" fxFlex fxLayout="column" fxLayoutAlign="start start">
<span class="tb-details-title">{{ headerTitle }}</span>
<span class="tb-details-subtitle">{{ headerSubtitle }}</span>

8
ui-ngx/src/app/modules/home/components/details-panel.component.scss

@ -20,6 +20,9 @@
height: 100%;
display: flex;
flex-direction: column;
}
:host ::ng-deep {
.mat-toolbar-tools {
height: 100%;
min-height: 100px;
@ -50,4 +53,9 @@
opacity: .8;
}
tb-dashboard {
.tb-dashboard-content {
background-color: $primary-hue-3 !important;
}
}
}

6
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -22,12 +22,14 @@
<span class="tb-entity-table-title" translate>widget-config.actions</span>
<span fxFlex></span>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
type="button"
(click)="addAction($event)"
matTooltip="{{ 'widget-config.add-action' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
type="button"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
@ -37,6 +39,7 @@
<mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-button mat-icon-button
type="button"
matTooltip="{{ 'widget-config.search-actions' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
@ -48,6 +51,7 @@
placeholder="{{ 'widget-config.search-actions' | translate }}"/>
</mat-form-field>
<button mat-button mat-icon-button (click)="exitFilterMode()"
type="button"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
@ -87,12 +91,14 @@
<mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }">
<div fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
type="button"
matTooltip="{{ 'widget-config.edit-action' | translate }}"
matTooltipPosition="above"
(click)="editAction($event, action)">
<mat-icon>edit</mat-icon>
</button>
<button mat-button mat-icon-button [disabled]="isLoading$ | async"
type="button"
matTooltip="{{ 'widget-config.delete-action' | translate }}"
matTooltipPosition="above"
(click)="deleteAction($event, action)">

6
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -29,7 +29,7 @@
<div class="tb-color-preview" (click)="showColorPicker(key)" style="margin-right: 5px;">
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div>
</div>
<div fxLayout="row">
<div style="flex: 1; min-width: 0px;" fxLayout="row">
<div class="tb-chip-label">
<span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm">
<span *ngIf="key.type === dataKeyTypes.attribute"
@ -54,7 +54,9 @@
</ng-template>
</div>
</div>
<button *ngIf="!disabled" (click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32">
<button *ngIf="!disabled"
type="button"
(click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32">
<mat-icon class="tb-mat-20">edit</mat-icon>
</button>
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>

1
ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss

@ -17,6 +17,7 @@
:host {
.mat-chip.mat-standard-chip {
.tb-attribute-chip {
max-width: 100%;
color: rgb(66, 66, 66);
font-weight: normal;
font-size: 16px;

1
ui-ngx/src/app/modules/home/components/widget/legend-config.component.html

@ -16,6 +16,7 @@
-->
<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled"
type="button"
mat-button mat-raised-button color="primary" (click)="openEditMode($event)">
<mat-icon class="material-icons">toc</mat-icon>
<span translate>legend.settings</span>

2
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -140,6 +140,7 @@
</tb-data-keys>
</section>
<button [disabled]="isLoading$ | async"
type="button"
mat-button mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
@ -153,6 +154,7 @@
</ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<button [disabled]="isLoading$ | async"
type="button"
mat-button mat-raised-button color="primary"
[fxShow]="modelValue?.typeParameters &&
(modelValue?.typeParameters.maxDatasources == -1 || dataSettings.get('datasources').controls.length < modelValue?.typeParameters.maxDatasources)"

2
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -996,11 +996,11 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
private elementClick($event: Event) {
$event.stopPropagation();
const e = ($event.target || $event.srcElement) as Element;
if (e.id) {
const descriptors = this.getActionDescriptors('elementClick');
if (descriptors.length) {
$event.stopPropagation();
descriptors.forEach((descriptor) => {
if (descriptor.name === e.id) {
const entityInfo = this.getActiveEntityInfo();

93
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -15,12 +15,12 @@
///
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { Widget, widgetType } from '@app/shared/models/widget.models';
import { Widget, widgetType, WidgetPosition } from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs';
import { isDefined, isUndefined } from '@app/core/utils';
import { guid, isDefined, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import * as deepEqual from 'deep-equal';
@ -46,18 +46,13 @@ export interface WidgetContextMenuItem extends ContextMenuItem {
}
export interface DashboardCallbacks {
onEditWidget?: ($event: Event, widget: Widget, index: number) => void;
onExportWidget?: ($event: Event, widget: Widget, index: number) => void;
onRemoveWidget?: ($event: Event, widget: Widget, index: number) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget, index: number) => void;
onWidgetClicked?: ($event: Event, widget: Widget, index: number) => void;
onEditWidget?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget, index: number) => Array<WidgetContextMenuItem>;
}
export interface WidgetPosition {
row: number;
column: number;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array<WidgetContextMenuItem>;
}
export interface IDashboardComponent {
@ -74,11 +69,14 @@ export interface IDashboardComponent {
onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void;
onResetTimewindow(): void;
resetHighlight(): void;
highlightWidget(index: number, delay?: number);
selectWidget(index: number, delay?: number);
highlightWidget(widgetId: string, delay?: number);
selectWidget(widgetId: string, delay?: number);
getSelectedWidget(): Widget;
getEventGridPosition(event: Event): WidgetPosition;
notifyGridsterOptionsChanged();
pauseChangeNotifications();
resumeChangeNotifications();
notifyLayoutUpdated();
}
declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update';
@ -86,7 +84,7 @@ declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update';
interface DashboardWidgetUpdateRecord {
widget?: Widget;
widgetLayout?: WidgetLayout;
widgetIndex: number;
widgetId: string;
operation: DashboardWidgetUpdateOperation;
}
@ -95,7 +93,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
highlightedMode = false;
dashboardWidgets: Array<DashboardWidget> = [];
widgets: Array<Widget>;
widgets: Iterable<Widget>;
widgetLayouts: WidgetLayouts;
[Symbol.iterator](): Iterator<DashboardWidget> {
@ -103,41 +101,30 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
}
constructor(private dashboard: IDashboardComponent,
private widgetsDiffer: IterableDiffer<Widget>,
private widgetLayoutsDiffer: KeyValueDiffer<string, WidgetLayout>) {
private widgetsDiffer: IterableDiffer<Widget>) {
}
doCheck() {
const widgetChange = this.widgetsDiffer.diff(this.widgets);
if (widgetChange !== null) {
const layouts: WidgetLayouts = {};
const updateRecords: Array<DashboardWidgetUpdateRecord> = [];
const widgetLayoutChange = this.widgetLayoutsDiffer.diff(this.widgetLayouts);
if (widgetLayoutChange !== null) {
widgetLayoutChange.forEachAddedItem((added) => {
layouts[added.key] = added.currentValue;
});
widgetLayoutChange.forEachChangedItem((changed) => {
layouts[changed.key] = changed.currentValue;
});
}
widgetChange.forEachAddedItem((added) => {
updateRecords.push({
widget: added.item,
widgetLayout: layouts[added.item.id],
widgetIndex: added.currentIndex,
widgetId: added.item.id,
widgetLayout: this.widgetLayouts[added.item.id],
operation: 'add'
});
});
widgetChange.forEachRemovedItem((removed) => {
let operation = updateRecords.find((record) => record.widgetIndex === removed.previousIndex);
let operation = updateRecords.find((record) => record.widgetId === removed.item.id);
if (operation) {
operation.operation = 'update';
} else {
operation = {
widgetIndex: removed.previousIndex,
widgetId: removed.item.id,
operation: 'remove'
};
updateRecords.push(operation);
@ -147,21 +134,21 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
switch (record.operation) {
case 'add':
this.dashboardWidgets.push(
new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout)
new DashboardWidget(this.dashboard, record.widget, record.widgetLayout)
);
break;
case 'remove':
let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex);
let index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId);
if (index > -1) {
this.dashboardWidgets.splice(index, 1);
}
break;
case 'update':
index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetIndex === record.widgetIndex);
index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === record.widgetId);
if (index > -1) {
const prevDashboardWidget = this.dashboardWidgets[index];
if (!deepEqual(prevDashboardWidget.widget, record.widget)) {
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetIndex, record.widgetLayout);
this.dashboardWidgets[index] = new DashboardWidget(this.dashboard, record.widget, record.widgetLayout);
this.dashboardWidgets[index].highlighted = prevDashboardWidget.highlighted;
this.dashboardWidgets[index].selected = prevDashboardWidget.selected;
} else {
@ -178,14 +165,25 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
}
}
setWidgets(widgets: Array<Widget>, widgetLayouts: WidgetLayouts) {
widgetLayoutsUpdated() {
for (const w of Object.keys(this.widgetLayouts)) {
const widgetLayout = this.widgetLayouts[w];
const index = this.dashboardWidgets.findIndex((dashboardWidget) => dashboardWidget.widgetId === w);
if (index > -1) {
this.dashboardWidgets[index].widgetLayout = widgetLayout;
}
}
this.updateRowsAndSort();
}
setWidgets(widgets: Iterable<Widget>, widgetLayouts: WidgetLayouts) {
this.highlightedMode = false;
this.widgets = widgets;
this.widgetLayouts = widgetLayouts;
}
highlightWidget(index: number): DashboardWidget {
const widget = this.findWidgetAtIndex(index);
highlightWidget(widgetId: string): DashboardWidget {
const widget = this.findWidgetById(widgetId);
if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) {
this.highlightedMode = true;
widget.highlighted = true;
@ -200,8 +198,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
}
}
selectWidget(index: number): DashboardWidget {
const widget = this.findWidgetAtIndex(index);
selectWidget(widgetId: string): DashboardWidget {
const widget = this.findWidgetById(widgetId);
if (widget && (!widget.selected)) {
widget.selected = true;
this.dashboardWidgets.forEach((dashboardWidget) => {
@ -237,8 +235,8 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.selected);
}
private findWidgetAtIndex(index: number): DashboardWidget {
return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetIndex === index);
private findWidgetById(widgetId: string): DashboardWidget {
return this.dashboardWidgets.find((dashboardWidget) => dashboardWidget.widgetId === widgetId);
}
private updateRowsAndSort() {
@ -306,6 +304,8 @@ export class DashboardWidget implements GridsterItem {
widgetContext: WidgetContext = {};
widgetId: string;
private gridsterItemComponentSubject = new Subject<GridsterItemComponentInterface>();
private gridsterItemComponentValue: GridsterItemComponentInterface;
@ -318,8 +318,11 @@ export class DashboardWidget implements GridsterItem {
constructor(
private dashboard: IDashboardComponent,
public widget: Widget,
public widgetIndex: number,
public widgetLayout?: WidgetLayout) {
if (!widget.id) {
widget.id = guid();
}
this.widgetId = widget.id;
this.updateWidgetParams();
}

57
ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html

@ -0,0 +1,57 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #widgetForm="ngForm" [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 900px;">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>widget.add</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;">
<tb-widget-config
[aliasController]="aliasController"
[functionsOnly]="false"
[entityAliases]="dashboard.configuration.entityAliases"
[dashboardStates]="dashboard.configuration.states"
formControlName="widgetConfig">
</tb-widget-config>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

137
ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts

@ -0,0 +1,137 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, widgetTypesData } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { EntityService } from '@core/http/entity.service';
import { Dashboard } from '@app/shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';
import { isDefined, isString } from '@core/utils';
export interface AddWidgetDialogData {
dashboard: Dashboard;
aliasController: IAliasController;
widget: Widget;
widgetInfo: WidgetInfo;
}
@Component({
selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}],
styleUrls: []
})
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {
widgetFormGroup: FormGroup;
dashboard: Dashboard;
aliasController: IAliasController;
widget: Widget;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>,
private fb: FormBuilder,
private utils: UtilsService,
private translate: TranslateService,
private entityService: EntityService) {
super(store, router, dialogRef);
this.dashboard = this.data.dashboard;
this.aliasController = this.data.aliasController;
this.widget = this.data.widget;
const widgetInfo = this.data.widgetInfo;
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const typeParameters = widgetInfo.typeParameters;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};
} else {
settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema;
}
let dataKeySettingsSchema;
if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') {
dataKeySettingsSchema = {};
} else {
dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
}
const widgetConfig: WidgetConfigComponentData = {
config: this.widget.config,
layout: {},
widgetType: this.widget.type,
typeParameters,
actionSources,
isDataEnabled,
settingsSchema,
dataKeySettingsSchema
};
this.widgetFormGroup = this.fb.group({
widgetConfig: [widgetConfig, []]
}
);
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
helpLinkIdForWidgetType(): string {
let link = 'widgetsConfig';
if (this.widget && this.widget.type) {
link = widgetTypesData.get(this.widget.type).configHelpLinkId;
}
return link;
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
this.submitted = true;
const widgetConfig: WidgetConfigComponentData = this.widgetFormGroup.get('widgetConfig').value;
this.widget.config = widgetConfig.config;
this.widget.config.mobileOrder = widgetConfig.layout.mobileOrder;
this.widget.config.mobileHeight = widgetConfig.layout.mobileHeight;
this.dialogRef.close(this.widget);
}
}

71
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html

@ -132,26 +132,13 @@
</section>
<div class="tb-absolute-fill tb-dashboard-layouts" fxLayout="{{forceDashboardMobileMode ? 'column' : 'row'}}"
[ngClass]="{ 'tb-padded' : !widgetEditMode && (isEdit || displayTitle()), 'tb-shrinked' : isEditingWidget }">
<div [fxShow]="layouts.main.show"
id="tb-main-layout"
[ngStyle]="{width: mainLayoutWidth(),
height: mainLayoutHeight()}">
<tb-dashboard-layout
[layoutCtx]="layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</div>
<mat-drawer-container *ngIf="layouts.right.show"
id="tb-right-layout">
<mat-drawer
[ngStyle]="{minWidth: rightLayoutWidth(),
maxWidth: rightLayoutWidth(),
height: rightLayoutHeight(),
zIndex: 25}"
<mat-drawer-container class="tb-absolute-fill">
<mat-drawer *ngIf="layouts.right.show"
id="tb-right-layout"
[ngStyle]="{minWidth: rightLayoutWidth(),
maxWidth: rightLayoutWidth(),
height: rightLayoutHeight(),
borderLeft: 'none'}"
disableClose="true"
position="end"
[mode]="isMobile ? 'over' : 'side'"
@ -165,6 +152,19 @@
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show"
id="tb-main-layout"
[ngStyle]="{width: mainLayoutWidth(),
height: mainLayoutHeight()}">
<tb-dashboard-layout
[layoutCtx]="layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode">
</tb-dashboard-layout>
</mat-drawer-content>
</mat-drawer-container>
</div>
<mat-drawer-container hasBackdrop="false" class="tb-widget-details-sidenav">
@ -194,6 +194,37 @@
</tb-details-panel>
</mat-drawer>
</mat-drawer-container>
<mat-drawer-container *ngIf="!widgetEditMode" hasBackdrop="false" class="tb-select-widget-sidenav">
<mat-drawer class="tb-details-drawer"
[opened]="isAddingWidget"
mode="over"
position="end">
<tb-details-panel *ngIf="isAddingWidget" fxFlex
headerTitle="{{'dashboard.select-widget-title' | translate}}"
headerHeightPx="120"
[isReadOnly]="true"
[isEdit]="false"
(closeDetails)="onAddWidgetClosed()">
<div class="header-pane" *ngIf="isAddingWidget">
<div fxLayout="row">
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
<tb-widgets-bundle-select fxFlexOffset="5"
fxFlex
required
[selectFirstBundle]="false"
[ngModel]="widgetsBundle"
(ngModelChange)="widgetsBundle = $event">
</tb-widgets-bundle-select>
</div>
</div>
<tb-dashboard-widget-select *ngIf="isAddingWidget"
[aliasController]="dashboardCtx.aliasController"
[widgetsBundle]="widgetsBundle"
(widgetSelected)="addWidgetFromType($event)">
</tb-dashboard-widget-select>
</tb-details-panel>
</mat-drawer>
</mat-drawer-container>
<!--tb-details-sidenav TODO -->
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<tb-footer-fab-buttons [fxShow]="!isAddingWidget && isEdit && !widgetEditMode"

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.scss

@ -136,6 +136,10 @@ div.tb-dashboard-page {
}
}
mat-drawer-container.tb-select-widget-sidenav {
position: initial;
}
section.tb-powered-by-footer {
position: absolute;
right: 25px;

274
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.ts

@ -14,16 +14,7 @@
/// limitations under the License.
///
import {
Component,
Inject,
OnDestroy,
OnInit,
ViewEncapsulation,
ViewChild,
NgZone,
ChangeDetectorRef, ChangeDetectionStrategy, ApplicationRef
} from '@angular/core';
import { ChangeDetectorRef, Component, Inject, NgZone, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -33,47 +24,42 @@ import { AuthService } from '@core/auth/auth.service';
import {
Dashboard,
DashboardConfiguration,
WidgetLayout,
DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo
DashboardLayoutsInfo,
DashboardStateLayouts, GridSettings,
WidgetLayout
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
import { WindowMessage } from '@shared/models/window-message.model';
import { deepClone, isDefined } from '@app/core/utils';
import {
DashboardContext, DashboardPageLayout,
DashboardContext,
DashboardPageLayout,
DashboardPageLayoutContext,
DashboardPageLayouts,
DashboardPageScope, IDashboardController
DashboardPageScope,
IDashboardController,
LayoutWidgetsArray
} from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Widget, widgetTypesData } from '@app/shared/models/widget.models';
import { Widget, WidgetConfig, WidgetPosition, widgetTypesData } from '@app/shared/models/widget.models';
import { environment as env } from '@env/environment';
import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller';
import { Observable, Subscription, of } from 'rxjs';
import { Observable, of, Subscription } from 'rxjs';
import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component';
import { IStateController } from '@core/api/widget-api.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service';
import {
WidgetContextMenuItem,
DashboardContextMenuItem,
IDashboardComponent, WidgetPosition
} from '../../models/dashboard-component.models';
import { DashboardContextMenuItem, WidgetContextMenuItem } from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { FormBuilder, FormGroup, NgForm } from '@angular/forms';
import { FormBuilder } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service';
import {
DeviceCredentialsDialogComponent,
DeviceCredentialsDialogData
} from '@home/pages/device/device-credentials-dialog.component';
import { DeviceCredentials } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import {
EntityAliasesDialogComponent,
@ -81,6 +67,18 @@ import {
} from '@home/components/alias/entity-aliases-dialog.component';
import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import {
ManageDashboardLayoutsDialogComponent,
ManageDashboardLayoutsDialogData
} from '@home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component';
import { SelectTargetLayoutDialogComponent } from '@home/pages/dashboard/layout/select-target-layout-dialog.component';
import {
DashboardSettingsDialogComponent,
DashboardSettingsDialogData
} from '@home/pages/dashboard/dashboard-settings-dialog.component';
@Component({
selector: 'tb-dashboard-page',
@ -109,6 +107,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
isMobile = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
forceDashboardMobileMode = false;
isAddingWidget = false;
widgetsBundle: WidgetsBundle = null;
isToolbarOpened = false;
isToolbarOpenedAnimate = false;
@ -127,12 +126,14 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
currentCustomerId: string;
currentDashboardScope: DashboardPageScope;
addingLayoutCtx: DashboardPageLayoutContext;
layouts: DashboardPageLayouts = {
main: {
show: false,
layoutCtx: {
id: 'main',
widgets: [],
widgets: null,
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false,
@ -144,7 +145,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
show: false,
layoutCtx: {
id: 'right',
widgets: [],
widgets: null,
widgetLayouts: {},
gridSettings: {},
ignoreLoading: false,
@ -216,6 +217,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private itembuffer: ItemBufferService,
private fb: FormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
private ngZone: NgZone,
private cd: ChangeDetectorRef) {
super(store);
@ -251,6 +253,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboard = data.dashboard;
this.dashboardConfiguration = this.dashboard.configuration;
this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard);
this.layouts.right.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboard);
this.widgetEditMode = data.widgetEditMode;
this.singlePageMode = data.singlePageMode;
@ -282,6 +286,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.isEditingWidget = false;
this.forceDashboardMobileMode = false;
this.isAddingWidget = false;
this.widgetsBundle = null;
this.isToolbarOpened = false;
this.isToolbarOpenedAnimate = false;
@ -475,8 +480,30 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
let gridSettings: GridSettings = null;
const layoutKeys = this.dashboardUtils.isSingleLayoutDashboard(this.dashboard);
if (layoutKeys) {
gridSettings = deepClone(this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout].gridSettings);
}
this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
settings: deepClone(this.dashboard.configuration.settings),
gridSettings
}
}).afterClosed().subscribe((data) => {
if (data) {
this.dashboard.configuration.settings = data.settings;
const newGridSettings = data.gridSettings;
if (newGridSettings) {
const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout];
this.dashboardUtils.updateLayoutSettings(layout, newGridSettings);
this.updateLayouts();
}
}
});
}
public manageDashboardStates($event: Event) {
@ -491,8 +518,23 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.dialog.open<ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogData,
DashboardStateLayouts>(ManageDashboardLayoutsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
layouts: deepClone(this.dashboard.configuration.states[this.dashboardCtx.state].layouts)
}
}).afterClosed().subscribe((layouts) => {
if (layouts) {
this.updateDashboardLayouts(layouts);
}
});
}
private updateDashboardLayouts(newLayouts: DashboardStateLayouts) {
this.dashboardUtils.setLayouts(this.dashboard, this.dashboardCtx.state, newLayouts);
this.updateLayouts();
}
private importWidget($event: Event) {
@ -526,7 +568,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.notifyDashboardUpdated();
}
public openDashboardState(state: string, openRightLayout: boolean) {
public openDashboardState(state: string, openRightLayout?: boolean) {
const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state);
if (layoutsData) {
this.dashboardCtx.state = state;
@ -546,21 +588,21 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
this.isRightLayoutOpened = openRightLayout ? true : false;
this.updateLayouts(layoutsData, layoutVisibilityChanged);
this.updateLayouts(layoutsData);
}
}
private updateLayouts(layoutsData: DashboardLayoutsInfo, layoutVisibilityChanged: boolean) {
private updateLayouts(layoutsData?: DashboardLayoutsInfo) {
if (!layoutsData) {
layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, this.dashboardCtx.state);
}
for (const l of Object.keys(this.layouts)) {
const layout: DashboardPageLayout = this.layouts[l];
if (layoutsData[l]) {
const layoutInfo: DashboardLayoutInfo = layoutsData[l];
if (layout.layoutCtx.id === 'main') {
layout.layoutCtx.ctrl.setResizing(layoutVisibilityChanged);
}
this.updateLayout(layout, layoutInfo);
} else {
this.updateLayout(layout, {widgets: [], widgetLayouts: {}, gridSettings: null});
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null});
}
}
}
@ -569,7 +611,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (layoutInfo.gridSettings) {
layout.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
layout.layoutCtx.widgets = layoutInfo.widgets;
layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (layout.show && layout.layoutCtx.ctrl) {
layout.layoutCtx.ctrl.reload();
@ -594,6 +636,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.entityAliasesUpdated();
this.updateLayouts();
} else {
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
}
@ -617,7 +660,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private notifyDashboardUpdated() {
if (this.widgetEditMode) {
const widget = this.layouts.main.layoutCtx.widgets[0];
const widget = this.layouts.main.layoutCtx.widgets.widgetByIndex(0);
const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id];
widget.sizeX = layout.sizeX;
widget.sizeY = layout.sizeY;
@ -643,8 +686,86 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if ($event) {
$event.stopPropagation();
}
// TODO:
this.dialogService.todo();
this.isAddingWidget = true;
this.addingLayoutCtx = layoutCtx;
}
onAddWidgetClosed() {
this.isAddingWidget = false;
}
private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) {
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget);
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id);
}
private selectTargetLayout(): Observable<DashboardLayoutId> {
const layouts = this.dashboardConfiguration.states[this.dashboardCtx.state].layouts;
const layoutIds = Object.keys(layouts);
if (layoutIds.length > 1) {
return this.dialog.open<SelectTargetLayoutDialogComponent, any,
DashboardLayoutId>(SelectTargetLayoutDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed();
} else {
return of(layoutIds[0] as DashboardLayoutId);
}
}
private addWidgetToDashboard(widget: Widget) {
if (this.addingLayoutCtx) {
this.addWidgetToLayout(widget, this.addingLayoutCtx.id);
this.addingLayoutCtx = null;
} else {
this.selectTargetLayout().subscribe((layoutId) => {
if (layoutId) {
this.addWidgetToLayout(widget, layoutId);
}
});
}
}
addWidgetFromType(widget: Widget) {
this.onAddWidgetClosed();
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig);
config.title = 'New ' + widgetTypeInfo.widgetName;
config.datasources = [];
const newWidget: Widget = {
isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: 'New widget',
sizeX: widgetTypeInfo.sizeX,
sizeY: widgetTypeInfo.sizeY,
config,
row: 0,
col: 0
};
if (widgetTypeInfo.typeParameters.useCustomDatasources) {
this.addWidgetToDashboard(newWidget);
} else {
this.dialog.open<AddWidgetDialogComponent, AddWidgetDialogData,
Widget>(AddWidgetDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController,
widget: newWidget,
widgetInfo: widgetTypeInfo
}
}).afterClosed().subscribe((addedWidget) => {
if (addedWidget) {
this.addWidgetToDashboard(addedWidget);
}
});
}
}
);
}
onRevertWidgetEdit() {
@ -660,14 +781,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
const widget = deepClone(this.editingWidget);
const widgetLayout = deepClone(this.editingWidgetLayout);
const id = this.editingWidgetOriginal.id;
const index = this.editingLayoutCtx.widgets.indexOf(this.editingWidgetOriginal);
this.dashboardConfiguration.widgets[id] = widget;
this.editingWidgetOriginal = widget;
this.editingWidgetLayoutOriginal = widgetLayout;
this.editingLayoutCtx.widgets[index] = widget;
this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout;
setTimeout(() => {
this.editingLayoutCtx.ctrl.highlightWidget(index, 0);
this.editingLayoutCtx.ctrl.highlightWidget(widget.id, 0);
}, 0);
}
@ -683,7 +802,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.forceDashboardMobileMode = false;
}
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
$event.stopPropagation();
if (this.editingWidgetOriginal === widget) {
this.onEditWidgetClosed();
@ -701,52 +820,73 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
const delayOffset = transition ? 350 : 0;
const delay = transition ? 400 : 300;
setTimeout(() => {
layoutCtx.ctrl.highlightWidget(index, delay);
layoutCtx.ctrl.highlightWidget(widget.id, delay);
}, delayOffset);
}
}
}
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
// TODO:
this.dialogService.todo();
this.itembuffer.copyWidget(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget);
}
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
// TODO:
this.dialogService.todo();
this.itembuffer.copyWidgetReference(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget);
}
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
// TODO:
this.dialogService.todo();
this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id,
pos, this.entityAliasesUpdated.bind(this)).subscribe(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
});
}
pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
// TODO:
this.dialogService.todo();
this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id,
pos).subscribe(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
});
}
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
// TODO:
this.dialogService.todo();
let title = widget.config.title;
if (!title || title.length === 0) {
title = this.widgetComponentService.getInstantWidgetInfo(widget).widgetName;
}
const confirmTitle = this.translate.instant('widget.remove-widget-title', {widgetTitle: title});
const confirmContent = this.translate.instant('widget.remove-widget-text');
this.dialogService.confirm(confirmTitle,
confirmContent,
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
).subscribe((res) => {
if (res) {
if (layoutCtx.widgets.removeWidgetId(widget.id)) {
this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget.id);
}
}
});
}
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
$event.stopPropagation();
// TODO:
this.dialogService.todo();
}
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
if (this.isEditingWidget) {
this.editWidget($event, layoutCtx, widget, index);
this.editWidget($event, layoutCtx, widget);
}
}
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number) {
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
if (this.isEdit && !this.isEditingWidget) {
layoutCtx.ctrl.selectWidget(index, 0);
layoutCtx.ctrl.selectWidget(widget.id, 0);
}
}
@ -795,13 +935,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return dashboardContextActions;
}
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem> {
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem> {
const widgetContextActions: Array<WidgetContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget) {
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.editWidget(event, layoutCtx, currentWidget, index);
this.editWidget(event, layoutCtx, currentWidget);
},
enabled: true,
value: 'action.edit',

83
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.models.ts

@ -15,14 +15,13 @@
///
import { DashboardLayoutId, GridSettings, WidgetLayout, Dashboard, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { Widget } from '@app/shared/models/widget.models';
import { Widget, WidgetPosition } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models';
import {
DashboardContextMenuItem,
WidgetContextMenuItem,
WidgetPosition
WidgetContextMenuItem
} from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs';
import { ChangeDetectorRef } from '@angular/core';
@ -43,13 +42,13 @@ export interface IDashboardController {
openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean);
addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>;
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, index: number): Array<WidgetContextMenuItem>;
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem>;
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
@ -58,7 +57,7 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext {
id: DashboardLayoutId;
widgets: Array<Widget>;
widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts;
gridSettings: GridSettings;
ctrl: ILayoutController;
@ -73,3 +72,69 @@ export interface DashboardPageLayout {
export declare type DashboardPageLayouts = {[key in DashboardLayoutId]: DashboardPageLayout};
export class LayoutWidgetsArray implements Iterable<Widget> {
private widgetIds: string[] = [];
private pointer = 0;
constructor(private dashboard: Dashboard) {
}
size() {
return this.widgetIds.length;
}
setWidgetIds(widgetIds: string[]) {
this.widgetIds = widgetIds;
}
addWidgetId(widgetId: string) {
this.widgetIds.push(widgetId);
}
removeWidgetId(widgetId: string): boolean {
const index = this.widgetIds.indexOf(widgetId);
if (index > -1) {
this.widgetIds.splice(index, 1);
return true;
}
return false;
}
[Symbol.iterator](): Iterator<Widget> {
let pointer = 0;
const widgetIds = this.widgetIds;
const dashboard = this.dashboard;
return {
next(value?: any): IteratorResult<Widget> {
if (pointer < widgetIds.length) {
const widgetId = widgetIds[pointer++];
const widget = dashboard.configuration.widgets[widgetId];
return {
done: false,
value: widget
};
} else {
return {
done: true,
value: null
};
}
}
};
}
public widgetByIndex(index: number): Widget {
const widgetId = this.widgetIds[index];
if (widgetId) {
return this.widgetById(widgetId);
} else {
return null;
}
}
private widgetById(widgetId: string): Widget {
return this.dashboard.configuration.widgets[widgetId];
}
}

167
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.html

@ -0,0 +1,167 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #settingsForm="ngForm" (ngSubmit)="save()">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>{{settings ? 'dashboard.settings' : 'layout.settings'}}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div *ngIf="settings" [formGroup]="settingsFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>dashboard.state-controller</mat-label>
<mat-select required matInput formControlName="stateControllerId">
<mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId">
{{stateControllerId}}
</mat-option>
</mat-select>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-checkbox fxFlex formControlName="toolbarAlwaysOpen">
{{ 'dashboard.toolbar-always-open' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="showTitle">
{{ 'dashboard.display-title' | translate }}
</mat-checkbox>
<tb-color-input fxFlex
label="{{'dashboard.title-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="titleColor">
</tb-color-input>
</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-checkbox fxFlex formControlName="showDashboardsSelect">
{{ 'dashboard.display-dashboards-selection' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="showEntitiesSelect">
{{ 'dashboard.display-entities-selection' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="showDashboardTimewindow">
{{ 'dashboard.display-dashboard-timewindow' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="showDashboardExport">
{{ 'dashboard.display-dashboard-export' | translate }}
</mat-checkbox>
</div>
</div>
<div *ngIf="gridSettings" [formGroup]="gridSettingsFormGroup">
<tb-color-input fxFlex
label="{{'layout.color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="color">
</tb-color-input>
<mat-form-field class="mat-block">
<mat-label translate>dashboard.columns-count</mat-label>
<input matInput formControlName="columns" type="number" step="any" min="10"
max="1000" required>
<mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('required')">
{{ 'dashboard.columns-count-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('min')">
{{ 'dashboard.min-columns-count-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('columns').hasError('max')">
{{ 'dashboard.max-columns-count-message' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>dashboard.widgets-margins</mat-label>
<input matInput formControlName="margin" type="number" step="any" min="0"
max="50" required>
<mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('required')">
{{ 'dashboard.margin-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('min')">
{{ 'dashboard.min-margin-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('margin').hasError('max')">
{{ 'dashboard.max-margin-message' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox fxFlex formControlName="autoFillHeight" style="display: block; padding-bottom: 12px;">
{{ 'dashboard.autofill-height' | translate }}
</mat-checkbox>
<tb-color-input fxFlex
label="{{'dashboard.background-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="backgroundColor">
</tb-color-input>
<tb-image-input fxFlex
label="{{'dashboard.background-image' | translate}}"
formControlName="backgroundImageUrl">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>dashboard.background-size-mode</mat-label>
<mat-select matInput formControlName="backgroundSizeMode">
<mat-option value="100%">Fit width</mat-option>
<mat-option value="auto 100%">Fit height</mat-option>
<mat-option value="cover">Cover</mat-option>
<mat-option value="contain">Contain</mat-option>
<mat-option value="auto">Original size</mat-option>
</mat-select>
</mat-form-field>
<small translate>dashboard.mobile-layout</small>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-checkbox fxFlex formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-checkbox>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>dashboard.mobile-row-height</mat-label>
<input matInput formControlName="mobileRowHeight" type="number" step="any" min="5"
max="200" required>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('required')">
{{ 'dashboard.mobile-row-height-required' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('min')">
{{ 'dashboard.min-mobile-row-height-message' | translate }}
</mat-error>
<mat-error *ngIf="gridSettingsFormGroup.get('mobileRowHeight').hasError('max')">
{{ 'dashboard.max-mobile-row-height-message' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || settingsFormGroup.invalid || gridSettingsFormGroup.invalid
|| (!settingsFormGroup.dirty && !gridSettingsFormGroup.dirty)">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

137
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-settings-dialog.component.ts

@ -0,0 +1,137 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DashboardSettings, GridSettings, StateControllerId } from '@app/shared/models/dashboard.models';
import { isUndefined } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { StatesControllerService } from './states/states-controller.service';
export interface DashboardSettingsDialogData {
settings?: DashboardSettings;
gridSettings?: GridSettings;
}
@Component({
selector: 'tb-dashboard-settings-dialog',
templateUrl: './dashboard-settings-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: DashboardSettingsDialogComponent}],
styleUrls: []
})
export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData>
implements OnInit, ErrorStateMatcher {
settings: DashboardSettings;
gridSettings: GridSettings;
settingsFormGroup: FormGroup;
gridSettingsFormGroup: FormGroup;
stateControllerIds: string[];
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DashboardSettingsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: FormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
private statesControllerService: StatesControllerService) {
super(store, router, dialogRef);
this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers());
this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings;
if (this.settings) {
this.settingsFormGroup = this.fb.group({
stateControllerId: [isUndefined(this.settings.stateControllerId) ? 'entity' : this.settings.stateControllerId, []],
toolbarAlwaysOpen: [isUndefined(this.settings.toolbarAlwaysOpen) ? true : this.settings.toolbarAlwaysOpen, []],
showTitle: [isUndefined(this.settings.showTitle) ? true : this.settings.showTitle, []],
titleColor: [isUndefined(this.settings.titleColor) ? 'rgba(0,0,0,0.870588)' : this.settings.titleColor, []],
showDashboardsSelect: [isUndefined(this.settings.showDashboardsSelect) ? true : this.settings.showDashboardsSelect, []],
showEntitiesSelect: [isUndefined(this.settings.showEntitiesSelect) ? true : this.settings.showEntitiesSelect, []],
showDashboardTimewindow: [isUndefined(this.settings.showDashboardTimewindow) ? true : this.settings.showDashboardTimewindow, []],
showDashboardExport: [isUndefined(this.settings.showDashboardExport) ? true : this.settings.showDashboardExport, []]
});
this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe(
(stateControllerId: StateControllerId) => {
if (stateControllerId !== 'default') {
this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true);
}
}
);
} else {
this.settingsFormGroup = this.fb.group({});
}
if (this.gridSettings) {
this.gridSettingsFormGroup = this.fb.group({
color: [this.gridSettings.color || 'rgba(0,0,0,0.870588)', []],
columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1000)]],
margin: [this.gridSettings.margin || 10, [Validators.required, Validators.min(0), Validators.max(50)]],
autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []],
backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []],
backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []],
backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []],
mobileAutoFillHeight: [isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight, []],
mobileRowHeight: [isUndefined(this.gridSettings.mobileRowHeight) ? 70 : this.gridSettings.mobileRowHeight,
[Validators.required, Validators.min(5), Validators.max(200)]]
});
} else {
this.gridSettingsFormGroup = this.fb.group({});
}
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
let settings: DashboardSettings = null;
let gridSettings: GridSettings = null;
if (this.settings) {
settings = {...this.settings, ...this.settingsFormGroup.value};
}
if (this.gridSettings) {
gridSettings = {...this.gridSettings, ...this.gridSettingsFormGroup.value};
}
this.dialogRef.close({settings, gridSettings});
}
}

84
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.html

@ -0,0 +1,84 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>
<mat-tab-group *ngIf="hasWidgetTypes()" class="tb-absolute-fill" fxFlex>
<mat-tab *ngIf="timeseriesWidgetTypes.length" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="timeseriesWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="latestWidgetTypes.length" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="latestWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="rpcWidgetTypes.length" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="rpcWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="alarmWidgetTypes.length" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="alarmWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="staticWidgetTypes.length" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="staticWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
</mat-tab-group>
<span translate *ngIf="widgetsBundle && !hasWidgetTypes()"
style="text-transform: uppercase; display: flex;"
fxLayoutAlign="center center"
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
<span translate *ngIf="!widgetsBundle"
style="text-transform: uppercase; display: flex;"
fxLayoutAlign="center center"
class="mat-headline tb-absolute-fill">widget.select-widgets-bundle</span>
</div>

23
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.scss

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
.mat-tab-group {
.mat-tab-body-wrapper {
height: 100%;
}
}
}

144
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-widget-select.component.ts

@ -0,0 +1,144 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
OnDestroy,
OnInit,
ViewEncapsulation,
Input,
Output,
EventEmitter,
OnChanges,
SimpleChanges
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { IAliasController } from '@core/api/widget-api.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetService } from '@core/http/widget.service';
import { widgetType, Widget } from '@shared/models/widget.models';
import { toWidgetInfo } from '@home/models/widget-component.models';
import { DashboardCallbacks } from '../../models/dashboard-component.models';
@Component({
selector: 'tb-dashboard-widget-select',
templateUrl: './dashboard-widget-select.component.html',
styleUrls: ['./dashboard-widget-select.component.scss']
})
export class DashboardWidgetSelectComponent implements OnInit, OnChanges {
@Input()
widgetsBundle: WidgetsBundle;
@Input()
aliasController: IAliasController;
@Output()
widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>();
timeseriesWidgetTypes: Array<Widget> = [];
latestWidgetTypes: Array<Widget> = [];
rpcWidgetTypes: Array<Widget> = [];
alarmWidgetTypes: Array<Widget> = [];
staticWidgetTypes: Array<Widget> = [];
callbacks: DashboardCallbacks = {
onWidgetClicked: this.onWidgetClicked.bind(this)
};
constructor(private widgetsService: WidgetService) {
}
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue && change.currentValue) {
if (propName === 'widgetsBundle') {
this.loadLibrary();
}
}
}
}
private loadLibrary() {
this.timeseriesWidgetTypes.length = 0;
this.latestWidgetTypes.length = 0;
this.rpcWidgetTypes.length = 0;
this.alarmWidgetTypes.length = 0;
this.staticWidgetTypes.length = 0;
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.widgetsService.getBundleWidgetTypes(bundleAlias,
isSystem).subscribe(
(types) => {
types = types.sort((a, b) => b.createdTime - a.createdTime);
let top = 0;
types.forEach((type) => {
const widgetTypeInfo = toWidgetInfo(type);
const widget: Widget = {
typeId: type.id,
isSystemType: isSystem,
bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName,
sizeX: widgetTypeInfo.sizeX,
sizeY: widgetTypeInfo.sizeY,
row: top,
col: 0,
config: JSON.parse(widgetTypeInfo.defaultConfig)
};
widget.config.title = widgetTypeInfo.widgetName;
switch (widgetTypeInfo.type) {
case widgetType.timeseries:
this.timeseriesWidgetTypes.push(widget);
break;
case widgetType.latest:
this.latestWidgetTypes.push(widget);
break;
case widgetType.rpc:
this.rpcWidgetTypes.push(widget);
break;
case widgetType.alarm:
this.alarmWidgetTypes.push(widget);
break;
case widgetType.static:
this.staticWidgetTypes.push(widget);
break;
}
top += widget.sizeY;
});
}
);
}
hasWidgetTypes() {
return this.timeseriesWidgetTypes.length > 0 ||
this.latestWidgetTypes.length > 0 ||
this.rpcWidgetTypes.length > 0 ||
this.alarmWidgetTypes.length > 0 ||
this.staticWidgetTypes.length > 0;
}
private onWidgetClicked($event: Event, widget: Widget, index: number): void {
this.widgetSelected.emit(widget);
}
}

18
ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts

@ -29,13 +29,22 @@ import { DashboardToolbarComponent } from './dashboard-toolbar.component';
import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module';
import { DashboardLayoutComponent } from './layout/dashboard-layout.component';
import { EditWidgetComponent } from './edit-widget.component';
import { DashboardWidgetSelectComponent } from './dashboard-widget-select.component';
import { AddWidgetDialogComponent } from './add-widget-dialog.component';
import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component';
import { SelectTargetLayoutDialogComponent } from './layout/select-target-layout-dialog.component';
import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component';
@NgModule({
entryComponents: [
DashboardFormComponent,
DashboardTabsComponent,
ManageDashboardCustomersDialogComponent,
MakeDashboardPublicDialogComponent
MakeDashboardPublicDialogComponent,
AddWidgetDialogComponent,
ManageDashboardLayoutsDialogComponent,
SelectTargetLayoutDialogComponent,
DashboardSettingsDialogComponent
],
declarations: [
DashboardFormComponent,
@ -45,7 +54,12 @@ import { EditWidgetComponent } from './edit-widget.component';
DashboardToolbarComponent,
DashboardPageComponent,
DashboardLayoutComponent,
EditWidgetComponent
EditWidgetComponent,
DashboardWidgetSelectComponent,
AddWidgetDialogComponent,
ManageDashboardLayoutsDialogComponent,
SelectTargetLayoutDialogComponent,
DashboardSettingsDialogComponent
],
imports: [
CommonModule,

22
ui-ngx/src/app/modules/home/pages/dashboard/edit-widget.component.ts

@ -132,26 +132,4 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
};
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
}
private createEntityAlias(alias: string, allowedEntityTypes: Array<EntityType>): Observable<EntityAlias> {
const singleEntityAlias: EntityAlias = {id: null, alias, filter: {resolveMultiple: false}};
return this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData,
EntityAlias>(EntityAliasDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: true,
allowedEntityTypes,
entityAliases: this.dashboard.configuration.entityAliases,
alias: singleEntityAlias
}
}).afterClosed().pipe(
tap((entityAlias) => {
if (entityAlias) {
this.dashboard.configuration.entityAliases[entityAlias.id] = entityAlias;
this.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases);
}
})
);
}
}

23
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.html

@ -17,14 +17,9 @@
-->
<hotkeys-cheatsheet></hotkeys-cheatsheet>
<div class="mat-content" style="position: relative; width: 100%; height: 100%;"
[ngStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none',
'background-repeat': 'no-repeat',
'background-attachment': 'scroll',
'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'}">
<section *ngIf="layoutCtx.widgets.length === 0" fxLayoutAlign="center center"
[style.backgroundImage]="backgroundImage"
[ngStyle]="dashboardStyle">
<section *ngIf="layoutCtx.widgets.size() === 0" fxLayoutAlign="center center"
[ngStyle]="{'color': layoutCtx.gridSettings.color}"
style="text-transform: uppercase; display: flex; z-index: 1; pointer-events: none;"
class="mat-headline tb-absolute-fill">
@ -37,18 +32,12 @@
{{ 'dashboard.add-widget' | translate }}
</button>
</section>
<tb-dashboard #dashboard [dashboardStyle]="{'background-color': layoutCtx.gridSettings.backgroundColor,
'background-image': layoutCtx.gridSettings.backgroundImageUrl ?
'url('+layoutCtx.gridSettings.backgroundImageUrl+')' : 'none',
'background-repeat': 'no-repeat',
'background-attachment': 'scroll',
'background-size': layoutCtx.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'}"
<tb-dashboard #dashboard [dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage"
[widgets]="layoutCtx.widgets"
[widgetLayouts]="layoutCtx.widgetLayouts"
[columns]="layoutCtx.gridSettings.columns"
[horizontalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[0] : 10"
[verticalMargin]="layoutCtx.gridSettings.margins ? layoutCtx.gridSettings.margins[1]: 10"
[margin]="layoutCtx.gridSettings.margin"
[aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[dashboardTimewindow]="dashboardCtx.dashboardTimewindow"

56
ui-ngx/src/app/modules/home/pages/dashboard/layout/dashboard-layout.component.ts

@ -34,6 +34,7 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys';
import { getCurrentIsLoading } from '@core/interceptors/load.selectors';
import { TranslateService } from '@ngx-translate/core';
import { ItemBufferService } from '@app/core/services/item-buffer.service';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
@Component({
selector: 'tb-dashboard-layout',
@ -43,12 +44,17 @@ import { ItemBufferService } from '@app/core/services/item-buffer.service';
export class DashboardLayoutComponent extends PageComponent implements ILayoutController, DashboardCallbacks, OnInit, OnDestroy {
layoutCtxValue: DashboardPageLayoutContext;
dashboardStyle: {[klass: string]: any} = null;
backgroundImage: SafeStyle | string;
@Input()
set layoutCtx(val: DashboardPageLayoutContext) {
this.layoutCtxValue = val;
if (this.layoutCtxValue) {
this.layoutCtxValue.ctrl = this;
if (this.dashboardStyle == null) {
this.loadDashboardStyle();
}
}
}
get layoutCtx(): DashboardPageLayoutContext {
@ -77,7 +83,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
constructor(protected store: Store<AppState>,
private hotkeysService: HotkeysService,
private translate: TranslateService,
private itembuffer: ItemBufferService) {
private itembuffer: ItemBufferService,
private sanitizer: DomSanitizer) {
super(store);
}
@ -165,54 +172,67 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
);
}
reload() {
private loadDashboardStyle() {
this.dashboardStyle = {'background-color': this.layoutCtx.gridSettings.backgroundColor,
'background-repeat': 'no-repeat',
'background-attachment': 'scroll',
'background-size': this.layoutCtx.gridSettings.backgroundSizeMode || '100%',
'background-position': '0% 0%'};
this.backgroundImage = this.layoutCtx.gridSettings.backgroundImageUrl ?
this.sanitizer.bypassSecurityTrustStyle('url(' + this.layoutCtx.gridSettings.backgroundImageUrl + ')') : 'none';
}
setResizing(layoutVisibilityChanged: boolean) {
reload() {
this.loadDashboardStyle();
this.dashboard.pauseChangeNotifications();
setTimeout(() => {
this.dashboard.resumeChangeNotifications();
this.dashboard.notifyLayoutUpdated();
}, 0);
}
resetHighlight() {
this.dashboard.resetHighlight();
}
highlightWidget(index: number, delay?: number) {
this.dashboard.highlightWidget(index, delay);
highlightWidget(widgetId: string, delay?: number) {
this.dashboard.highlightWidget(widgetId, delay);
}
selectWidget(index: number, delay?: number) {
this.dashboard.selectWidget(index, delay);
selectWidget(widgetId: string, delay?: number) {
this.dashboard.selectWidget(widgetId, delay);
}
addWidget($event: Event) {
this.layoutCtx.dashboardCtrl.addWidget($event, this.layoutCtx);
}
onEditWidget($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget, index);
onEditWidget($event: Event, widget: Widget): void {
this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget);
}
onExportWidget($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, index);
onExportWidget($event: Event, widget: Widget): void {
this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget);
}
onRemoveWidget($event: Event, widget: Widget, index: number): void {
onRemoveWidget($event: Event, widget: Widget): void {
return this.layoutCtx.dashboardCtrl.removeWidget($event, this.layoutCtx, widget);
}
onWidgetMouseDown($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget, index);
onWidgetMouseDown($event: Event, widget: Widget): void {
this.layoutCtx.dashboardCtrl.widgetMouseDown($event, this.layoutCtx, widget);
}
onWidgetClicked($event: Event, widget: Widget, index: number): void {
this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget, index);
onWidgetClicked($event: Event, widget: Widget): void {
this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget);
}
prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
}
prepareWidgetContextMenu($event: Event, widget: Widget, index: number): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, index);
prepareWidgetContextMenu($event: Event, widget: Widget): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget);
}
copyWidget($event: Event, widget: Widget) {

31
ui-ngx/src/app/modules/home/pages/dashboard/layout/layout-button.scss

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host ::ng-deep {
button.tb-layout-button {
width: 100%;
max-width: 240px;
height: 100%;
.mat-button-wrapper {
padding: 40px;
line-height: 18px;
span {
font-size: 18px;
font-weight: 400;
white-space: normal;
}
}
}
}

5
ui-ngx/src/app/modules/home/pages/dashboard/layout/layout.models.ts

@ -19,10 +19,9 @@ import { WidgetLayout } from '@shared/models/dashboard.models';
export interface ILayoutController {
reload();
setResizing(layoutVisibilityChanged: boolean);
resetHighlight();
highlightWidget(index: number, delay?: number);
selectWidget(index: number, delay?: number);
highlightWidget(widgetId: string, delay?: number);
selectWidget(widgetId: string, delay?: number);
pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent);
}

71
ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.html

@ -0,0 +1,71 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #widgetForm="ngForm" [formGroup]="layoutsFormGroup" (ngSubmit)="save()">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>layout.manage</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-checkbox fxFlex formControlName="main">
{{ 'layout.main' | translate }}
</mat-checkbox>
<mat-checkbox fxFlex formControlName="right">
{{ 'layout.right' | translate }}
</mat-checkbox>
</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<button fxFlex fxLayout="column"
type="button" mat-button mat-raised-button color="primary"
class="tb-layout-button"
(click)="openLayoutSettings('main')">
<span translate>layout.main</span>
</button>
<button fxFlex fxLayout="column" [fxShow]="layoutsFormGroup.get('right').value"
type="button" mat-button mat-raised-button color="primary"
class="tb-layout-button"
(click)="openLayoutSettings('right')">
<span translate>layout.right</span>
</button>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || layoutsFormGroup.invalid || !layoutsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

125
ui-ngx/src/app/modules/home/pages/dashboard/layout/manage-dashboard-layouts-dialog.component.ts

@ -0,0 +1,125 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, widgetTypesData } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { EntityService } from '@core/http/entity.service';
import { Dashboard, DashboardLayoutId, DashboardStateLayouts } from '@app/shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';
import { deepClone, isDefined, isString } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { MatDialog } from '@angular/material/dialog';
import {
DashboardSettingsDialogComponent,
DashboardSettingsDialogData
} from '@home/pages/dashboard/dashboard-settings-dialog.component';
export interface ManageDashboardLayoutsDialogData {
layouts: DashboardStateLayouts;
}
@Component({
selector: 'tb-manage-dashboard-layouts-dialog',
templateUrl: './manage-dashboard-layouts-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: ManageDashboardLayoutsDialogComponent}],
styleUrls: ['./layout-button.scss']
})
export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>
implements OnInit, ErrorStateMatcher {
layoutsFormGroup: FormGroup;
layouts: DashboardStateLayouts;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ManageDashboardLayoutsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>,
private fb: FormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
private dialog: MatDialog) {
super(store, router, dialogRef);
this.layouts = this.data.layouts;
this.layoutsFormGroup = this.fb.group({
main: [{value: isDefined(this.layouts.main), disabled: true}, []],
right: [isDefined(this.layouts.right), []],
}
);
for (const l of Object.keys(this.layoutsFormGroup.controls)) {
const control = this.layoutsFormGroup.controls[l];
if (!this.layouts[l]) {
this.layouts[l] = this.dashboardUtils.createDefaultLayoutData();
}
}
}
ngOnInit(): void {
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
openLayoutSettings(layoutId: DashboardLayoutId) {
const gridSettings = deepClone(this.layouts[layoutId].gridSettings);
this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
settings: null,
gridSettings
}
}).afterClosed().subscribe((data) => {
if (data && data.gridSettings) {
this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings);
this.layoutsFormGroup.markAsDirty();
}
});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
for (const l of Object.keys(this.layoutsFormGroup.controls)) {
const control = this.layoutsFormGroup.controls[l];
if (!control.value) {
delete this.layouts[l];
}
}
this.dialogRef.close(this.layouts);
}
}

48
ui-ngx/src/app/modules/home/pages/dashboard/layout/select-target-layout-dialog.component.html

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form #widgetForm="ngForm">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>layout.select</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" fxLayout="column" fxLayoutGap="8px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<button fxFlex fxLayout="column"
type="button" mat-button mat-raised-button color="primary"
class="tb-layout-button"
(click)="selectLayout('main')">
<span translate>layout.main</span>
</button>
<button fxFlex fxLayout="column"
type="button" mat-button mat-raised-button color="primary"
class="tb-layout-button"
(click)="selectLayout('right')">
<span translate>layout.right</span>
</button>
</div>
</fieldset>
</div>
</form>

50
ui-ngx/src/app/modules/home/pages/dashboard/layout/select-target-layout-dialog.component.ts

@ -0,0 +1,50 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { DashboardLayoutId } from '@app/shared/models/dashboard.models';
@Component({
selector: 'tb-select-target-layout-dialog',
templateUrl: './select-target-layout-dialog.component.html',
styleUrls: ['./layout-button.scss']
})
export class SelectTargetLayoutDialogComponent extends DialogComponent<SelectTargetLayoutDialogComponent, DashboardLayoutId>
implements OnInit {
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<SelectTargetLayoutDialogComponent, DashboardLayoutId>) {
super(store, router, dialogRef);
}
ngOnInit(): void {
}
selectLayout(layoutId: DashboardLayoutId) {
this.dialogRef.close(layoutId);
}
cancel(): void {
this.dialogRef.close(null);
}
}

45
ui-ngx/src/app/shared/components/image-input.component.html

@ -0,0 +1,45 @@
<!--
Copyright © 2016-2019 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-container">
<label class="tb-title">{{label}}</label>
<ng-container #flow="flow"
[flowConfig]="{singleFile: true}">
<div class="tb-image-select-container">
<div class="tb-image-preview-container">
<div *ngIf="!safeImageUrl" translate>dashboard.no-image</div>
<img *ngIf="safeImageUrl" class="tb-image-preview" [src]="safeImageUrl" />
</div>
<div class="tb-image-clear-container">
<button mat-button mat-icon-button color="primary"
type="button"
(click)="clearImage()"
class="tb-image-clear-btn"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="drop-area tb-flow-drop"
flowDrop
[flow]="flow.flowJs">
<label for="select" translate>dashboard.drop-image</label>
<input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="select">
</div>
</div>
</ng-container>
</div>

104
ui-ngx/src/app/shared/components/image-input.component.scss

@ -0,0 +1,104 @@
/**
* Copyright © 2016-2019 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../scss/constants";
$previewSize: 100px !default;
:host {
.tb-container {
margin-top: 0px;
label.tb-title {
display: block;
padding-bottom: 8px;
}
}
.tb-image-select-container {
position: relative;
width: 100%;
height: $previewSize;
}
.tb-image-preview {
width: auto;
max-width: $previewSize;
height: auto;
max-height: $previewSize;
}
.tb-image-preview-container {
position: relative;
float: left;
width: $previewSize;
height: $previewSize;
margin-right: 12px;
vertical-align: top;
border: solid 1px;
div {
width: 100%;
font-size: 18px;
text-align: center;
}
div,
.tb-image-preview {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.tb-image-clear-container {
position: relative;
float: right;
width: 48px;
height: $previewSize;
}
.tb-image-clear-btn {
position: absolute !important;
top: 50%;
transform: translate(0%, -50%) !important;
}
.file-input {
display: none;
}
.tb-flow-drop {
position: relative;
height: $previewSize;
overflow: hidden;
border: dashed 2px;
label {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
font-size: 16px;
text-align: center;
@media #{$mat-gt-sm} {
font-size: 24px;
}
}
}
}

143
ui-ngx/src/app/shared/components/image-input.component.ts

@ -0,0 +1,143 @@
///
/// Copyright © 2016-2019 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DataKey, DatasourceType } from '@shared/models/widget.models';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of, Subscription } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import { alarmFields } from '@shared/models/alarm.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DialogService } from '@core/services/dialog.service';
import { FlowDirective } from '@flowjs/ngx-flow';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Component({
selector: 'tb-image-input',
templateUrl: './image-input.component.html',
styleUrls: ['./image-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ImageInputComponent),
multi: true
}
]
})
export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
@Input()
label: string;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
const newVal = coerceBooleanProperty(value);
if (this.requiredValue !== newVal) {
this.requiredValue = newVal;
}
}
@Input()
disabled: boolean;
imageUrl: string;
safeImageUrl: SafeUrl;
@ViewChild('flow', {static: true})
flow: FlowDirective;
autoUploadSubscription: Subscription;
private propagateChange = null;
constructor(protected store: Store<AppState>,
private sanitizer: DomSanitizer) {
super(store);
}
ngAfterViewInit() {
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'fileAdded') {
const file = (event.event[0] as flowjs.FlowFile).file;
const reader = new FileReader();
reader.onload = (loadEvent) => {
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {
this.imageUrl = reader.result;
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
this.updateModel();
}
};
reader.readAsDataURL(file);
}
});
}
ngOnDestroy() {
this.autoUploadSubscription.unsubscribe();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string): void {
this.imageUrl = value;
if (this.imageUrl) {
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
} else {
this.safeImageUrl = null;
}
}
private updateModel() {
this.propagateChange(this.imageUrl);
}
clearImage() {
this.imageUrl = null;
this.safeImageUrl = null;
this.updateModel();
}
}

3
ui-ngx/src/app/shared/components/time/timewindow.component.html

@ -16,6 +16,7 @@
-->
<button *ngIf="asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin" [disabled]="disabled"
type="button"
mat-raised-button color="primary" (click)="openEditMode($event)">
<mat-icon class="material-icons">query_builder</mat-icon>
<span>{{innerValue?.displayValue}}</span>
@ -23,6 +24,7 @@
<section *ngIf="!asButton" cdkOverlayOrigin #timewindowPanelOrigin="cdkOverlayOrigin"
class="tb-timewindow" fxLayout="row" fxLayoutAlign="start center">
<button *ngIf="direction === 'left'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32"
type="button"
(click)="openEditMode($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">
@ -35,6 +37,7 @@
{{innerValue?.displayValue}}
</span>
<button *ngIf="direction === 'right'" [disabled]="disabled" mat-button mat-icon-button class="tb-mat-32"
type="button"
(click)="openEditMode($event)"
matTooltip="{{ 'timewindow.edit' | translate }}"
[matTooltipPosition]="tooltipPosition">

8
ui-ngx/src/app/shared/components/widgets-bundle-select.component.html

@ -24,10 +24,16 @@
panelClass="tb-widgets-bundle-select"
placeholder="{{ 'widget.select-widgets-bundle' | translate }}"
(ngModelChange)="widgetsBundleChanged()">
<mat-select-trigger>
<div class="tb-bundle-item">
<span>{{widgetsBundle?.title}}</span>
<span translate class="tb-bundle-system" *ngIf="isSystem(widgetsBundle)">widgets-bundle.system</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle">
<div class="tb-bundle-item">
<span>{{widgetsBundle.title}}</span>
<span translate class="tb-bundle-system" *ngIf="isSystem(item)">widgets-bundle.system</span>
<span translate class="tb-bundle-system" *ngIf="isSystem(widgetsBundle)">widgets-bundle.system</span>
</div>
</mat-option>
</mat-select>

53
ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss

@ -20,8 +20,8 @@ tb-widgets-bundle-select {
}
.tb-bundle-item {
height: 24px;
line-height: 24px;
height: 26px;
line-height: 26px;
}
}
@ -63,28 +63,43 @@ tb-widgets-bundle-select,
mat-toolbar {
tb-widgets-bundle-select {
mat-select {
background: rgba(255, 255, 255, .2);
padding: 5px 20px;
.mat-form-field-wrapper {
padding-bottom: 0 !important;
.mat-form-field-infix {
background: rgba(255, 255, 255, .2);
padding: 5px 20px !important;
border: none;
.mat-select-value-text {
font-size: 1.2rem;
color: #fff;
mat-select {
.mat-select-value-text {
font-size: 1.2rem;
color: #fff;
span:first-child::after {
color: #fff;
span:first-child::after {
color: #fff;
}
}
.mat-select-value {
vertical-align: middle;
min-height: 30px;
height: 30px;
padding: 2px 2px 1px;
.mat-select-placeholder {
color: #fff;
opacity: .8;
}
}
}
}
.mat-select-value.mat-select-placeholder {
color: #fff;
opacity: .8;
.mat-select.mat-select-invalid {
.mat-select-arrow {
color: #fff !important;
}
}
}
}
mat-select.ng-invalid.ng-touched {
.mat-select-value-text {
color: #fff !important;
.mat-form-field-underline {
display: none;
}
}
}

7
ui-ngx/src/app/shared/models/alias.models.ts

@ -132,10 +132,13 @@ export interface EntityAliasFilter extends EntityFilters {
resolveMultiple?: boolean;
}
export interface EntityAlias {
id: string;
export interface EntityAliasInfo {
alias: string;
filter: EntityAliasFilter;
}
export interface EntityAlias extends EntityAliasInfo {
id: string;
[key: string]: any;
}

13
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -30,12 +30,12 @@ export interface DashboardInfo extends BaseData<DashboardId> {
}
export interface WidgetLayout {
sizeX: number;
sizeY: number;
sizeX?: number;
sizeY?: number;
mobileHeight?: number;
mobileOrder?: number;
col: number;
row: number;
col?: number;
row?: number;
}
export interface WidgetLayouts {
@ -46,14 +46,13 @@ export interface GridSettings {
backgroundColor?: string;
color?: string;
columns?: number;
margins?: [number, number];
margin?: number;
backgroundSizeMode?: string;
backgroundImageUrl?: string;
autoFillHeight?: boolean;
mobileAutoFillHeight?: boolean;
mobileRowHeight?: number;
[key: string]: any;
// TODO:
}
export interface DashboardLayout {
@ -62,7 +61,7 @@ export interface DashboardLayout {
}
export interface DashboardLayoutInfo {
widgets?: Array<Widget>;
widgetIds?: string[];
widgetLayouts?: WidgetLayouts;
gridSettings?: GridSettings;
}

10
ui-ngx/src/app/shared/models/widget.models.ts

@ -392,3 +392,13 @@ export interface JsonSettingsSchema {
};
form?: any[];
}
export interface WidgetPosition {
row: number;
column: number;
}
export interface WidgetSize {
sizeX: number;
sizeY: number;
}

14
ui-ngx/src/app/shared/shared.module.ts

@ -20,6 +20,8 @@ import { FooterComponent } from './components/footer.component';
import { LogoComponent } from './components/logo.component';
import { TbSnackBarComponent, ToastDirective } from './components/toast.directive';
import { BreadcrumbComponent } from '@app/shared/components/breadcrumb.component';
import { NgxFlowModule, FlowInjectionToken } from '@flowjs/ngx-flow';
import Flow from '@flowjs/flow.js';
import {
MatAutocompleteModule,
@ -108,6 +110,7 @@ import { JsFuncComponent } from './components/js-func.component';
import { JsonFormComponent } from './components/json-form/json-form.component';
import { MaterialIconsDialogComponent } from '@shared/components/dialog/material-icons-dialog.component';
import { MaterialIconSelectComponent } from '@shared/components/material-icon-select.component';
import { ImageInputComponent } from './components/image-input.component';
@NgModule({
providers: [
@ -115,7 +118,11 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
HighlightPipe,
TruncatePipe
TruncatePipe,
{
provide: FlowInjectionToken,
useValue: Flow
}
],
entryComponents: [
TbSnackBarComponent,
@ -173,6 +180,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
ColorInputComponent,
MaterialIconSelectComponent,
JsonFormComponent,
ImageInputComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -222,7 +230,8 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
OverlayModule,
ShareButtonsModule,
HotkeyModule,
ColorPickerModule
ColorPickerModule,
NgxFlowModule
],
exports: [
FooterComponent,
@ -308,6 +317,7 @@ import { MaterialIconSelectComponent } from '@shared/components/material-icon-se
ColorInputComponent,
MaterialIconSelectComponent,
JsonFormComponent,
ImageInputComponent,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

3
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -502,6 +502,9 @@
"min-columns-count-message": "Only 10 minimum column count is allowed.",
"max-columns-count-message": "Only 1000 maximum column count is allowed.",
"widgets-margins": "Margin between widgets",
"margin-required": "Margin value is required.",
"min-margin-message": "Only 0 is allowed as minimum margin value.",
"max-margin-message": "Only 50 is allowed as maximum margin value.",
"horizontal-margin": "Horizontal margin",
"horizontal-margin-required": "Horizontal margin value is required.",
"min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.",

2
ui-ngx/src/scss/constants.scss

@ -27,3 +27,5 @@ $mat-gt-sm: "screen and (min-width: 960px)";
$mat-gt-md: "screen and (min-width: 1280px)";
$mat-gt-xmd: "screen and (min-width: 1600px)";
$mat-gt-xl: "screen and (min-width: 1920px)";
$primary-hue-3: rgb(207, 216, 220) !default;

1
ui-ngx/src/styles.scss

@ -702,7 +702,6 @@ mat-label {
display: flex;
flex-direction: column;
overflow: auto;
height: 100%;
}
.mat-dialog-content {
margin: 0;

Loading…
Cancel
Save