Browse Source

Merge branch 'master' into fix/version-conflict

pull/11426/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
cbf0f527d2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1401
      application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg
  2. 1633
      application/src/main/data/json/system/scada_symbols/elevated-tank.svg
  3. 1407
      application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
  4. 1392
      application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg
  5. 1453
      application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg
  6. 1455
      application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg
  7. 1394
      application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg
  8. 1056
      application/src/main/data/json/system/scada_symbols/pool.svg
  9. 1426
      application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
  10. 1456
      application/src/main/data/json/system/scada_symbols/spherical-tank.svg
  11. 1462
      application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg
  12. 1468
      application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
  13. 1435
      application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg
  14. 1455
      application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg
  15. 1374
      application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg
  16. 4
      application/src/main/data/json/system/scada_symbols/vertical-tank.svg
  17. 223
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  18. 79
      ui-ngx/src/app/core/services/item-buffer.service.ts
  19. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  20. 8
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  21. 57
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  22. 20
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  23. 218
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  24. 21
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
  25. 43
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  26. 148
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts
  27. 13
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss
  28. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  29. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  30. 66
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html
  31. 85
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts
  32. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
  33. 67
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts
  34. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts
  35. 86
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html
  36. 48
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss
  37. 227
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts
  38. 29
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html
  39. 34
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.scss
  40. 81
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts
  41. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  42. 31
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  43. 10
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  44. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  45. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  46. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  47. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  48. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  49. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  50. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts
  51. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html
  52. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  53. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  54. 16
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  55. 30
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  56. 93
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  57. 4
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  58. 4
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  59. 1
      ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts
  60. 5
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  61. 57
      ui-ngx/src/app/shared/models/dashboard.models.ts
  62. 3
      ui-ngx/src/app/shared/shared.module.ts
  63. 1
      ui-ngx/src/assets/locale/locale.constant-ar_AE.json
  64. 1
      ui-ngx/src/assets/locale/locale.constant-ca_ES.json
  65. 1
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  66. 37
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  67. 1
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  68. 1
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  69. 1
      ui-ngx/src/assets/locale/locale.constant-lt_LT.json
  70. 1
      ui-ngx/src/assets/locale/locale.constant-nl_BE.json
  71. 1
      ui-ngx/src/assets/locale/locale.constant-pl_PL.json
  72. 1
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  73. 1
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  74. 1
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json
  75. 6
      ui-ngx/src/styles.scss

1401
application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 112 KiB

1633
application/src/main/data/json/system/scada_symbols/elevated-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 138 KiB

1407
application/src/main/data/json/system/scada_symbols/horizontal-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 114 KiB

1392
application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 112 KiB

1453
application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 119 KiB

1455
application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 120 KiB

1394
application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 113 KiB

1056
application/src/main/data/json/system/scada_symbols/pool.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 73 KiB

1426
application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 104 KiB

1456
application/src/main/data/json/system/scada_symbols/spherical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 118 KiB

1462
application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 120 KiB

1468
application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 121 KiB

1435
application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 108 KiB

1455
application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 120 KiB

1374
application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 101 KiB

4
application/src/main/data/json/system/scada_symbols/vertical-tank.svg

@ -311,7 +311,7 @@
"id": "valueBoxColor", "id": "valueBoxColor",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#F3F3F3",
"required": null, "required": null,
"subLabel": null, "subLabel": null,
"divider": null, "divider": null,
@ -633,7 +633,7 @@
</g><mask id="path-215-inside-2_1687_130892" fill="#ffffff"> </g><mask id="path-215-inside-2_1687_130892" fill="#ffffff">
<path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z"/> <path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z"/>
</mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box"> </mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box">
<path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#fff" fill-opacity=".75" tb:tag="value-box-background"/> <path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="301.5625" y="93.546875" dominant-baseline="middle" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan>1660 gal</tspan></text> <text x="301.5625" y="93.546875" dominant-baseline="middle" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan>1660 gal</tspan></text>
</g><defs> </g><defs>

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

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

