From 11ab1ca5994565017e90c36d4ead51ef683d6d00 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 4 Dec 2025 11:43:48 +0200 Subject: [PATCH] Pushes --- .../components/PanelChrome/PanelChrome.tsx | 4 +- .../scene/DashboardLayoutOrchestrator.tsx | 186 ++++++++++-------- .../scene/layout-auto-grid/AutoGridLayout.tsx | 1 - .../DefaultGridLayoutManager.tsx | 36 +++- .../scene/types/DashboardLayoutGrid.ts | 15 +- 5 files changed, 148 insertions(+), 94 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 23c6f1597b0..c2c80322cc2 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -346,6 +346,8 @@ export function PanelChrome({ onMouseMove={onMouseMove} onMouseEnter={onMouseEnter} ref={ref} + draggable={true} + unselectable="on" >
{loadingState === LoadingState.Loading ? ( @@ -390,8 +392,6 @@ export function PanelChrome({ onMouseEnter={isSelectable ? onHeaderEnter : undefined} onMouseLeave={isSelectable ? onHeaderLeave : undefined} onPointerUp={onPointerUp} - draggable={true} - unselectable="on" onDragStart={(evt) => evt.dataTransfer.setData('text/plain', '')} > {statusMessage && ( diff --git a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx index 4629156a28f..284a0dacd1e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx @@ -1,27 +1,19 @@ import { PointerEvent as ReactPointerEvent } from 'react'; -import { logWarning } from '@grafana/runtime'; -import { - sceneGraph, - SceneObjectBase, - SceneObjectRef, - SceneObjectState, - VizPanel, - SceneGridItemLike, -} from '@grafana/scenes'; +import { sceneGraph, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; import { createPointerDistance } from '@grafana/ui'; import { DashboardScene } from './DashboardScene'; import { DashboardLayoutGrid, isDashboardLayoutGrid } from './types/DashboardLayoutGrid'; +import { DashboardLayoutItem } from './types/DashboardLayoutItem'; import { isDashboardLayoutManager } from './types/DashboardLayoutManager'; -interface DashboardLayoutOrchestratorState extends SceneObjectState { - draggingGridItem?: SceneObjectRef; -} +interface DashboardLayoutOrchestratorState extends SceneObjectState {} export class DashboardLayoutOrchestrator extends SceneObjectBase { private _sourceGrid: DashboardLayoutGrid | null = null; - private _lastGrid: DashboardLayoutGrid | null = null; + private _currentGrid: DashboardLayoutGrid | null = null; + private _layoutItem: DashboardLayoutItem | null = null; private _pointerDistance = createPointerDistance(); private _isSelectedObject = false; @@ -38,79 +30,11 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase { document.body.removeEventListener('pointermove', this._onPointerMove); document.body.removeEventListener('pointerup', this._stopDraggingSync); + document.body.classList.remove('dashboard-draggable-transparent-selection'); }; } - public startDraggingSync(evt: ReactPointerEvent, gridItem: SceneGridItemLike, layoutGrid: DashboardLayoutGrid): void { - this._pointerDistance.set(evt); - this._isSelectedObject = false; - - (sceneGraph.findAllObjects( - this._getDashboard(), - (obj) => isDashboardLayoutManager(obj) && isDashboardLayoutGrid(obj) - ) as DashboardLayoutGrid[]).forEach((layout) => layout.startOrchestratorSync?.()); - - this._sourceGrid = layoutGrid; - this._lastGrid = layoutGrid; - - document.body.addEventListener('pointermove', this._onPointerMove); - document.body.addEventListener('pointerup', this._stopDraggingSync); - - this.setState({ draggingGridItem: gridItem.getRef() }); - } - - private _stopDraggingSync(_evt: PointerEvent) { - const gridItem = this.state.draggingGridItem?.resolve(); - - if (this._sourceGrid !== this._lastGrid) { - // Wrapped in setTimeout to ensure that any event handlers are called - // Useful for allowing react-grid-layout to remove placeholders, etc. - setTimeout(() => { - if (gridItem) { - // Always use grid item dragging - this._sourceGrid?.draggedItemOutside?.(gridItem); - this._lastGrid?.draggedItemInside?.(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({ draggingGridItem: undefined }); - } - - private _onPointerMove(evt: PointerEvent) { - if (!this._isSelectedObject && this.state.draggingGridItem && this._pointerDistance.check(evt)) { - this._isSelectedObject = true; - 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._getLayoutGridUnderMouse(evt) ?? this._sourceGrid; - - if (!dropTarget) { - return; - } - - if (dropTarget !== this._lastGrid) { - this._lastGrid?.setIsDropTarget?.(false); - this._lastGrid = dropTarget; - - if (dropTarget !== this._sourceGrid) { - dropTarget.setIsDropTarget?.(true); - } - } - } - - private _getDashboard(): DashboardScene { + public getDashboard(): DashboardScene { if (!(this.parent instanceof DashboardScene)) { throw new Error('Parent is not a DashboardScene'); } @@ -118,10 +42,74 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase el.getAttribute('data-grid-manager-key') === - this._sourceGrid?.state.key); + const cursorIsInSourceTarget = elementsUnderPoint.some( + (el) => el.getAttribute('data-grid-manager-key') === this._sourceGrid?.state.key + ); if (cursorIsInSourceTarget) { return null; @@ -135,7 +123,7 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase { + if (!this._sourceGrid || !this._currentGrid || !this._layoutItem) { + return; + } + + sceneGraph.findAllObjects(this.getDashboard(), (obj) => isDashboardLayoutManager(obj) && isDashboardLayoutGrid(obj)).forEach((obj) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + (obj as DashboardLayoutGrid).stopOrchestratorSync?.(this._sourceGrid!, this._currentGrid!, this._layoutItem!); + }); + + this._isSelectedObject = false; + this._sourceGrid = null; + this._currentGrid = null; + this._layoutItem = null; + }); + } } 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 45353082b40..d0bea2c9a4d 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 @@ -88,7 +88,6 @@ export class AutoGridLayout extends SceneObjectBase impleme this._resetPanelPositionAndSize(); document.body.removeEventListener('pointermove', this._onDrag); document.body.removeEventListener('pointerup', this._onDragEnd); - document.body.classList.remove('dashboard-draggable-transparent-selection'); }; } 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 fc5cc9e968d..ebdd5fd3579 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -15,6 +15,8 @@ import { SceneGridItemLike, useSceneObjectState, SceneObject, + SceneGridLayoutDragStartEvent, + SceneGridPlaceholderItem } from '@grafana/scenes'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2'; import { useStyles2 } from '@grafana/ui'; @@ -48,6 +50,7 @@ import { clearClipboard, getDashboardGridItemFromClipboard } from '../layouts-sh import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles'; import { getIsLazy } from '../layouts-shared/utils'; import { DashboardLayoutGrid } from '../types/DashboardLayoutGrid'; +import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -127,7 +130,8 @@ export class DefaultGridLayoutManager this.subscribeToEvent(SceneGridLayoutDragStartEvent, ({ payload: { evt, panel }}) => { const gridItem = panel.parent; if (gridItem instanceof DashboardGridItem) { - getLayoutOrchestratorFor(this)?.startDraggingSync(evt, gridItem, this.state.grid); + this.state.grid.setPlaceholderSize(gridItem.state.width ?? NEW_PANEL_WIDTH, gridItem.state.height ?? NEW_PANEL_HEIGHT); + getLayoutOrchestratorFor(this)?.startDraggingSync(evt, gridItem, this); } }) ) @@ -562,6 +566,36 @@ export class DefaultGridLayoutManager this.state.grid.setState({ children: [...this.state.grid.state.children, gridItem] }); } + public setIsDropTarget(flag: boolean, sourceGrid: DashboardLayoutGrid) { + const newState = { + isDragging: flag, + isOutsideDragging: sourceGrid !== this, + }; + + if (newState.isDragging !== this.state.grid.state.isDragging || newState.isOutsideDragging !== this.state.grid.state.isOutsideDragging) { + this.state.grid.setState(newState); + } + } + + public stopOrchestratorSync(sourceGrid: DashboardLayoutGrid, targetGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem) { + console.log('stop orchestrator sync'); + + const isSourceGrid = sourceGrid === this; + const isTargetGrid = targetGrid === this; + + if (!isSourceGrid && !isTargetGrid) { + return; + } + + if (isSourceGrid && !isTargetGrid) { + this.state.grid.setState({ children: this.state.grid.state.children.filter((child) => child !== layoutItem) }); + } else if (!isSourceGrid && isTargetGrid) { + // From outside drag + } else { + // Inside drag + } + } + 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/types/DashboardLayoutGrid.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts index a0271fb03ca..b17eafe0af5 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts @@ -1,5 +1,6 @@ import { SceneGridItemLike } from '@grafana/scenes'; +import { DashboardLayoutItem } from './DashboardLayoutItem'; import { DashboardLayoutManager } from './DashboardLayoutManager'; export interface DashboardLayoutGrid extends DashboardLayoutManager { @@ -12,10 +13,18 @@ export interface DashboardLayoutGrid extends DashboardLayoutManager { */ addGridItem(gridItem: SceneGridItemLike): void; + /** + * Start the synchronization of the orchestrator with the grid drag + */ startOrchestratorSync?(): void; - draggedItemOutside?(gridItem: SceneGridItemLike): void; - draggedItemInside?(gridItem: SceneGridItemLike): void; - setIsDropTarget?(flag: boolean): void; + + stopOrchestratorSync?(sourceGrid: DashboardLayoutGrid, targetGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem): void; + + /** + * Toggle the grid as the current drop target + * Useful for toggling between inner drag and outer drag + */ + setIsDropTarget?(flag: boolean, sourceGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem): void; } export function isDashboardLayoutGrid(obj: DashboardLayoutManager): obj is DashboardLayoutGrid {