diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index ae4d2e5c41..fa5c0abfab 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -18,11 +18,11 @@ import { Injectable } from '@angular/core'; import { UtilsService } from '@core/services/utils.service'; import { TimeService } from '@core/services/time.service'; import { + BreakpointLayoutInfo, Dashboard, DashboardConfiguration, DashboardLayout, DashboardLayoutId, - DashboardLayoutInfo, DashboardLayoutsInfo, DashboardState, DashboardStateLayouts, @@ -520,16 +520,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; @@ -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 { const widgetsArray: Array = []; const dashboardConfiguration = dashboard.configuration; @@ -564,11 +576,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(); @@ -626,12 +642,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); } @@ -700,11 +721,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) { diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index e1941590ea..b523ea0828 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/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.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint); + const originalSize = this.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; } @@ -387,13 +390,17 @@ export class ItemBufferService { 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 gridSettings = null; const state = dashboard.configuration.states[sourceState]; const layoutCount = Object.keys(state.layouts).length; if (state) { - const layout = state.layouts[sourceLayout]; + let layout = state.layouts[sourceLayout]; + if (breakpoint !== 'default') { + layout = layout.breakpoints[breakpoint]; + } if (layout) { gridSettings = layout.gridSettings; @@ -407,8 +414,12 @@ export class ItemBufferService { return originalColumns; } - private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetSize { - const layout = dashboard.configuration.states[sourceState].layouts[sourceLayout]; + private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, + breakpoint: string): WidgetSize { + let layout = dashboard.configuration.states[sourceState].layouts[sourceLayout]; + if (breakpoint !== 'default') { + layout = layout.breakpoints[breakpoint]; + } const widgetLayout = layout.widgets[widget.id]; return { sizeX: widgetLayout.sizeX, @@ -432,16 +443,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.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint); + const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint); return { dashboardId: dashboard.id.id, sourceState, sourceLayout, widgetId: widget.id, originalSize, - originalColumns + originalColumns, + breakpoint }; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index e923036571..de5bd65061 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -58,6 +58,9 @@ view_compact {{'layout.layouts' | translate}} + + this.dashboard, dashboardTimewindow: null, state: null, + breakpoint: null, stateController: null, stateChanged: null, stateId: null, @@ -280,24 +280,28 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC show: false, layoutCtx: { id: 'main', + breakpoint: 'default', widgets: null, widgetLayouts: {}, gridSettings: {}, ignoreLoading: true, ctrl: null, - dashboardCtrl: this + dashboardCtrl: this, + layoutData: null } }, right: { show: false, layoutCtx: { id: 'right', + breakpoint: 'default', widgets: null, widgetLayouts: {}, gridSettings: {}, ignoreLoading: true, ctrl: null, - dashboardCtrl: this + dashboardCtrl: this, + layoutData: null } } }; @@ -331,6 +335,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent; + private changeMobileSize = new Subject(); + constructor(protected store: Store, @Inject(WINDOW) private window: Window, @Inject(DOCUMENT) private document: Document, @@ -339,7 +345,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, @@ -357,7 +362,6 @@ 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); @@ -402,13 +406,56 @@ 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([ + 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(); } - )); + ) + ); + if (this.isMobileApp && this.syncStateWithQueryParam) { this.mobileService.registerToggleLayoutFunction(() => { setTimeout(() => { @@ -694,7 +741,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) { @@ -1025,21 +1072,14 @@ 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.ctrl?.updatedCurrentBreakpoint(null, layout.show); this.updateLayoutSizes(); } @@ -1158,8 +1198,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(); } @@ -1303,12 +1345,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC 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) { @@ -1343,7 +1385,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(); } } @@ -1352,7 +1395,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); } widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { @@ -1402,7 +1445,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' diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts index b2ed609c29..7f4fbd26a9 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts @@ -14,7 +14,13 @@ /// 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 { Timewindow } from '@shared/models/time/time.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models'; @@ -34,6 +40,7 @@ export interface DashboardPageInitData { export interface DashboardContext { instanceId: string; state: string; + breakpoint: string; getDashboard: () => Dashboard; dashboardTimewindow: Timewindow; aliasController: IAliasController; @@ -63,6 +70,8 @@ export interface IDashboardController { export interface DashboardPageLayoutContext { id: DashboardLayoutId; + layoutData: DashboardLayoutInfo; + breakpoint: string; widgets: LayoutWidgetsArray; widgetLayouts: WidgetLayouts; gridSettings: GridSettings; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index 36fb4d2427..312ae4a52c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -36,6 +36,8 @@ import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { map } from 'rxjs/operators'; +import { deepClone, isNotEmptyStr } from '@core/utils'; +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; @Component({ selector: 'tb-dashboard-layout', @@ -95,7 +97,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo private translate: TranslateService, private itembuffer: ItemBufferService, private imagePipe: ImagePipe, - private sanitizer: DomSanitizer) { + private sanitizer: DomSanitizer, + private dashboardUtils: DashboardUtilsService,) { super(store); this.initHotKeys(); } @@ -161,7 +164,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); } @@ -268,4 +271,45 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo 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]; + } + } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts index 3ea9f553a8..a905e5c079 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts @@ -21,6 +21,8 @@ export interface ILayoutController { selectWidget(widgetId: string, delay?: number); pasteWidget($event: MouseEvent); pasteWidgetReference($event: MouseEvent); + updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean); + createBreakpointConfig(breakpoint?: string); } export enum LayoutWidthType { diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.html new file mode 100644 index 0000000000..8345b8d05a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.html @@ -0,0 +1,25 @@ + + + + {{ layout }} + + diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.scss new file mode 100644 index 0000000000..253ce75901 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.scss @@ -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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.ts new file mode 100644 index 0000000000..ccce03bd20 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-layout.component.ts @@ -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(); + + 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(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 5390a915d3..bf383d9f12 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -171,6 +171,9 @@ import { import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; +import { + SelectDashboardLayoutComponent +} from '@home/components/dashboard-page/layout/select-dashboard-layout.component'; @NgModule({ declarations: @@ -280,6 +283,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet DashboardPageComponent, DashboardStateComponent, DashboardLayoutComponent, + SelectDashboardLayoutComponent, EditWidgetComponent, DashboardWidgetSelectComponent, AddWidgetDialogComponent, @@ -411,6 +415,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet DashboardPageComponent, DashboardStateComponent, DashboardLayoutComponent, + SelectDashboardLayoutComponent, EditWidgetComponent, DashboardWidgetSelectComponent, AddWidgetDialogComponent, diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 9902177ebd..5dd6ff5ed1 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/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, '_'); diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 36a14f59c6..bfc3a077df 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -67,9 +67,12 @@ export interface GridSettings { export interface DashboardLayout { widgets: WidgetLayouts; gridSettings: GridSettings; + breakpoints?: {[breakpoint: string]: Omit}; } -export interface DashboardLayoutInfo { +export declare type DashboardLayoutInfo = {[breakpoint: string]: BreakpointLayoutInfo}; + +export interface BreakpointLayoutInfo { widgetIds?: string[]; widgetLayouts?: WidgetLayouts; gridSettings?: GridSettings;