@ -18,11 +18,16 @@ import { Injectable } from '@angular/core';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { import {
BreakpointId,
breakpointIdIconMap,
breakpointIdTranslationMap,
BreakpointInfo,
BreakpointLayoutInfo,
BreakpointSystemId,
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
DashboardLayout, DashboardLayout,
DashboardLayoutId, DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo, DashboardLayoutsInfo,
DashboardState, DashboardState,
DashboardStateLayouts, DashboardStateLayouts,
@ -41,6 +46,7 @@ import {
Widget, Widget,
WidgetConfig, WidgetConfig,
WidgetConfigMode, WidgetConfigMode,
WidgetSize,
widgetType, widgetType,
WidgetTypeDescriptor WidgetTypeDescriptor
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
@ -51,14 +57,19 @@ import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DashboardUtilsService { export class DashboardUtilsService {
private systemBreakpoints: {[key in BreakpointSystemId]?: BreakpointInfo};
constructor(private utils: UtilsService, constructor(private utils: UtilsService,
private timeService: TimeService) { private timeService: TimeService,
private translate: TranslateService) {
} }
public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard { public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard {
@ -523,6 +534,28 @@ export class DashboardUtilsService {
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
public isReferenceWidget(dashboard: Dashboard, widgetId: string): boolean {
const states = dashboard.configuration.states;
let foundWidgetRefs = 0;
for (const state of Object.values(states)) {
for (const layout of Object.values(state.layouts)) {
if (layout.widgets[widgetId]) {
foundWidgetRefs++;
}
if (layout.breakpoints) {
for (const breakpoint of Object.values(layout.breakpoints)) {
if (breakpoint.widgets[widgetId]) {
foundWidgetRefs++;
}
}
}
}
}
return foundWidgetRefs > 1;
}
public getRootStateId(states: {[id: string]: DashboardState }): string { public getRootStateId(states: {[id: string]: DashboardState }): string {
for (const stateId of Object.keys(states)) { for (const stateId of Object.keys(states)) {
const state = states[stateId]; const state = states[stateId];
@ -542,16 +575,14 @@ export class DashboardUtilsService {
for (const l of Object.keys(state.layouts)) { for (const l of Object.keys(state.layouts)) {
const layout: DashboardLayout = state.layouts[l]; const layout: DashboardLayout = state.layouts[l];
if (layout) { if (layout) {
result[l] = { result[l]= {
widgetIds: [], default: this.getBreakpointLayoutData(layout)
widgetLayouts: {}, };
gridSettings: {} if (layout.breakpoints) {
} as DashboardLayoutInfo; for (const breakpoint of Object.keys(layout.breakpoints)) {
for (const id of Object.keys(layout.widgets)) { result[l][breakpoint] = this.getBreakpointLayoutData(layout.breakpoints[breakpoint]);
result[l].widgetIds.push(id); }
} }
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
} }
} }
return result; return result;
@ -560,6 +591,20 @@ export class DashboardUtilsService {
} }
} }
private getBreakpointLayoutData(layout: DashboardLayout): BreakpointLayoutInfo {
const result: BreakpointLayoutInfo = {
widgetIds: [],
widgetLayouts: {},
gridSettings: {}
};
for (const id of Object.keys(layout.widgets)) {
result.widgetIds.push(id);
}
result.widgetLayouts = layout.widgets;
result.gridSettings = layout.gridSettings;
return result;
}
public getWidgetsArray(dashboard: Dashboard): Array<Widget> { public getWidgetsArray(dashboard: Dashboard): Array<Widget> {
const widgetsArray: Array<Widget> = []; const widgetsArray: Array<Widget> = [];
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
@ -586,11 +631,15 @@ export class DashboardUtilsService {
originalColumns?: number, originalColumns?: number,
originalSize?: {sizeX: number; sizeY: number}, originalSize?: {sizeX: number; sizeY: number},
row?: number, row?: number,
column?: number): void { column?: number,
breakpoint = 'default'): void {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const state = states[targetState]; const state = states[targetState];
const layout = state.layouts[targetLayout]; let layout = state.layouts[targetLayout];
if (breakpoint !== 'default' && layout.breakpoints?.[breakpoint]) {
layout = layout.breakpoints[breakpoint];
}
const layoutCount = Object.keys(state.layouts).length; const layoutCount = Object.keys(state.layouts).length;
if (!widget.id) { if (!widget.id) {
widget.id = this.utils.guid(); widget.id = this.utils.guid();
@ -648,12 +697,17 @@ export class DashboardUtilsService {
public removeWidgetFromLayout(dashboard: Dashboard, public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string, targetState: string,
targetLayout: DashboardLayoutId, targetLayout: DashboardLayoutId,
widgetId: string) { widgetId: string,
breakpoint: string) {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const state = states[targetState]; const state = states[targetState];
const layout = state.layouts[targetLayout]; const layout = state.layouts[targetLayout];
delete layout.widgets[widgetId]; if (layout.breakpoints[breakpoint]) {
delete layout.breakpoints[breakpoint].widgets[widgetId];
} else {
delete layout.widgets[widgetId];
}
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
@ -721,11 +775,19 @@ export class DashboardUtilsService {
for (const s of Object.keys(states)) { for (const s of Object.keys(states)) {
const state = states[s]; const state = states[s];
for (const l of Object.keys(state.layouts)) { for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l]; const layout: DashboardLayout = state.layouts[l];
if (layout.widgets[widgetId]) { if (layout.widgets[widgetId]) {
found = true; found = true;
break; break;
} }
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
if (layout.breakpoints[breakpoint].widgets[widgetId]) {
found = true;
break;
}
}
}
} }
} }
if (!found) { if (!found) {
@ -882,4 +944,133 @@ export class DashboardUtilsService {
} }
} }
replaceReferenceWithWidgetCopy(widget: Widget,
dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
breakpointId: string,
isRemoveWidget: boolean): Widget {
const newWidget = deepClone(widget);
newWidget.id = this.utils.guid();
const originalColumns = this.getOriginalColumns(dashboard, targetState, targetLayout, breakpointId);
const originalSize = this.getOriginalSize(dashboard, targetState, targetLayout, widget, breakpointId);
const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[targetState].layouts[targetLayout], breakpointId);
const widgetLayout = layout.widgets[widget.id];
const targetRow = widgetLayout.row;
const targetColumn = widgetLayout.col;
if (isRemoveWidget) {
this.removeWidgetFromLayout(dashboard, targetState, targetLayout, widget.id, breakpointId);
}
this.addWidgetToLayout(dashboard, targetState, targetLayout, newWidget, originalColumns, originalSize,
targetRow, targetColumn, breakpointId);
return newWidget;
}
getDashboardLayoutConfig(layout: DashboardLayout, breakpointId: string): DashboardLayout {
if (breakpointId !== 'default') {
return layout.breakpoints[breakpointId];
}
return layout;
}
getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, breakpointId: string): number {
let originalColumns = 24;
let gridSettings = null;
const state = dashboard.configuration.states[sourceState];
const layoutCount = Object.keys(state.layouts).length;
if (state) {
const layout = this.getDashboardLayoutConfig(state.layouts[sourceLayout], breakpointId);
if (layout) {
gridSettings = layout.gridSettings;
}
}
if (gridSettings && gridSettings.columns) {
originalColumns = gridSettings.columns;
}
originalColumns = originalColumns * layoutCount;
return originalColumns;
}
getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpointId: string): WidgetSize {
const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[sourceState].layouts[sourceLayout], breakpointId);
const widgetLayout = layout.widgets[widget.id];
return {
sizeX: widgetLayout.sizeX,
sizeY: widgetLayout.sizeY
};
}
private loadSystemBreakpoints() {
this.systemBreakpoints = {};
const dashboardMediaBreakpointIds: BreakpointSystemId[] = ['xs', 'sm', 'md', 'lg', 'xl'];
dashboardMediaBreakpointIds.forEach(breakpoint => {
const value = MediaBreakpoints[breakpoint];
const minWidth = value.match(/min-width:\s*(\d+)px/)?.[1];
const maxWidth = value.match(/max-width:\s*(\d+)px/)?.[1];
this.systemBreakpoints[breakpoint] = ({
id: breakpoint,
minWidth: minWidth ? Number(minWidth) : undefined,
maxWidth: maxWidth ? Number(maxWidth) : undefined,
value
});
});
}
getListBreakpoint(): BreakpointInfo[] {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
const breakpointsList = Object.values(this.systemBreakpoints);
breakpointsList.unshift({id: 'default'});
return breakpointsList;
}
getBreakpoints(): string[] {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return Object.values(this.systemBreakpoints).map(item => item.value);
}
getBreakpointInfoByValue(breakpointValue: string): BreakpointInfo {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return Object.values(this.systemBreakpoints).find(item => item.value === breakpointValue);
}
getBreakpointInfoById(breakpointId: BreakpointId): BreakpointInfo {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return this.systemBreakpoints[breakpointId];
}
getBreakpointName(breakpointId: BreakpointId): string {
if (breakpointIdTranslationMap.has(breakpointId)) {
return this.translate.instant(breakpointIdTranslationMap.get(breakpointId));
}
return breakpointId;
}
getBreakpointIcon(breakpointId: BreakpointId): string {
if (breakpointIdIconMap.has(breakpointId)) {
return breakpointIdIconMap.get(breakpointId);
}
return 'desktop_windows';
}
getBreakpointSizeDescription(breakpointId: BreakpointId): string {
const currentData = this.getBreakpointInfoById(breakpointId);
const minStr = isDefined(currentData?.minWidth) ? `min ${currentData.minWidth}px` : '';
const maxStr = isDefined(currentData?.maxWidth) ? `max ${currentData.maxWidth}px` : '';
return minStr && maxStr ? `${minStr} - ${maxStr}` : `${minStr}${maxStr}`;
}
} }

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

@ -56,6 +56,7 @@ export interface WidgetReference {
widgetId: string; widgetId: string;
originalSize: WidgetSize; originalSize: WidgetSize;
originalColumns: number; originalColumns: number;
breakpoint: string;
} }
export interface RuleNodeConnection { export interface RuleNodeConnection {
@ -85,7 +86,8 @@ export class ItemBufferService {
private ruleChainService: RuleChainService, private ruleChainService: RuleChainService,
private utils: UtilsService) {} private utils: UtilsService) {}
public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: string): WidgetItem {
const aliasesInfo: AliasesInfo = { const aliasesInfo: AliasesInfo = {
datasourceAliases: {}, datasourceAliases: {},
targetDeviceAlias: null targetDeviceAlias: null
@ -93,8 +95,8 @@ export class ItemBufferService {
const filtersInfo: FiltersInfo = { const filtersInfo: FiltersInfo = {
datasourceFilters: {} datasourceFilters: {}
}; };
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources; const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources;
if (widget.config && dashboard.configuration if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) { && dashboard.configuration.entityAliases) {
@ -146,13 +148,14 @@ export class ItemBufferService {
}; };
} }
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_ITEM, widgetItem); this.storeSet(WIDGET_ITEM, widgetItem);
} }
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); widget: Widget, breakpoint: string): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_REFERENCE, widgetReference); this.storeSet(WIDGET_REFERENCE, widgetReference);
} }
@ -160,11 +163,11 @@ export class ItemBufferService {
return this.storeHas(WIDGET_ITEM); return this.storeHas(WIDGET_ITEM);
} }
public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId, breakpoint: string): boolean {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) { if (widgetReference) {
if (widgetReference.dashboardId === dashboard.id.id) { if (widgetReference.dashboardId === dashboard.id.id) {
if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout || widgetReference.breakpoint !== breakpoint)
&& dashboard.configuration.widgets[widgetReference.widgetId]) { && dashboard.configuration.widgets[widgetReference.widgetId]) {
return true; return true;
} }
@ -174,7 +177,9 @@ export class ItemBufferService {
} }
public pasteWidget(targetDashboard: Dashboard, targetState: string, public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition, targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition,
onAliasesUpdateFunction: () => void, onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<Widget> { onFiltersUpdateFunction: () => void): Observable<Widget> {
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM); const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
@ -194,7 +199,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState, return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo, filtersInfo, targetLayout, widget, aliasesInfo, filtersInfo,
onAliasesUpdateFunction, onFiltersUpdateFunction, onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, targetRow, targetColumn).pipe( originalColumns, originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget) map(() => widget)
); );
} else { } else {
@ -202,8 +207,11 @@ export class ItemBufferService {
} }
} }
public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, public pasteWidgetReference(targetDashboard: Dashboard,
targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> { targetState: string,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition): Observable<Widget> {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) { if (widgetReference) {
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
@ -219,7 +227,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState, return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null, targetLayout, widget, null,
null, null, null, originalColumns, null, null, null, originalColumns,
originalSize, targetRow, targetColumn).pipe( originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget) map(() => widget)
); );
} else { } else {
@ -239,7 +247,8 @@ export class ItemBufferService {
originalColumns: number, originalColumns: number,
originalSize: WidgetSize, originalSize: WidgetSize,
row: number, row: number,
column: number): Observable<Dashboard> { column: number,
breakpoint = 'default'): Observable<Dashboard> {
let theDashboard: Dashboard; let theDashboard: Dashboard;
if (dashboard) { if (dashboard) {
theDashboard = dashboard; theDashboard = dashboard;
@ -270,7 +279,7 @@ export class ItemBufferService {
} }
} }
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column); originalColumns, originalSize, row, column, breakpoint);
if (callAliasUpdateFunction) { if (callAliasUpdateFunction) {
onAliasesUpdateFunction(); onAliasesUpdateFunction();
} }
@ -387,35 +396,6 @@ export class ItemBufferService {
return ruleChainImport; return ruleChainImport;
} }
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 { private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo {
return { return {
alias: entityAlias.alias, alias: entityAlias.alias,
@ -432,16 +412,17 @@ export class ItemBufferService {
} }
private prepareWidgetReference(dashboard: Dashboard, sourceState: string, private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference { sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
return { return {
dashboardId: dashboard.id.id, dashboardId: dashboard.id.id,
sourceState, sourceState,
sourceLayout, sourceLayout,
widgetId: widget.id, widgetId: widget.id,
originalSize, originalSize,
originalColumns originalColumns,
breakpoint
}; };
} }

3
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -44,7 +44,8 @@
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[hideHeader]="hideHeader" [hideHeader]="hideHeader"
[scada]="data.scada" [showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
isAdd isAdd
formControlName="widgetConfig"> formControlName="widgetConfig">
</tb-widget-config> </tb-widget-config>

8
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -37,7 +37,8 @@ export interface AddWidgetDialogData {
stateController: IStateController; stateController: IStateController;
widget: Widget; widget: Widget;
widgetInfo: WidgetInfo; widgetInfo: WidgetInfo;
scada: boolean; showLayoutConfig: boolean;
isDefaultBreakpoint: boolean;
} }
@Component({ @Component({
@ -60,6 +61,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
stateController: IStateController; stateController: IStateController;
widget: Widget; widget: Widget;
showLayoutConfig = true;
isDefaultBreakpoint = true;
widgetConfig: WidgetConfigComponentData; widgetConfig: WidgetConfigComponentData;
previewMode = false; previewMode = false;
@ -92,6 +96,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.aliasController = this.data.aliasController; this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController; this.stateController = this.data.stateController;
this.widget = this.data.widget; this.widget = this.data.widget;
this.showLayoutConfig = this.data.showLayoutConfig;
this.isDefaultBreakpoint = this.data.isDefaultBreakpoint;
const widgetInfo = this.data.widgetInfo; const widgetInfo = this.data.widgetInfo;

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

@ -29,22 +29,11 @@
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
</tb-user-menu> </tb-user-menu>
<ng-container *ngIf="isEdit"> <ng-container *ngIf="isEdit">
<div style="flex: 1 1 100%" fxHide.gt-sm></div>
<div fxFlex.lt-md fxLayout="row" <div fxFlex.lt-md fxLayout="row"
fxLayoutAlign.gt-sm="start center" fxLayoutAlign.gt-sm="start center"
fxLayoutAlign="end center" fxLayoutGap.lt-lg="6px" fxLayoutGap.gt-md="12px"> fxLayoutAlign="end center" fxLayoutGap.lt-lg="3px" fxLayoutGap.lg="6px" fxLayoutGap.gt-lg="12px">
<button fxHide.gt-md mat-icon-button <mat-divider class="tb-toolbar-divider" vertical></mat-divider>
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
</button>
<button fxHide.lt-lg mat-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
{{'dashboard.states-short' | translate}}
</button>
<button fxHide.gt-md mat-icon-button <button fxHide.gt-md mat-icon-button
matTooltip="{{'layout.manage' | translate}}" matTooltip="{{'layout.manage' | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
@ -58,6 +47,9 @@
<mat-icon>view_compact</mat-icon> <mat-icon>view_compact</mat-icon>
{{'layout.layouts' | translate}} {{'layout.layouts' | translate}}
</button> </button>
<tb-select-dashboard-breakpoint
[dashboardCtrl]="this">
</tb-select-dashboard-breakpoint>
</div> </div>
</ng-container> </ng-container>
<tb-states-component fxFlex.lt-md <tb-states-component fxFlex.lt-md
@ -70,6 +62,21 @@
[syncStateWithQueryParam]="syncStateWithQueryParam" [syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states"> [states]="dashboardConfiguration.states">
</tb-states-component> </tb-states-component>
<ng-container *ngIf="isEdit">
<button fxHide.gt-md mat-icon-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
</button>
<button fxHide.lt-lg mat-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
{{'dashboard.states-short' | translate}}
</button>
</ng-container>
<img *ngIf="showDashboardLogo()" [src]="dashboardLogo | image | async" <img *ngIf="showDashboardLogo()" [src]="dashboardLogo | image | async"
aria-label="dashboard_logo" class="dashboard_logo"/> aria-label="dashboard_logo" class="dashboard_logo"/>
</div> </div>
@ -88,7 +95,7 @@
(click)="toggleLayouts()"> (click)="toggleLayouts()">
<mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon> <mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
</button> </button>
<button [fxShow]="!hideFullscreenButton()" mat-icon-button <button [fxShow]="!hideFullscreenButton()" mat-icon-button fxHide.xs
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen"> (click)="isFullscreen = !isFullscreen">
@ -230,13 +237,13 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-container *ngIf="isEdit"> <ng-container *ngIf="isEdit">
<button fxHide.gt-xs mat-icon-button <button fxHide.gt-md mat-icon-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}" matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="addWidget($event)"> (click)="addWidget($event)">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
<button fxHide.lt-sm mat-stroked-button <button fxHide.lt-lg mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}" matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="addWidget($event)"> (click)="addWidget($event)">
@ -282,7 +289,7 @@
</mat-form-field> </mat-form-field>
</section> </section>
<mat-drawer-container class="tb-dashboard-layouts" fxFlex <mat-drawer-container class="tb-dashboard-layouts" fxFlex
[ngClass]="{ 'tb-shrinked' : isEditingWidget }"> [class]="{'tb-shrinked' : isEditingWidget}">
<mat-drawer *ngIf="layouts.right.show" <mat-drawer *ngIf="layouts.right.show"
id="tb-right-layout" id="tb-right-layout"
[ngStyle]="{minWidth: rightLayoutSize.width, [ngStyle]="{minWidth: rightLayoutSize.width,
@ -308,9 +315,11 @@
</mat-drawer> </mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show" <mat-drawer-content [fxShow]="layouts.main.show"
id="tb-main-layout" id="tb-main-layout"
[ngStyle]="{width: mainLayoutSize.width, [style]="{width: mainLayoutSize.width,
height: mainLayoutSize.height}"> height: mainLayoutSize.height}">
<tb-dashboard-layout <tb-dashboard-layout
[style.max-width]="mainLayoutSize.maxWidth"
[style.min-width]="mainLayoutSize.minWidth"
[dashboardCheatSheet]="cheatSheetComponent" [dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx" [layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx" [dashboardCtx]="dashboardCtx"
@ -321,6 +330,11 @@
[parentDashboard]="parentDashboard" [parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent"> [popoverComponent]="popoverComponent">
</tb-dashboard-layout> </tb-dashboard-layout>
<div class="layout-filler"
*ngIf="isEdit"
[fxShow]="showMainLayoutFiller">
{{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</div>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>
<section data-html2canvas-ignore fxLayout="row" class="layout-wrap tb-header-buttons tb-enter-edit-mode" fxLayoutAlign="start end" *ngIf="!readonly && (hideToolbar || widgetEditMode)"> <section data-html2canvas-ignore fxLayout="row" class="layout-wrap tb-header-buttons tb-enter-edit-mode" fxLayoutAlign="start end" *ngIf="!readonly && (hideToolbar || widgetEditMode)">
@ -380,7 +394,8 @@
[widgetEditMode]="widgetEditMode" [widgetEditMode]="widgetEditMode"
[widget]="editingWidget" [widget]="editingWidget"
[widgetLayout]="editingWidgetLayout" [widgetLayout]="editingWidgetLayout"
[scada]="editingLayoutCtx?.gridSettings?.layoutType === LayoutType.scada" [showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)"
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'"
(revertWidgetConfig)="onRevertWidgetEdit()" (revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()"> (applyWidgetConfig)="saveWidget()">
</tb-edit-widget> </tb-edit-widget>

20
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -131,6 +131,26 @@ div.tb-dashboard-page {
width: max(40%, calc(100% - 1192px)); width: max(40%, calc(100% - 1192px));
} }
} }
#tb-main-layout {
display: flex;
tb-dashboard-layout {
width: 100%;
}
.layout-filler {
flex: 1;
background: repeating-linear-gradient(-16deg, #d3d3d3, #d3d3d3 3px, #e0e0e0 3px, #e0e0e0 11px);
border-bottom: 2px dashed #bdbdbd;
border-right: 2px dashed #bdbdbd;
writing-mode: vertical-rl;
text-align: center;
transform: rotate(180deg);
color: #616161;
letter-spacing: .25px;
}
}
} }
mat-drawer.tb-details-drawer { mat-drawer.tb-details-drawer {

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

@ -40,8 +40,9 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import { import {
BreakpointId,
BreakpointInfo,
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
DashboardLayoutId, DashboardLayoutId,
@ -52,6 +53,7 @@ import {
GridSettings, GridSettings,
LayoutDimension, LayoutDimension,
LayoutType, LayoutType,
ViewFormatType,
WidgetLayout WidgetLayout
} from '@app/shared/models/dashboard.models'; } from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
@ -67,7 +69,7 @@ import {
IDashboardController, IDashboardController,
LayoutWidgetsArray LayoutWidgetsArray
} from './dashboard-page.models'; } from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
@ -84,7 +86,7 @@ import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller'; import { AliasController } from '@core/api/alias-controller';
import { Observable, of, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { import {
@ -93,7 +95,6 @@ import {
WidgetContextMenuItem WidgetContextMenuItem
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service'; import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { UntypedFormBuilder } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service'; import { ItemBufferService } from '@core/services/item-buffer.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
@ -139,14 +140,14 @@ import {
DashboardImageDialogData, DashboardImageDialogData,
DashboardImageDialogResult DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component'; } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { SafeUrl } from '@angular/platform-browser';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { catchError, map, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, skip, tap } from 'rxjs/operators';
import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models'; import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -262,7 +263,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addingLayoutCtx: DashboardPageLayoutContext; addingLayoutCtx: DashboardPageLayoutContext;
mainLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'}; mainLayoutSize: {width: string; height: string; maxWidth: string; minWidth: string} =
{width: '100%', height: '100%', maxWidth: '100%', minWidth: '100%'};
rightLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'}; rightLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'};
private dashboardLogoCache: SafeUrl; private dashboardLogoCache: SafeUrl;
@ -275,6 +277,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
getDashboard: () => this.dashboard, getDashboard: () => this.dashboard,
dashboardTimewindow: null, dashboardTimewindow: null,
state: null, state: null,
breakpoint: null,
stateController: null, stateController: null,
stateChanged: null, stateChanged: null,
stateId: null, stateId: null,
@ -287,26 +290,32 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
show: false, show: false,
layoutCtx: { layoutCtx: {
id: 'main', id: 'main',
breakpoint: 'default',
widgets: null, widgets: null,
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: true, ignoreLoading: true,
ctrl: null, ctrl: null,
dashboardCtrl: this, dashboardCtrl: this,
displayGrid: 'onDrag&Resize' displayGrid: 'onDrag&Resize',
layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
} }
}, },
right: { right: {
show: false, show: false,
layoutCtx: { layoutCtx: {
id: 'right', id: 'right',
breakpoint: 'default',
widgets: null, widgets: null,
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: true, ignoreLoading: true,
ctrl: null, ctrl: null,
dashboardCtrl: this, dashboardCtrl: this,
displayGrid: 'onDrag&Resize' displayGrid: 'onDrag&Resize',
layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
} }
} }
}; };
@ -340,6 +349,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent; @ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent;
private changeMobileSize = new Subject<boolean>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
@ -348,7 +359,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private router: Router, private router: Router,
private utils: UtilsService, private utils: UtilsService,
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private authService: AuthService,
private entityService: EntityService, private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private widgetComponentService: WidgetComponentService, private widgetComponentService: WidgetComponentService,
@ -356,7 +366,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private importExport: ImportExportService, private importExport: ImportExportService,
private mobileService: MobileService, private mobileService: MobileService,
private fb: UntypedFormBuilder,
private dialog: MatDialog, private dialog: MatDialog,
public translate: TranslateService, public translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
@ -366,12 +375,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer,
public elRef: ElementRef, public elRef: ElementRef,
private injector: Injector) { private injector: Injector) {
super(store); super(store);
if (isDefinedAndNotNull(embeddedValue)) { if (isDefinedAndNotNull(this.embeddedValue)) {
this.embedded = embeddedValue; this.embedded = this.embeddedValue;
} }
} }
@ -411,13 +419,42 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
)); ));
} }
this.rxSubscriptions.push(this.breakpointObserver this.rxSubscriptions.push(
.observe(MediaBreakpoints['gt-sm']) this.changeMobileSize.pipe(
.subscribe((state: BreakpointState) => { distinctUntilChanged(),
this.isMobile = !state.matches; ).subscribe((state) => {
this.isMobile = state;
this.updateLayoutSizes();
})
);
this.rxSubscriptions.push(
this.breakpointObserver.observe(
this.dashboardUtils.getBreakpoints()
).pipe(
map(value => this.parseBreakpointsResponse(value.breakpoints)),
tap((value) => {
this.dashboardCtx.breakpoint = value.id;
this.changeMobileSize.next(this.isMobileSize(value));
}),
distinctUntilChanged((_, next) => {
if (this.layouts.right.show || this.isEdit) {
return true;
}
let nextBreakpointConfiguration: BreakpointId = 'default';
if (!!this.layouts.main.layoutCtx.layoutData?.[next.id]) {
nextBreakpointConfiguration = next.id;
}
return this.layouts.main.layoutCtx.breakpoint === nextBreakpointConfiguration;
}),
skip(1)
).subscribe(() => {
this.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint();
this.updateLayoutSizes(); this.updateLayoutSizes();
} }
)); )
);
if (this.isMobileApp && this.syncStateWithQueryParam) { if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.registerToggleLayoutFunction(() => { this.mobileService.registerToggleLayoutFunction(() => {
setTimeout(() => { setTimeout(() => {
@ -565,6 +602,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (this.dashboardResize$) { if (this.dashboardResize$) {
this.dashboardResize$.disconnect(); this.dashboardResize$.disconnect();
} }
this.layouts.main.layoutCtx.layoutDataChanged.unsubscribe();
this.layouts.right.layoutCtx.layoutDataChanged.unsubscribe();
} }
public runChangeDetection() { public runChangeDetection() {
@ -588,7 +627,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
public hideFullscreenButton(): boolean { public hideFullscreenButton(): boolean {
return (this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode || this.isEdit); return (this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode);
} }
public toolbarAlwaysOpen(): boolean { public toolbarAlwaysOpen(): boolean {
@ -703,7 +742,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened); this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened);
} }
private updateLayoutSizes() { public updateLayoutSizes() {
let changeMainLayoutSize = false; let changeMainLayoutSize = false;
let changeRightLayoutSize = false; let changeRightLayoutSize = false;
if (this.dashboardCtx.state) { if (this.dashboardCtx.state) {
@ -718,6 +757,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private updateMainLayoutSize(): boolean { private updateMainLayoutSize(): boolean {
const prevMainLayoutWidth = this.mainLayoutSize.width; const prevMainLayoutWidth = this.mainLayoutSize.width;
const prevMainLayoutHeight = this.mainLayoutSize.height; const prevMainLayoutHeight = this.mainLayoutSize.height;
const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth;
if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') { if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
this.mainLayoutSize.width = '100%'; this.mainLayoutSize.width = '100%';
} else { } else {
@ -728,7 +768,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} else { } else {
this.mainLayoutSize.height = '0px'; this.mainLayoutSize.height = '0px';
} }
return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height; if (this.isEdit && !this.isEditingWidget) {
const xOffset = this.dashboardContainer.nativeElement.getBoundingClientRect().x;
const breakpoint = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint);
let maxWidth: string;
if (breakpoint?.maxWidth) {
if (this.isMobileSize(breakpoint)) {
maxWidth = `${breakpoint.maxWidth}px`;
} else {
maxWidth = `${breakpoint.maxWidth - xOffset}px`;
}
} else {
maxWidth = '100%';
}
const minWidth = breakpoint?.minWidth ? `${breakpoint.minWidth}px` : undefined;
this.mainLayoutSize.maxWidth = maxWidth;
this.mainLayoutSize.minWidth = minWidth;
} else {
this.mainLayoutSize.maxWidth = '100%';
this.mainLayoutSize.minWidth = undefined;
}
return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height ||
prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth;
} }
private updateRightLayoutSize(): boolean { private updateRightLayoutSize(): boolean {
@ -864,6 +928,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: { data: {
settings: deepClone(this.dashboard.configuration.settings), settings: deepClone(this.dashboard.configuration.settings),
gridSettings, gridSettings,
breakpointId: this.layouts.main.layoutCtx.breakpoint
} }
}).afterClosed().subscribe((data) => { }).afterClosed().subscribe((data) => {
if (data) { if (data) {
@ -1055,21 +1120,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateLayout(layout, layoutInfo); this.updateLayout(layout, layoutInfo);
} else { } else {
layout.show = false; layout.show = false;
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); this.updateLayout(layout, {default: {widgetIds: [], widgetLayouts: {}, gridSettings: null}});
} }
} }
} }
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
if (layoutInfo.gridSettings) { layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.gridSettings = layoutInfo.gridSettings; layout.layoutCtx.layoutDataChanged.next();
} layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show);
layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (layout.show && layout.layoutCtx.ctrl) {
layout.layoutCtx.ctrl.reload();
}
layout.layoutCtx.ignoreLoading = true;
this.updateLayoutSizes(); this.updateLayoutSizes();
} }
@ -1196,8 +1255,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) { private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) {
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget); const layoutCtx = this.layouts[layoutId].layoutCtx;
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id); this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget, undefined,
undefined, -1, -1, layoutCtx.breakpoint);
layoutCtx.widgets.addWidgetId(widget.id);
this.runChangeDetection(); this.runChangeDetection();
} }
@ -1257,10 +1318,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
col: 0 col: 0
}; };
newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget); newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget);
let isDefaultBreakpoint = true;
if (this.addingLayoutCtx?.breakpoint) {
isDefaultBreakpoint = this.addingLayoutCtx.breakpoint === 'default';
} else if (!this.layouts.right.show) {
isDefaultBreakpoint = this.layouts.main.layoutCtx.breakpoint === 'default';
}
const scada = this.isAddingToScadaLayout(); const scada = this.isAddingToScadaLayout();
if (scada) { if (scada) {
newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget); newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget);
} }
let showLayoutConfig = true;
if (scada || this.layouts.right.show || !this.showLayoutConfigInEdit(this.layouts.main.layoutCtx)) {
showLayoutConfig = false;
}
if (widgetTypeInfo.typeParameters.useCustomDatasources) { if (widgetTypeInfo.typeParameters.useCustomDatasources) {
this.addWidgetToDashboard(newWidget); this.addWidgetToDashboard(newWidget);
} else { } else {
@ -1276,7 +1347,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
stateController: this.dashboardCtx.stateController, stateController: this.dashboardCtx.stateController,
widget: newWidget, widget: newWidget,
widgetInfo: widgetTypeInfo, widgetInfo: widgetTypeInfo,
scada showLayoutConfig,
isDefaultBreakpoint
} }
}).afterClosed().subscribe((addedWidget) => { }).afterClosed().subscribe((addedWidget) => {
if (addedWidget) { if (addedWidget) {
@ -1355,18 +1427,38 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
} }
showLayoutConfigInEdit(layoutCtx: DashboardPageLayoutContext): boolean {
return layoutCtx?.gridSettings?.layoutType === LayoutType.divider ||
layoutCtx?.gridSettings?.layoutType === LayoutType.default &&
(layoutCtx?.breakpoint === 'default' ||
layoutCtx?.breakpoint !== 'default' && layoutCtx?.gridSettings?.viewFormat === ViewFormatType.list);
}
replaceReferenceWithWidgetCopy($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
$event.stopPropagation();
const isRemove = layoutCtx.widgets.removeWidgetId(widget.id);
const widgetCopy = this.dashboardUtils.replaceReferenceWithWidgetCopy(widget, this.dashboard, this.dashboardCtx.state,
layoutCtx.id, layoutCtx.breakpoint, isRemove);
layoutCtx.widgets.addWidgetId(widgetCopy.id);
this.runChangeDetection();
}
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
this.itembuffer.copyWidget(this.dashboard, this.itembuffer.copyWidget(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget); this.dashboardCtx.state, layoutCtx.id, widget, layoutCtx.breakpoint);
} }
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
this.itembuffer.copyWidgetReference(this.dashboard, this.itembuffer.copyWidgetReference(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget); this.dashboardCtx.state, layoutCtx.id, widget, layoutCtx.breakpoint);
} }
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint,
pos, this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe( pos, this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe(
(widget) => { (widget) => {
layoutCtx.widgets.addWidgetId(widget.id); layoutCtx.widgets.addWidgetId(widget.id);
@ -1375,7 +1467,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint,
pos).subscribe( pos).subscribe(
(widget) => { (widget) => {
layoutCtx.widgets.addWidgetId(widget.id); layoutCtx.widgets.addWidgetId(widget.id);
@ -1397,7 +1489,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (layoutCtx.widgets.removeWidgetId(widget.id)) { if (layoutCtx.widgets.removeWidgetId(widget.id)) {
this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget.id); this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id,
widget.id, layoutCtx.breakpoint);
this.runChangeDetection(); this.runChangeDetection();
} }
} }
@ -1406,7 +1499,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) { exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) {
$event.stopPropagation(); $event.stopPropagation();
this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle); this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle, layoutCtx.breakpoint);
} }
dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext) { dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext) {
@ -1462,7 +1555,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
action: ($event) => { action: ($event) => {
layoutCtx.ctrl.pasteWidgetReference($event); layoutCtx.ctrl.pasteWidgetReference($event);
}, },
enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id), enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint),
value: 'action.paste-reference', value: 'action.paste-reference',
icon: 'content_paste', icon: 'content_paste',
shortcut: 'M-I' shortcut: 'M-I'
@ -1482,7 +1575,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return dashboardContextActions; return dashboardContextActions;
} }
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem> { prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
const widgetContextActions: Array<WidgetContextMenuItem> = []; const widgetContextActions: Array<WidgetContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget) { if (this.isEdit && !this.isEditingWidget) {
widgetContextActions.push( widgetContextActions.push(
@ -1495,6 +1588,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
icon: 'edit' icon: 'edit'
} }
); );
if (isReference) {
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.replaceReferenceWithWidgetCopy(event, layoutCtx, currentWidget);
},
enabled: true,
value: 'widget.replace-reference-with-widget-copy',
icon: 'mdi:file-replace-outline'
}
);
}
if (!this.widgetEditMode) { if (!this.widgetEditMode) {
widgetContextActions.push( widgetContextActions.push(
{ {
@ -1653,4 +1758,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}); });
} }
} }
get showMainLayoutFiller(): boolean {
const layoutMaxWidth = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint)?.maxWidth || Infinity;
const dashboardMaxWidth = this.dashboardUtils.getBreakpointInfoById(this.dashboardCtx.breakpoint)?.maxWidth || Infinity;
return !this.layouts.right.show && layoutMaxWidth < dashboardMaxWidth && !this.isEditingWidget;
}
get currentBreakpointValue(): string {
return this.dashboardUtils.getBreakpointSizeDescription(this.layouts.main.layoutCtx.breakpoint);
}
private parseBreakpointsResponse(breakpoints: {[key: string]: boolean}): BreakpointInfo {
const activeBreakpoints: BreakpointInfo[] = [];
Object.keys(breakpoints).map((key) => {
if (breakpoints[key]) {
activeBreakpoints.push(this.dashboardUtils.getBreakpointInfoByValue(key));
}
});
return activeBreakpoints.pop();
}
private isMobileSize(breakpoint: BreakpointInfo): boolean {
if (breakpoint?.maxWidth) {
return breakpoint.maxWidth < 960;
}
return false;
}
} }

