UI: Added support difference dashboard layout for one state

This commit is contained in:
Vladyslav Prykhodko 2024-08-05 00:49:43 +03:00
parent ea662bc26a
commit 963160efa6
13 changed files with 353 additions and 70 deletions

View File

@ -18,11 +18,11 @@ import { Injectable } from '@angular/core';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { import {
BreakpointLayoutInfo,
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
DashboardLayout, DashboardLayout,
DashboardLayoutId, DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo, DashboardLayoutsInfo,
DashboardState, DashboardState,
DashboardStateLayouts, DashboardStateLayouts,
@ -521,15 +521,13 @@ export class DashboardUtilsService {
const layout: DashboardLayout = state.layouts[l]; const layout: DashboardLayout = state.layouts[l];
if (layout) { if (layout) {
result[l]= { result[l]= {
widgetIds: [], default: this.getBreakpointLayoutData(layout)
widgetLayouts: {}, };
gridSettings: {} if (layout.breakpoints) {
} as DashboardLayoutInfo; for (const breakpoint of Object.keys(layout.breakpoints)) {
for (const id of Object.keys(layout.widgets)) { result[l][breakpoint] = this.getBreakpointLayoutData(layout.breakpoints[breakpoint]);
result[l].widgetIds.push(id); }
} }
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
} }
} }
return result; return result;
@ -538,6 +536,20 @@ export class DashboardUtilsService {
} }
} }
private getBreakpointLayoutData(layout: DashboardLayout): BreakpointLayoutInfo {
const result: BreakpointLayoutInfo = {
widgetIds: [],
widgetLayouts: {},
gridSettings: {}
};
for (const id of Object.keys(layout.widgets)) {
result.widgetIds.push(id);
}
result.widgetLayouts = layout.widgets;
result.gridSettings = layout.gridSettings;
return result;
}
public getWidgetsArray(dashboard: Dashboard): Array<Widget> { public getWidgetsArray(dashboard: Dashboard): Array<Widget> {
const widgetsArray: Array<Widget> = []; const widgetsArray: Array<Widget> = [];
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
@ -564,11 +576,15 @@ export class DashboardUtilsService {
originalColumns?: number, originalColumns?: number,
originalSize?: {sizeX: number; sizeY: number}, originalSize?: {sizeX: number; sizeY: number},
row?: number, row?: number,
column?: number): void { column?: number,
breakpoint = 'default'): void {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const state = states[targetState]; const state = states[targetState];
const layout = state.layouts[targetLayout]; let layout = state.layouts[targetLayout];
if (breakpoint !== 'default' && layout.breakpoints?.[breakpoint]) {
layout = layout.breakpoints[breakpoint];
}
const layoutCount = Object.keys(state.layouts).length; const layoutCount = Object.keys(state.layouts).length;
if (!widget.id) { if (!widget.id) {
widget.id = this.utils.guid(); widget.id = this.utils.guid();
@ -626,12 +642,17 @@ export class DashboardUtilsService {
public removeWidgetFromLayout(dashboard: Dashboard, public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string, targetState: string,
targetLayout: DashboardLayoutId, targetLayout: DashboardLayoutId,
widgetId: string) { widgetId: string,
breakpoint: string) {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const state = states[targetState]; const state = states[targetState];
const layout = state.layouts[targetLayout]; const layout = state.layouts[targetLayout];
if (layout.breakpoints[breakpoint]) {
delete layout.breakpoints[breakpoint].widgets[widgetId];
} else {
delete layout.widgets[widgetId]; delete layout.widgets[widgetId];
}
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
@ -700,11 +721,19 @@ export class DashboardUtilsService {
for (const s of Object.keys(states)) { for (const s of Object.keys(states)) {
const state = states[s]; const state = states[s];
for (const l of Object.keys(state.layouts)) { for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l]; const layout: DashboardLayout = state.layouts[l];
if (layout.widgets[widgetId]) { if (layout.widgets[widgetId]) {
found = true; found = true;
break; break;
} }
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
if (layout.breakpoints[breakpoint].widgets[widgetId]) {
found = true;
break;
}
}
}
} }
} }
if (!found) { if (!found) {

View File

@ -56,6 +56,7 @@ export interface WidgetReference {
widgetId: string; widgetId: string;
originalSize: WidgetSize; originalSize: WidgetSize;
originalColumns: number; originalColumns: number;
breakpoint: string;
} }
export interface RuleNodeConnection { export interface RuleNodeConnection {
@ -85,7 +86,8 @@ export class ItemBufferService {
private ruleChainService: RuleChainService, private ruleChainService: RuleChainService,
private utils: UtilsService) {} private utils: UtilsService) {}
public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: string): WidgetItem {
const aliasesInfo: AliasesInfo = { const aliasesInfo: AliasesInfo = {
datasourceAliases: {}, datasourceAliases: {},
targetDeviceAlias: null targetDeviceAlias: null
@ -93,8 +95,8 @@ export class ItemBufferService {
const filtersInfo: FiltersInfo = { const filtersInfo: FiltersInfo = {
datasourceFilters: {} datasourceFilters: {}
}; };
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources; const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources;
if (widget.config && dashboard.configuration if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) { && dashboard.configuration.entityAliases) {
@ -146,13 +148,14 @@ export class ItemBufferService {
}; };
} }
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_ITEM, widgetItem); this.storeSet(WIDGET_ITEM, widgetItem);
} }
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); widget: Widget, breakpoint: string): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_REFERENCE, widgetReference); this.storeSet(WIDGET_REFERENCE, widgetReference);
} }
@ -160,11 +163,11 @@ export class ItemBufferService {
return this.storeHas(WIDGET_ITEM); return this.storeHas(WIDGET_ITEM);
} }
public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId, breakpoint: string): boolean {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) { if (widgetReference) {
if (widgetReference.dashboardId === dashboard.id.id) { if (widgetReference.dashboardId === dashboard.id.id) {
if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout || widgetReference.breakpoint !== breakpoint)
&& dashboard.configuration.widgets[widgetReference.widgetId]) { && dashboard.configuration.widgets[widgetReference.widgetId]) {
return true; return true;
} }
@ -387,13 +390,17 @@ export class ItemBufferService {
return ruleChainImport; return ruleChainImport;
} }
private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
breakpoint: string): number {
let originalColumns = 24; let originalColumns = 24;
let gridSettings = null; let gridSettings = null;
const state = dashboard.configuration.states[sourceState]; const state = dashboard.configuration.states[sourceState];
const layoutCount = Object.keys(state.layouts).length; const layoutCount = Object.keys(state.layouts).length;
if (state) { if (state) {
const layout = state.layouts[sourceLayout]; let layout = state.layouts[sourceLayout];
if (breakpoint !== 'default') {
layout = layout.breakpoints[breakpoint];
}
if (layout) { if (layout) {
gridSettings = layout.gridSettings; gridSettings = layout.gridSettings;
@ -407,8 +414,12 @@ export class ItemBufferService {
return originalColumns; return originalColumns;
} }
private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetSize { private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget,
const layout = dashboard.configuration.states[sourceState].layouts[sourceLayout]; breakpoint: string): WidgetSize {
let layout = dashboard.configuration.states[sourceState].layouts[sourceLayout];
if (breakpoint !== 'default') {
layout = layout.breakpoints[breakpoint];
}
const widgetLayout = layout.widgets[widget.id]; const widgetLayout = layout.widgets[widget.id];
return { return {
sizeX: widgetLayout.sizeX, sizeX: widgetLayout.sizeX,
@ -432,16 +443,17 @@ export class ItemBufferService {
} }
private prepareWidgetReference(dashboard: Dashboard, sourceState: string, private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference { sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: string): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
return { return {
dashboardId: dashboard.id.id, dashboardId: dashboard.id.id,
sourceState, sourceState,
sourceLayout, sourceLayout,
widgetId: widget.id, widgetId: widget.id,
originalSize, originalSize,
originalColumns originalColumns,
breakpoint
}; };
} }

View File

@ -58,6 +58,9 @@
<mat-icon>view_compact</mat-icon> <mat-icon>view_compact</mat-icon>
{{'layout.layouts' | translate}} {{'layout.layouts' | translate}}
</button> </button>
<tb-select-dashboard-layout
[dashboardCtrl]="this">
</tb-select-dashboard-layout>
</div> </div>
</ng-container> </ng-container>
<tb-states-component fxFlex.lt-md <tb-states-component fxFlex.lt-md

View File

@ -40,7 +40,6 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import { import {
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
@ -66,7 +65,7 @@ import {
IDashboardController, IDashboardController,
LayoutWidgetsArray LayoutWidgetsArray
} from './dashboard-page.models'; } from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
@ -83,7 +82,7 @@ import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller'; import { AliasController } from '@core/api/alias-controller';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subject, Subscription } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { import {
@ -138,14 +137,14 @@ import {
DashboardImageDialogData, DashboardImageDialogData,
DashboardImageDialogResult DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component'; } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { SafeUrl } from '@angular/platform-browser';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { 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 { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -268,6 +267,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
getDashboard: () => this.dashboard, getDashboard: () => this.dashboard,
dashboardTimewindow: null, dashboardTimewindow: null,
state: null, state: null,
breakpoint: null,
stateController: null, stateController: null,
stateChanged: null, stateChanged: null,
stateId: null, stateId: null,
@ -280,24 +280,28 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
show: false, show: false,
layoutCtx: { layoutCtx: {
id: 'main', id: 'main',
breakpoint: 'default',
widgets: null, widgets: null,
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: true, ignoreLoading: true,
ctrl: null, ctrl: null,
dashboardCtrl: this dashboardCtrl: this,
layoutData: null
} }
}, },
right: { right: {
show: false, show: false,
layoutCtx: { layoutCtx: {
id: 'right', id: 'right',
breakpoint: 'default',
widgets: null, widgets: null,
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: true, ignoreLoading: true,
ctrl: null, ctrl: null,
dashboardCtrl: this dashboardCtrl: this,
layoutData: null
} }
} }
}; };
@ -331,6 +335,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent; @ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent;
private changeMobileSize = new Subject<boolean>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
@ -339,7 +345,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private router: Router, private router: Router,
private utils: UtilsService, private utils: UtilsService,
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private authService: AuthService,
private entityService: EntityService, private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private widgetComponentService: WidgetComponentService, private widgetComponentService: WidgetComponentService,
@ -357,7 +362,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer,
public elRef: ElementRef, public elRef: ElementRef,
private injector: Injector) { private injector: Injector) {
super(store); super(store);
@ -402,13 +406,56 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
)); ));
} }
this.rxSubscriptions.push(this.breakpointObserver this.rxSubscriptions.push(
.observe(MediaBreakpoints['gt-sm']) this.changeMobileSize.pipe(
.subscribe((state: BreakpointState) => { distinctUntilChanged(),
this.isMobile = !state.matches; ).subscribe((state) => {
this.isMobile = state;
this.updateLayoutSizes();
})
);
this.rxSubscriptions.push(
this.breakpointObserver.observe([
MediaBreakpoints.xs,
MediaBreakpoints.sm,
MediaBreakpoints.md,
MediaBreakpoints.lg,
MediaBreakpoints.xl
]).pipe(
map(value => {
if (value.breakpoints[MediaBreakpoints.xs]) {
return 'xs';
} else if (value.breakpoints[MediaBreakpoints.sm]) {
return 'sm';
} else if (value.breakpoints[MediaBreakpoints.md]) {
return 'md';
} else if (value.breakpoints[MediaBreakpoints.lg]) {
return 'lg';
} else {
return 'xl';
}
}),
tap((value) => {
this.dashboardCtx.breakpoint = value;
this.changeMobileSize.next(value === 'xs' || value === 'sm');
}),
distinctUntilChanged((prev, next) => {
if (this.layouts.right.show || this.isEdit) {
return true;
}
const allowAdditionalPrevLayout = !!this.layouts.main.layoutCtx.layoutData?.[prev];
const allowAdditionalNextLayout = !!this.layouts.main.layoutCtx?.layoutData?.[next];
return !(allowAdditionalNextLayout || allowAdditionalPrevLayout !== allowAdditionalNextLayout);
}),
skip(1)
).subscribe((value) => {
this.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint();
this.updateLayoutSizes(); this.updateLayoutSizes();
} }
)); )
);
if (this.isMobileApp && this.syncStateWithQueryParam) { if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.registerToggleLayoutFunction(() => { this.mobileService.registerToggleLayoutFunction(() => {
setTimeout(() => { setTimeout(() => {
@ -694,7 +741,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened); this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened);
} }
private updateLayoutSizes() { public updateLayoutSizes() {
let changeMainLayoutSize = false; let changeMainLayoutSize = false;
let changeRightLayoutSize = false; let changeRightLayoutSize = false;
if (this.dashboardCtx.state) { if (this.dashboardCtx.state) {
@ -1025,21 +1072,14 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateLayout(layout, layoutInfo); this.updateLayout(layout, layoutInfo);
} else { } else {
layout.show = false; layout.show = false;
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); this.updateLayout(layout, {default: {widgetIds: [], widgetLayouts: {}, gridSettings: null}});
} }
} }
} }
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
if (layoutInfo.gridSettings) { layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.gridSettings = layoutInfo.gridSettings; layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(null, layout.show);
}
layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (layout.show && layout.layoutCtx.ctrl) {
layout.layoutCtx.ctrl.reload();
}
layout.layoutCtx.ignoreLoading = true;
this.updateLayoutSizes(); this.updateLayoutSizes();
} }
@ -1158,8 +1198,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) { private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) {
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget); const layoutCtx = this.layouts[layoutId].layoutCtx;
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id); this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget, undefined,
undefined, -1, -1, layoutCtx.breakpoint);
layoutCtx.widgets.addWidgetId(widget.id);
this.runChangeDetection(); this.runChangeDetection();
} }
@ -1303,12 +1345,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
this.itembuffer.copyWidget(this.dashboard, this.itembuffer.copyWidget(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget); this.dashboardCtx.state, layoutCtx.id, widget, layoutCtx.breakpoint);
} }
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
this.itembuffer.copyWidgetReference(this.dashboard, this.itembuffer.copyWidgetReference(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget); this.dashboardCtx.state, layoutCtx.id, widget, layoutCtx.breakpoint);
} }
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
@ -1343,7 +1385,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (layoutCtx.widgets.removeWidgetId(widget.id)) { if (layoutCtx.widgets.removeWidgetId(widget.id)) {
this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget.id); this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id,
widget.id, layoutCtx.breakpoint);
this.runChangeDetection(); this.runChangeDetection();
} }
} }
@ -1352,7 +1395,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) { exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) {
$event.stopPropagation(); $event.stopPropagation();
this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle); this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle, layoutCtx.breakpoint);
} }
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
@ -1402,7 +1445,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
action: ($event) => { action: ($event) => {
layoutCtx.ctrl.pasteWidgetReference($event); layoutCtx.ctrl.pasteWidgetReference($event);
}, },
enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id), enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint),
value: 'action.paste-reference', value: 'action.paste-reference',
icon: 'content_paste', icon: 'content_paste',
shortcut: 'M-I' shortcut: 'M-I'

