diff --git a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx index ff46c11cc0e..94484509991 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx @@ -1,13 +1,21 @@ import { PointerEvent as ReactPointerEvent } from 'react'; -import { sceneGraph, SceneObjectBase, SceneObjectRef, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { logWarning } from '@grafana/runtime'; +import { + sceneGraph, + SceneObjectBase, + SceneObjectRef, + SceneObjectState, + VizPanel, + SceneGridItemLike, +} from '@grafana/scenes'; import { createPointerDistance } from '@grafana/ui'; import { DashboardScene } from './DashboardScene'; import { DashboardDropTarget, isDashboardDropTarget } from './types/DashboardDropTarget'; interface DashboardLayoutOrchestratorState extends SceneObjectState { - draggingPanel?: SceneObjectRef; + draggingGridItem?: SceneObjectRef; } export class DashboardLayoutOrchestrator extends SceneObjectBase { @@ -32,11 +40,11 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase { - this._sourceDropTarget?.draggedPanelOutside?.(panel!); - this._lastDropTarget?.draggedPanelInside?.(panel!); + if (gridItem) { + // Always use grid item dragging + this._sourceDropTarget?.draggedGridItemOutside?.(gridItem); + this._lastDropTarget?.draggedGridItemInside?.(gridItem); + } else { + const warningMessage = 'No grid item to drag'; + console.warn(warningMessage); + logWarning(warningMessage); + } }); } document.body.removeEventListener('pointermove', this._onPointerMove); document.body.removeEventListener('pointerup', this._stopDraggingSync); - this.setState({ draggingPanel: undefined }); + this.setState({ draggingGridItem: undefined }); } private _onPointerMove(evt: PointerEvent) { - if (!this._isSelectedObject && this.state.draggingPanel && this._pointerDistance.check(evt)) { + if (!this._isSelectedObject && this.state.draggingGridItem && this._pointerDistance.check(evt)) { this._isSelectedObject = true; - const panel = this.state.draggingPanel?.resolve(); - this._getDashboard().state.editPane.selectObject(panel, panel.state.key!, { force: true, multi: false }); + const gridItem = this.state.draggingGridItem?.resolve(); + if (gridItem && 'state' in gridItem && 'body' in gridItem.state && gridItem.state.body instanceof VizPanel) { + const panel = gridItem.state.body; + this._getDashboard().state.editPane.selectObject(panel, panel.state.key!, { force: true, multi: false }); + } } const dropTarget = this._getDropTargetUnderMouse(evt) ?? this._sourceDropTarget; diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx index 7ba318f2f4b..7e8570a3eec 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayout.tsx @@ -1,6 +1,6 @@ import { createRef, CSSProperties, PointerEvent as ReactPointerEvent } from 'react'; -import { SceneLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { SceneLayout, SceneObjectBase, SceneObjectState, VizPanel, SceneGridItemLike } from '@grafana/scenes'; import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { getLayoutOrchestratorFor } from '../../utils/utils'; @@ -110,7 +110,12 @@ export class AutoGridLayout extends SceneObjectBase impleme public getDragHooks() { return { - onDragStart: this._onDragStart, + onDragStart: (evt: ReactPointerEvent, panel: VizPanel) => { + const gridItem = panel.parent; + if (gridItem instanceof AutoGridItem) { + this._onDragStart(evt, gridItem); + } + }, }; } @@ -127,7 +132,7 @@ export class AutoGridLayout extends SceneObjectBase impleme } // Start inside dragging - private _onDragStart(evt: ReactPointerEvent, panel: VizPanel) { + private _onDragStart(evt: ReactPointerEvent, gridItem: SceneGridItemLike) { if (!this._canDrag(evt)) { return; } @@ -135,11 +140,11 @@ export class AutoGridLayout extends SceneObjectBase impleme evt.preventDefault(); evt.stopPropagation(); - if (!(panel.parent instanceof AutoGridItem)) { + if (!(gridItem instanceof AutoGridItem)) { throw new Error('Dragging wrong item'); } - this._draggedGridItem = panel.parent; + this._draggedGridItem = gridItem; const { top, left, width, height } = this._draggedGridItem.getBoundingBox(); this._initialGridItemPosition = { pageX: evt.pageX, pageY: evt.pageY, top, left: left }; @@ -152,7 +157,7 @@ export class AutoGridLayout extends SceneObjectBase impleme document.body.addEventListener('pointerup', this._onDragEnd); document.body.classList.add('dashboard-draggable-transparent-selection'); - getLayoutOrchestratorFor(this)?.startDraggingSync(evt, panel); + getLayoutOrchestratorFor(this)?.startDraggingSync(evt, this._draggedGridItem); } // Stop inside dragging diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx index 737629562fc..74ce363a970 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx @@ -1,6 +1,13 @@ import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { + SceneComponentProps, + SceneObject, + SceneObjectBase, + SceneObjectState, + VizPanel, + SceneGridItemLike, +} from '@grafana/scenes'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { GRID_CELL_VMARGIN } from 'app/core/constants'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -325,6 +332,33 @@ export class AutoGridLayoutManager extends SceneObjectBase) { 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 294dc4230f7..44a69b8fb23 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -125,9 +125,12 @@ export class DefaultGridLayoutManager private _activationHandler() { if (config.featureToggles.dashboardNewLayouts) { this._subs.add( - this.subscribeToEvent(SceneGridLayoutDragStartEvent, ({ payload: { evt, panel } }) => - getLayoutOrchestratorFor(this)?.startDraggingSync(evt, panel) - ) + this.subscribeToEvent(SceneGridLayoutDragStartEvent, ({ payload: { evt, panel } }) => { + const gridItem = panel.parent; + if (gridItem instanceof DashboardGridItem) { + getLayoutOrchestratorFor(this)?.startDraggingSync(evt, gridItem); + } + }) ); } @@ -515,6 +518,51 @@ export class DefaultGridLayoutManager }); } + public addGridItem(gridItem: SceneGridItemLike): void { + if (!(gridItem instanceof DashboardGridItem)) { + // If it's an AutoGridItem, convert it to DashboardGridItem + if (gridItem instanceof AutoGridItem) { + if (!(gridItem.state.body instanceof VizPanel)) { + throw new Error('AutoGridItem body is not a VizPanel'); + } + const panel = gridItem.state.body; + panel.clearParent(); + + const emptySpace = findSpaceForNewPanel(this.state.grid); + const newGridItem = new DashboardGridItem({ + x: emptySpace?.x ?? 0, + y: emptySpace?.y ?? 0, + width: emptySpace?.width ?? NEW_PANEL_WIDTH, + height: emptySpace?.height ?? NEW_PANEL_HEIGHT, + itemHeight: emptySpace?.height ?? NEW_PANEL_HEIGHT, + body: panel, + variableName: gridItem.state.variableName, + }); + + this.state.grid.setState({ children: [...this.state.grid.state.children, newGridItem] }); + return; + } + throw new Error('Grid item must be a DashboardGridItem or AutoGridItem'); + } + + // Move the whole grid item to another CustomGrid + // Clear parent before moving + gridItem.clearParent(); + + // Find empty space for the grid item, preserving its size + const emptySpace = findSpaceForNewPanel(this.state.grid); + if (emptySpace) { + // Update position to empty space, but keep original size + gridItem.setState({ + x: emptySpace.x, + y: emptySpace.y, + // Keep original width and height + }); + } + + this.state.grid.setState({ children: [...this.state.grid.state.children, gridItem] }); + } + public static createFromLayout(currentLayout: DashboardLayoutManager): DefaultGridLayoutManager { const panels = currentLayout.getVizPanels(); const isLazy = getIsLazy(getDashboardSceneFor(currentLayout).state.preload)!; 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 c9f7b91bf28..49a76045502 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -1,13 +1,15 @@ import React from 'react'; import { t } from '@grafana/i18n'; +import { logWarning } from '@grafana/runtime'; import { sceneGraph, SceneObject, SceneObjectBase, SceneObjectState, VariableDependencyConfig, - VizPanel, + SceneGridItemLike, + SceneGridLayout, } from '@grafana/scenes'; import { RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import appEvents from 'app/core/app_events'; @@ -20,11 +22,15 @@ import { ConditionalRenderingGroup } from '../../conditional-rendering/group/Con import { serializeRow } from '../../serialization/layoutSerializers/RowsLayoutSerializer'; import { getElements } from '../../serialization/layoutSerializers/utils'; import { getDashboardSceneFor } from '../../utils/utils'; +import { AutoGridItem } from '../layout-auto-grid/AutoGridItem'; +import { AutoGridLayout } from '../layout-auto-grid/AutoGridLayout'; import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager'; +import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { clearClipboard } from '../layouts-shared/paste'; import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { BulkActionElement } from '../types/BulkActionElement'; import { DashboardDropTarget } from '../types/DashboardDropTarget'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement'; import { LayoutParent } from '../types/LayoutParent'; @@ -168,14 +174,35 @@ export class RowItem } } - public draggedPanelOutside(panel: VizPanel) { - this.getLayout().removePanel?.(panel); + public draggedGridItemOutside?(gridItem: SceneGridItemLike): void { + // Remove from source layout + if (gridItem instanceof DashboardGridItem || gridItem instanceof AutoGridItem) { + const layout = gridItem.parent; + if (gridItem instanceof DashboardGridItem && layout instanceof SceneGridLayout) { + const newChildren = layout.state.children.filter((child) => child !== gridItem); + layout.setState({ children: newChildren }); + } else if (gridItem instanceof AutoGridItem && layout instanceof AutoGridLayout) { + const newChildren = layout.state.children.filter((child) => child !== gridItem); + layout.setState({ children: newChildren }); + } else { + const warningMessage = 'Grid item has unexpected parent type'; + console.warn(warningMessage); + logWarning(warningMessage); + } + } this.setIsDropTarget(false); } - public draggedPanelInside(panel: VizPanel) { - panel.clearParent(); - this.getLayout().addPanel(panel); + public draggedGridItemInside(gridItem: SceneGridItemLike): void { + const layout = this.getLayout(); + + if (isDashboardLayoutGrid(layout)) { + layout.addGridItem(gridItem); + } else { + const warningMessage = 'Layout manager does not support addGridItem'; + console.warn(warningMessage); + logWarning(warningMessage); + } this.setIsDropTarget(false); } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx index f49e2a37746..5db9caebbf6 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -1,13 +1,15 @@ import React from 'react'; import { t } from '@grafana/i18n'; +import { logWarning } from '@grafana/runtime'; import { SceneObjectState, SceneObjectBase, sceneGraph, VariableDependencyConfig, SceneObject, - VizPanel, + SceneGridItemLike, + SceneGridLayout, } from '@grafana/scenes'; import { TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { LS_TAB_COPY_KEY } from 'app/core/constants'; @@ -20,11 +22,15 @@ import { ConditionalRenderingGroup } from '../../conditional-rendering/group/Con import { serializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer'; import { getElements } from '../../serialization/layoutSerializers/utils'; import { getDashboardSceneFor } from '../../utils/utils'; +import { AutoGridItem } from '../layout-auto-grid/AutoGridItem'; +import { AutoGridLayout } from '../layout-auto-grid/AutoGridLayout'; import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager'; +import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { clearClipboard } from '../layouts-shared/paste'; import { scrollCanvasElementIntoView } from '../layouts-shared/scrollCanvasElementIntoView'; import { BulkActionElement } from '../types/BulkActionElement'; import { DashboardDropTarget } from '../types/DashboardDropTarget'; +import { isDashboardLayoutGrid } from '../types/DashboardLayoutGrid'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement'; import { LayoutParent } from '../types/LayoutParent'; @@ -186,18 +192,38 @@ export class TabItem } } - public draggedPanelOutside(panel: VizPanel) { - this.getLayout().removePanel?.(panel); + public draggedGridItemOutside?(gridItem: SceneGridItemLike): void { + // Remove from source layout + if (gridItem instanceof DashboardGridItem || gridItem instanceof AutoGridItem) { + const layout = gridItem.parent; + if (gridItem instanceof DashboardGridItem && layout instanceof SceneGridLayout) { + const newChildren = layout.state.children.filter((child) => child !== gridItem); + layout.setState({ children: newChildren }); + } else if (gridItem instanceof AutoGridItem && layout instanceof AutoGridLayout) { + const newChildren = layout.state.children.filter((child) => child !== gridItem); + layout.setState({ children: newChildren }); + } else { + const warningMessage = 'Grid item has unexpected parent type'; + console.warn(warningMessage); + logWarning(warningMessage); + } + } this.setIsDropTarget(false); } - public draggedPanelInside(panel: VizPanel) { - panel.clearParent(); - this.getLayout().addPanel(panel); + public draggedGridItemInside(gridItem: SceneGridItemLike): void { + const layout = this.getLayout(); + + if (isDashboardLayoutGrid(layout)) { + layout.addGridItem(gridItem); + } else { + const warningMessage = 'Layout manager does not support addGridItem'; + console.warn(warningMessage); + logWarning(warningMessage); + } this.setIsDropTarget(false); const parentLayout = this.getParentLayout(); - if (parentLayout.state.currentTabSlug !== this.getSlug()) { parentLayout.setState({ currentTabSlug: this.getSlug() }); } diff --git a/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts b/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts index de588c5efbd..8e28d5be9f0 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts @@ -1,10 +1,10 @@ -import { SceneObject, VizPanel } from '@grafana/scenes'; +import { SceneObject, SceneGridItemLike } from '@grafana/scenes'; export interface DashboardDropTarget extends SceneObject { isDashboardDropTarget: Readonly; setIsDropTarget?(isDropTarget: boolean): void; - draggedPanelOutside?(panel: VizPanel): void; - draggedPanelInside?(panel: VizPanel): void; + draggedGridItemOutside?(gridItem: SceneGridItemLike): void; + draggedGridItemInside?(gridItem: SceneGridItemLike): void; } export function isDashboardDropTarget(scene: SceneObject): scene is DashboardDropTarget { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts index 3f6bcb2bcc6..877a84893aa 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts @@ -1,3 +1,5 @@ +import { SceneGridItemLike } from '@grafana/scenes'; + import { DashboardLayoutManager } from './DashboardLayoutManager'; export interface DashboardLayoutGrid extends DashboardLayoutManager { @@ -5,8 +7,12 @@ export interface DashboardLayoutGrid extends DashboardLayoutManager { * Merge the layout with another layout */ mergeGrid(other: DashboardLayoutGrid): void; + /** + * Add a grid item to the layout + */ + addGridItem(gridItem: SceneGridItemLike): void; } export function isDashboardLayoutGrid(obj: DashboardLayoutManager): obj is DashboardLayoutGrid { - return 'mergeGrid' in obj; + return 'mergeGrid' in obj && 'addGridItem' in obj; }