Dashboards: Prevent rows nesting (#99246)

This commit is contained in:
Bogdan Matei 2025-01-22 15:57:45 +02:00 committed by GitHub
parent 51b4ac50aa
commit d2d6dd2e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 14 deletions

View File

@ -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<SceneGridItemLike[]>((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<DefaultGridLayoutManager>) => {
return <model.state.grid.Component model={model.state.grid} />;
};

View File

@ -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 {

View File

@ -95,13 +95,13 @@ export class RowItem extends SceneObjectBase<RowItemState> implements LayoutPare
};
public static Component = ({ model }: SceneComponentProps<RowItem>) => {
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<HTMLDivElement>(null);
const shouldGrow = !isCollapsed && height === 'expand';
const { isSelected, onSelect } = useElementSelection(model.state.key);
const { isSelected, onSelect } = useElementSelection(key);
return (
<div

View File

@ -1,10 +1,20 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import {
SceneComponentProps,
sceneGraph,
SceneGridItemLike,
SceneGridRow,
SceneObjectBase,
SceneObjectState,
VizPanel,
} from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { DashboardScene } from '../DashboardScene';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
import { DashboardLayoutManager, LayoutRegistryItem } from '../types';
@ -101,6 +111,50 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> 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] });

View File

@ -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) => ({
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 (
<Select
options={options}
value={currentLayoutOption}
onChange={(option) => changeLayoutTo(layoutManager, option.value!)}
value={currentOption}
onChange={(option) => {
if (option.value?.id !== currentOption?.value.id) {
changeLayoutTo(layoutManager, option.value!);
}
}}
/>
);
}

View File

@ -0,0 +1,17 @@
import { SceneObject } from '@grafana/scenes';
import { DashboardLayoutManager, isDashboardLayoutManager } from '../types';
export function findParentLayout(sceneObject: SceneObject): DashboardLayoutManager | null {
let parent = sceneObject.parent;
while (parent) {
if (isDashboardLayoutManager(parent)) {
return parent;
}
parent = parent.parent;
}
return null;
}