21
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts

@ -14,14 +14,22 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Dashboard, DashboardLayoutId, GridSettings, WidgetLayouts } from '@app/shared/models/dashboard.models'; import {
BreakpointId,
Dashboard,
DashboardLayoutId,
DashboardLayoutInfo,
GridSettings,
WidgetLayouts
} from '@app/shared/models/dashboard.models';
import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; import { Widget, WidgetPosition } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models'; import { ILayoutController } from './layout/layout.models';
import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { ElementRef } from '@angular/core';
export declare type DashboardPageScope = 'tenant' | 'customer'; export declare type DashboardPageScope = 'tenant' | 'customer';
@ -35,6 +43,7 @@ export interface DashboardPageInitData {
export interface DashboardContext { export interface DashboardContext {
instanceId: string; instanceId: string;
state: string; state: string;
breakpoint: BreakpointId;
getDashboard: () => Dashboard; getDashboard: () => Dashboard;
dashboardTimewindow: Timewindow; dashboardTimewindow: Timewindow;
aliasController: IAliasController; aliasController: IAliasController;
@ -46,17 +55,20 @@ export interface DashboardContext {
export interface IDashboardController { export interface IDashboardController {
dashboardCtx: DashboardContext; dashboardCtx: DashboardContext;
dashboardContainer: ElementRef;
elRef: ElementRef;
openRightLayout(); openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean); openDashboardState(stateId: string, openRightLayout: boolean);
addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
replaceReferenceWithWidgetCopy($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string); exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext); dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>; prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>;
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem>; prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem>;
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
@ -65,6 +77,9 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext { export interface DashboardPageLayoutContext {
id: DashboardLayoutId; id: DashboardLayoutId;
layoutData: DashboardLayoutInfo;
layoutDataChanged: BehaviorSubject<void>;
breakpoint: BreakpointId;
widgets: LayoutWidgetsArray; widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts; widgetLayouts: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;

43
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -105,18 +105,18 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<div *ngIf="gridSettings" class="tb-form-panel no-border no-padding" [formGroup]="gridSettingsFormGroup"> <div *ngIf="gridSettings" class="tb-form-panel no-border no-padding" [formGroup]="gridSettingsFormGroup">
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>dashboard.layout</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="layoutType">
<mat-option *ngFor="let type of layoutTypes" [value]="type">
{{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.layout-settings</div> <div class="tb-form-panel-title" translate [translateParams]="{type: layoutSettingsType}">dashboard.layout-settings-type</div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dashboard.view-format</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="viewFormat">
<mat-option *ngFor="let type of viewFormatTypes" [value]="type">
{{ viewFormatTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>dashboard.columns-count</div> <div translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()" <mat-form-field *ngIf="!isScada()"
@ -188,6 +188,25 @@
{{ 'dashboard.autofill-height' | translate }} {{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>dashboard.background-color</div> <div translate>dashboard.background-color</div>
<tb-color-input asBoxInput <tb-color-input asBoxInput
@ -212,7 +231,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="!isScada() || isRightLayout" class="tb-form-panel stroked"> <div *ngIf="!isScada() && isDefaultBreakpoint" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.mobile-layout</div> <div class="tb-form-panel-title" translate>dashboard.mobile-layout</div>
<div *ngIf="isRightLayout" class="tb-form-row"> <div *ngIf="isRightLayout" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst"> <mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst">

148
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -24,20 +24,26 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
BreakpointId,
DashboardSettings, DashboardSettings,
GridSettings, GridSettings,
LayoutType, LayoutType,
layoutTypes, layoutTypeTranslationMap, StateControllerId,
StateControllerId ViewFormatType,
viewFormatTypes,
viewFormatTypeTranslationMap
} from '@app/shared/models/dashboard.models'; } from '@app/shared/models/dashboard.models';
import { isDefined, isUndefined } from '@core/utils'; import { isDefined, isUndefined } from '@core/utils';
import { StatesControllerService } from './states/states-controller.service'; import { StatesControllerService } from './states/states-controller.service';
import { merge } from 'rxjs'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { merge, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export interface DashboardSettingsDialogData { export interface DashboardSettingsDialogData {
settings?: DashboardSettings; settings?: DashboardSettings;
gridSettings?: GridSettings; gridSettings?: GridSettings;
isRightLayout?: boolean; isRightLayout?: boolean;
breakpointId?: BreakpointId;
} }
@Component({ @Component({
@ -47,11 +53,10 @@ export interface DashboardSettingsDialogData {
styleUrls: ['./dashboard-settings-dialog.component.scss'] styleUrls: ['./dashboard-settings-dialog.component.scss']
}) })
export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData> export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData>
implements OnInit, ErrorStateMatcher { implements OnDestroy, ErrorStateMatcher {
layoutTypes = layoutTypes; viewFormatTypes = viewFormatTypes;
viewFormatTypeTranslationMap = viewFormatTypeTranslationMap;
layoutTypeTranslations = layoutTypeTranslationMap;
settings: DashboardSettings; settings: DashboardSettings;
gridSettings: GridSettings; gridSettings: GridSettings;
@ -62,10 +67,17 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
stateControllerIds: string[]; stateControllerIds: string[];
layoutSettingsType: string;
submitted = false; submitted = false;
scadaColumns: number[] = []; scadaColumns: number[] = [];
private layoutType = LayoutType.default;
private breakpointId: BreakpointId = 'default';
private destroy$ = new Subject<void>();
private stateControllerTranslationMap = new Map<string, string>([ private stateControllerTranslationMap = new Map<string, string>([
['default', 'dashboard.state-controller-default'], ['default', 'dashboard.state-controller-default'],
]); ]);
@ -77,7 +89,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>, public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: FormBuilder, private fb: FormBuilder,
private translate: TranslateService, private translate: TranslateService,
private statesControllerService: StatesControllerService) { private statesControllerService: StatesControllerService,
private dashboardUtils: DashboardUtilsService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers()); this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers());
@ -85,6 +98,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
this.settings = this.data.settings; this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings; this.gridSettings = this.data.gridSettings;
this.isRightLayout = this.data.isRightLayout; this.isRightLayout = this.data.isRightLayout;
this.breakpointId = this.data.breakpointId;
if (this.settings) { if (this.settings) {
const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle; const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle;
@ -116,14 +130,18 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
disabled: hideToolbar}, []], disabled: hideToolbar}, []],
dashboardCss: [isUndefined(this.settings.dashboardCss) ? '' : this.settings.dashboardCss, []], dashboardCss: [isUndefined(this.settings.dashboardCss) ? '' : this.settings.dashboardCss, []],
}); });
this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe( this.settingsFormGroup.get('stateControllerId').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(stateControllerId: StateControllerId) => { (stateControllerId: StateControllerId) => {
if (stateControllerId !== 'default') { if (stateControllerId !== 'default') {
this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true); this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true);
} }
} }
); );
this.settingsFormGroup.get('showTitle').valueChanges.subscribe( this.settingsFormGroup.get('showTitle').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(showTitleValue: boolean) => { (showTitleValue: boolean) => {
if (showTitleValue) { if (showTitleValue) {
this.settingsFormGroup.get('titleColor').enable(); this.settingsFormGroup.get('titleColor').enable();
@ -132,7 +150,9 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
} }
); );
this.settingsFormGroup.get('showDashboardLogo').valueChanges.subscribe( this.settingsFormGroup.get('showDashboardLogo').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(showDashboardLogoValue: boolean) => { (showDashboardLogoValue: boolean) => {
if (showDashboardLogoValue) { if (showDashboardLogoValue) {
this.settingsFormGroup.get('dashboardLogoUrl').enable(); this.settingsFormGroup.get('dashboardLogoUrl').enable();
@ -141,7 +161,9 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
} }
); );
this.settingsFormGroup.get('hideToolbar').valueChanges.subscribe( this.settingsFormGroup.get('hideToolbar').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(hideToolbarValue: boolean) => { (hideToolbarValue: boolean) => {
if (hideToolbarValue) { if (hideToolbarValue) {
this.settingsFormGroup.get('toolbarAlwaysOpen').disable(); this.settingsFormGroup.get('toolbarAlwaysOpen').disable();
@ -167,6 +189,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
if (this.gridSettings) { if (this.gridSettings) {
if (isDefined(this.gridSettings.layoutType)) {
this.layoutType = this.gridSettings.layoutType;
}
if (this.layoutType !== LayoutType.divider) {
this.layoutSettingsType = this.dashboardUtils.getBreakpointName(this.breakpointId);
} else if (this.isRightLayout) {
this.layoutSettingsType = this.translate.instant('layout.right');
} else {
this.layoutSettingsType = this.translate.instant('layout.left');
}
let columns = 24; let columns = 24;
while (columns <= 1008) { while (columns <= 1008) {
this.scadaColumns.push(columns); this.scadaColumns.push(columns);
@ -174,14 +206,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight; const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight;
this.gridSettingsFormGroup = this.fb.group({ this.gridSettingsFormGroup = this.fb.group({
layoutType: [this.gridSettings.layoutType || LayoutType.default, []],
columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1008)]], columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1008)]],
minColumns: [this.gridSettings.minColumns || this.gridSettings.columns || 24, minColumns: [this.gridSettings.minColumns || this.gridSettings.columns || 24,
[Validators.required, Validators.min(10), Validators.max(1008)]], [Validators.required, Validators.min(10), Validators.max(1008)]],
margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10, margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10,
[Validators.required, Validators.min(0), Validators.max(50)]], [Validators.required, Validators.min(0), Validators.max(50)]],
outerMargin: [isUndefined(this.gridSettings.outerMargin) ? true : this.gridSettings.outerMargin, []], outerMargin: [isUndefined(this.gridSettings.outerMargin) ? true : this.gridSettings.outerMargin, []],
viewFormat: [isUndefined(this.gridSettings.viewFormat) ? ViewFormatType.grid : this.gridSettings.viewFormat, []],
autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []], autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []],
rowHeight: [isUndefined(this.gridSettings.rowHeight) ? 70 : this.gridSettings.rowHeight,
[Validators.required, Validators.min(5), Validators.max(200)]],
backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []], backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []],
backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []], backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []],
backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []], backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []],
@ -194,27 +228,56 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst; isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst;
this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, [])); this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, []));
} }
merge(this.gridSettingsFormGroup.get('layoutType').valueChanges, if (this.isScada()) {
this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges).subscribe( this.gridSettingsFormGroup.get('margin').disable();
() => { this.gridSettingsFormGroup.get('outerMargin').disable();
this.updateGridSettingsFormState(); this.gridSettingsFormGroup.get('viewFormat').disable({emitEvent: false});
this.gridSettingsFormGroup.get('rowHeight').disable();
this.gridSettingsFormGroup.get('autoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileAutoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
const columnsFields: number = this.gridSettingsFormGroup.get('columns').value;
if (columnsFields % 24 !== 0) {
const newColumns = Math.min(1008, 24 * Math.ceil(columnsFields / 24));
this.gridSettingsFormGroup.get('columns').patchValue(newColumns);
} }
); } else {
this.updateGridSettingsFormState(); merge(
this.gridSettingsFormGroup.get('viewFormat').valueChanges,
this.breakpointId !== 'default'
? this.gridSettingsFormGroup.get('autoFillHeight').valueChanges
: this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges
).pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.updateGridSettingsFormState();
});
this.updateGridSettingsFormState();
}
if (this.layoutType === LayoutType.default && this.breakpointId !== 'default' || this.isScada()) {
this.gridSettingsFormGroup.get('mobileAutoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
}
} else { } else {
this.gridSettingsFormGroup = this.fb.group({}); this.gridSettingsFormGroup = this.fb.group({});
} }
} }
ngOnInit(): void { ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
isScada() { isScada() {
if (this.gridSettingsFormGroup) { return this.layoutType === LayoutType.scada;
return this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada; }
} else {
return false; get isDefaultLayout() {
} return this.layoutType === LayoutType.default;
}
get isDefaultBreakpoint(): boolean {
return this.breakpointId === 'default';
} }
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -248,29 +311,28 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
private updateGridSettingsFormState() { private updateGridSettingsFormState() {
const isScada: boolean = this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada; if (this.breakpointId === 'default') {
if (isScada) {
this.gridSettingsFormGroup.get('margin').disable();
this.gridSettingsFormGroup.get('outerMargin').disable();
this.gridSettingsFormGroup.get('autoFillHeight').disable();
this.gridSettingsFormGroup.get('mobileAutoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
const columns: number = this.gridSettingsFormGroup.get('columns').value;
if (columns % 24 !== 0) {
const newColumns = Math.min(1008, 24 * Math.ceil(columns / 24));
this.gridSettingsFormGroup.get('columns').patchValue(newColumns);
}
} else {
this.gridSettingsFormGroup.get('margin').enable();
this.gridSettingsFormGroup.get('outerMargin').enable();
this.gridSettingsFormGroup.get('autoFillHeight').enable();
this.gridSettingsFormGroup.get('mobileAutoFillHeight').enable({emitEvent: false});
const mobileAutoFillHeight: boolean = this.gridSettingsFormGroup.get('mobileAutoFillHeight').value; const mobileAutoFillHeight: boolean = this.gridSettingsFormGroup.get('mobileAutoFillHeight').value;
if (mobileAutoFillHeight) { if (mobileAutoFillHeight) {
this.gridSettingsFormGroup.get('mobileRowHeight').disable(); this.gridSettingsFormGroup.get('mobileRowHeight').disable();
} else { } else {
this.gridSettingsFormGroup.get('mobileRowHeight').enable(); this.gridSettingsFormGroup.get('mobileRowHeight').enable();
} }
} else {
const autoFillHeight: boolean = this.gridSettingsFormGroup.get('autoFillHeight').value;
const viewFormat: ViewFormatType = this.gridSettingsFormGroup.get('viewFormat').value;
if (viewFormat !== ViewFormatType.list || autoFillHeight) {
this.gridSettingsFormGroup.get('rowHeight').disable();
} else {
this.gridSettingsFormGroup.get('rowHeight').enable();
}
if (viewFormat === ViewFormatType.list) {
this.gridSettingsFormGroup.get('columns').disable();
this.gridSettingsFormGroup.get('minColumns').disable();
} else {
this.gridSettingsFormGroup.get('columns').enable();
this.gridSettingsFormGroup.get('minColumns').enable();
}
} }
} }
} }

