2025-04-02 16:18:28 +08:00
|
|
|
import { css, cx } from '@emotion/css';
|
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
2025-06-12 17:03:52 +08:00
|
|
|
import { t } from '@grafana/i18n';
|
2024-10-23 16:55:45 +08:00
|
|
|
import { config } from '@grafana/runtime';
|
2024-09-27 21:11:28 +08:00
|
|
|
import {
|
|
|
|
SceneObjectState,
|
|
|
|
SceneGridLayout,
|
|
|
|
SceneObjectBase,
|
|
|
|
SceneGridRow,
|
|
|
|
VizPanel,
|
|
|
|
sceneGraph,
|
|
|
|
sceneUtils,
|
|
|
|
SceneComponentProps,
|
2025-01-22 21:57:45 +08:00
|
|
|
SceneGridItemLike,
|
2025-02-06 22:30:54 +08:00
|
|
|
useSceneObjectState,
|
2025-03-27 21:52:26 +08:00
|
|
|
SceneGridLayoutDragStartEvent,
|
2025-06-25 16:02:42 +08:00
|
|
|
SceneObject,
|
2024-09-27 21:11:28 +08:00
|
|
|
} from '@grafana/scenes';
|
2025-07-30 21:01:27 +08:00
|
|
|
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
2025-04-02 16:18:28 +08:00
|
|
|
import { useStyles2 } from '@grafana/ui';
|
2024-09-27 21:11:28 +08:00
|
|
|
import { GRID_COLUMN_COUNT } from 'app/core/constants';
|
2025-02-06 22:30:54 +08:00
|
|
|
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
2024-09-27 21:11:28 +08:00
|
|
|
|
2025-03-18 20:34:14 +08:00
|
|
|
import {
|
2025-06-03 20:13:17 +08:00
|
|
|
dashboardEditActions,
|
2025-03-18 20:34:14 +08:00
|
|
|
NewObjectAddedToCanvasEvent,
|
|
|
|
ObjectRemovedFromCanvasEvent,
|
|
|
|
ObjectsReorderedOnCanvasEvent,
|
|
|
|
} from '../../edit-pane/shared';
|
2025-03-28 00:25:53 +08:00
|
|
|
import { serializeDefaultGridLayout } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer';
|
2025-08-20 16:21:18 +08:00
|
|
|
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
2025-02-06 17:57:08 +08:00
|
|
|
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
2024-09-27 21:11:28 +08:00
|
|
|
import {
|
|
|
|
forceRenderChildren,
|
|
|
|
getPanelIdForVizPanel,
|
|
|
|
NEW_PANEL_HEIGHT,
|
|
|
|
NEW_PANEL_WIDTH,
|
|
|
|
getVizPanelKeyForPanelId,
|
2025-02-03 17:46:47 +08:00
|
|
|
getGridItemKeyForPanelId,
|
2025-03-13 15:25:55 +08:00
|
|
|
useDashboard,
|
2025-03-27 21:52:26 +08:00
|
|
|
getLayoutOrchestratorFor,
|
2025-04-02 20:35:51 +08:00
|
|
|
getDashboardSceneFor,
|
2024-09-27 21:11:28 +08:00
|
|
|
} from '../../utils/utils';
|
2025-08-25 18:23:11 +08:00
|
|
|
import { useSoloPanelContext } from '../SoloPanelContext';
|
2025-05-07 23:07:39 +08:00
|
|
|
import { AutoGridItem } from '../layout-auto-grid/AutoGridItem';
|
2025-04-02 16:18:28 +08:00
|
|
|
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
2025-04-02 20:35:51 +08:00
|
|
|
import { clearClipboard, getDashboardGridItemFromClipboard } from '../layouts-shared/paste';
|
2025-05-29 22:45:42 +08:00
|
|
|
import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles';
|
2025-08-01 18:46:41 +08:00
|
|
|
import { getIsLazy } from '../layouts-shared/utils';
|
2025-02-05 17:08:41 +08:00
|
|
|
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
2025-02-11 20:08:07 +08:00
|
|
|
import { LayoutRegistryItem } from '../types/LayoutRegistryItem';
|
2024-09-27 21:11:28 +08:00
|
|
|
|
2024-11-05 15:05:09 +08:00
|
|
|
import { DashboardGridItem } from './DashboardGridItem';
|
2025-02-03 17:46:47 +08:00
|
|
|
import { RowRepeaterBehavior } from './RowRepeaterBehavior';
|
2025-04-02 16:18:28 +08:00
|
|
|
import { findSpaceForNewPanel } from './findSpaceForNewPanel';
|
2025-02-03 17:46:47 +08:00
|
|
|
import { RowActions } from './row-actions/RowActions';
|
2024-11-05 15:05:09 +08:00
|
|
|
|
2024-09-27 21:11:28 +08:00
|
|
|
interface DefaultGridLayoutManagerState extends SceneObjectState {
|
|
|
|
grid: SceneGridLayout;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class DefaultGridLayoutManager
|
|
|
|
extends SceneObjectBase<DefaultGridLayoutManagerState>
|
|
|
|
implements DashboardLayoutManager
|
|
|
|
{
|
2025-02-07 18:57:54 +08:00
|
|
|
public static Component = DefaultGridLayoutManagerRenderer;
|
|
|
|
|
2025-02-05 17:08:41 +08:00
|
|
|
public readonly isDashboardLayoutManager = true;
|
|
|
|
|
2025-02-11 20:08:07 +08:00
|
|
|
public static readonly descriptor: LayoutRegistryItem = {
|
2025-02-05 19:14:03 +08:00
|
|
|
get name() {
|
2025-03-06 18:39:45 +08:00
|
|
|
return t('dashboard.default-layout.name', 'Custom');
|
2025-02-05 19:14:03 +08:00
|
|
|
},
|
|
|
|
get description() {
|
2025-03-17 20:50:34 +08:00
|
|
|
return t('dashboard.default-layout.description', 'Position and size each panel individually');
|
2025-02-05 19:14:03 +08:00
|
|
|
},
|
2025-04-01 16:15:01 +08:00
|
|
|
id: 'GridLayout',
|
2025-02-05 17:08:41 +08:00
|
|
|
createFromLayout: DefaultGridLayoutManager.createFromLayout,
|
2025-03-17 20:50:34 +08:00
|
|
|
isGridLayout: true,
|
2025-04-07 20:30:09 +08:00
|
|
|
icon: 'window-grid',
|
2025-02-05 17:08:41 +08:00
|
|
|
};
|
|
|
|
|
2025-03-28 00:25:53 +08:00
|
|
|
public serialize(): DashboardV2Spec['layout'] {
|
|
|
|
return serializeDefaultGridLayout(this);
|
|
|
|
}
|
|
|
|
|
2025-02-05 17:08:41 +08:00
|
|
|
public readonly descriptor = DefaultGridLayoutManager.descriptor;
|
2024-12-10 14:21:30 +08:00
|
|
|
|
2025-03-18 20:34:14 +08:00
|
|
|
public constructor(state: DefaultGridLayoutManagerState) {
|
|
|
|
super(state);
|
|
|
|
|
|
|
|
this.addActivationHandler(() => this._activationHandler());
|
|
|
|
}
|
|
|
|
|
2025-08-15 22:01:01 +08:00
|
|
|
public merge(other: DashboardLayoutManager) {
|
|
|
|
if (!(other instanceof DefaultGridLayoutManager)) {
|
|
|
|
throw new Error('Cannot merge non-default grid layout');
|
|
|
|
}
|
|
|
|
|
|
|
|
let offset = 0;
|
|
|
|
for (const child of this.state.grid.state.children) {
|
|
|
|
const newOffset = (child.state.y ?? 0) + (child.state.height ?? 0);
|
|
|
|
if (newOffset > offset) {
|
|
|
|
offset = newOffset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-18 19:56:00 +08:00
|
|
|
const sourceGrid = other.state.grid;
|
|
|
|
const movedChildren = [...sourceGrid.state.children];
|
|
|
|
|
|
|
|
for (const child of movedChildren) {
|
|
|
|
const currentY = child.state.y ?? 0;
|
|
|
|
child.setState({ y: currentY + offset });
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove from source and append to destination
|
|
|
|
sourceGrid.setState({ children: [] });
|
2025-10-07 20:57:05 +08:00
|
|
|
for (const child of movedChildren) {
|
|
|
|
child.clearParent();
|
|
|
|
}
|
2025-09-18 19:56:00 +08:00
|
|
|
this.state.grid.setState({ children: [...this.state.grid.state.children, ...movedChildren] });
|
2025-08-15 22:01:01 +08:00
|
|
|
}
|
|
|
|
|
2025-03-18 20:34:14 +08:00
|
|
|
private _activationHandler() {
|
2025-03-27 21:52:26 +08:00
|
|
|
if (config.featureToggles.dashboardNewLayouts) {
|
|
|
|
this._subs.add(
|
|
|
|
this.subscribeToEvent(SceneGridLayoutDragStartEvent, ({ payload: { evt, panel } }) =>
|
|
|
|
getLayoutOrchestratorFor(this)?.startDraggingSync(evt, panel)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-03-18 20:34:14 +08:00
|
|
|
this._subs.add(
|
|
|
|
this.state.grid.subscribeToState(({ children: newChildren }, { children: prevChildren }) => {
|
|
|
|
if (newChildren.length === prevChildren.length) {
|
|
|
|
this.publishEvent(new ObjectsReorderedOnCanvasEvent(this.state.grid), true);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public addPanel(vizPanel: VizPanel) {
|
2025-02-06 17:57:08 +08:00
|
|
|
const panelId = dashboardSceneGraph.getNextPanelId(this);
|
2024-10-23 16:55:45 +08:00
|
|
|
|
|
|
|
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
|
|
|
|
vizPanel.clearParent();
|
|
|
|
|
2025-04-02 16:18:28 +08:00
|
|
|
// With new edit mode we add panels to the bottom of the grid
|
|
|
|
if (config.featureToggles.dashboardNewLayouts) {
|
|
|
|
const emptySpace = findSpaceForNewPanel(this.state.grid);
|
|
|
|
const newGridItem = new DashboardGridItem({
|
|
|
|
...emptySpace,
|
|
|
|
body: vizPanel,
|
|
|
|
key: getGridItemKeyForPanelId(panelId),
|
|
|
|
});
|
|
|
|
|
2025-06-03 20:13:17 +08:00
|
|
|
dashboardEditActions.addElement({
|
|
|
|
addedObject: vizPanel,
|
|
|
|
source: this,
|
|
|
|
perform: () => {
|
|
|
|
this.state.grid.setState({ children: [...this.state.grid.state.children, newGridItem] });
|
|
|
|
},
|
|
|
|
undo: () => {
|
|
|
|
this.state.grid.setState({
|
|
|
|
children: this.state.grid.state.children.filter((child) => child !== newGridItem),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
2025-04-02 16:18:28 +08:00
|
|
|
} else {
|
|
|
|
const newGridItem = new DashboardGridItem({
|
|
|
|
height: NEW_PANEL_HEIGHT,
|
|
|
|
width: NEW_PANEL_WIDTH,
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
body: vizPanel,
|
|
|
|
key: getGridItemKeyForPanelId(panelId),
|
|
|
|
});
|
|
|
|
|
|
|
|
this.state.grid.setState({ children: [newGridItem, ...this.state.grid.state.children] });
|
|
|
|
}
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|
|
|
|
|
2025-04-02 20:35:51 +08:00
|
|
|
public pastePanel() {
|
|
|
|
const emptySpace = findSpaceForNewPanel(this.state.grid);
|
2025-06-03 20:13:17 +08:00
|
|
|
const newGridItem = getDashboardGridItemFromClipboard(getDashboardSceneFor(this), emptySpace);
|
|
|
|
|
2025-06-10 15:22:06 +08:00
|
|
|
if (config.featureToggles.dashboardNewLayouts) {
|
|
|
|
dashboardEditActions.edit({
|
|
|
|
description: t('dashboard.edit-actions.paste-panel', 'Paste panel'),
|
|
|
|
addedObject: newGridItem.state.body,
|
|
|
|
source: this,
|
|
|
|
perform: () => {
|
|
|
|
this.state.grid.setState({ children: [...this.state.grid.state.children, newGridItem] });
|
|
|
|
},
|
|
|
|
undo: () => {
|
|
|
|
this.state.grid.setState({
|
|
|
|
children: this.state.grid.state.children.filter((child) => child !== newGridItem),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.state.grid.setState({ children: [...this.state.grid.state.children, newGridItem] });
|
|
|
|
}
|
2025-06-03 20:13:17 +08:00
|
|
|
|
2025-04-02 20:35:51 +08:00
|
|
|
clearClipboard();
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public removePanel(panel: VizPanel) {
|
2024-09-27 21:11:28 +08:00
|
|
|
const gridItem = panel.parent!;
|
|
|
|
|
|
|
|
if (!(gridItem instanceof DashboardGridItem)) {
|
|
|
|
throw new Error('Trying to remove panel that is not inside a DashboardGridItem');
|
|
|
|
}
|
|
|
|
|
|
|
|
const layout = this.state.grid;
|
|
|
|
|
|
|
|
let row: SceneGridRow | undefined;
|
|
|
|
|
|
|
|
try {
|
|
|
|
row = sceneGraph.getAncestor(gridItem, SceneGridRow);
|
|
|
|
} catch {
|
|
|
|
row = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (row) {
|
|
|
|
row.setState({ children: row.state.children.filter((child) => child !== gridItem) });
|
|
|
|
layout.forceRender();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-06-03 20:13:17 +08:00
|
|
|
if (!config.featureToggles.dashboardNewLayouts) {
|
|
|
|
// No undo/redo support in legacy edit mode
|
|
|
|
layout.setState({ children: layout.state.children.filter((child) => child !== gridItem) });
|
|
|
|
return;
|
|
|
|
}
|
2025-03-12 15:35:44 +08:00
|
|
|
|
2025-06-03 20:13:17 +08:00
|
|
|
dashboardEditActions.removeElement({
|
|
|
|
removedObject: gridItem.state.body,
|
|
|
|
source: this,
|
|
|
|
perform: () => layout.setState({ children: layout.state.children.filter((child) => child !== gridItem) }),
|
|
|
|
undo: () => layout.setState({ children: [...layout.state.children, gridItem] }),
|
|
|
|
});
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public duplicatePanel(vizPanel: VizPanel) {
|
2024-09-27 21:11:28 +08:00
|
|
|
const gridItem = vizPanel.parent;
|
|
|
|
if (!(gridItem instanceof DashboardGridItem)) {
|
|
|
|
console.error('Trying to duplicate a panel that is not inside a DashboardGridItem');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let panelState;
|
|
|
|
let panelData;
|
|
|
|
let newGridItem;
|
|
|
|
|
2025-02-06 17:57:08 +08:00
|
|
|
const newPanelId = dashboardSceneGraph.getNextPanelId(this);
|
2024-09-27 21:11:28 +08:00
|
|
|
const grid = this.state.grid;
|
|
|
|
|
|
|
|
if (gridItem instanceof DashboardGridItem) {
|
|
|
|
panelState = sceneUtils.cloneSceneObjectState(gridItem.state.body.state);
|
|
|
|
panelData = sceneGraph.getData(gridItem.state.body).clone();
|
|
|
|
} else {
|
|
|
|
panelState = sceneUtils.cloneSceneObjectState(vizPanel.state);
|
|
|
|
panelData = sceneGraph.getData(vizPanel).clone();
|
|
|
|
}
|
|
|
|
|
|
|
|
// when we duplicate a panel we don't want to clone the alert state
|
|
|
|
delete panelData.state.data?.alertState;
|
|
|
|
|
2025-07-29 20:34:38 +08:00
|
|
|
const newPanel = new VizPanel({
|
|
|
|
...panelState,
|
|
|
|
$data: panelData,
|
|
|
|
key: getVizPanelKeyForPanelId(newPanelId),
|
|
|
|
});
|
2025-03-10 15:03:55 +08:00
|
|
|
|
2024-09-27 21:11:28 +08:00
|
|
|
newGridItem = new DashboardGridItem({
|
|
|
|
x: gridItem.state.x,
|
|
|
|
y: gridItem.state.y,
|
|
|
|
height: gridItem.state.height,
|
2024-12-12 18:57:42 +08:00
|
|
|
itemHeight: gridItem.state.height,
|
2024-09-27 21:11:28 +08:00
|
|
|
width: gridItem.state.width,
|
|
|
|
variableName: gridItem.state.variableName,
|
|
|
|
repeatDirection: gridItem.state.repeatDirection,
|
|
|
|
maxPerRow: gridItem.state.maxPerRow,
|
2025-02-07 18:57:54 +08:00
|
|
|
key: getGridItemKeyForPanelId(newPanelId),
|
2025-03-10 15:03:55 +08:00
|
|
|
body: newPanel,
|
2024-09-27 21:11:28 +08:00
|
|
|
});
|
|
|
|
|
2025-07-29 20:34:38 +08:00
|
|
|
// No undo/redo support in legacy edit mode
|
|
|
|
if (!config.featureToggles.dashboardNewLayouts) {
|
|
|
|
if (gridItem.parent instanceof SceneGridRow) {
|
|
|
|
const row = gridItem.parent;
|
|
|
|
|
|
|
|
row.setState({ children: [...row.state.children, newGridItem] });
|
|
|
|
grid.forceRender();
|
|
|
|
return;
|
|
|
|
}
|
2024-09-27 21:11:28 +08:00
|
|
|
|
2025-07-29 20:34:38 +08:00
|
|
|
grid.setState({ children: [...grid.state.children, newGridItem] });
|
|
|
|
this.publishEvent(new NewObjectAddedToCanvasEvent(newPanel), true);
|
2024-09-27 21:11:28 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-07-29 20:34:38 +08:00
|
|
|
const parent = gridItem.parent instanceof SceneGridRow ? gridItem.parent : grid;
|
|
|
|
dashboardEditActions.edit({
|
|
|
|
description: t('dashboard.edit-actions.duplicate-panel', 'Duplicate panel'),
|
|
|
|
addedObject: newGridItem.state.body,
|
|
|
|
source: this,
|
|
|
|
perform: () => {
|
|
|
|
const oldGridItemIndex = parent.state.children.indexOf(gridItem);
|
|
|
|
const newChildrenArray = [...parent.state.children];
|
|
|
|
newChildrenArray.splice(oldGridItemIndex + 1, 0, newGridItem);
|
|
|
|
parent.setState({ children: newChildrenArray });
|
|
|
|
},
|
|
|
|
undo: () => {
|
|
|
|
parent.setState({
|
|
|
|
children: parent.state.children.filter((child) => child !== newGridItem),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|
|
|
|
|
2025-03-20 19:39:48 +08:00
|
|
|
public duplicate(): DashboardLayoutManager {
|
2025-04-11 18:17:32 +08:00
|
|
|
const children = this.state.grid.state.children;
|
|
|
|
const hasGridItem = children.find((child) => child instanceof DashboardGridItem);
|
|
|
|
const clonedChildren: SceneGridItemLike[] = [];
|
|
|
|
|
|
|
|
if (children.length) {
|
|
|
|
let panelId = hasGridItem ? dashboardSceneGraph.getNextPanelId(hasGridItem.state.body) : 1;
|
|
|
|
|
|
|
|
children.forEach((child) => {
|
|
|
|
if (child instanceof DashboardGridItem) {
|
|
|
|
const clone = child.clone({
|
|
|
|
key: undefined,
|
|
|
|
body: child.state.body.clone({
|
|
|
|
key: getVizPanelKeyForPanelId(panelId),
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
clonedChildren.push(clone);
|
|
|
|
panelId++;
|
|
|
|
} else {
|
|
|
|
clonedChildren.push(child.clone({ key: undefined }));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-03-20 19:39:48 +08:00
|
|
|
const clone = this.clone({
|
|
|
|
key: undefined,
|
|
|
|
grid: this.state.grid.clone({
|
|
|
|
key: undefined,
|
2025-04-11 18:17:32 +08:00
|
|
|
children: clonedChildren,
|
2025-03-20 19:39:48 +08:00
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
return clone;
|
|
|
|
}
|
|
|
|
|
2024-09-27 21:11:28 +08:00
|
|
|
public getVizPanels(): VizPanel[] {
|
|
|
|
const panels: VizPanel[] = [];
|
|
|
|
|
|
|
|
this.state.grid.forEachChild((child) => {
|
|
|
|
if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow)) {
|
|
|
|
throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene');
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
if (child instanceof DashboardGridItem && child.state.body instanceof VizPanel) {
|
|
|
|
panels.push(child.state.body);
|
2024-09-27 21:11:28 +08:00
|
|
|
} else if (child instanceof SceneGridRow) {
|
|
|
|
child.forEachChild((child) => {
|
2025-02-07 18:57:54 +08:00
|
|
|
if (child instanceof DashboardGridItem && child.state.body instanceof VizPanel) {
|
|
|
|
panels.push(child.state.body);
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return panels;
|
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public addNewRow(): SceneGridRow {
|
|
|
|
const id = dashboardSceneGraph.getNextPanelId(this);
|
|
|
|
|
|
|
|
const row = new SceneGridRow({
|
|
|
|
key: getVizPanelKeyForPanelId(id),
|
2025-05-29 04:14:43 +08:00
|
|
|
title: t('dashboard-scene.default-grid-layout-manager.row.title.row-title', 'Row title'),
|
2025-02-07 18:57:54 +08:00
|
|
|
actions: new RowActions({}),
|
|
|
|
y: 0,
|
2024-09-27 21:11:28 +08:00
|
|
|
});
|
2025-02-07 18:57:54 +08:00
|
|
|
|
|
|
|
const sceneGridLayout = this.state.grid;
|
|
|
|
|
|
|
|
// find all panels until the first row and put them into the newly created row. If there are no other rows,
|
|
|
|
// add all panels to the row. If there are no panels just create an empty row
|
|
|
|
const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow);
|
|
|
|
const rowChildren = sceneGridLayout.state.children
|
|
|
|
.splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow)
|
|
|
|
.map((child) => child.clone());
|
|
|
|
|
|
|
|
if (rowChildren) {
|
|
|
|
row.setState({ children: rowChildren });
|
|
|
|
}
|
|
|
|
|
|
|
|
sceneGridLayout.setState({ children: [row, ...sceneGridLayout.state.children] });
|
|
|
|
|
2025-03-21 21:28:44 +08:00
|
|
|
this.publishEvent(new NewObjectAddedToCanvasEvent(row), true);
|
2025-02-07 18:57:54 +08:00
|
|
|
return row;
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public editModeChanged(isEditing: boolean) {
|
|
|
|
const updateResizeAndDragging = () => {
|
|
|
|
this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing });
|
|
|
|
forceRenderChildren(this.state.grid, true);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (config.featureToggles.dashboardNewLayouts) {
|
|
|
|
// We do this in a timeout to wait a bit with enabling dragging as dragging enables grid animations
|
|
|
|
// if we show the edit pane without animations it opens much faster and feels more responsive
|
|
|
|
setTimeout(updateResizeAndDragging, 10);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateResizeAndDragging();
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public activateRepeaters() {
|
2025-02-14 00:41:09 +08:00
|
|
|
if (!this.isActive) {
|
|
|
|
this.activate();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.state.grid.isActive) {
|
|
|
|
this.state.grid.activate();
|
|
|
|
}
|
|
|
|
|
2024-09-27 21:11:28 +08:00
|
|
|
this.state.grid.forEachChild((child) => {
|
|
|
|
if (child instanceof DashboardGridItem && !child.isActive) {
|
|
|
|
child.activate();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (child instanceof SceneGridRow && child.state.$behaviors) {
|
|
|
|
for (const behavior of child.state.$behaviors) {
|
|
|
|
if (behavior instanceof RowRepeaterBehavior && !child.isActive) {
|
|
|
|
child.activate();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
child.state.children.forEach((child) => {
|
|
|
|
if (child instanceof DashboardGridItem && !child.isActive) {
|
|
|
|
child.activate();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-06-25 16:02:42 +08:00
|
|
|
public getOutlineChildren(): SceneObject[] {
|
|
|
|
const children: SceneObject[] = [];
|
|
|
|
|
|
|
|
for (const child of this.state.grid.state.children) {
|
|
|
|
// Flatten repeated grid items
|
|
|
|
if (child instanceof DashboardGridItem) {
|
2025-08-20 16:21:18 +08:00
|
|
|
children.push(child.state.body, ...(child.state.repeatedPanels || []));
|
2025-06-25 16:02:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return children;
|
|
|
|
}
|
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
2025-08-20 16:21:18 +08:00
|
|
|
return this.clone({});
|
2025-02-03 17:46:47 +08:00
|
|
|
}
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
public removeRow(row: SceneGridRow, removePanels = false) {
|
|
|
|
const sceneGridLayout = this.state.grid;
|
|
|
|
|
|
|
|
const children = sceneGridLayout.state.children.filter((child) => child.state.key !== row.state.key);
|
|
|
|
|
|
|
|
if (!removePanels) {
|
|
|
|
const rowChildren = row.state.children.map((child) => child.clone());
|
|
|
|
const indexOfRow = sceneGridLayout.state.children.findIndex((child) => child.state.key === row.state.key);
|
|
|
|
|
|
|
|
children.splice(indexOfRow, 0, ...rowChildren);
|
|
|
|
}
|
|
|
|
|
2025-03-18 20:34:14 +08:00
|
|
|
this.publishEvent(new ObjectRemovedFromCanvasEvent(row), true);
|
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
sceneGridLayout.setState({ children });
|
|
|
|
}
|
|
|
|
|
|
|
|
public collapseAllRows() {
|
|
|
|
this.state.grid.state.children.forEach((child) => {
|
|
|
|
if (!(child instanceof SceneGridRow)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!child.state.isCollapsed) {
|
|
|
|
this.state.grid.toggleRow(child);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public expandAllRows() {
|
|
|
|
this.state.grid.state.children.forEach((child) => {
|
|
|
|
if (!(child instanceof SceneGridRow)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (child.state.isCollapsed) {
|
|
|
|
this.state.grid.toggleRow(child);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-10-23 16:55:45 +08:00
|
|
|
public static createFromLayout(currentLayout: DashboardLayoutManager): DefaultGridLayoutManager {
|
|
|
|
const panels = currentLayout.getVizPanels();
|
2025-08-01 18:46:41 +08:00
|
|
|
const isLazy = getIsLazy(getDashboardSceneFor(currentLayout).state.preload)!;
|
|
|
|
return DefaultGridLayoutManager.fromVizPanels(panels, isLazy);
|
2024-10-23 16:55:45 +08:00
|
|
|
}
|
|
|
|
|
2025-08-01 18:46:41 +08:00
|
|
|
public static fromVizPanels(panels: VizPanel[] = [], isLazy?: boolean | undefined): DefaultGridLayoutManager {
|
2024-09-27 21:11:28 +08:00
|
|
|
const children: DashboardGridItem[] = [];
|
|
|
|
const panelHeight = 10;
|
|
|
|
const panelWidth = GRID_COLUMN_COUNT / 3;
|
|
|
|
let currentY = 0;
|
|
|
|
let currentX = 0;
|
|
|
|
|
|
|
|
for (let panel of panels) {
|
2025-05-07 23:07:39 +08:00
|
|
|
const variableName = panel.parent instanceof AutoGridItem ? panel.parent.state.variableName : undefined;
|
|
|
|
|
2024-10-23 16:55:45 +08:00
|
|
|
panel.clearParent();
|
|
|
|
|
2024-09-27 21:11:28 +08:00
|
|
|
children.push(
|
|
|
|
new DashboardGridItem({
|
2025-02-07 18:57:54 +08:00
|
|
|
key: getGridItemKeyForPanelId(getPanelIdForVizPanel(panel)),
|
2024-09-27 21:11:28 +08:00
|
|
|
x: currentX,
|
|
|
|
y: currentY,
|
|
|
|
width: panelWidth,
|
|
|
|
height: panelHeight,
|
2025-05-07 23:07:39 +08:00
|
|
|
itemHeight: panelHeight,
|
2024-09-27 21:11:28 +08:00
|
|
|
body: panel,
|
2025-05-07 23:07:39 +08:00
|
|
|
variableName,
|
2024-09-27 21:11:28 +08:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
currentX += panelWidth;
|
|
|
|
|
2025-03-19 17:41:32 +08:00
|
|
|
if (currentX + panelWidth > GRID_COLUMN_COUNT) {
|
2024-09-27 21:11:28 +08:00
|
|
|
currentX = 0;
|
|
|
|
currentY += panelHeight;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new DefaultGridLayoutManager({
|
|
|
|
grid: new SceneGridLayout({
|
|
|
|
children: children,
|
2024-11-20 21:09:56 +08:00
|
|
|
isDraggable: true,
|
|
|
|
isResizable: true,
|
2025-08-01 18:46:41 +08:00
|
|
|
isLazy,
|
2024-09-27 21:11:28 +08:00
|
|
|
}),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
public static fromGridItems(
|
|
|
|
gridItems: SceneGridItemLike[],
|
|
|
|
isDraggable?: boolean,
|
2025-08-01 18:46:41 +08:00
|
|
|
isResizable?: boolean,
|
|
|
|
isLazy?: boolean | undefined
|
2025-02-03 17:46:47 +08:00
|
|
|
): DefaultGridLayoutManager {
|
2025-01-22 21:57:45 +08:00
|
|
|
const children = gridItems.reduce<SceneGridItemLike[]>((acc, gridItem) => {
|
|
|
|
gridItem.clearParent();
|
|
|
|
acc.push(gridItem);
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return new DefaultGridLayoutManager({
|
|
|
|
grid: new SceneGridLayout({
|
|
|
|
children,
|
2025-02-03 17:46:47 +08:00
|
|
|
isDraggable,
|
|
|
|
isResizable,
|
2025-08-01 18:46:41 +08:00
|
|
|
isLazy,
|
2025-01-22 21:57:45 +08:00
|
|
|
}),
|
|
|
|
});
|
|
|
|
}
|
2025-02-07 18:57:54 +08:00
|
|
|
}
|
2025-01-22 21:57:45 +08:00
|
|
|
|
2025-02-07 18:57:54 +08:00
|
|
|
function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<DefaultGridLayoutManager>) {
|
|
|
|
const { children } = useSceneObjectState(model.state.grid, { shouldActivateOrKeepAlive: true });
|
2025-03-13 15:25:55 +08:00
|
|
|
const dashboard = useDashboard(model);
|
2025-04-02 16:18:28 +08:00
|
|
|
const { isEditing } = dashboard.useState();
|
2025-08-20 16:21:18 +08:00
|
|
|
const hasClonedParents = isRepeatCloneOrChildOf(model);
|
2025-04-02 16:18:28 +08:00
|
|
|
const styles = useStyles2(getStyles);
|
2025-04-09 18:02:11 +08:00
|
|
|
const showCanvasActions = isEditing && config.featureToggles.dashboardNewLayouts && !hasClonedParents;
|
2025-08-25 18:23:11 +08:00
|
|
|
const soloPanelContext = useSoloPanelContext();
|
|
|
|
|
|
|
|
if (soloPanelContext) {
|
|
|
|
return children.map((child) => <child.Component model={child} key={child.state.key!} />);
|
|
|
|
}
|
2025-02-06 22:30:54 +08:00
|
|
|
|
2025-03-19 19:53:58 +08:00
|
|
|
// If we are top level layout and we have no children, show empty state
|
2025-02-07 18:57:54 +08:00
|
|
|
if (model.parent === dashboard && children.length === 0) {
|
|
|
|
return (
|
|
|
|
<DashboardEmpty dashboard={dashboard} canCreate={!!dashboard.state.meta.canEdit} key="dashboard-empty-state" />
|
|
|
|
);
|
|
|
|
}
|
2025-02-06 22:30:54 +08:00
|
|
|
|
2025-04-02 16:18:28 +08:00
|
|
|
return (
|
|
|
|
<div className={cx(styles.container, isEditing && styles.containerEditing)}>
|
|
|
|
{model.state.grid.Component && <model.state.grid.Component model={model.state.grid} />}
|
2025-04-03 18:32:47 +08:00
|
|
|
{showCanvasActions && (
|
2025-04-02 16:18:28 +08:00
|
|
|
<div className={styles.actionsWrapper}>
|
|
|
|
<CanvasGridAddActions layoutManager={model} />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-08-25 18:23:11 +08:00
|
|
|
const OriginalSceneGridRowRenderer = SceneGridRow.Component;
|
|
|
|
// @ts-expect-error
|
|
|
|
SceneGridRow.Component = SceneGridRowRenderer;
|
|
|
|
|
|
|
|
function SceneGridRowRenderer({ model }: SceneComponentProps<SceneGridRow>) {
|
|
|
|
const soloPanelContext = useSoloPanelContext();
|
|
|
|
|
|
|
|
if (soloPanelContext) {
|
|
|
|
return model.state.children.map((child) => <child.Component model={child} key={child.state.key!} />);
|
|
|
|
}
|
|
|
|
|
|
|
|
return <OriginalSceneGridRowRenderer model={model} />;
|
|
|
|
}
|
|
|
|
|
2025-04-02 16:18:28 +08:00
|
|
|
function getStyles(theme: GrafanaTheme2) {
|
|
|
|
return {
|
|
|
|
container: css({
|
|
|
|
width: '100%',
|
|
|
|
display: 'flex',
|
|
|
|
flexGrow: 1,
|
|
|
|
flexDirection: 'column',
|
|
|
|
}),
|
|
|
|
containerEditing: css({
|
|
|
|
// In editing the add actions should live at the bottom of the grid so we have to
|
|
|
|
// disable flex grow on the SceneGridLayouts first div
|
|
|
|
'> div:first-child': {
|
|
|
|
flexGrow: `0 !important`,
|
|
|
|
minHeight: '250px',
|
|
|
|
},
|
2025-05-29 22:45:42 +08:00
|
|
|
...dashboardCanvasAddButtonHoverStyles,
|
2025-04-02 16:18:28 +08:00
|
|
|
}),
|
|
|
|
actionsWrapper: css({
|
|
|
|
position: 'relative',
|
|
|
|
paddingBottom: theme.spacing(5),
|
|
|
|
}),
|
|
|
|
};
|
2024-09-27 21:11:28 +08:00
|
|
|
}
|