View File

@ -14,7 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Dashboard, DashboardLayoutId, GridSettings, WidgetLayouts } from '@app/shared/models/dashboard.models'; import {
Dashboard,
DashboardLayoutId,
DashboardLayoutInfo,
GridSettings,
WidgetLayouts
} from '@app/shared/models/dashboard.models';
import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; import { Widget, WidgetPosition } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
@ -34,6 +40,7 @@ export interface DashboardPageInitData {
export interface DashboardContext { export interface DashboardContext {
instanceId: string; instanceId: string;
state: string; state: string;
breakpoint: string;
getDashboard: () => Dashboard; getDashboard: () => Dashboard;
dashboardTimewindow: Timewindow; dashboardTimewindow: Timewindow;
aliasController: IAliasController; aliasController: IAliasController;
@ -63,6 +70,8 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext { export interface DashboardPageLayoutContext {
id: DashboardLayoutId; id: DashboardLayoutId;
layoutData: DashboardLayoutInfo;
breakpoint: string;
widgets: LayoutWidgetsArray; widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts; widgetLayouts: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;

View File

@ -36,6 +36,8 @@ import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { deepClone, isNotEmptyStr } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
@Component({ @Component({
selector: 'tb-dashboard-layout', selector: 'tb-dashboard-layout',
@ -95,7 +97,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
private translate: TranslateService, private translate: TranslateService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private imagePipe: ImagePipe, private imagePipe: ImagePipe,
private sanitizer: DomSanitizer) { private sanitizer: DomSanitizer,
private dashboardUtils: DashboardUtilsService,) {
super(store); super(store);
this.initHotKeys(); this.initHotKeys();
} }
@ -161,7 +164,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
new Hotkey('ctrl+i', (event: KeyboardEvent) => { new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
this.dashboardCtx.state, this.layoutCtx.id)) { this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) {
event.preventDefault(); event.preventDefault();
this.pasteWidgetReference(event); this.pasteWidgetReference(event);
} }
@ -268,4 +271,45 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos); this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos);
} }
updatedCurrentBreakpoint(breakpoint?: string, showLayout = true) {
if (!isNotEmptyStr(breakpoint)) {
breakpoint = this.dashboardCtx.breakpoint;
}
this.layoutCtx.breakpoint = breakpoint;
const layoutInfo = this.getLayoutDataForBreakpoint(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;
}
private getLayoutDataForBreakpoint(breakpoint: string) {
if (this.layoutCtx.layoutData[breakpoint]) {
return this.layoutCtx.layoutData[breakpoint];
}
return this.layoutCtx.layoutData.default;
}
createBreakpointConfig(breakpoint: string) {
const currentDashboard = this.dashboardCtx.getDashboard();
const dashboardConfiguration = currentDashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[this.dashboardCtx.state];
const layout = state.layouts[this.layoutCtx.id];
if (!layout.breakpoints) {
layout.breakpoints = {};
}
layout.breakpoints[breakpoint] = {
gridSettings: deepClone(layout.gridSettings),
widgets: deepClone(layout.widgets),
};
this.layoutCtx.layoutData =
this.dashboardUtils.getStateLayoutsData(currentDashboard, this.dashboardCtx.state)[this.layoutCtx.id];
}
} }

View File

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

View File

@ -0,0 +1,25 @@
<!--
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="select-dashboard-layout"
[(ngModel)]="selectLayout"
(ngModelChange)="selectLayoutChanged()">
<mat-option *ngFor="let layout of layouts" [value]="layout">
{{ layout }}
</mat-option>
</mat-select>

View File

@ -0,0 +1,30 @@
/**
* 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
}
:host ::ng-deep {
.mat-mdc-select.select-dashboard-layout {
.mat-mdc-select-value {
max-width: 200px;
}
.mat-mdc-select-arrow {
width: 24px;
}
}
}

View File

@ -0,0 +1,77 @@
///
/// 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';
@Component({
selector: 'tb-select-dashboard-layout',
templateUrl: './select-dashboard-layout.component.html',
styleUrls: ['./select-dashboard-layout.component.scss']
})
export class SelectDashboardLayoutComponent implements OnInit, OnDestroy {
@Input()
dashboardCtrl: DashboardPageComponent;
layout = {
default: 'Default',
xs: 'xs',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl'
};
layouts = Object.keys(this.layout);
selectLayout = 'default';
private allowBreakpointsSize = new Set<string>();
private stateChanged$: Subscription;
constructor() { }
ngOnInit() {
this.stateChanged$ = this.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => {
if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) {
this.allowBreakpointsSize = new Set(Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData));
} else {
this.allowBreakpointsSize.add('default');
}
if (this.allowBreakpointsSize.has(this.dashboardCtrl.layouts.main.layoutCtx.breakpoint)) {
this.selectLayout = this.dashboardCtrl.layouts.main.layoutCtx.breakpoint;
} else {
this.selectLayout = 'default';
this.dashboardCtrl.layouts.main.layoutCtx.breakpoint = this.selectLayout;
}
});
}
ngOnDestroy() {
this.stateChanged$.unsubscribe();
}
selectLayoutChanged() {
if (!this.dashboardCtrl.layouts.main.layoutCtx.layoutData[this.selectLayout]) {
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.createBreakpointConfig(this.selectLayout);
}
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectLayout);
this.dashboardCtrl.updateLayoutSizes();
}
}

View File

@ -171,6 +171,9 @@ import {
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
import {
SelectDashboardLayoutComponent
} from '@home/components/dashboard-page/layout/select-dashboard-layout.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -280,6 +283,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardLayoutComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,
@ -411,6 +415,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardLayoutComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,

View File

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

View File

@ -67,9 +67,12 @@ export interface GridSettings {
export interface DashboardLayout { export interface DashboardLayout {
widgets: WidgetLayouts; widgets: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;
breakpoints?: {[breakpoint: string]: Omit<DashboardLayout, 'breakpoints'>};
} }
export interface DashboardLayoutInfo { export declare type DashboardLayoutInfo = {[breakpoint: string]: BreakpointLayoutInfo};
export interface BreakpointLayoutInfo {
widgetIds?: string[]; widgetIds?: string[];
widgetLayouts?: WidgetLayouts; widgetLayouts?: WidgetLayouts;
gridSettings?: GridSettings; gridSettings?: GridSettings;