13
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss

@ -174,14 +174,23 @@ tb-dashboard-toolbar {
@media #{$mat-sm} { @media #{$mat-sm} {
gap: 6px; gap: 6px;
&.tb-left-panel {
gap: 3px;
}
} }
@media #{$mat-md} { @media #{$mat-md} {
gap: 6px; gap: 3px;
&.tb-left-panel {
gap: 1px;
}
} }
@media #{$mat-gt-md} { @media #{$mat-gt-md} {
gap: 12px; gap: 6px;
&.tb-left-panel {
gap: 3px;
}
} }
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {

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

@ -24,7 +24,8 @@
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[hideToggleHeader]="previewMode" [hideToggleHeader]="previewMode"
[scada]="scada" [showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
formControlName="widgetConfig"> formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix"> <div class="tb-widget-config-header-prefix">
<button mat-button <button mat-button

6
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -60,7 +60,11 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input() @Input()
@coerceBoolean() @coerceBoolean()
scada = false; showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint= true;
@Output() @Output()
applyWidgetConfig = new EventEmitter<void>(); applyWidgetConfig = new EventEmitter<void>();

66
ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html

@ -0,0 +1,66 @@
<!--
Copyright © 2016-2024 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.
-->
<mat-toolbar color="primary">
<h2 translate>layout.add-new-breakpoint</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content style="width: 400px">
<form [formGroup]="addBreakpointFormGroup">
<mat-form-field appearance="outline">
<mat-label translate>layout.breakpoint</mat-label>
<mat-select formControlName="newBreakpointId" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(this.addBreakpointFormGroup.get('newBreakpointId').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of allowBreakpointIds" [value]="breakpoint">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label translate>layout.copy-from</mat-label>
<mat-select formControlName="copyFrom" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(this.addBreakpointFormGroup.get('copyFrom').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of selectedBreakpointIds" [value]="breakpoint">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
</mat-select>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="save()"
[disabled]="addBreakpointFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
</div>

85
ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts

@ -0,0 +1,85 @@
///
/// Copyright © 2016-2024 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 } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { BreakpointId } from '@shared/models/dashboard.models';
export interface AddNewBreakpointDialogData {
allowBreakpointIds: string[];
selectedBreakpointIds: string[];
}
export interface AddNewBreakpointDialogResult {
newBreakpointId: BreakpointId;
copyFrom: BreakpointId;
}
@Component({
selector: 'add-new-breakpoint-dialog',
templateUrl: './add-new-breakpoint-dialog.component.html',
})
export class AddNewBreakpointDialogComponent extends DialogComponent<AddNewBreakpointDialogComponent, AddNewBreakpointDialogResult> {
addBreakpointFormGroup: FormGroup;
allowBreakpointIds = [];
selectedBreakpointIds = [];
constructor(protected store: Store<AppState>,
protected router: Router,
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA) private data: AddNewBreakpointDialogData,
protected dialogRef: MatDialogRef<AddNewBreakpointDialogComponent, AddNewBreakpointDialogResult>,
private dashboardUtils: DashboardUtilsService,) {
super(store, router, dialogRef);
this.allowBreakpointIds = this.data.allowBreakpointIds;
this.selectedBreakpointIds = this.data.selectedBreakpointIds;
this.addBreakpointFormGroup = this.fb.group({
newBreakpointId: [{value: this.allowBreakpointIds[0], disabled: this.allowBreakpointIds.length === 1}],
copyFrom: [{value: 'default', disabled: this.selectedBreakpointIds.length === 1}],
});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.dialogRef.close(this.addBreakpointFormGroup.getRawValue());
}
getName(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointName(breakpointId);
}
getIcon(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointIcon(breakpointId);
}
getSizeDescription(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointSizeDescription(breakpointId);
}
}

4
ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html

@ -53,8 +53,8 @@
[isEditingWidget]="isEditingWidget" [isEditingWidget]="isEditingWidget"
[autofillHeight]="autoFillHeight" [autofillHeight]="autoFillHeight"
[mobileAutofillHeight]="mobileAutoFillHeight" [mobileAutofillHeight]="mobileAutoFillHeight"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight" [mobileRowHeight]="mobielRowHeigth"
[isMobile]="isMobile" [isMobile]="isMobileValue"
[isMobileDisabled]="isMobileDisabled" [isMobileDisabled]="isMobileDisabled"
[disableWidgetInteraction]="isEdit" [disableWidgetInteraction]="isEdit"
[isEditActionEnabled]="isEdit && !isEditingWidget" [isEditActionEnabled]="isEdit && !isEditingWidget"

67
ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts

@ -37,7 +37,8 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { LayoutType } from '@shared/models/dashboard.models'; import { BreakpointId, LayoutType, ViewFormatType } from '@shared/models/dashboard.models';
import { isNotEmptyStr } from '@core/utils';
@Component({ @Component({
selector: 'tb-dashboard-layout', selector: 'tb-dashboard-layout',
@ -85,11 +86,27 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
} }
get mobileAutoFillHeight(): boolean { get mobileAutoFillHeight(): boolean {
return (this.isEdit || this.isScada) ? false : this.layoutCtx.gridSettings.mobileAutoFillHeight; if (this.isEdit || this.isScada) {
return false;
} else if (this.layoutCtx.breakpoint !== 'default' && this.layoutCtx.gridSettings.viewFormat === ViewFormatType.list) {
return this.layoutCtx.gridSettings.autoFillHeight;
}
return this.layoutCtx.gridSettings.mobileAutoFillHeight;
}
get isMobileValue(): boolean {
return this.isMobile || (this.layoutCtx.breakpoint !== 'default' && this.layoutCtx.gridSettings.viewFormat === ViewFormatType.list);
} }
get isMobileDisabled(): boolean { get isMobileDisabled(): boolean {
return this.widgetEditMode || this.isScada; return this.widgetEditMode || this.isScada || (this.layoutCtx.breakpoint !== 'default' && !this.isMobileValue);
}
get mobielRowHeigth(): number {
if (this.layoutCtx.breakpoint !== 'default' && this.layoutCtx.gridSettings.viewFormat === ViewFormatType.list) {
return this.layoutCtx.gridSettings.rowHeight;
}
return this.layoutCtx.gridSettings.mobileRowHeight;
} }
get colWidthInteger(): boolean { get colWidthInteger(): boolean {
@ -129,11 +146,13 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
private rxSubscriptions = new Array<Subscription>(); private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>, constructor(
private translate: TranslateService, protected store: Store<AppState>,
private itembuffer: ItemBufferService, private translate: TranslateService,
private imagePipe: ImagePipe, private itembuffer: ItemBufferService,
private sanitizer: DomSanitizer) { private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
) {
super(store); super(store);
this.initHotKeys(); this.initHotKeys();
} }
@ -199,7 +218,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
new Hotkey('ctrl+i', (event: KeyboardEvent) => { new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
this.dashboardCtx.state, this.layoutCtx.id)) { this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) {
event.preventDefault(); event.preventDefault();
this.pasteWidgetReference(event); this.pasteWidgetReference(event);
} }
@ -264,6 +283,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget); this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget);
} }
replaceReferenceWithWidgetCopy($event: Event, widget: Widget): void {
this.layoutCtx.dashboardCtrl.replaceReferenceWithWidgetCopy($event, this.layoutCtx, widget);
}
onExportWidget($event: Event, widget: Widget, widgetTitle: string): void { onExportWidget($event: Event, widget: Widget, widgetTitle: string): void {
this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, widgetTitle); this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, widgetTitle);
} }
@ -284,12 +307,12 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget); this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget);
} }
prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> { prepareDashboardContextMenu(_: Event): Array<DashboardContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx); return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
} }
prepareWidgetContextMenu($event: Event, widget: Widget): Array<WidgetContextMenuItem> { prepareWidgetContextMenu(_: Event, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget); return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, isReference);
} }
copyWidget($event: Event, widget: Widget) { copyWidget($event: Event, widget: Widget) {
@ -310,4 +333,24 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos); this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos);
} }
updatedCurrentBreakpoint(breakpointId?: BreakpointId, showLayout = true) {
if (!isNotEmptyStr(breakpointId)) {
breakpointId = this.dashboardCtx.breakpoint;
}
if (this.layoutCtx.layoutData[breakpointId]) {
this.layoutCtx.breakpoint = breakpointId;
} else {
this.layoutCtx.breakpoint = 'default';
}
const layoutInfo = this.layoutCtx.layoutData[this.layoutCtx.breakpoint];
if (layoutInfo.gridSettings) {
this.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
this.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
this.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (showLayout && this.layoutCtx.ctrl) {
this.layoutCtx.ctrl.reload();
}
this.layoutCtx.ignoreLoading = true;
}
} }

