Browse Source

Merge branch 'master' into feature/menu-refactoring

feature/menu-refactoring
Igor Kulikov 2 years ago
parent
commit
150285b9c9
  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",
"name": "{i18n:scada.symbol.value-box}",
"type": "color",
"default": "#FFFFFF",
"default": "#F3F3F3",
"required": null,
"subLabel": null,
"divider": null,
@ -633,7 +633,7 @@
</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"/>
</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"/>
<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>

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 { TimeService } from '@core/services/time.service';
import {
BreakpointId,
breakpointIdIconMap,
breakpointIdTranslationMap,
BreakpointInfo,
BreakpointLayoutInfo,
BreakpointSystemId,
Dashboard,
DashboardConfiguration,
DashboardLayout,
DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo,
DashboardState,
DashboardStateLayouts,
@ -41,6 +46,7 @@ import {
Widget,
WidgetConfig,
WidgetConfigMode,
WidgetSize,
widgetType,
WidgetTypeDescriptor
} 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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root'
})
export class DashboardUtilsService {
private systemBreakpoints: {[key in BreakpointSystemId]?: BreakpointInfo};
constructor(private utils: UtilsService,
private timeService: TimeService) {
private timeService: TimeService,
private translate: TranslateService) {
}
public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard {
@ -523,6 +534,28 @@ export class DashboardUtilsService {
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 {
for (const stateId of Object.keys(states)) {
const state = states[stateId];
@ -542,16 +575,14 @@ export class DashboardUtilsService {
for (const l of Object.keys(state.layouts)) {
const layout: DashboardLayout = state.layouts[l];
if (layout) {
result[l] = {
widgetIds: [],
widgetLayouts: {},
gridSettings: {}
} as DashboardLayoutInfo;
for (const id of Object.keys(layout.widgets)) {
result[l].widgetIds.push(id);
result[l]= {
default: this.getBreakpointLayoutData(layout)
};
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
result[l][breakpoint] = this.getBreakpointLayoutData(layout.breakpoints[breakpoint]);
}
}
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
}
}
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> {
const widgetsArray: Array<Widget> = [];
const dashboardConfiguration = dashboard.configuration;
@ -586,11 +631,15 @@ export class DashboardUtilsService {
originalColumns?: number,
originalSize?: {sizeX: number; sizeY: number},
row?: number,
column?: number): void {
column?: number,
breakpoint = 'default'): void {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
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;
if (!widget.id) {
widget.id = this.utils.guid();
@ -648,12 +697,17 @@ export class DashboardUtilsService {
public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
widgetId: string) {
widgetId: string,
breakpoint: string) {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[targetState];
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);
}
@ -721,11 +775,19 @@ export class DashboardUtilsService {
for (const s of Object.keys(states)) {
const state = states[s];
for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l];
const layout: DashboardLayout = state.layouts[l];
if (layout.widgets[widgetId]) {
found = true;
break;
}
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
if (layout.breakpoints[breakpoint].widgets[widgetId]) {
found = true;
break;
}
}
}
}
}
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;
originalSize: WidgetSize;
originalColumns: number;
breakpoint: string;
}
export interface RuleNodeConnection {
@ -85,7 +86,8 @@ export class ItemBufferService {
private ruleChainService: RuleChainService,
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 = {
datasourceAliases: {},
targetDeviceAlias: null
@ -93,8 +95,8 @@ export class ItemBufferService {
const filtersInfo: FiltersInfo = {
datasourceFilters: {}
};
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources;
if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) {
@ -146,13 +148,14 @@ export class ItemBufferService {
};
}
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_ITEM, widgetItem);
}
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: string): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_REFERENCE, widgetReference);
}
@ -160,11 +163,11 @@ export class ItemBufferService {
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);
if (widgetReference) {
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]) {
return true;
}
@ -174,7 +177,9 @@ export class ItemBufferService {
}
public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition,
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<Widget> {
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
@ -194,7 +199,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo, filtersInfo,
onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, targetRow, targetColumn).pipe(
originalColumns, originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget)
);
} else {
@ -202,8 +207,11 @@ export class ItemBufferService {
}
}
public pasteWidgetReference(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> {
public pasteWidgetReference(targetDashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition): Observable<Widget> {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) {
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
@ -219,7 +227,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null,
null, null, null, originalColumns,
originalSize, targetRow, targetColumn).pipe(
originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget)
);
} else {
@ -239,7 +247,8 @@ export class ItemBufferService {
originalColumns: number,
originalSize: WidgetSize,
row: number,
column: number): Observable<Dashboard> {
column: number,
breakpoint = 'default'): Observable<Dashboard> {
let theDashboard: Dashboard;
if (dashboard) {
theDashboard = dashboard;
@ -270,7 +279,7 @@ export class ItemBufferService {
}
}
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column);
originalColumns, originalSize, row, column, breakpoint);
if (callAliasUpdateFunction) {
onAliasesUpdateFunction();
}
@ -387,35 +396,6 @@ export class ItemBufferService {
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 {
return {
alias: entityAlias.alias,
@ -432,16 +412,17 @@ export class ItemBufferService {
}
private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): WidgetReference {
const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
return {
dashboardId: dashboard.id.id,
sourceState,
sourceLayout,
widgetId: widget.id,
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"
[widget]="widget"
[hideHeader]="hideHeader"
[scada]="data.scada"
[showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
isAdd
formControlName="widgetConfig">
</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;
widget: Widget;
widgetInfo: WidgetInfo;
scada: boolean;
showLayoutConfig: boolean;
isDefaultBreakpoint: boolean;
}
@Component({
@ -60,6 +61,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
stateController: IStateController;
widget: Widget;
showLayoutConfig = true;
isDefaultBreakpoint = true;
widgetConfig: WidgetConfigComponentData;
previewMode = false;
@ -92,6 +96,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController;
this.widget = this.data.widget;
this.showLayoutConfig = this.data.showLayoutConfig;
this.isDefaultBreakpoint = this.data.isDefaultBreakpoint;
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>
<ng-container *ngIf="isEdit">
<div style="flex: 1 1 100%" fxHide.gt-sm></div>
<div fxFlex.lt-md fxLayout="row"
fxLayoutAlign.gt-sm="start center"
fxLayoutAlign="end center" fxLayoutGap.lt-lg="6px" fxLayoutGap.gt-md="12px">
<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>
fxLayoutAlign="end center" fxLayoutGap.lt-lg="3px" fxLayoutGap.lg="6px" fxLayoutGap.gt-lg="12px">
<mat-divider class="tb-toolbar-divider" vertical></mat-divider>
<button fxHide.gt-md mat-icon-button
matTooltip="{{'layout.manage' | translate}}"
matTooltipPosition="below"
@ -58,6 +47,9 @@
<mat-icon>view_compact</mat-icon>
{{'layout.layouts' | translate}}
</button>
<tb-select-dashboard-breakpoint
[dashboardCtrl]="this">
</tb-select-dashboard-breakpoint>
</div>
</ng-container>
<tb-states-component fxFlex.lt-md
@ -70,6 +62,21 @@
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</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"
aria-label="dashboard_logo" class="dashboard_logo"/>
</div>
@ -88,7 +95,7 @@
(click)="toggleLayouts()">
<mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
</button>
<button [fxShow]="!hideFullscreenButton()" mat-icon-button
<button [fxShow]="!hideFullscreenButton()" mat-icon-button fxHide.xs
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen">
@ -230,13 +237,13 @@
</ng-container>
</ng-template>
<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 }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
</button>
<button fxHide.lt-sm mat-stroked-button
<button fxHide.lt-lg mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
@ -282,7 +289,7 @@
</mat-form-field>
</section>
<mat-drawer-container class="tb-dashboard-layouts" fxFlex
[ngClass]="{ 'tb-shrinked' : isEditingWidget }">
[class]="{'tb-shrinked' : isEditingWidget}">
<mat-drawer *ngIf="layouts.right.show"
id="tb-right-layout"
[ngStyle]="{minWidth: rightLayoutSize.width,
@ -308,9 +315,11 @@
</mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show"
id="tb-main-layout"
[ngStyle]="{width: mainLayoutSize.width,
height: mainLayoutSize.height}">
[style]="{width: mainLayoutSize.width,
height: mainLayoutSize.height}">
<tb-dashboard-layout
[style.max-width]="mainLayoutSize.maxWidth"
[style.min-width]="mainLayoutSize.minWidth"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
@ -321,6 +330,11 @@
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
<div class="layout-filler"
*ngIf="isEdit"
[fxShow]="showMainLayoutFiller">
{{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</div>
</mat-drawer-content>
</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)">
@ -380,7 +394,8 @@
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"
[scada]="editingLayoutCtx?.gridSettings?.layoutType === LayoutType.scada"
[showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)"
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</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));
}
}
#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 {

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 { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import {
BreakpointId,
BreakpointInfo,
Dashboard,
DashboardConfiguration,
DashboardLayoutId,
@ -52,6 +53,7 @@ import {
GridSettings,
LayoutDimension,
LayoutType,
ViewFormatType,
WidgetLayout
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
@ -67,7 +69,7 @@ import {
IDashboardController,
LayoutWidgetsArray
} from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model';
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 { EntityService } from '@core/http/entity.service';
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 { DashboardService } from '@core/http/dashboard.service';
import {
@ -93,7 +95,6 @@ import {
WidgetContextMenuItem
} from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { UntypedFormBuilder } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service';
import { MatDialog } from '@angular/material/dialog';
import {
@ -139,14 +140,14 @@ import {
DashboardImageDialogData,
DashboardImageDialogResult
} 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 { DOCUMENT } from '@angular/common';
import { IAliasController } from '@core/api/widget-api.models';
import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service';
import { map, tap } from 'rxjs/operators';
import { distinctUntilChanged, map, skip, tap } from 'rxjs/operators';
import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer';
@ -262,7 +263,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
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%'};
private dashboardLogoCache: SafeUrl;
@ -275,6 +277,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
getDashboard: () => this.dashboard,
dashboardTimewindow: null,
state: null,
breakpoint: null,
stateController: null,
stateChanged: null,
stateId: null,
@ -287,26 +290,32 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
show: false,
layoutCtx: {
id: 'main',
breakpoint: 'default',
widgets: null,
widgetLayouts: {},
gridSettings: {},
ignoreLoading: true,
ctrl: null,
dashboardCtrl: this,
displayGrid: 'onDrag&Resize'
displayGrid: 'onDrag&Resize',
layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
}
},
right: {
show: false,
layoutCtx: {
id: 'right',
breakpoint: 'default',
widgets: null,
widgetLayouts: {},
gridSettings: {},
ignoreLoading: true,
ctrl: null,
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;
private changeMobileSize = new Subject<boolean>();
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document,
@ -348,7 +359,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private router: Router,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private authService: AuthService,
private entityService: EntityService,
private dialogService: DialogService,
private widgetComponentService: WidgetComponentService,
@ -356,7 +366,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private itembuffer: ItemBufferService,
private importExport: ImportExportService,
private mobileService: MobileService,
private fb: UntypedFormBuilder,
private dialog: MatDialog,
public translate: TranslateService,
private popoverService: TbPopoverService,
@ -366,12 +375,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer,
public elRef: ElementRef,
private injector: Injector) {
super(store);
if (isDefinedAndNotNull(embeddedValue)) {
this.embedded = embeddedValue;
if (isDefinedAndNotNull(this.embeddedValue)) {
this.embedded = this.embeddedValue;
}
}
@ -411,13 +419,42 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
));
}
this.rxSubscriptions.push(this.breakpointObserver
.observe(MediaBreakpoints['gt-sm'])
.subscribe((state: BreakpointState) => {
this.isMobile = !state.matches;
this.rxSubscriptions.push(
this.changeMobileSize.pipe(
distinctUntilChanged(),
).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();
}
));
)
);
if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.registerToggleLayoutFunction(() => {
setTimeout(() => {
@ -565,6 +602,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (this.dashboardResize$) {
this.dashboardResize$.disconnect();
}
this.layouts.main.layoutCtx.layoutDataChanged.unsubscribe();
this.layouts.right.layoutCtx.layoutDataChanged.unsubscribe();
}
public runChangeDetection() {
@ -588,7 +627,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
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 {
@ -703,7 +742,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened);
}
private updateLayoutSizes() {
public updateLayoutSizes() {
let changeMainLayoutSize = false;
let changeRightLayoutSize = false;
if (this.dashboardCtx.state) {
@ -718,6 +757,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private updateMainLayoutSize(): boolean {
const prevMainLayoutWidth = this.mainLayoutSize.width;
const prevMainLayoutHeight = this.mainLayoutSize.height;
const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth;
if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
this.mainLayoutSize.width = '100%';
} else {
@ -728,7 +768,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} else {
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 {
@ -864,6 +928,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: {
settings: deepClone(this.dashboard.configuration.settings),
gridSettings,
breakpointId: this.layouts.main.layoutCtx.breakpoint
}
}).afterClosed().subscribe((data) => {
if (data) {
@ -1056,21 +1121,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateLayout(layout, layoutInfo);
} else {
layout.show = false;
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null});
this.updateLayout(layout, {default: {widgetIds: [], widgetLayouts: {}, gridSettings: null}});
}
}
}
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
if (layoutInfo.gridSettings) {
layout.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
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;
layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.layoutDataChanged.next();
layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show);
this.updateLayoutSizes();
}
@ -1189,8 +1248,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) {
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget);
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id);
const layoutCtx = this.layouts[layoutId].layoutCtx;
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget, undefined,
undefined, -1, -1, layoutCtx.breakpoint);
layoutCtx.widgets.addWidgetId(widget.id);
this.runChangeDetection();
}
@ -1250,10 +1311,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
col: 0
};
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();
if (scada) {
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) {
this.addWidgetToDashboard(newWidget);
} else {
@ -1269,7 +1340,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
stateController: this.dashboardCtx.stateController,
widget: newWidget,
widgetInfo: widgetTypeInfo,
scada
showLayoutConfig,
isDefaultBreakpoint
}
}).afterClosed().subscribe((addedWidget) => {
if (addedWidget) {
@ -1348,18 +1420,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) {
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) {
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) {
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(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
@ -1368,7 +1460,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
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(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
@ -1390,7 +1482,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
).subscribe((res) => {
if (res) {
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();
}
}
@ -1399,7 +1492,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) {
$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) {
@ -1455,7 +1548,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
action: ($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',
icon: 'content_paste',
shortcut: 'M-I'
@ -1475,7 +1568,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return dashboardContextActions;
}
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem> {
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
const widgetContextActions: Array<WidgetContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget) {
widgetContextActions.push(
@ -1488,6 +1581,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
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) {
widgetContextActions.push(
{
@ -1646,4 +1751,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.
///
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 { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models';
import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { ElementRef } from '@angular/core';
export declare type DashboardPageScope = 'tenant' | 'customer';
@ -35,6 +43,7 @@ export interface DashboardPageInitData {
export interface DashboardContext {
instanceId: string;
state: string;
breakpoint: BreakpointId;
getDashboard: () => Dashboard;
dashboardTimewindow: Timewindow;
aliasController: IAliasController;
@ -46,17 +55,20 @@ export interface DashboardContext {
export interface IDashboardController {
dashboardCtx: DashboardContext;
dashboardContainer: ElementRef;
elRef: ElementRef;
openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean);
addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
replaceReferenceWithWidgetCopy($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
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);
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
@ -65,6 +77,9 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext {
id: DashboardLayoutId;
layoutData: DashboardLayoutInfo;
layoutDataChanged: BehaviorSubject<void>;
breakpoint: BreakpointId;
widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts;
gridSettings: GridSettings;

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

@ -105,18 +105,18 @@
</mat-expansion-panel>
</div>
<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-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 translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()"
@ -188,6 +188,25 @@
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
</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 translate>dashboard.background-color</div>
<tb-color-input asBoxInput
@ -212,7 +231,7 @@
</mat-form-field>
</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 *ngIf="isRightLayout" class="tb-form-row">
<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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@ -24,20 +24,26 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
import {
BreakpointId,
DashboardSettings,
GridSettings,
LayoutType,
layoutTypes, layoutTypeTranslationMap,
StateControllerId
StateControllerId,
ViewFormatType,
viewFormatTypes,
viewFormatTypeTranslationMap
} from '@app/shared/models/dashboard.models';
import { isDefined, isUndefined } from '@core/utils';
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 {
settings?: DashboardSettings;
gridSettings?: GridSettings;
isRightLayout?: boolean;
breakpointId?: BreakpointId;
}
@Component({
@ -47,11 +53,10 @@ export interface DashboardSettingsDialogData {
styleUrls: ['./dashboard-settings-dialog.component.scss']
})
export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData>
implements OnInit, ErrorStateMatcher {
implements OnDestroy, ErrorStateMatcher {
layoutTypes = layoutTypes;
layoutTypeTranslations = layoutTypeTranslationMap;
viewFormatTypes = viewFormatTypes;
viewFormatTypeTranslationMap = viewFormatTypeTranslationMap;
settings: DashboardSettings;
gridSettings: GridSettings;
@ -62,10 +67,17 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
stateControllerIds: string[];
layoutSettingsType: string;
submitted = false;
scadaColumns: number[] = [];
private layoutType = LayoutType.default;
private breakpointId: BreakpointId = 'default';
private destroy$ = new Subject<void>();
private stateControllerTranslationMap = new Map<string, string>([
['default', 'dashboard.state-controller-default'],
]);
@ -77,7 +89,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: FormBuilder,
private translate: TranslateService,
private statesControllerService: StatesControllerService) {
private statesControllerService: StatesControllerService,
private dashboardUtils: DashboardUtilsService) {
super(store, router, dialogRef);
this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers());
@ -85,6 +98,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings;
this.isRightLayout = this.data.isRightLayout;
this.breakpointId = this.data.breakpointId;
if (this.settings) {
const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle;
@ -116,14 +130,18 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
disabled: hideToolbar}, []],
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) => {
if (stateControllerId !== 'default') {
this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true);
}
}
);
this.settingsFormGroup.get('showTitle').valueChanges.subscribe(
this.settingsFormGroup.get('showTitle').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(showTitleValue: boolean) => {
if (showTitleValue) {
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) => {
if (showDashboardLogoValue) {
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) => {
if (hideToolbarValue) {
this.settingsFormGroup.get('toolbarAlwaysOpen').disable();
@ -167,6 +189,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
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;
while (columns <= 1008) {
this.scadaColumns.push(columns);
@ -174,14 +206,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight;
this.gridSettingsFormGroup = this.fb.group({
layoutType: [this.gridSettings.layoutType || LayoutType.default, []],
columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1008)]],
minColumns: [this.gridSettings.minColumns || this.gridSettings.columns || 24,
[Validators.required, Validators.min(10), Validators.max(1008)]],
margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10,
[Validators.required, Validators.min(0), Validators.max(50)]],
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, []],
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)', []],
backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []],
backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []],
@ -194,27 +228,56 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst;
this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, []));
}
merge(this.gridSettingsFormGroup.get('layoutType').valueChanges,
this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges).subscribe(
() => {
this.updateGridSettingsFormState();
if (this.isScada()) {
this.gridSettingsFormGroup.get('margin').disable();
this.gridSettingsFormGroup.get('outerMargin').disable();
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);
}
);
this.updateGridSettingsFormState();
} else {
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 {
this.gridSettingsFormGroup = this.fb.group({});
}
}
ngOnInit(): void {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
isScada() {
if (this.gridSettingsFormGroup) {
return this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada;
} else {
return false;
}
return this.layoutType === LayoutType.scada;
}
get isDefaultLayout() {
return this.layoutType === LayoutType.default;
}
get isDefaultBreakpoint(): boolean {
return this.breakpointId === 'default';
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -248,29 +311,28 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
private updateGridSettingsFormState() {
const isScada: boolean = this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada;
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});
if (this.breakpointId === 'default') {
const mobileAutoFillHeight: boolean = this.gridSettingsFormGroup.get('mobileAutoFillHeight').value;
if (mobileAutoFillHeight) {
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
} else {
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} {
gap: 6px;
&.tb-left-panel {
gap: 3px;
}
}
@media #{$mat-md} {
gap: 6px;
gap: 3px;
&.tb-left-panel {
gap: 1px;
}
}
@media #{$mat-gt-md} {
gap: 12px;
gap: 6px;
&.tb-left-panel {
gap: 3px;
}
}
@media #{$mat-gt-sm} {

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

@ -24,7 +24,8 @@
[dashboard]="dashboard"
[widget]="widget"
[hideToggleHeader]="previewMode"
[scada]="scada"
[showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix">
<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()
@coerceBoolean()
scada = false;
showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint= true;
@Output()
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"
[autofillHeight]="autoFillHeight"
[mobileAutofillHeight]="mobileAutoFillHeight"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
[isMobile]="isMobile"
[mobileRowHeight]="mobielRowHeigth"
[isMobile]="isMobileValue"
[isMobileDisabled]="isMobileDisabled"
[disableWidgetInteraction]="isEdit"
[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 { map } from 'rxjs/operators';
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({
selector: 'tb-dashboard-layout',
@ -85,11 +86,27 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}
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 {
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 {
@ -129,11 +146,13 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private itembuffer: ItemBufferService,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer) {
constructor(
protected store: Store<AppState>,
private translate: TranslateService,
private itembuffer: ItemBufferService,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
) {
super(store);
this.initHotKeys();
}
@ -199,7 +218,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
this.dashboardCtx.state, this.layoutCtx.id)) {
this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) {
event.preventDefault();
this.pasteWidgetReference(event);
}
@ -264,6 +283,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
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 {
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);
}
prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> {
prepareDashboardContextMenu(_: Event): Array<DashboardContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
}
prepareWidgetContextMenu($event: Event, widget: Widget): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget);
prepareWidgetContextMenu(_: Event, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, isReference);
}
copyWidget($event: Event, widget: Widget) {
@ -310,4 +333,24 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
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);
pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent);
updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean);
}
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>
</button>
</mat-toolbar>
<div mat-dialog-content fxLayout="column" fxLayoutGap="20px" style="min-width: 300px; overflow: hidden;">
<div fxLayout="row" fxLayoutAlign="start center">
<mat-slide-toggle formControlName="right" color="accent">
{{ 'layout.divider' | translate }}
</mat-slide-toggle>
</div>
<div mat-dialog-content style="min-width: 300px; overflow: hidden;">
<section class="tb-form-panel no-padding no-border">
<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 *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">
<mat-button-toggle-group aria-label="Select width value type"
formControlName="type"
[fxShow]="layoutsFormGroup.get('right').value">
[fxShow]="isDividerLayout">
<mat-button-toggle fxFlex value="percentage">
{{ 'layout.percentage-width' | translate }}
</mat-button-toggle>
@ -52,7 +113,7 @@
<div fxLayout="column" fxLayoutAlign="start center" fxFlex="{{ buttonFlexValue() }}" class="tb-layout-preview-container-main">
<button mat-icon-button
type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main')"
class="tb-layout-preview-element"
@ -72,7 +133,7 @@
<span [matTooltip]="layoutButtonText('main')"
#mainLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ (layoutsFormGroup.value.right ? 'layout.left' : 'layout.main') | translate }}
{{ (isDividerLayout ? 'layout.left' : 'layout.main') | translate }}
</span>
</button>
<div fxFlex class="tb-layout-preview-element tb-layout-preview-input" *ngIf="showPreviewInputs('main')">
@ -93,10 +154,10 @@
required>
</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
type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('right')"
class="tb-layout-preview-element"
@ -138,7 +199,7 @@
</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"
step="1"
min="{{ layoutPercentageSize.MIN }}"
@ -167,6 +228,7 @@
</div>
</div>
</div>
</ng-template>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<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;
@import '../theme.scss';
@import '../scss/constants.scss';
$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 {

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.
///
import { Component, Inject, SkipSelf, ViewChild } from '@angular/core';
import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DashboardLayoutId, DashboardStateLayouts, LayoutDimension } from '@app/shared/models/dashboard.models';
import { deepClone, isDefined } from '@core/utils';
import {
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 {
DashboardSettingsDialogComponent,
@ -46,11 +55,25 @@ import {
} from '@home/components/dashboard-page/layout/layout.models';
import { Subscription } from 'rxjs';
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 {
layouts: DashboardStateLayouts;
}
export interface DashboardLayoutSettings {
icon: string;
name: string;
descriptionSize?: string;
layout: DashboardLayout;
breakpoint: string;
}
@Component({
selector: 'tb-manage-dashboard-layouts-dialog',
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']
})
export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>
implements ErrorStateMatcher {
implements ErrorStateMatcher, OnDestroy {
@ViewChild('tooltip', {static: true}) tooltip: MatTooltip;
@ViewChild('tooltip') tooltip: MatTooltip;
layoutsFormGroup: UntypedFormGroup;
@ -70,29 +93,43 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutFixedSize = LayoutFixedSize;
layoutTypes = layoutTypes;
layoutTypeTranslations = layoutTypeTranslationMap;
layoutBreakpoints: DashboardLayoutSettings[] = [];
private readonly layouts: DashboardStateLayouts;
private subscriptions: Array<Subscription> = [];
private submitted = false;
allowBreakpointIds = [];
selectedBreakpointIds = ['default'];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
private dialog: MatDialog) {
private dialog: MatDialog,
private dialogs: DialogService) {
super(store, router, dialogRef);
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({
layoutType: [layoutType],
main: [{value: isDefined(this.layouts.main), disabled: true}],
right: [isDefined(this.layouts.right)],
sliderPercentage: [50],
sliderFixed: [this.layoutFixedSize.MIN],
leftWidthPercentage: [50,
@ -141,7 +178,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
sliderFixed: this.layouts.main.gridSettings.layoutDimension.fixedWidth
}, {emitEvent: false});
} else {
const leftWidthPercentage: number = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage);
const leftWidthPercentage = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage);
this.layoutsFormGroup.patchValue({
leftWidthPercentage,
sliderPercentage: leftWidthPercentage,
@ -158,6 +195,21 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
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.layoutsFormGroup.get('sliderPercentage').valueChanges
.subscribe(
@ -209,8 +261,10 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
return originalErrorState || customErrorState;
}
openLayoutSettings(layoutId: DashboardLayoutId) {
const gridSettings = deepClone(this.layouts[layoutId].gridSettings);
openLayoutSettings(layoutId: DashboardLayoutId, breakpointId: BreakpointId = 'default') {
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,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
disableClose: true,
@ -218,11 +272,12 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
data: {
settings: null,
gridSettings,
isRightLayout: this.layoutsFormGroup.get('right').value && layoutId === 'right'
isRightLayout: this.isDividerLayout && layoutId === 'right',
breakpointId
}
}).afterClosed().subscribe((data) => {
if (data && data.gridSettings) {
this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings);
this.dashboardUtils.updateLayoutSettings(layout, data.gridSettings);
this.layoutsFormGroup.markAsDirty();
}
});
@ -234,20 +289,22 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
save(): void {
this.submitted = true;
const layouts = ['main', 'right'];
for (const l of layouts) {
const control = this.layoutsFormGroup.controls[l];
if (!control.value) {
if (this.layouts[l]) {
delete this.layouts[l];
}
const layoutType = this.layoutsFormGroup.value.layoutType;
this.layouts.main.gridSettings.layoutType = layoutType;
if (!this.isDividerLayout) {
delete this.layouts.right;
for (const breakpoint of Object.values(this.layouts.main.breakpoints)) {
breakpoint.gridSettings.layoutType = layoutType;
}
} else {
delete this.layouts.main.breakpoints;
this.layouts.right.gridSettings.layoutType = layoutType;
}
delete this.layouts.main.gridSettings.layoutDimension;
if (this.layouts.right?.gridSettings) {
delete this.layouts.right.gridSettings.layoutDimension;
}
if (this.layoutsFormGroup.value.right) {
if (this.isDividerLayout) {
const formValues = this.layoutsFormGroup.value;
const widthType = formValues.type;
const layoutDimension: LayoutDimension = {
@ -271,7 +328,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
buttonFlexValue(): number {
const formValues = this.layoutsFormGroup.value;
if (formValues.right) {
if (this.isDividerLayout) {
if (formValues.type !== LayoutWidthType.FIXED) {
return formValues.leftWidthPercentage;
} else {
@ -288,7 +345,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
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);
this.layoutsFormGroup.get(key).setValue(valueToSet, {emitEvent: false});
this.layoutsFormGroup.get('sliderPercentage')
@ -296,7 +353,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
}
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').markAsDirty();
}
@ -339,7 +396,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonClass(side: DashboardLayoutId, border: boolean = false): string {
const formValues = this.layoutsFormGroup.value;
if (formValues.right) {
if (this.isDividerLayout) {
let classString = border ? 'tb-layout-button-main ' : '';
if (!(formValues.fixedLayout === side || formValues.type === LayoutWidthType.PERCENTAGE)) {
classString += 'tb-fixed-layout-button';
@ -350,7 +407,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonText(side: DashboardLayoutId): string {
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') {
return this.translate.instant('layout.left-side');
} else {
@ -361,6 +418,116 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
showPreviewInputs(side: DashboardLayoutId): boolean {
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"
(click)="item.action(widgetContextMenuEvent, widget)">
<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>
</button>
</div>

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

@ -17,7 +17,6 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
Input,
@ -46,7 +45,6 @@ import {
} from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@ -203,9 +201,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetContextMenuEvent: MouseEvent;
dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((index, item) => {
return item;
}),
this.differs.find([]).create<Widget>((_, item) => item),
this.kvDiffers.find([]).create<string, WidgetLayout>()
);
@ -218,11 +214,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
constructor(protected store: Store<AppState>,
public utils: UtilsService,
private timeService: TimeService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers,
private cd: ChangeDetectorRef,
private ngZone: NgZone) {
super(store);
this.authUser = getCurrentAuthUser(store);
@ -257,8 +251,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
draggable: {enabled: this.isEdit && !this.isEditingWidget},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => {
itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
},
colWidthUpdateCallback: (colWidth) => {
@ -421,7 +415,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
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) {
$event.preventDefault();
$event.stopPropagation();
@ -459,6 +453,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
case WidgetComponentActionType.REMOVE:
this.removeWidget($event, widget);
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) {
if ($event) {
$event.stopPropagation();
@ -558,7 +564,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widget.gridsterItemComponent$().subscribe((gridsterItem) => {
const gridsterItemElement = gridsterItem.el as HTMLElement;
const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
let scrollTop;
let scrollTop: number;
if (this.isMobileSize) {
scrollTop = gridsterItemElement.offsetTop;
} else {
@ -592,8 +598,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} else {
this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical;
}
const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
this.gridsterOpts.mobileBreakpoint = this.isMobileSize ? 20000 : 0;
const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight);
if (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 { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-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 { ManageDashboardStatesDialogComponent } from '@home/components/dashboard-page/states/manage-dashboard-states-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 { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.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({
declarations:
@ -285,11 +291,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent,
DashboardStateComponent,
DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent,
DashboardWidgetSelectComponent,
AddWidgetDialogComponent,
MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent,
@ -418,11 +426,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent,
DashboardStateComponent,
DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent,
DashboardWidgetSelectComponent,
AddWidgetDialogComponent,
MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
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 { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
@ -78,7 +77,7 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs
}
],
standalone: true,
imports: [CommonModule, SharedModule, TruncateWithTooltipDirective]
imports: [CommonModule, SharedModule]
})
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 { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-data-keys-panel',
@ -52,7 +51,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
CommonModule,
SharedModule,
GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
]
})
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 { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-security-config',
@ -64,7 +63,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [
CommonModule,
SharedModule,
TruncateWithTooltipDirective,
]
})
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 { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-slave-config',
@ -73,7 +72,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
],
})
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 { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { helpBaseUrl } from '@shared/models/constants';
@Component({
@ -78,7 +77,6 @@ import { helpBaseUrl } from '@shared/models/constants';
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
],
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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
@ -62,7 +61,6 @@ import {
CommonModule,
SharedModule,
SecurityConfigComponent,
TruncateWithTooltipDirective,
]
})
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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-workers-config-control',
@ -45,7 +44,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [
CommonModule,
SharedModule,
TruncateWithTooltipDirective,
],
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 #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">
<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 }}
</mat-slide-toggle>
<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 {
TypeValuePanelComponent
} 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 {
ModbusRpcParametersComponent
ModbusRpcParametersComponent
} 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';
@ -233,7 +232,6 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
TruncateWithTooltipDirective,
ModbusRpcParametersComponent,
],
exports: [

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

@ -169,12 +169,12 @@
</div>
</div>
<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">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>

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

@ -38,7 +38,7 @@ import {
JsonSchema,
JsonSettingsSchema,
TargetDevice,
TargetDeviceType, targetDeviceValid,
targetDeviceValid,
Widget,
WidgetConfigMode,
widgetType
@ -84,8 +84,8 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
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 Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -152,7 +152,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@Input()
@coerceBoolean()
scada = false;
showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint = true;
@Input() disabled: boolean;
@ -345,10 +349,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'actions'
}
);
if (!this.scada) {
if (this.showLayoutConfig) {
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'
}
);

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
* limitations under the License.
*/
@import '../scss/constants';
.tb-widget-container {
position: absolute;
inset: 0;
@ -182,12 +184,30 @@ gridster-item:hover {
line-height: 12px;
font-weight: 500;
color: rgba(0, 0, 0, 0.76);
.tb-widget-actions-panel {
.tb-widget-action-container {
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 8px;
flex-direction: column;
gap: 6px;
.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 ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@ -47,7 +48,8 @@ export enum WidgetComponentActionType {
CONTEXT_MENU,
EDIT,
EXPORT,
REMOVE
REMOVE,
REPLACE_REFERENCE_WITH_WIDGET_COPY,
}
export class WidgetComponentAction {
@ -117,6 +119,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
hovered = false;
isReferenceWidget = false;
get widgetEditActionsEnabled(): boolean {
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 renderer: Renderer2,
private container: ViewContainerRef,
private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) {
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('click', (e) => this.onClicked(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 {
@ -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) {
this.widgetComponentAction.emit({
event,
@ -240,9 +254,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
}
private initEditWidgetActionTooltip() {
private initEditWidgetActionTooltip(parent: HTMLElement) {
from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({
parent: $(parent),
delay: this.widget.selected ? [0, 10000000] : [0, 100],
distance: 2,
zIndex: 151,
@ -262,7 +277,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
content: '',
functionPosition: (instance, helper, position) => {
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;
return position;
},
@ -280,6 +297,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
functionAfter: () => {
this.hovered = false;
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');
@ -327,29 +349,43 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
@Component({
template: `<div class="tb-widget-actions-panel">
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isEditActionEnabled"
(click)="container.onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[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>`,
template: `
<div class="tb-widget-action-container">
<div class="tb-widget-reference-panel tb-primary-fill" *ngIf="container.widget.isReference">
{{ 'widget.reference' | translate }}
<button mat-icon-button class="tb-mat-16"
color="primary"
[fxShow]="container.isEditActionEnabled"
(click)="container.onReplaceReferenceWithWidgetCopy($event)"
matTooltip="{{ 'widget.replace-reference-with-widget-copy' | translate }}"
matTooltipPosition="above">
<tb-icon matButtonIcon>mdi:file-replace-outline</tb-icon>
</button>
</div>
<div class="tb-widget-actions-panel">
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isEditActionEnabled"
(click)="container.onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[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: [],
encapsulation: ViewEncapsulation.None
})
@ -361,7 +397,8 @@ export class EditWidgetActionsTooltipComponent implements AfterViewInit {
@Output()
viewInited = new EventEmitter();
constructor(public element: ElementRef<HTMLElement>) {
constructor(public element: ElementRef<HTMLElement>,
public cd: ChangeDetectorRef) {
}
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 {
onEditWidget?: ($event: Event, widget: Widget) => void;
replaceReferenceWithWidgetCopy?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onDashboardMouseDown?: ($event: Event) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array<WidgetContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget, isReference: boolean) => Array<WidgetContextMenuItem>;
}
export interface IDashboardComponent {
@ -343,6 +344,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private selectedCallback: (selected: boolean) => void = () => {};
isFullscreen = false;
isReference = false;
color: 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;
deviceService: DeviceService;
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';
@Directive({
standalone: true,
selector: '[tbTruncateWithTooltip]',
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) {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
public exportWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget,
widgetTitle: string, breakpoint: string) {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName;
let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : '');
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 { TenantId } from '@shared/models/id/tenant-id';
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 { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.models';
@ -51,7 +51,8 @@ export interface WidgetLayouts {
export enum LayoutType {
default = 'default',
scada = 'scada'
scada = 'scada',
divider = 'divider',
}
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>(
[
[ 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;
margin?: number;
outerMargin?: boolean;
viewFormat?: ViewFormatType;
backgroundSizeMode?: string;
backgroundImageUrl?: string;
autoFillHeight?: boolean;
rowHeight?: number;
mobileAutoFillHeight?: boolean;
mobileRowHeight?: number;
mobileDisplayLayoutFirst?: boolean;
@ -83,14 +101,45 @@ export interface GridSettings {
export interface DashboardLayout {
widgets: WidgetLayouts;
gridSettings: GridSettings;
breakpoints?: {[breakpointId in BreakpointId]?: Omit<DashboardLayout, 'breakpoints'>};
}
export interface DashboardLayoutInfo {
export declare type DashboardLayoutInfo = {[breakpointId: string]: BreakpointLayoutInfo};
export interface BreakpointLayoutInfo {
widgetIds?: string[];
widgetLayouts?: WidgetLayouts;
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 {
type?: LayoutDimensionType;
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 { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
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 { TranslateModule } from '@ngx-translate/core';
import { TbCheckboxComponent } from '@shared/components/tb-checkbox.component';
@ -356,6 +357,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
NavTreeComponent,
LedLightComponent,
MarkdownEditorComponent,
TruncateWithTooltipDirective,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,
@ -611,6 +613,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
NavTreeComponent,
LedLightComponent,
MarkdownEditorComponent,
TruncateWithTooltipDirective,
NospacePipe,
MillisecondsToTimeStringPipe,
EnumToArrayPipe,

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

@ -1283,7 +1283,6 @@
"maximum-upload-file-size": "الحد الأقصى لحجم الملف المرفوع: {{ size }}",
"cannot-upload-file": "لا يمكن تحميل الملف",
"settings": "الإعدادات",
"layout-settings": "إعدادات التخطيط",
"columns-count": "عدد الأعمدة",
"columns-count-required": "عدد الأعمدة مطلوب.",
"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 }}",
"cannot-upload-file": "No s'ha pogut pujar el fitxer",
"settings": "Configuració",
"layout-settings": "Paràmetres de la disposició",
"columns-count": "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.",

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

@ -745,7 +745,6 @@
"maximum-upload-file-size": "Maximální velikost souboru: {{ size }}",
"cannot-upload-file": "Soubor nelze nahrát",
"settings": "Nastavení",
"layout-settings": "Nastavení rozložení",
"columns-count": "Počet sloupců",
"columns-count-required": "Počet sloupců je povinný.",
"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-type-default": "Default",
"layout-type-scada": "SCADA",
"layout-settings": "Layout settings",
"layout-type-divider": "Divider",
"layout-settings-type": "Layout settings: {{ type }}",
"columns-count": "Columns count",
"columns-count-required": "Columns count is required.",
"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.",
"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.",
"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",
"title-settings": "Title settings",
"display-title": "Display dashboard title",
@ -1275,7 +1280,19 @@
"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",
"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": {
"settings": "Settings",
@ -3828,7 +3845,14 @@
"left-width-percentage-required": "Left percentage is required",
"divider": "Divider",
"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": {
"direction": "Direction",
@ -5653,6 +5677,7 @@
"edit": "Edit widget",
"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.",
"replace-reference-with-widget-copy": "Replace reference with widget copy",
"timeseries": "Time series",
"search-data": "Search data",
"no-data-found": "No data found",
@ -5733,7 +5758,8 @@
"search": "Search widget",
"filter": "Widget filter type",
"loading-widgets": "Loading widgets...",
"widget-template-error": "Invalid widget HTML template."
"widget-template-error": "Invalid widget HTML template.",
"reference": "Reference"
},
"widget-action": {
"header-button": "Widget header button",
@ -5941,7 +5967,8 @@
"card-appearance": "Card appearance",
"color": "Color",
"tooltip": "Tooltip",
"units-required": "Unit is required."
"units-required": "Unit is required.",
"list-layout": "List layout"
},
"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 }}",
"cannot-upload-file": "No es posible subir el fichero",
"settings": "Ajustes",
"layout-settings": "Ajustes de diseño",
"columns-count": "Número de columnas",
"columns-count-required": "Número de columnas requerido.",
"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",
"maximum-upload-file-size": "Taille de fichier maximum: {{ size }}",
"cannot-upload-file": "Le téléchargement a échoué",
"layout-settings": "Paramètres de mise en page",
"margin-required": "Valeur de marge requise.",
"min-margin-message": "0 est la valeur minimum 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 }}",
"cannot-upload-file": "Failo įkelti nepavyko",
"settings": "Nustatymai",
"layout-settings": "Išdėstymo nustatymai",
"columns-count": "Stulpelių skaičius",
"columns-count-required": "Stulpelių skaičius būtinas.",
"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 }}",
"cannot-upload-file": "Kan bestand niet uploaden",
"settings": "Instellingen",
"layout-settings": "Lay-out instellingen",
"columns-count": "Aantal kolommen",
"columns-count-required": "Het aantal kolommen is vereist.",
"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 }}",
"cannot-upload-file": "Nie można przesłać pliku",
"settings": "Ustawienia",
"layout-settings": "Ustawienia układu",
"columns-count": "Liczba kolumn",
"columns-count-required": "Liczba kolumn jest wymagana.",
"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 }}",
"cannot-upload-file": "Dosya yüklenemiyor",
"settings": "Ayarlar",
"layout-settings": "Görünüm ayarları",
"columns-count": "Sütun sayısı",
"columns-count-required": "Sütun sayısı gerekli.",
"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 }}",
"cannot-upload-file": "无法上传文件",
"settings": "设置",
"layout-settings": "布局设置",
"columns-count": "列数",
"columns-count-required": "需要列数。",
"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 }}",
"cannot-upload-file": "無法上傳文件",
"settings": "設定",
"layout-settings": "佈局設定",
"columns-count": "列數",
"columns-count-required": "需要列數。",
"min-columns-count-message": "只允許最少10列",

6
ui-ngx/src/styles.scss

@ -921,6 +921,12 @@ mat-icon {
}
.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 {
@include tb-mat-icon-button-size(20);
.mat-icon {

Loading…
Cancel
Save