diff --git a/ui-ngx/package-lock.json b/ui-ngx/package-lock.json index 19e65a601c..7a5069a590 100644 --- a/ui-ngx/package-lock.json +++ b/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", diff --git a/ui-ngx/package.json b/ui-ngx/package.json index ce64c8037e..57760a98e3 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts index 343592ac64..6e202cbef0 100644 --- a/ui-ngx/src/app/core/core.module.ts +++ b/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) { diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 9ae6eac9f5..d965912a12 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/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}, targetDevicesByAliasId: {[aliasId: string]: Array>}): DashboardConfiguration { diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index edcca2701b..a7dd4db778 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/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 { + 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 { + 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 { + 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); + } } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 26a9ade0ba..3bbd97e30e 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/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): Array { diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 394ed143df..9960b5bbf4 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -353,3 +353,13 @@ export function deepClone(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(); +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html index 6fe9e3f2bc..73945b8602 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html @@ -17,6 +17,7 @@ -->
@@ -114,7 +115,7 @@ close diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss index 409a4444d1..4a75664657 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html index f143856ec3..b285e2d5ba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html @@ -16,6 +16,7 @@ --> + + + +
+
+ + +
+
+
+ + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts new file mode 100644 index 0000000000..7614f13a60 --- /dev/null +++ b/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 + implements OnInit, ErrorStateMatcher { + + widgetFormGroup: FormGroup; + + dashboard: Dashboard; + aliasController: IAliasController; + widget: Widget; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html index 6556cb219e..7a13498ee7 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html @@ -132,26 +132,13 @@
-
- - -
- - + + + + +
@@ -194,6 +194,37 @@ + + + +
+
+ {{ 'widgets-bundle.current' | translate }} + + +
+
+ + +
+
+