1
ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts

@ -21,6 +21,7 @@ export interface ILayoutController {
selectWidget(widgetId: string, delay?: number); selectWidget(widgetId: string, delay?: number);
pasteWidget($event: MouseEvent); pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent); pasteWidgetReference($event: MouseEvent);
updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean);
} }
export enum LayoutWidthType { export enum LayoutWidthType {

86
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html

@ -25,16 +25,77 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content fxLayout="column" fxLayoutGap="20px" style="min-width: 300px; overflow: hidden;"> <div mat-dialog-content style="min-width: 300px; overflow: hidden;">
<div fxLayout="row" fxLayoutAlign="start center"> <section class="tb-form-panel no-padding no-border">
<mat-slide-toggle formControlName="right" color="accent"> <div class="tb-form-row space-between">
{{ 'layout.divider' | translate }} <div class="fixed-title-width" translate>dashboard.layout</div>
</mat-slide-toggle> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
</div> <mat-select formControlName="layoutType">
<mat-option *ngFor="let type of layoutTypes" [value]="type">
{{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!isDividerLayout; else dividerLayout" class="tb-form-panel stroked">
<div class="tb-form-table tb-layout-breakpoints">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-icon"></div>
<div class="tb-form-table-header-cell tb-breakpoint" translate>layout.breakpoints</div>
<div class="tb-form-table-header-cell tb-size" translate>layout.size</div>
<div class="tb-form-table-header-cell tb-actions"></div>
</div>
<div class="tb-form-table-body no-gap">
<ng-container *ngFor="let breakpoint of layoutBreakpoints; let last = last">
<div class="tb-form-table-row no-padding-right">
<div class="tb-form-table-row-cell tb-icon">
<tb-icon>{{ breakpoint.icon }}</tb-icon>
</div>
<div class="tb-form-table-row-cell tb-breakpoint">
{{ breakpoint.name }}
</div>
<div class="tb-form-table-row-cell tb-size" tbTruncateWithTooltip>
{{ breakpoint.descriptionSize }}
</div>
<div class="tb-form-table-row-cell-buttons tb-actions">
<button mat-icon-button
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main', breakpoint.breakpoint)"
type="button" >
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
[disabled]="breakpoint.breakpoint === 'default'"
[class.tb-hidden]="breakpoint.breakpoint === 'default'"
(click)="deleteBreakpoint($event, breakpoint.breakpoint)"
type="button" >
<tb-icon>delete</tb-icon>
</button>
</div>
</div>
<mat-divider *ngIf="!last"></mat-divider>
</ng-container>
</div>
</div>
<button mat-button fxFlex class="tb-add-breakpoint-button"
matTooltip="{{ 'layout.add-new-breakpoint' | translate }}"
matTooltipPosition="above"
color="primary"
type="button"
[disabled]="!allowBreakpointIds.length"
(click)="addBreakpoint()">
<mat-icon>add</mat-icon>
</button>
</div>
</section>
<ng-template #dividerLayout>
<div fxLayout="column" fxLayoutAlign="start center" fxLayoutGap="20px"> <div fxLayout="column" fxLayoutAlign="start center" fxLayoutGap="20px">
<mat-button-toggle-group aria-label="Select width value type" <mat-button-toggle-group aria-label="Select width value type"
formControlName="type" formControlName="type"
[fxShow]="layoutsFormGroup.get('right').value"> [fxShow]="isDividerLayout">
<mat-button-toggle fxFlex value="percentage"> <mat-button-toggle fxFlex value="percentage">
{{ 'layout.percentage-width' | translate }} {{ 'layout.percentage-width' | translate }}
</mat-button-toggle> </mat-button-toggle>
@ -52,7 +113,7 @@
<div fxLayout="column" fxLayoutAlign="start center" fxFlex="{{ buttonFlexValue() }}" class="tb-layout-preview-container-main"> <div fxLayout="column" fxLayoutAlign="start center" fxFlex="{{ buttonFlexValue() }}" class="tb-layout-preview-container-main">
<button mat-icon-button <button mat-icon-button
type="button" type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}" matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="openLayoutSettings('main')" (click)="openLayoutSettings('main')"
class="tb-layout-preview-element" class="tb-layout-preview-element"
@ -72,7 +133,7 @@
<span [matTooltip]="layoutButtonText('main')" <span [matTooltip]="layoutButtonText('main')"
#mainLayoutTooltip="matTooltip" #mainLayoutTooltip="matTooltip"
matTooltipPosition="above"> matTooltipPosition="above">
{{ (layoutsFormGroup.value.right ? 'layout.left' : 'layout.main') | translate }} {{ (isDividerLayout ? 'layout.left' : 'layout.main') | translate }}
</span> </span>
</button> </button>
<div fxFlex class="tb-layout-preview-element tb-layout-preview-input" *ngIf="showPreviewInputs('main')"> <div fxFlex class="tb-layout-preview-element tb-layout-preview-input" *ngIf="showPreviewInputs('main')">
@ -93,10 +154,10 @@
required> required>
</div> </div>
</div> </div>
<div fxLayout="column" fxLayoutAlign="start center" fxFlex *ngIf="layoutsFormGroup.get('right').value" matTooltip=""> <div fxLayout="column" fxLayoutAlign="start center" fxFlex *ngIf="isDividerLayout" matTooltip="">
<button mat-icon-button <button mat-icon-button
type="button" type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}" matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="openLayoutSettings('right')" (click)="openLayoutSettings('right')"
class="tb-layout-preview-element" class="tb-layout-preview-element"
@ -138,7 +199,7 @@
</div> </div>
</div> </div>
</div> </div>
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="8px" *ngIf="layoutsFormGroup.get('right').value" style="width: 80%"> <div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="8px" *ngIf="isDividerLayout" style="width: 80%">
<mat-slider *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE" <mat-slider *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE"
step="1" step="1"
min="{{ layoutPercentageSize.MIN }}" min="{{ layoutPercentageSize.MIN }}"
@ -167,6 +228,7 @@
</div> </div>
</div> </div>
</div> </div>
</ng-template>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button <button mat-button

48
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss

@ -15,6 +15,7 @@
*/ */
@use '@angular/material' as mat; @use '@angular/material' as mat;
@import '../theme.scss'; @import '../theme.scss';
@import '../scss/constants.scss';
$tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text); $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);
@ -121,6 +122,53 @@ $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);
} }
} }
} }
.tb-layout-breakpoints {
.tb-form-table-header-cell, .tb-form-table-row-cell {
&.tb-icon {
width: 29px;
min-width: 29px;
max-width: 29px;
display: flex;
place-content: center;
}
&.tb-breakpoint {
flex: 1 1 15%;
width: 15%;
min-width: 100px;
}
&.tb-size {
flex: 1 1 85%;
width: 200px;
}
&.tb-actions {
width: 96pxpx;
min-width: 96px;
max-width: 96px;
display: flex;
justify-content: end;
}
}
.mat-divider {
margin-top: 6px;
margin-bottom: 6px;
}
}
.tb-add-breakpoint-button {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #FFFFFF;
padding: 8px;
border: 2px dashed rgba(0, 0, 0, 0.08);
border-radius: 10px;
}
} }
:host ::ng-deep { :host ::ng-deep {

227
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts

@ -14,26 +14,35 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, SkipSelf, ViewChild } from '@angular/core'; import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
AbstractControl, AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DashboardLayoutId, DashboardStateLayouts, LayoutDimension } from '@app/shared/models/dashboard.models'; import {
import { deepClone, isDefined } from '@core/utils'; BreakpointId,
DashboardLayout,
DashboardLayoutId,
DashboardStateLayouts,
LayoutDimension,
LayoutType,
layoutTypes,
layoutTypeTranslationMap,
ViewFormatType
} from '@app/shared/models/dashboard.models';
import { deepClone, isDefined, isEqual } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { import {
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
@ -46,11 +55,25 @@ import {
} from '@home/components/dashboard-page/layout/layout.models'; } from '@home/components/dashboard-page/layout/layout.models';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import {
AddNewBreakpointDialogComponent,
AddNewBreakpointDialogData,
AddNewBreakpointDialogResult
} from '@home/components/dashboard-page/layout/add-new-breakpoint-dialog.component';
import { DialogService } from '@core/services/dialog.service';
export interface ManageDashboardLayoutsDialogData { export interface ManageDashboardLayoutsDialogData {
layouts: DashboardStateLayouts; layouts: DashboardStateLayouts;
} }
export interface DashboardLayoutSettings {
icon: string;
name: string;
descriptionSize?: string;
layout: DashboardLayout;
breakpoint: string;
}
@Component({ @Component({
selector: 'tb-manage-dashboard-layouts-dialog', selector: 'tb-manage-dashboard-layouts-dialog',
templateUrl: './manage-dashboard-layouts-dialog.component.html', templateUrl: './manage-dashboard-layouts-dialog.component.html',
@ -58,9 +81,9 @@ export interface ManageDashboardLayoutsDialogData {
styleUrls: ['./manage-dashboard-layouts-dialog.component.scss', '../../../components/dashboard/layout-button.scss'] styleUrls: ['./manage-dashboard-layouts-dialog.component.scss', '../../../components/dashboard/layout-button.scss']
}) })
export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts> export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>
implements ErrorStateMatcher { implements ErrorStateMatcher, OnDestroy {
@ViewChild('tooltip', {static: true}) tooltip: MatTooltip; @ViewChild('tooltip') tooltip: MatTooltip;
layoutsFormGroup: UntypedFormGroup; layoutsFormGroup: UntypedFormGroup;
@ -70,29 +93,43 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutFixedSize = LayoutFixedSize; layoutFixedSize = LayoutFixedSize;
layoutTypes = layoutTypes;
layoutTypeTranslations = layoutTypeTranslationMap;
layoutBreakpoints: DashboardLayoutSettings[] = [];
private readonly layouts: DashboardStateLayouts; private readonly layouts: DashboardStateLayouts;
private subscriptions: Array<Subscription> = []; private subscriptions: Array<Subscription> = [];
private submitted = false; private submitted = false;
allowBreakpointIds = [];
selectedBreakpointIds = ['default'];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData, @Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>, protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private translate: TranslateService, private translate: TranslateService,
private dialog: MatDialog) { private dialog: MatDialog,
private dialogs: DialogService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.layouts = this.data.layouts; this.layouts = this.data.layouts;
let layoutType = LayoutType.default;
if (isDefined(this.layouts.right)) {
layoutType = LayoutType.divider;
} else if (isDefined(this.layouts.main.gridSettings.layoutType)) {
layoutType = this.layouts.main.gridSettings.layoutType;
}
this.layoutsFormGroup = this.fb.group({ this.layoutsFormGroup = this.fb.group({
layoutType: [layoutType],
main: [{value: isDefined(this.layouts.main), disabled: true}], main: [{value: isDefined(this.layouts.main), disabled: true}],
right: [isDefined(this.layouts.right)],
sliderPercentage: [50], sliderPercentage: [50],
sliderFixed: [this.layoutFixedSize.MIN], sliderFixed: [this.layoutFixedSize.MIN],
leftWidthPercentage: [50, leftWidthPercentage: [50,
@ -141,7 +178,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
sliderFixed: this.layouts.main.gridSettings.layoutDimension.fixedWidth sliderFixed: this.layouts.main.gridSettings.layoutDimension.fixedWidth
}, {emitEvent: false}); }, {emitEvent: false});
} else { } else {
const leftWidthPercentage: number = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage); const leftWidthPercentage = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage);
this.layoutsFormGroup.patchValue({ this.layoutsFormGroup.patchValue({
leftWidthPercentage, leftWidthPercentage,
sliderPercentage: leftWidthPercentage, sliderPercentage: leftWidthPercentage,
@ -158,6 +195,21 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
this.layouts.right = this.dashboardUtils.createDefaultLayoutData(); this.layouts.right = this.dashboardUtils.createDefaultLayoutData();
} }
this.addLayoutConfiguration('default');
if (!this.isDividerLayout && this.layouts.main.breakpoints) {
for (const breakpoint of (Object.keys(this.layouts.main.breakpoints) as BreakpointId[])) {
this.addLayoutConfiguration(breakpoint);
this.selectedBreakpointIds.push(breakpoint);
}
}
this.allowBreakpointIds = Object.values(this.dashboardUtils.getListBreakpoint())
.filter((item) => !this.selectedBreakpointIds.includes(item.id))
.map(item => item.id);
this.sortLayoutBreakpoints();
this.subscriptions.push( this.subscriptions.push(
this.layoutsFormGroup.get('sliderPercentage').valueChanges this.layoutsFormGroup.get('sliderPercentage').valueChanges
.subscribe( .subscribe(
@ -209,8 +261,10 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
openLayoutSettings(layoutId: DashboardLayoutId) { openLayoutSettings(layoutId: DashboardLayoutId, breakpointId: BreakpointId = 'default') {
const gridSettings = deepClone(this.layouts[layoutId].gridSettings); const layout = this.dashboardUtils.getDashboardLayoutConfig(this.layouts[layoutId], breakpointId);
const gridSettings = layout.gridSettings;
gridSettings.layoutType = this.layoutsFormGroup.get('layoutType').value;
this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData, this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, { DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
disableClose: true, disableClose: true,
@ -218,11 +272,12 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
data: { data: {
settings: null, settings: null,
gridSettings, gridSettings,
isRightLayout: this.layoutsFormGroup.get('right').value && layoutId === 'right' isRightLayout: this.isDividerLayout && layoutId === 'right',
breakpointId
} }
}).afterClosed().subscribe((data) => { }).afterClosed().subscribe((data) => {
if (data && data.gridSettings) { if (data && data.gridSettings) {
this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings); this.dashboardUtils.updateLayoutSettings(layout, data.gridSettings);
this.layoutsFormGroup.markAsDirty(); this.layoutsFormGroup.markAsDirty();
} }
}); });
@ -234,20 +289,22 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
save(): void { save(): void {
this.submitted = true; this.submitted = true;
const layouts = ['main', 'right']; const layoutType = this.layoutsFormGroup.value.layoutType;
for (const l of layouts) { this.layouts.main.gridSettings.layoutType = layoutType;
const control = this.layoutsFormGroup.controls[l]; if (!this.isDividerLayout) {
if (!control.value) { delete this.layouts.right;
if (this.layouts[l]) { for (const breakpoint of Object.values(this.layouts.main.breakpoints)) {
delete this.layouts[l]; breakpoint.gridSettings.layoutType = layoutType;
}
} }
} else {
delete this.layouts.main.breakpoints;
this.layouts.right.gridSettings.layoutType = layoutType;
} }
delete this.layouts.main.gridSettings.layoutDimension; delete this.layouts.main.gridSettings.layoutDimension;
if (this.layouts.right?.gridSettings) { if (this.layouts.right?.gridSettings) {
delete this.layouts.right.gridSettings.layoutDimension; delete this.layouts.right.gridSettings.layoutDimension;
} }
if (this.layoutsFormGroup.value.right) { if (this.isDividerLayout) {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
const widthType = formValues.type; const widthType = formValues.type;
const layoutDimension: LayoutDimension = { const layoutDimension: LayoutDimension = {
@ -271,7 +328,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
buttonFlexValue(): number { buttonFlexValue(): number {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
if (formValues.right) { if (this.isDividerLayout) {
if (formValues.type !== LayoutWidthType.FIXED) { if (formValues.type !== LayoutWidthType.FIXED) {
return formValues.leftWidthPercentage; return formValues.leftWidthPercentage;
} else { } else {
@ -288,7 +345,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
return this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED ? value : `${value}|${100 - value}`; return this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED ? value : `${value}|${100 - value}`;
} }
private layoutControlChange(key: string, value) { private layoutControlChange(key: string, value: number) {
const valueToSet = 100 - Number(value); const valueToSet = 100 - Number(value);
this.layoutsFormGroup.get(key).setValue(valueToSet, {emitEvent: false}); this.layoutsFormGroup.get(key).setValue(valueToSet, {emitEvent: false});
this.layoutsFormGroup.get('sliderPercentage') this.layoutsFormGroup.get('sliderPercentage')
@ -296,7 +353,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
} }
setFixedLayout(layout: string): void { setFixedLayout(layout: string): void {
if (this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED && this.layoutsFormGroup.get('right').value) { if (this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED && this.isDividerLayout) {
this.layoutsFormGroup.get('fixedLayout').setValue(layout); this.layoutsFormGroup.get('fixedLayout').setValue(layout);
this.layoutsFormGroup.get('fixedLayout').markAsDirty(); this.layoutsFormGroup.get('fixedLayout').markAsDirty();
} }
@ -339,7 +396,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonClass(side: DashboardLayoutId, border: boolean = false): string { layoutButtonClass(side: DashboardLayoutId, border: boolean = false): string {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
if (formValues.right) { if (this.isDividerLayout) {
let classString = border ? 'tb-layout-button-main ' : ''; let classString = border ? 'tb-layout-button-main ' : '';
if (!(formValues.fixedLayout === side || formValues.type === LayoutWidthType.PERCENTAGE)) { if (!(formValues.fixedLayout === side || formValues.type === LayoutWidthType.PERCENTAGE)) {
classString += 'tb-fixed-layout-button'; classString += 'tb-fixed-layout-button';
@ -350,7 +407,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonText(side: DashboardLayoutId): string { layoutButtonText(side: DashboardLayoutId): string {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
if (!(formValues.fixedLayout === side || !formValues.right || formValues.type === LayoutWidthType.PERCENTAGE)) { if (!(formValues.fixedLayout === side || !this.isDividerLayout || formValues.type === LayoutWidthType.PERCENTAGE)) {
if (side === 'main') { if (side === 'main') {
return this.translate.instant('layout.left-side'); return this.translate.instant('layout.left-side');
} else { } else {
@ -361,6 +418,116 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
showPreviewInputs(side: DashboardLayoutId): boolean { showPreviewInputs(side: DashboardLayoutId): boolean {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
return formValues.right && (formValues.type === LayoutWidthType.PERCENTAGE || formValues.fixedLayout === side); return this.isDividerLayout && (formValues.type === LayoutWidthType.PERCENTAGE || formValues.fixedLayout === side);
}
get isDividerLayout(): boolean {
return this.layoutsFormGroup.get('layoutType').value === LayoutType.divider;
}
addBreakpoint() {
this.dialog.open<AddNewBreakpointDialogComponent, AddNewBreakpointDialogData,
AddNewBreakpointDialogResult>(AddNewBreakpointDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
allowBreakpointIds: this.allowBreakpointIds,
selectedBreakpointIds: this.selectedBreakpointIds
}
}).afterClosed().subscribe((data) => {
if (data) {
this.createdNewBreakpoint(data.newBreakpointId, data.copyFrom);
this.layoutsFormGroup.markAsDirty();
}
});
}
deleteBreakpoint($event: Event, breakpointId: string): void {
if ($event) {
$event.stopPropagation();
}
const title = this.translate.instant('layout.delete-breakpoint-title', {name: breakpointId});
const content = this.translate.instant('layout.delete-breakpoint-text');
this.dialogs.confirm(title, content, this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe((res) => {
if (res) {
delete this.layouts.main.breakpoints[breakpointId];
if (isEqual(this.layouts.main.breakpoints, {})) {
delete this.layouts.main.breakpoints;
}
this.layoutBreakpoints = this.layoutBreakpoints.filter((item) => item.breakpoint !== breakpointId);
this.allowBreakpointIds.push(breakpointId);
this.selectedBreakpointIds = this.selectedBreakpointIds.filter((item) => item !== breakpointId);
this.sortLayoutBreakpoints();
this.layoutsFormGroup.markAsDirty();
}
}
);
}
private createdNewBreakpoint(newBreakpointId: BreakpointId, copyFromBreakpointId: BreakpointId): void {
const layoutConfig = this.layouts.main;
const sourceLayout = copyFromBreakpointId === 'default' ? layoutConfig : layoutConfig.breakpoints[copyFromBreakpointId];
const gridSettings = deepClone(sourceLayout.gridSettings);
const widgets = deepClone(sourceLayout.widgets);
if (copyFromBreakpointId === 'default') {
const breakpointInfo = this.dashboardUtils.getBreakpointInfoById(newBreakpointId);
if (breakpointInfo?.maxWidth < 960) {
gridSettings.viewFormat = ViewFormatType.list;
gridSettings.rowHeight = gridSettings.mobileRowHeight;
gridSettings.autoFillHeight = gridSettings.mobileAutoFillHeight;
}
for (const widgetId in widgets) {
if (widgets[widgetId]) {
delete widgets[widgetId].desktopHide;
delete widgets[widgetId].mobileHide;
}
}
}
if (!layoutConfig.breakpoints) {
layoutConfig.breakpoints = {};
}
layoutConfig.breakpoints[newBreakpointId] = {
gridSettings,
widgets,
};
this.selectedBreakpointIds.push(newBreakpointId);
this.allowBreakpointIds = this.allowBreakpointIds.filter((item) => item !== newBreakpointId);
this.addLayoutConfiguration(newBreakpointId);
this.sortLayoutBreakpoints();
}
private addLayoutConfiguration(breakpointId: BreakpointId) {
const layout = breakpointId === 'default' ? this.layouts.main : this.layouts.main.breakpoints[breakpointId];
const size = breakpointId === 'default' ? '' : this.dashboardUtils.getBreakpointSizeDescription(breakpointId);
this.layoutBreakpoints.push({
icon: this.dashboardUtils.getBreakpointIcon(breakpointId),
name: this.dashboardUtils.getBreakpointName(breakpointId),
layout,
descriptionSize: size,
breakpoint: breakpointId
});
}
private sortLayoutBreakpoints() {
this.layoutBreakpoints.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a.breakpoint)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b.breakpoint)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
this.selectedBreakpointIds.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
this.allowBreakpointIds.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
} }
} }

29
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html

@ -0,0 +1,29 @@
<!--
Copyright © 2016-2024 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.
-->
<mat-select
[class.hidden]="breakpointIds.length < 2"
class="select-dashboard-breakpoint"
[(ngModel)]="selectedBreakpoint"
(ngModelChange)="selectLayoutChanged()">
<mat-select-trigger>{{ getName(selectedBreakpoint) }}</mat-select-trigger>
<mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId">
<mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon>
<div>{{ getName(breakpointId) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div>
</mat-option>
</mat-select>

34
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2024 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 {
pointer-events: all;
width: min-content; //for Safari
.hidden {
display: none;
}
}
:host ::ng-deep {
.mat-mdc-select.select-dashboard-breakpoint {
.mat-mdc-select-value {
max-width: 200px;
}
.mat-mdc-select-arrow {
width: 24px;
}
}
}

81
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts

@ -0,0 +1,81 @@
///
/// Copyright © 2016-2024 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, Input, OnDestroy, OnInit } from '@angular/core';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { Subscription } from 'rxjs';
import { BreakpointId } from '@shared/models/dashboard.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
@Component({
selector: 'tb-select-dashboard-breakpoint',
templateUrl: './select-dashboard-breakpoint.component.html',
styleUrls: ['./select-dashboard-breakpoint.component.scss']
})
export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy {
@Input()
dashboardCtrl: DashboardPageComponent;
selectedBreakpoint: BreakpointId = 'default';
breakpointIds: Array<string> = ['default'];
private layoutDataChanged$: Subscription;
constructor(private dashboardUtils: DashboardUtilsService) {
}
ngOnInit() {
this.layoutDataChanged$ = this.dashboardCtrl.layouts.main.layoutCtx.layoutDataChanged.subscribe(() => {
if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) {
this.breakpointIds = Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData);
this.breakpointIds.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
if (this.breakpointIds.indexOf(this.dashboardCtrl.layouts.main.layoutCtx.breakpoint) > -1) {
this.selectedBreakpoint = this.dashboardCtrl.layouts.main.layoutCtx.breakpoint;
} else {
this.selectedBreakpoint = 'default';
this.dashboardCtrl.layouts.main.layoutCtx.breakpoint = this.selectedBreakpoint;
}
}
});
}
ngOnDestroy() {
this.layoutDataChanged$.unsubscribe();
}
selectLayoutChanged() {
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectedBreakpoint);
this.dashboardCtrl.updateLayoutSizes();
}
getName(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointName(breakpointId);
}
getIcon(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointIcon(breakpointId);
}
getSizeDescription(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointSizeDescription(breakpointId);
}
}

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

@ -58,7 +58,7 @@
[disabled]="!item.enabled" [disabled]="!item.enabled"
(click)="item.action(widgetContextMenuEvent, widget)"> (click)="item.action(widgetContextMenuEvent, widget)">
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span> <span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon> <tb-icon matMenuItemIcon *ngIf="item.icon">{{item.icon}}</tb-icon>
<span translate>{{item.value}}</span> <span translate>{{item.value}}</span>
</button> </button>
</div> </div>

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

@ -17,7 +17,6 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
DoCheck, DoCheck,
Input, Input,
@ -46,7 +45,6 @@ import {
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs'; import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
@ -203,9 +201,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetContextMenuEvent: MouseEvent; widgetContextMenuEvent: MouseEvent;
dashboardWidgets = new DashboardWidgets(this, dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((index, item) => { this.differs.find([]).create<Widget>((_, item) => item),
return item;
}),
this.kvDiffers.find([]).create<string, WidgetLayout>() this.kvDiffers.find([]).create<string, WidgetLayout>()
); );
@ -218,11 +214,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
public utils: UtilsService, public utils: UtilsService,
private timeService: TimeService, private timeService: TimeService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers, private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers, private kvDiffers: KeyValueDiffers,
private cd: ChangeDetectorRef,
private ngZone: NgZone) { private ngZone: NgZone) {
super(store); super(store);
this.authUser = getCurrentAuthUser(store); this.authUser = getCurrentAuthUser(store);
@ -257,8 +251,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
displayGrid: this.displayGrid, displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50}, resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
draggable: {enabled: this.isEdit && !this.isEditingWidget}, draggable: {enabled: this.isEdit && !this.isEditingWidget},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => { itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
}, },
colWidthUpdateCallback: (colWidth) => { colWidthUpdateCallback: (colWidth) => {
@ -421,7 +415,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget); const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference);
if (items && items.length) { if (items && items.length) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
@ -459,6 +453,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
case WidgetComponentActionType.REMOVE: case WidgetComponentActionType.REMOVE:
this.removeWidget($event, widget); this.removeWidget($event, widget);
break; break;
case WidgetComponentActionType.REPLACE_REFERENCE_WITH_WIDGET_COPY:
this.replaceReferenceWithWidgetCopy($event, widget);
break;
} }
} }
@ -483,6 +480,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} }
} }
private replaceReferenceWithWidgetCopy($event: Event, widget: DashboardWidget) {
if ($event) {
$event.stopPropagation();
}
if (this.isEditActionEnabled && this.callbacks && this.callbacks.replaceReferenceWithWidgetCopy) {
this.callbacks.replaceReferenceWithWidgetCopy($event, widget.widget);
}
}
private exportWidget($event: Event, widget: DashboardWidget) { private exportWidget($event: Event, widget: DashboardWidget) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -558,7 +564,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widget.gridsterItemComponent$().subscribe((gridsterItem) => { widget.gridsterItemComponent$().subscribe((gridsterItem) => {
const gridsterItemElement = gridsterItem.el as HTMLElement; const gridsterItemElement = gridsterItem.el as HTMLElement;
const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2; const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
let scrollTop; let scrollTop: number;
if (this.isMobileSize) { if (this.isMobileSize) {
scrollTop = gridsterItemElement.offsetTop; scrollTop = gridsterItemElement.offsetTop;
} else { } else {
@ -592,8 +598,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} else { } else {
this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical; this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical;
} }
const mobileBreakPoint = this.isMobileSize ? 20000 : 0; this.gridsterOpts.mobileBreakpoint = this.isMobileSize ? 20000 : 0;
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight); const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight);
if (this.gridsterOpts.fixedRowHeight !== rowSize) { if (this.gridsterOpts.fixedRowHeight !== rowSize) {
this.gridsterOpts.fixedRowHeight = rowSize; this.gridsterOpts.fixedRowHeight = rowSize;

10
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -114,6 +114,9 @@ import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget
import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
import { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-dialog.component'; import { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-dialog.component';
import { ManageDashboardLayoutsDialogComponent } from '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component'; import { ManageDashboardLayoutsDialogComponent } from '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component';
import {
AddNewBreakpointDialogComponent
} from '@home/components/dashboard-page/layout/add-new-breakpoint-dialog.component';
import { DashboardSettingsDialogComponent } from '@home/components/dashboard-page/dashboard-settings-dialog.component'; import { DashboardSettingsDialogComponent } from '@home/components/dashboard-page/dashboard-settings-dialog.component';
import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component'; import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component';
import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component'; import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component';
@ -175,6 +178,9 @@ import { WidgetConfigComponentsModule } from '@home/components/widget/config/wid
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import {
SelectDashboardBreakpointComponent
} from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -285,11 +291,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,
MoveWidgetsDialogComponent, MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent, DashboardStateDialogComponent,
@ -418,11 +426,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,
MoveWidgetsDialogComponent, MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent, DashboardStateDialogComponent,

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -57,7 +57,6 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({ @Component({
@ -78,7 +77,7 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs
} }
], ],
standalone: true, standalone: true,
imports: [CommonModule, SharedModule, TruncateWithTooltipDirective] imports: [CommonModule, SharedModule]
}) })
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -41,7 +41,6 @@ import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-data-keys-panel', selector: 'tb-modbus-data-keys-panel',
@ -52,7 +51,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
CommonModule, CommonModule,
SharedModule, SharedModule,
GatewayHelpLinkPipe, GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
] ]
}) })
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts

@ -42,7 +42,6 @@ import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-security-config', selector: 'tb-modbus-security-config',
@ -64,7 +63,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
TruncateWithTooltipDirective,
] ]
}) })
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -48,7 +48,6 @@ import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipe
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-slave-config', selector: 'tb-modbus-slave-config',
@ -73,7 +72,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
ModbusValuesComponent, ModbusValuesComponent,
ModbusSecurityConfigComponent, ModbusSecurityConfigComponent,
GatewayPortTooltipPipe, GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
], ],
}) })
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -52,7 +52,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { helpBaseUrl } from '@shared/models/constants'; import { helpBaseUrl } from '@shared/models/constants';
@Component({ @Component({
@ -78,7 +77,6 @@ import { helpBaseUrl } from '@shared/models/constants';
ModbusValuesComponent, ModbusValuesComponent,
ModbusSecurityConfigComponent, ModbusSecurityConfigComponent,
GatewayPortTooltipPipe, GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
], ],
styleUrls: ['./modbus-slave-dialog.component.scss'], styleUrls: ['./modbus-slave-dialog.component.scss'],
}) })

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts

