From d2d6dd2e5ff0c6d7b05d28e72a4d592b98e477a2 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 22 Jan 2025 15:57:45 +0200 Subject: [PATCH] Dashboards: Prevent rows nesting (#99246) --- .../DefaultGridLayoutManager.tsx | 23 ++++++++ .../ResponsiveGridLayoutManager.tsx | 7 ++- .../scene/layout-rows/RowItem.tsx | 4 +- .../scene/layout-rows/RowsLayoutManager.tsx | 56 ++++++++++++++++++- .../DashboardLayoutSelector.tsx | 30 +++++++--- .../scene/layouts-shared/utils.ts | 17 ++++++ 6 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layouts-shared/utils.ts diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index f3f318c2c01..f6d2156b1c6 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -8,6 +8,7 @@ import { sceneGraph, sceneUtils, SceneComponentProps, + SceneGridItemLike, } from '@grafana/scenes'; import { GRID_COLUMN_COUNT } from 'app/core/constants'; @@ -385,6 +386,28 @@ export class DefaultGridLayoutManager }); } + /** + * Useful for preserving items positioning when switching layouts + * @param gridItems + * @returns + */ + public static fromGridItems(gridItems: SceneGridItemLike[]): DefaultGridLayoutManager { + const children = gridItems.reduce((acc, gridItem) => { + gridItem.clearParent(); + acc.push(gridItem); + + return acc; + }, []); + + return new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children, + isDraggable: true, + isResizable: true, + }), + }); + } + public static Component = ({ model }: SceneComponentProps) => { return ; }; diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 51219ae9178..f1245deb58d 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -3,7 +3,8 @@ import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectSt import { Select } from '@grafana/ui'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; +import { getDashboardSceneFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; +import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; import { ResponsiveGridItem } from './ResponsiveGridItem'; @@ -32,7 +33,9 @@ export class ResponsiveGridLayoutManager } public addNewRow(): void { - throw new Error('Method not implemented.'); + const rowsLayout = RowsLayoutManager.createFromLayout(this); + rowsLayout.addNewRow(); + getDashboardSceneFor(this).switchLayout(rowsLayout); } public getNextPanelId(): number { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index eb168e80bea..cd95834bcad 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -95,13 +95,13 @@ export class RowItem extends SceneObjectBase implements LayoutPare }; public static Component = ({ model }: SceneComponentProps) => { - const { layout, title, isCollapsed, height = 'expand', isHeaderHidden } = model.useState(); + const { layout, title, isCollapsed, height = 'expand', isHeaderHidden, key } = model.useState(); const { isEditing, showHiddenElements } = getDashboardSceneFor(model).useState(); const styles = useStyles2(getStyles); const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); const ref = useRef(null); const shouldGrow = !isCollapsed && height === 'expand'; - const { isSelected, onSelect } = useElementSelection(model.state.key); + const { isSelected, onSelect } = useElementSelection(key); return (
i } public static createFromLayout(layout: DashboardLayoutManager): RowsLayoutManager { + if (layout instanceof DefaultGridLayoutManager) { + const config: Array<{ + title?: string; + isCollapsed?: boolean; + children: SceneGridItemLike[]; + }> = []; + let children: SceneGridItemLike[] | undefined; + + layout.state.grid.forEachChild((child) => { + if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) { + throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene'); + } + + if (child instanceof SceneGridRow) { + if (!child.state.key?.includes('-clone-')) { + config.push({ + title: child.state.title, + isCollapsed: !!child.state.isCollapsed, + children: child.state.children, + }); + children = undefined; + } + } else { + if (!children) { + children = []; + config.push({ children }); + } + + children.push(child); + } + }); + + const rows = config.map( + (rowConfig) => + new RowItem({ + title: rowConfig.title ?? 'Row title', + isCollapsed: !!rowConfig.isCollapsed, + layout: DefaultGridLayoutManager.fromGridItems(rowConfig.children), + }) + ); + + return new RowsLayoutManager({ rows }); + } + const row = new RowItem({ layout: layout.clone(), title: 'Row title' }); return new RowsLayoutManager({ rows: [row] }); diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx index ff81e9c6ffc..d1af76ef4f2 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -7,26 +7,38 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types'; import { layoutRegistry } from './layoutRegistry'; +import { findParentLayout } from './utils'; export interface Props { layoutManager: DashboardLayoutManager; } -export function DashboardLayoutSelector({ layoutManager }: { layoutManager: DashboardLayoutManager }) { - const layouts = layoutRegistry.list(); - const options = layouts.map((layout) => ({ - label: layout.name, - value: layout, - })); +export function DashboardLayoutSelector({ layoutManager }: Props) { + const options = useMemo(() => { + const parentLayout = findParentLayout(layoutManager); + const parentLayoutId = parentLayout?.getDescriptor().id; + + return layoutRegistry + .list() + .filter((layout) => layout.id !== parentLayoutId) + .map((layout) => ({ + label: layout.name, + value: layout, + })); + }, [layoutManager]); const currentLayoutId = layoutManager.getDescriptor().id; - const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId); + const currentOption = options.find((option) => option.value.id === currentLayoutId); return (