@ -35,7 +35,6 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { import {
SecurityConfigComponent SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
@ -62,7 +61,6 @@ import {
CommonModule, CommonModule,
SharedModule, SharedModule,
SecurityConfigComponent, SecurityConfigComponent,
TruncateWithTooltipDirective,
] ]
}) })
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts

@ -35,7 +35,6 @@ import { CommonModule } from '@angular/common';
import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-workers-config-control', selector: 'tb-workers-config-control',
@ -45,7 +44,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
TruncateWithTooltipDirective,
], ],
providers: [ providers: [
{ {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html

@ -40,7 +40,7 @@
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div> <div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div>
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between" [class]="propertyRow.rowClass"> <div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width" formControlName="{{ propertyRow.switch.id }}"> <mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }} {{ propertyRow.label | customTranslate }}
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label | customTranslate }}</div> <div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label | customTranslate }}</div>

4
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -141,9 +141,8 @@ import {
import { import {
TypeValuePanelComponent TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { import {
ModbusRpcParametersComponent ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
@ -233,7 +232,6 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
KeyValueIsNotEmptyPipe, KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent, ModbusBasicConfigComponent,
EllipsisChipListDirective, EllipsisChipListDirective,
TruncateWithTooltipDirective,
ModbusRpcParametersComponent, ModbusRpcParametersComponent,
], ],
exports: [ exports: [

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

@ -169,12 +169,12 @@
</div> </div>
</div> </div>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content"> <div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-form-panel"> <div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide"> <mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }} {{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide"> <mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }} {{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle> </mat-slide-toggle>

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

@ -38,7 +38,7 @@ import {
JsonSchema, JsonSchema,
JsonSettingsSchema, JsonSettingsSchema,
TargetDevice, TargetDevice,
TargetDeviceType, targetDeviceValid, targetDeviceValid,
Widget, Widget,
WidgetConfigMode, WidgetConfigMode,
widgetType widgetType
@ -84,8 +84,8 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component';
import Timeout = NodeJS.Timeout;
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -152,7 +152,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@Input() @Input()
@coerceBoolean() @coerceBoolean()
scada = false; showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint = true;
@Input() disabled: boolean; @Input() disabled: boolean;
@ -345,10 +349,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'actions' value: 'actions'
} }
); );
if (!this.scada) { if (this.showLayoutConfig) {
this.headerOptions.push( this.headerOptions.push(
{ {
name: this.translate.instant('widget-config.mobile'), name: this.isDefaultBreakpoint
? this.translate.instant('widget-config.mobile')
: this.translate.instant('widget-config.list-layout'),
value: 'mobile' value: 'mobile'
} }
); );

30
ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../scss/constants';
.tb-widget-container { .tb-widget-container {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -182,12 +184,30 @@ gridster-item:hover {
line-height: 12px; line-height: 12px;
font-weight: 500; font-weight: 500;
color: rgba(0, 0, 0, 0.76); color: rgba(0, 0, 0, 0.76);
.tb-widget-actions-panel { .tb-widget-action-container {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
place-content: center flex-start; gap: 6px;
align-items: center;
gap: 8px; .tb-widget-actions-panel {
display: flex;
flex-direction: row;
place-content: center space-between;
align-items: center;
gap: 8px;
}
.tb-widget-reference-panel {
display: flex;
flex-direction: row;
place-content: center space-between;
align-items: center;
gap: 8px;
font-weight: normal;
color: $tb-primary-color;
font-size: 11px;
padding: 4px 4px 4px 6px;
border-radius: 2px;
}
} }
} }
} }

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

@ -40,6 +40,7 @@ import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs'; import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
export enum WidgetComponentActionType { export enum WidgetComponentActionType {
MOUSE_DOWN, MOUSE_DOWN,
@ -47,7 +48,8 @@ export enum WidgetComponentActionType {
CONTEXT_MENU, CONTEXT_MENU,
EDIT, EDIT,
EXPORT, EXPORT,
REMOVE REMOVE,
REPLACE_REFERENCE_WITH_WIDGET_COPY,
} }
export class WidgetComponentAction { export class WidgetComponentAction {
@ -117,6 +119,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>(); widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
hovered = false; hovered = false;
isReferenceWidget = false;
get widgetEditActionsEnabled(): boolean { get widgetEditActionsEnabled(): boolean {
return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen; return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen;
@ -130,6 +133,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private renderer: Renderer2, private renderer: Renderer2,
private container: ViewContainerRef, private container: ViewContainerRef,
private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) { private utils: UtilsService) {
super(store); super(store);
} }
@ -144,7 +148,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
$(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
$(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent)); $(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
this.initEditWidgetActionTooltip(); const dashboardElement = this.widget.widgetContext.dashboardPageElement;
if (dashboardElement) {
this.initEditWidgetActionTooltip(dashboardElement);
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -216,6 +223,13 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}); });
} }
onReplaceReferenceWithWidgetCopy(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.REPLACE_REFERENCE_WITH_WIDGET_COPY
});
}
onExport(event: MouseEvent) { onExport(event: MouseEvent) {
this.widgetComponentAction.emit({ this.widgetComponentAction.emit({
event, event,
@ -240,9 +254,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
} }
private initEditWidgetActionTooltip() { private initEditWidgetActionTooltip(parent: HTMLElement) {
from(import('tooltipster')).subscribe(() => { from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({ $(this.gridsterItem.el).tooltipster({
parent: $(parent),
delay: this.widget.selected ? [0, 10000000] : [0, 100], delay: this.widget.selected ? [0, 10000000] : [0, 100],
distance: 2, distance: 2,
zIndex: 151, zIndex: 151,
@ -262,7 +277,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
content: '', content: '',
functionPosition: (instance, helper, position) => { functionPosition: (instance, helper, position) => {
const clientRect = helper.origin.getBoundingClientRect(); const clientRect = helper.origin.getBoundingClientRect();
position.coord.left = clientRect.right - position.size.width; const container = parent.getBoundingClientRect();
position.coord.left = clientRect.right - position.size.width - container.left;
position.coord.top = position.coord.top - container.top;
position.target = clientRect.right; position.target = clientRect.right;
return position; return position;
}, },
@ -280,6 +297,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
functionAfter: () => { functionAfter: () => {
this.hovered = false; this.hovered = false;
this.cd.markForCheck(); this.cd.markForCheck();
},
functionBefore: () => {
this.widget.isReference = this.dashboardUtils.isReferenceWidget(
this.widget.widgetContext.dashboard.stateController.dashboardCtrl.dashboardCtx.getDashboard(), this.widget.widgetId);
componentRef.instance.cd.detectChanges();
} }
}); });
this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance'); this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
@ -327,29 +349,43 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
@Component({ @Component({
template: `<div class="tb-widget-actions-panel"> template: `
<button mat-icon-button class="tb-mat-20" <div class="tb-widget-action-container">
[fxShow]="container.isEditActionEnabled" <div class="tb-widget-reference-panel tb-primary-fill" *ngIf="container.widget.isReference">
(click)="container.onEdit($event)" {{ 'widget.reference' | translate }}
matTooltip="{{ 'widget.edit' | translate }}" <button mat-icon-button class="tb-mat-16"
matTooltipPosition="above"> color="primary"
<tb-icon>edit</tb-icon> [fxShow]="container.isEditActionEnabled"
</button> (click)="container.onReplaceReferenceWithWidgetCopy($event)"
<button mat-icon-button class="tb-mat-20" matTooltip="{{ 'widget.replace-reference-with-widget-copy' | translate }}"
[fxShow]="container.isExportActionEnabled" matTooltipPosition="above">
(click)="container.onExport($event)" <tb-icon matButtonIcon>mdi:file-replace-outline</tb-icon>
matTooltip="{{ 'widget.export' | translate }}" </button>
matTooltipPosition="above"> </div>
<tb-icon>file_download</tb-icon> <div class="tb-widget-actions-panel">
</button> <button mat-icon-button class="tb-mat-20"
<button mat-icon-button class="tb-mat-20" [fxShow]="container.isEditActionEnabled"
[fxShow]="container.isRemoveActionEnabled" (click)="container.onEdit($event)"
(click)="container.onRemove($event);" matTooltip="{{ 'widget.edit' | translate }}"
matTooltip="{{ 'widget.remove' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <tb-icon>edit</tb-icon>
<tb-icon>close</tb-icon> </button>
</button> <button mat-icon-button class="tb-mat-20"
</div>`, [fxShow]="container.isExportActionEnabled"
(click)="container.onExport($event)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<tb-icon>file_download</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isRemoveActionEnabled"
(click)="container.onRemove($event);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<tb-icon>close</tb-icon>
</button>
</div>
</div>`,
styles: [], styles: [],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
@ -361,7 +397,8 @@ export class EditWidgetActionsTooltipComponent implements AfterViewInit {
@Output() @Output()
viewInited = new EventEmitter(); viewInited = new EventEmitter();
constructor(public element: ElementRef<HTMLElement>) { constructor(public element: ElementRef<HTMLElement>,
public cd: ChangeDetectorRef) {
} }
ngAfterViewInit() { ngAfterViewInit() {

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

@ -65,13 +65,14 @@ export interface WidgetContextMenuItem extends ContextMenuItem {
export interface DashboardCallbacks { export interface DashboardCallbacks {
onEditWidget?: ($event: Event, widget: Widget) => void; onEditWidget?: ($event: Event, widget: Widget) => void;
replaceReferenceWithWidgetCopy?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void; onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => void; onRemoveWidget?: ($event: Event, widget: Widget) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void; onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onDashboardMouseDown?: ($event: Event) => void; onDashboardMouseDown?: ($event: Event) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void; onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>; prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array<WidgetContextMenuItem>; prepareWidgetContextMenu?: ($event: Event, widget: Widget, isReference: boolean) => Array<WidgetContextMenuItem>;
} }
export interface IDashboardComponent { export interface IDashboardComponent {
@ -343,6 +344,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private selectedCallback: (selected: boolean) => void = () => {}; private selectedCallback: (selected: boolean) => void = () => {};
isFullscreen = false; isFullscreen = false;
isReference = false;
color: string; color: string;
backgroundColor: string; backgroundColor: string;

4
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -180,6 +180,10 @@ export class WidgetContext {
} }
} }
get dashboardPageElement(): HTMLElement {
return this.dashboard?.stateController?.dashboardCtrl?.elRef?.nativeElement;
}
authService: AuthService; authService: AuthService;
deviceService: DeviceService; deviceService: DeviceService;
assetService: AssetService; assetService: AssetService;

1
ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts

@ -29,7 +29,6 @@ import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Directive({ @Directive({
standalone: true,
selector: '[tbTruncateWithTooltip]', selector: '[tbTruncateWithTooltip]',
providers: [MatTooltip], providers: [MatTooltip],
}) })

5
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -198,8 +198,9 @@ export class ImportExportService {
); );
} }
public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, widgetTitle: string) { public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget,
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); widgetTitle: string, breakpoint: string) {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName; const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName;
let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : '');
fileName = fileName.toLowerCase().replace(/\W/g, '_'); fileName = fileName.toLowerCase().replace(/\W/g, '_');

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

@ -18,7 +18,7 @@ import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { DashboardId } from '@shared/models/id/dashboard-id'; import { DashboardId } from '@shared/models/id/dashboard-id';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { ShortCustomerInfo } from '@shared/models/customer.model'; import { ShortCustomerInfo } from '@shared/models/customer.model';
import { LegendPosition, Widget } from './widget.models'; import { Widget } from './widget.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { EntityAliases } from './alias.models'; import { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.models'; import { Filters } from '@shared/models/query/query.models';
@ -51,7 +51,8 @@ export interface WidgetLayouts {
export enum LayoutType { export enum LayoutType {
default = 'default', default = 'default',
scada = 'scada' scada = 'scada',
divider = 'divider',
} }
export const layoutTypes = Object.keys(LayoutType) as LayoutType[]; export const layoutTypes = Object.keys(LayoutType) as LayoutType[];
@ -59,7 +60,22 @@ export const layoutTypes = Object.keys(LayoutType) as LayoutType[];
export const layoutTypeTranslationMap = new Map<LayoutType, string>( export const layoutTypeTranslationMap = new Map<LayoutType, string>(
[ [
[ LayoutType.default, 'dashboard.layout-type-default' ], [ LayoutType.default, 'dashboard.layout-type-default' ],
[ LayoutType.scada, 'dashboard.layout-type-scada' ] [ LayoutType.scada, 'dashboard.layout-type-scada' ],
[ LayoutType.divider, 'dashboard.layout-type-divider' ],
]
);
export enum ViewFormatType {
grid = 'grid',
list = 'list',
}
export const viewFormatTypes = Object.keys(ViewFormatType) as ViewFormatType[];
export const viewFormatTypeTranslationMap = new Map<ViewFormatType, string>(
[
[ ViewFormatType.grid, 'dashboard.view-format-type-grid' ],
[ ViewFormatType.list, 'dashboard.view-format-type-list' ],
] ]
); );
@ -70,9 +86,11 @@ export interface GridSettings {
minColumns?: number; minColumns?: number;
margin?: number; margin?: number;
outerMargin?: boolean; outerMargin?: boolean;
viewFormat?: ViewFormatType;
backgroundSizeMode?: string; backgroundSizeMode?: string;
backgroundImageUrl?: string; backgroundImageUrl?: string;
autoFillHeight?: boolean; autoFillHeight?: boolean;
rowHeight?: number;
mobileAutoFillHeight?: boolean; mobileAutoFillHeight?: boolean;
mobileRowHeight?: number; mobileRowHeight?: number;
mobileDisplayLayoutFirst?: boolean; mobileDisplayLayoutFirst?: boolean;
@ -83,14 +101,45 @@ export interface GridSettings {
export interface DashboardLayout { export interface DashboardLayout {
widgets: WidgetLayouts; widgets: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;
breakpoints?: {[breakpointId in BreakpointId]?: Omit<DashboardLayout, 'breakpoints'>};
} }
export interface DashboardLayoutInfo { export declare type DashboardLayoutInfo = {[breakpointId: string]: BreakpointLayoutInfo};
export interface BreakpointLayoutInfo {
widgetIds?: string[]; widgetIds?: string[];
widgetLayouts?: WidgetLayouts; widgetLayouts?: WidgetLayouts;
gridSettings?: GridSettings; gridSettings?: GridSettings;
} }
export declare type BreakpointSystemId = 'default' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export declare type BreakpointId = BreakpointSystemId | string;
export interface BreakpointInfo {
id: BreakpointId;
maxWidth?: number;
minWidth?: number;
value?: string;
}
export const breakpointIdTranslationMap = new Map<BreakpointId, string>([
['default', 'dashboard.breakpoints-id.default'],
['xs', 'dashboard.breakpoints-id.xs'],
['sm', 'dashboard.breakpoints-id.sm'],
['md', 'dashboard.breakpoints-id.md'],
['lg', 'dashboard.breakpoints-id.lg'],
['xl', 'dashboard.breakpoints-id.xl'],
]);
export const breakpointIdIconMap = new Map<BreakpointId, string>([
['default', 'desktop_windows'],
['xs', 'phone_iphone'],
['sm', 'tablet_mac'],
['md', 'computer'],
['lg', 'monitor'],
['xl', 'desktop_windows'],
]);
export interface LayoutDimension { export interface LayoutDimension {
type?: LayoutDimensionType; type?: LayoutDimensionType;
fixedWidth?: number; fixedWidth?: number;

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

@ -67,6 +67,7 @@ import { ColorPickerModule } from '@iplab/ngx-color-picker';
import { NgxHmCarouselModule } from 'ngx-hm-carousel'; import { NgxHmCarouselModule } from 'ngx-hm-carousel';
import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
import { UserMenuComponent } from '@shared/components/user-menu.component'; import { UserMenuComponent } from '@shared/components/user-menu.component';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { NospacePipe } from '@shared/pipe/nospace.pipe'; import { NospacePipe } from '@shared/pipe/nospace.pipe';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component'; import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
@ -356,6 +357,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
NavTreeComponent, NavTreeComponent,
LedLightComponent, LedLightComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
TruncateWithTooltipDirective,
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,
@ -611,6 +613,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
NavTreeComponent, NavTreeComponent,
LedLightComponent, LedLightComponent,
MarkdownEditorComponent, MarkdownEditorComponent,
TruncateWithTooltipDirective,
NospacePipe, NospacePipe,
MillisecondsToTimeStringPipe, MillisecondsToTimeStringPipe,
EnumToArrayPipe, EnumToArrayPipe,

1
ui-ngx/src/assets/locale/locale.constant-ar_AE.json

@ -1283,7 +1283,6 @@
"maximum-upload-file-size": "الحد الأقصى لحجم الملف المرفوع: {{ size }}", "maximum-upload-file-size": "الحد الأقصى لحجم الملف المرفوع: {{ size }}",
"cannot-upload-file": "لا يمكن تحميل الملف", "cannot-upload-file": "لا يمكن تحميل الملف",
"settings": "الإعدادات", "settings": "الإعدادات",
"layout-settings": "إعدادات التخطيط",
"columns-count": "عدد الأعمدة", "columns-count": "عدد الأعمدة",
"columns-count-required": "عدد الأعمدة مطلوب.", "columns-count-required": "عدد الأعمدة مطلوب.",
"min-columns-count-message": "الحد الأدنى المسموح به هو 10 أعمدة.", "min-columns-count-message": "الحد الأدنى المسموح به هو 10 أعمدة.",

1
ui-ngx/src/assets/locale/locale.constant-ca_ES.json

@ -1056,7 +1056,6 @@
"maximum-upload-file-size": "Mida màxima del fitxer de pujada: {{ size }}", "maximum-upload-file-size": "Mida màxima del fitxer de pujada: {{ size }}",
"cannot-upload-file": "No s'ha pogut pujar el fitxer", "cannot-upload-file": "No s'ha pogut pujar el fitxer",
"settings": "Configuració", "settings": "Configuració",
"layout-settings": "Paràmetres de la disposició",
"columns-count": "Número de columnes", "columns-count": "Número de columnes",
"columns-count-required": "Cal número de columnes.", "columns-count-required": "Cal número de columnes.",
"min-columns-count-message": "Sol es permet un número mínim de 10 columnes.", "min-columns-count-message": "Sol es permet un número mínim de 10 columnes.",

1
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -745,7 +745,6 @@
"maximum-upload-file-size": "Maximální velikost souboru: {{ size }}", "maximum-upload-file-size": "Maximální velikost souboru: {{ size }}",
"cannot-upload-file": "Soubor nelze nahrát", "cannot-upload-file": "Soubor nelze nahrát",
"settings": "Nastavení", "settings": "Nastavení",
"layout-settings": "Nastavení rozložení",
"columns-count": "Počet sloupců", "columns-count": "Počet sloupců",
"columns-count-required": "Počet sloupců je povinný.", "columns-count-required": "Počet sloupců je povinný.",
"min-columns-count-message": "Minimální povolený počet sloupců je 10.", "min-columns-count-message": "Minimální povolený počet sloupců je 10.",

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

@ -1171,7 +1171,8 @@
"layout": "Layout", "layout": "Layout",
"layout-type-default": "Default", "layout-type-default": "Default",
"layout-type-scada": "SCADA", "layout-type-scada": "SCADA",
"layout-settings": "Layout settings", "layout-type-divider": "Divider",
"layout-settings-type": "Layout settings: {{ type }}",
"columns-count": "Columns count", "columns-count": "Columns count",
"columns-count-required": "Columns count is required.", "columns-count-required": "Columns count is required.",
"min-columns-count-message": "Only 10 minimum column count is allowed.", "min-columns-count-message": "Only 10 minimum column count is allowed.",
@ -1197,6 +1198,10 @@
"mobile-row-height-required": "Mobile row height value is required.", "mobile-row-height-required": "Mobile row height value is required.",
"min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.", "min-mobile-row-height-message": "Only 5 pixels is allowed as minimum mobile row height value.",
"max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.", "max-mobile-row-height-message": "Only 200 pixels is allowed as maximum mobile row height value.",
"row-height": "Row height",
"row-height-required": "Row height value is required.",
"min-row-height-message": "Only 5 pixels is allowed as minimum row height value.",
"max-row-height-message": "Only 200 pixels is allowed as maximum row height value.",
"display-first-in-mobile-view": "Display first in mobile view", "display-first-in-mobile-view": "Display first in mobile view",
"title-settings": "Title settings", "title-settings": "Title settings",
"display-title": "Display dashboard title", "display-title": "Display dashboard title",
@ -1275,7 +1280,19 @@
"assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge", "assign-dashboard-to-edge-text": "Please select the dashboards to assign to the edge",
"non-existent-dashboard-state-error": "Dashboard state with id \"{{ stateId }}\" is not found", "non-existent-dashboard-state-error": "Dashboard state with id \"{{ stateId }}\" is not found",
"edit-mode": "Edit mode", "edit-mode": "Edit mode",
"duplicate-state-action": "Duplicate state" "duplicate-state-action": "Duplicate state",
"breakpoint-value": "Breakpoint ({{ value }})",
"breakpoints-id": {
"default": "Default",
"xs": "Mobile (xs)",
"sm": "Tablet (sm)",
"md": "Laptop (md)",
"lg": "Desktop (lg)",
"xl": "Desktop (xl)"
},
"view-format-type-grid": "Grid",
"view-format-type-list": "List",
"view-format": "View format"
}, },
"datakey": { "datakey": {
"settings": "Settings", "settings": "Settings",
@ -3827,7 +3844,14 @@
"left-width-percentage-required": "Left percentage is required", "left-width-percentage-required": "Left percentage is required",
"divider": "Divider", "divider": "Divider",
"right-side": "Right side layout", "right-side": "Right side layout",
"left-side": "Left side layout" "left-side": "Left side layout",
"add-new-breakpoint": "Add new breakpoint",
"breakpoint": "Breakpoint",
"breakpoints": "Breakpoints",
"copy-from": "Copy from",
"size": "Size",
"delete-breakpoint-title": "Are you sure you want to delete the breakpoint '{{name}}'?",
"delete-breakpoint-text": "Please note, after confirmation, the breakpoint will become unrecoverable, and the settings will revert to the default breakpoint."
}, },
"legend": { "legend": {
"direction": "Direction", "direction": "Direction",
@ -5652,6 +5676,7 @@
"edit": "Edit widget", "edit": "Edit widget",
"remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?", "remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?",
"remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.", "remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.",
"replace-reference-with-widget-copy": "Replace reference with widget copy",
"timeseries": "Time series", "timeseries": "Time series",
"search-data": "Search data", "search-data": "Search data",
"no-data-found": "No data found", "no-data-found": "No data found",
@ -5732,7 +5757,8 @@
"search": "Search widget", "search": "Search widget",
"filter": "Widget filter type", "filter": "Widget filter type",
"loading-widgets": "Loading widgets...", "loading-widgets": "Loading widgets...",
"widget-template-error": "Invalid widget HTML template." "widget-template-error": "Invalid widget HTML template.",
"reference": "Reference"
}, },
"widget-action": { "widget-action": {
"header-button": "Widget header button", "header-button": "Widget header button",
@ -5940,7 +5966,8 @@
"card-appearance": "Card appearance", "card-appearance": "Card appearance",
"color": "Color", "color": "Color",
"tooltip": "Tooltip", "tooltip": "Tooltip",
"units-required": "Unit is required." "units-required": "Unit is required.",
"list-layout": "List layout"
}, },
"widget-type": { "widget-type": {
"import": "Import widget type", "import": "Import widget type",

1
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -1066,7 +1066,6 @@
"maximum-upload-file-size": "Tamaño máximo de fichero: {{ size }}", "maximum-upload-file-size": "Tamaño máximo de fichero: {{ size }}",
"cannot-upload-file": "No es posible subir el fichero", "cannot-upload-file": "No es posible subir el fichero",
"settings": "Ajustes", "settings": "Ajustes",
"layout-settings": "Ajustes de diseño",
"columns-count": "Número de columnas", "columns-count": "Número de columnas",
"columns-count-required": "Número de columnas requerido.", "columns-count-required": "Número de columnas requerido.",
"min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.", "min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.",

1
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -830,7 +830,6 @@
"empty-image": "Aucune image", "empty-image": "Aucune image",
"maximum-upload-file-size": "Taille de fichier maximum: {{ size }}", "maximum-upload-file-size": "Taille de fichier maximum: {{ size }}",
"cannot-upload-file": "Le téléchargement a échoué", "cannot-upload-file": "Le téléchargement a échoué",
"layout-settings": "Paramètres de mise en page",
"margin-required": "Valeur de marge requise.", "margin-required": "Valeur de marge requise.",
"min-margin-message": "0 est la valeur minimum permise.", "min-margin-message": "0 est la valeur minimum permise.",
"max-margin-message": "50 est la valeur maximum permise.", "max-margin-message": "50 est la valeur maximum permise.",

1
ui-ngx/src/assets/locale/locale.constant-lt_LT.json

@ -1256,7 +1256,6 @@
"maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}", "maximum-upload-file-size": "Maksimalus įkeliamo failo dydis: {{ size }}",
"cannot-upload-file": "Failo įkelti nepavyko", "cannot-upload-file": "Failo įkelti nepavyko",
"settings": "Nustatymai", "settings": "Nustatymai",
"layout-settings": "Išdėstymo nustatymai",
"columns-count": "Stulpelių skaičius", "columns-count": "Stulpelių skaičius",
"columns-count-required": "Stulpelių skaičius būtinas.", "columns-count-required": "Stulpelių skaičius būtinas.",
"min-columns-count-message": "Minimalus stulpelių skaičius - 10.", "min-columns-count-message": "Minimalus stulpelių skaičius - 10.",

1
ui-ngx/src/assets/locale/locale.constant-nl_BE.json

@ -1168,7 +1168,6 @@
"maximum-upload-file-size": "Maximale bestandsgrootte uploaden: {{ size }}", "maximum-upload-file-size": "Maximale bestandsgrootte uploaden: {{ size }}",
"cannot-upload-file": "Kan bestand niet uploaden", "cannot-upload-file": "Kan bestand niet uploaden",
"settings": "Instellingen", "settings": "Instellingen",
"layout-settings": "Lay-out instellingen",
"columns-count": "Aantal kolommen", "columns-count": "Aantal kolommen",
"columns-count-required": "Het aantal kolommen is vereist.", "columns-count-required": "Het aantal kolommen is vereist.",
"min-columns-count-message": "Er is slechts een minimum van 10 kolommen toegestaan.", "min-columns-count-message": "Er is slechts een minimum van 10 kolommen toegestaan.",

1
ui-ngx/src/assets/locale/locale.constant-pl_PL.json

@ -1256,7 +1256,6 @@
"maximum-upload-file-size": "Maksymalny rozmiar przesyłanego pliku: {{ size }}", "maximum-upload-file-size": "Maksymalny rozmiar przesyłanego pliku: {{ size }}",
"cannot-upload-file": "Nie można przesłać pliku", "cannot-upload-file": "Nie można przesłać pliku",
"settings": "Ustawienia", "settings": "Ustawienia",
"layout-settings": "Ustawienia układu",
"columns-count": "Liczba kolumn", "columns-count": "Liczba kolumn",
"columns-count-required": "Liczba kolumn jest wymagana.", "columns-count-required": "Liczba kolumn jest wymagana.",
"min-columns-count-message": "Dozwolona jest tylko minimalna liczba kolumn wynosząca 10.", "min-columns-count-message": "Dozwolona jest tylko minimalna liczba kolumn wynosząca 10.",

1
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -748,7 +748,6 @@
"maximum-upload-file-size": "Maksimum yükleme dosyası boyutu: {{ size }}", "maximum-upload-file-size": "Maksimum yükleme dosyası boyutu: {{ size }}",
"cannot-upload-file": "Dosya yüklenemiyor", "cannot-upload-file": "Dosya yüklenemiyor",
"settings": "Ayarlar", "settings": "Ayarlar",
"layout-settings": "Görünüm ayarları",
"columns-count": "Sütun sayısı", "columns-count": "Sütun sayısı",
"columns-count-required": "Sütun sayısı gerekli.", "columns-count-required": "Sütun sayısı gerekli.",
"min-columns-count-message": "Yalnızca 10 minimum sütun sayısına izin verilir.", "min-columns-count-message": "Yalnızca 10 minimum sütun sayısına izin verilir.",

1
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -1097,7 +1097,6 @@
"maximum-upload-file-size": "最大上传文件大小: {{ size }}", "maximum-upload-file-size": "最大上传文件大小: {{ size }}",
"cannot-upload-file": "无法上传文件", "cannot-upload-file": "无法上传文件",
"settings": "设置", "settings": "设置",
"layout-settings": "布局设置",
"columns-count": "列数", "columns-count": "列数",
"columns-count-required": "需要列数。", "columns-count-required": "需要列数。",
"min-columns-count-message": "只允许最少10列", "min-columns-count-message": "只允许最少10列",

1
ui-ngx/src/assets/locale/locale.constant-zh_TW.json

@ -828,7 +828,6 @@
"maximum-upload-file-size": "最大上傳文件大小: {{ size }}", "maximum-upload-file-size": "最大上傳文件大小: {{ size }}",
"cannot-upload-file": "無法上傳文件", "cannot-upload-file": "無法上傳文件",
"settings": "設定", "settings": "設定",
"layout-settings": "佈局設定",
"columns-count": "列數", "columns-count": "列數",
"columns-count-required": "需要列數。", "columns-count-required": "需要列數。",
"min-columns-count-message": "只允許最少10列", "min-columns-count-message": "只允許最少10列",

6
ui-ngx/src/styles.scss

@ -921,6 +921,12 @@ mat-icon {
} }
.mat-mdc-icon-button { .mat-mdc-icon-button {
&.tb-mat-16 {
@include tb-mat-icon-button-size(16);
.mat-icon {
@include tb-mat-icon-size(16);
}
}
&.tb-mat-20 { &.tb-mat-20 {
@include tb-mat-icon-button-size(20); @include tb-mat-icon-button-size(20);
.mat-icon { .mat-icon {

Loading…
Cancel
Save