From 4ae0ad15e60ac1e0d741e3de86be1b53a77e260f Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 4 Dec 2025 14:13:52 +0200 Subject: [PATCH] Pushes --- .../scene/DashboardLayoutOrchestrator.tsx | 143 +++++++-------- .../scene/layout-auto-grid/AutoGridItem.tsx | 4 + .../scene/layout-auto-grid/AutoGridLayout.tsx | 166 +++++++++--------- .../AutoGridLayoutManager.tsx | 21 ++- .../layout-default/DashboardGridItem.tsx | 4 + .../DefaultGridLayoutManager.tsx | 65 ++++--- .../scene/types/DashboardDropTarget.ts | 12 -- .../scene/types/DashboardLayoutGrid.ts | 16 +- .../scene/types/DashboardLayoutItem.ts | 5 + 9 files changed, 228 insertions(+), 208 deletions(-) delete mode 100644 public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts diff --git a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx index 284a0dacd1e..5426fa936ab 100644 --- a/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardLayoutOrchestrator.tsx @@ -1,6 +1,6 @@ import { PointerEvent as ReactPointerEvent } from 'react'; -import { sceneGraph, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { createPointerDistance } from '@grafana/ui'; import { DashboardScene } from './DashboardScene'; @@ -16,12 +16,13 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase this._activationHandler()); } @@ -29,12 +30,12 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase { document.body.removeEventListener('pointermove', this._onPointerMove); - document.body.removeEventListener('pointerup', this._stopDraggingSync); + document.body.removeEventListener('pointerup', this._onPointerUp); document.body.classList.remove('dashboard-draggable-transparent-selection'); }; } - public getDashboard(): DashboardScene { + private _getDashboard(): DashboardScene { if (!(this.parent instanceof DashboardScene)) { throw new Error('Parent is not a DashboardScene'); } @@ -42,80 +43,18 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase isDashboardLayoutManager(obj) && isDashboardLayoutGrid(obj) + ) as DashboardLayoutGrid[]; } private _getCurrentGrid(evt: MouseEvent): DashboardLayoutGrid | null { - const elementsUnderPoint = document.elementsFromPoint(evt.clientX, evt.clientY); - const cursorIsInSourceTarget = elementsUnderPoint.some( - (el) => el.getAttribute('data-grid-manager-key') === this._sourceGrid?.state.key - ); - - if (cursorIsInSourceTarget) { - return null; - } - - const key = elementsUnderPoint + const key = document + .elementsFromPoint(evt.clientX, evt.clientY) + .reverse() ?.find((element) => element.getAttribute('data-grid-manager-key')) ?.getAttribute('data-grid-manager-key'); @@ -123,18 +62,57 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase grid.state.key === key); - if (!sceneObject || !isDashboardLayoutManager(sceneObject) || !isDashboardLayoutGrid(sceneObject)) { + if (!grid) { return null; } - return sceneObject; + return grid; } - private _stopDraggingSync(_evt: PointerEvent) { + public startDragging(evt: ReactPointerEvent, layoutItem: DashboardLayoutItem, layoutGrid: DashboardLayoutGrid) { + this._pointerDistance.set(evt); + + this._isSelectedObject = false; + this._sourceGrid = layoutGrid; + this._currentGrid = layoutGrid; + this._layoutItem = layoutItem; + + this._grids = this._findAllGrids(); + + this._grids.forEach((grid) => + grid.onDragStart?.(this._sourceGrid!, this._layoutItem!, evt.nativeEvent) + ); + + document.body.addEventListener('pointermove', this._onPointerMove); + document.body.addEventListener('pointerup', this._onPointerUp); + document.body.classList.add('dashboard-draggable-transparent-selection'); + } + + private _onPointerMove(evt: PointerEvent) { + // Select the panel if needed + if (!this._isSelectedObject && this._pointerDistance.check(evt)) { + this._isSelectedObject = true; + + this._getDashboard().state.editPane.selectObject( + this._layoutItem!.getElementBody()!, + this._layoutItem!.getElementBody()!.state.key!, + { + force: true, + multi: false, + } + ); + } + + this._currentGrid = this._getCurrentGrid(evt) ?? this._currentGrid; + + this._grids.forEach((grid) => grid.onDrag?.(this._sourceGrid!, this._currentGrid!, this._layoutItem!, evt)); + } + + private _onPointerUp(evt: PointerEvent) { document.body.removeEventListener('pointermove', this._onPointerMove); - document.body.removeEventListener('pointerup', this._stopDraggingSync); + document.body.removeEventListener('pointerup', this._onPointerUp); document.body.classList.remove('dashboard-draggable-transparent-selection'); // Wrapped in setTimeout to ensure that any event handlers are called @@ -144,10 +122,7 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase 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._grids.forEach((obj) => obj.onDragStop?.(this._sourceGrid!, this._currentGrid!, this._layoutItem!, evt)); this._isSelectedObject = false; this._sourceGrid = null; diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx index 5a88e451fb0..638b7d25f33 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridItem.tsx @@ -74,6 +74,10 @@ export class AutoGridItem extends SceneObjectBase implements this.setState({ body }); } + public getElementBody(): VizPanel { + return this.state.body; + } + public performRepeat() { if (!this.state.variableName || sceneGraph.hasVariableDependencyInLoadingState(this)) { return; 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 d0bea2c9a4d..5fbbb499a04 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,11 +1,14 @@ import { createRef, CSSProperties, PointerEvent as ReactPointerEvent } from 'react'; -import { SceneLayout, SceneObjectBase, SceneObjectState, VizPanel, SceneGridItemLike } from '@grafana/scenes'; +import { SceneLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; import { isRepeatCloneOrChildOf } from '../../utils/clone'; import { getLayoutOrchestratorFor } from '../../utils/utils'; +import { DashboardLayoutGrid } from '../types/DashboardLayoutGrid'; +import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; import { AutoGridItem } from './AutoGridItem'; +import { AutoGridLayoutManager } from './AutoGridLayoutManager'; import { AutoGridLayoutRenderer } from './AutoGridLayoutRenderer'; import { DRAGGED_ITEM_HEIGHT, DRAGGED_ITEM_LEFT, DRAGGED_ITEM_TOP, DRAGGED_ITEM_WIDTH } from './const'; @@ -75,20 +78,6 @@ export class AutoGridLayout extends SceneObjectBase impleme children: state.children ?? [], ...state, }); - - this._onDragStart = this._onDragStart.bind(this); - this._onDragEnd = this._onDragEnd.bind(this); - this._onDrag = this._onDrag.bind(this); - - this.addActivationHandler(() => this._activationHandler()); - } - - private _activationHandler() { - return () => { - this._resetPanelPositionAndSize(); - document.body.removeEventListener('pointermove', this._onDrag); - document.body.removeEventListener('pointerup', this._onDragEnd); - }; } public isDraggable(): boolean { @@ -112,78 +101,72 @@ export class AutoGridLayout extends SceneObjectBase impleme onDragStart: (evt: ReactPointerEvent, panel: VizPanel) => { const gridItem = panel.parent; if (gridItem instanceof AutoGridItem) { - this._onDragStart(evt, gridItem); + getLayoutOrchestratorFor(this)?.startDragging(evt, gridItem, this._getLayoutManager()); } }, }; } - private _canDrag(evt: ReactPointerEvent): boolean { - if (!this.isDraggable()) { - return false; + // private _canDrag(evt: PointerEvent): boolean { + // if (!this.isDraggable()) { + // return false; + // } + // + // if (!(evt.target instanceof Element)) { + // return false; + // } + // + // return !!evt.target.closest(`.${this.getDragClass()}`) && !evt.target.closest(`.${this.getDragClassCancel()}`); + // } + + private _getLayoutManager(): AutoGridLayoutManager { + if (!(this.parent instanceof AutoGridLayoutManager)) { + throw new Error('Parent of AutoGridLayout must be AutoGridLayoutManager'); } - if (!(evt.target instanceof Element)) { - return false; - } - - return !!evt.target.closest(`.${this.getDragClass()}`) && !evt.target.closest(`.${this.getDragClassCancel()}`); + return this.parent; } - // Start inside dragging - private _onDragStart(evt: ReactPointerEvent, gridItem: SceneGridItemLike) { - if (!this._canDrag(evt)) { - return; + public onDragStart(sourceGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem, evt: PointerEvent) { + // if (!this._canDrag(evt)) { + // return; + // } + // + // if (!(layoutItem instanceof AutoGridItem)) { + // throw new Error('Dragging wrong item'); + // } + + if (layoutItem instanceof AutoGridItem) { + this._draggedGridItem = sourceGrid === this._getLayoutManager() ? layoutItem : layoutItem.clone(); + } else { + this._draggedGridItem = new AutoGridItem({ + body: layoutItem.getElementBody().clone(), + }); } - // evt.preventDefault(); - // evt.stopPropagation(); - - if (!(gridItem instanceof AutoGridItem)) { - throw new Error('Dragging wrong item'); + if (!this.state.children.includes(this._draggedGridItem)) { + this.setState({ children: [...this.state.children, this._draggedGridItem] }); } - this._draggedGridItem = gridItem; + setTimeout(() => { + const { top, left, width, height } = this._draggedGridItem!.getBoundingBox(); + this._initialGridItemPosition = { pageX: evt.pageX, pageY: evt.pageY, top, left: left }; + this._updatePanelSize(width, height); + this._updatePanelPosition(top, left); - const { top, left, width, height } = this._draggedGridItem.getBoundingBox(); - this._initialGridItemPosition = { pageX: evt.pageX, pageY: evt.pageY, top, left: left }; - this._updatePanelSize(width, height); - this._updatePanelPosition(top, left); - - this.setState({ draggingKey: this._draggedGridItem.state.key }); - - document.body.addEventListener('pointermove', this._onDrag); - document.body.addEventListener('pointerup', this._onDragEnd); - document.body.classList.add('dashboard-draggable-transparent-selection'); - - getLayoutOrchestratorFor(this)?.startDraggingSync(evt, this._draggedGridItem, this); + this.setState({ draggingKey: this._draggedGridItem!.state.key }); + }); } - // Stop inside dragging - private _onDragEnd() { - window.getSelection()?.removeAllRanges(); - - this._draggedGridItem = null; - this._initialGridItemPosition = null; - this._resetPanelPositionAndSize(); - - this.setState({ draggingKey: undefined }); - - document.body.removeEventListener('pointermove', this._onDrag); - document.body.removeEventListener('pointerup', this._onDragEnd); - document.body.classList.remove('dashboard-draggable-transparent-selection'); - } - - // Handle inside drag moves - private _onDrag(evt: PointerEvent) { - if (!this._draggedGridItem || !this._initialGridItemPosition) { - this._onDragEnd(); - return; - } - + public onDrag( + sourceGrid: DashboardLayoutGrid, + targetGrid: DashboardLayoutGrid, + layoutItem: DashboardLayoutItem, + evt: PointerEvent + ) { this._updatePanelPosition( - this._initialGridItemPosition.top + (evt.pageY - this._initialGridItemPosition.pageY), - this._initialGridItemPosition.left + (evt.pageX - this._initialGridItemPosition.pageX) + this._initialGridItemPosition!.top + (evt.pageY - this._initialGridItemPosition!.pageY), + this._initialGridItemPosition!.left + (evt.pageX - this._initialGridItemPosition!.pageX) ); const dropTargetGridItemKey = document @@ -200,6 +183,31 @@ export class AutoGridLayout extends SceneObjectBase impleme } } + public onDragStop(sourceGrid: DashboardLayoutGrid, + targetGrid: DashboardLayoutGrid, + layoutItem: DashboardLayoutItem, + evt: PointerEvent) { + if (targetGrid !== this._getLayoutManager()) { + this.setState({ children: this.state.children.filter((child) => child !== this._draggedGridItem!) }); + } + + this._draggedGridItem = null; + } + + // public onDragStop() { + // window.getSelection()?.removeAllRanges(); + // + // this._draggedGridItem = null; + // this._initialGridItemPosition = null; + // this._resetPanelPositionAndSize(); + // + // this.setState({ draggingKey: undefined }); + // + // document.body.removeEventListener('pointermove', this._onDrag); + // document.body.removeEventListener('pointerup', this._onDragEnd); + // document.body.classList.remove('dashboard-draggable-transparent-selection'); + // } + // Handle dragging an item from the same grid over another item from the same grid private _onDragOverItem(key: string) { const children = [...this.state.children]; @@ -226,18 +234,18 @@ export class AutoGridLayout extends SceneObjectBase impleme this._setContainerStyle(DRAGGED_ITEM_HEIGHT, `${Math.floor(height)}px`); } - private _resetPanelPositionAndSize() { - this._removeContainerStyle(DRAGGED_ITEM_TOP); - this._removeContainerStyle(DRAGGED_ITEM_LEFT); - this._removeContainerStyle(DRAGGED_ITEM_WIDTH); - this._removeContainerStyle(DRAGGED_ITEM_HEIGHT); - } + // private _resetPanelPositionAndSize() { + // this._removeContainerStyle(DRAGGED_ITEM_TOP); + // this._removeContainerStyle(DRAGGED_ITEM_LEFT); + // this._removeContainerStyle(DRAGGED_ITEM_WIDTH); + // this._removeContainerStyle(DRAGGED_ITEM_HEIGHT); + // } private _setContainerStyle(name: string, value: string) { this.containerRef.current?.style.setProperty(name, value); } - private _removeContainerStyle(name: string) { - this.containerRef.current?.style.removeProperty(name); - } + // private _removeContainerStyle(name: string) { + // this.containerRef.current?.style.removeProperty(name); + // } } 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 a9c7455e6ae..d5e664882b6 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 @@ -24,6 +24,7 @@ import { import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { clearClipboard, getAutoGridItemFromClipboard } from '../layouts-shared/paste'; import { DashboardLayoutGrid } from '../types/DashboardLayoutGrid'; +import { DashboardLayoutItem } from '../types/DashboardLayoutItem'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -360,8 +361,26 @@ export class AutoGridLayoutManager extends SceneObjectBase { + this.subscribeToEvent(SceneGridLayoutDragStartEvent, ({ payload: { evt, panel } }) => { const gridItem = panel.parent; if (gridItem instanceof DashboardGridItem) { - this.state.grid.setPlaceholderSize(gridItem.state.width ?? NEW_PANEL_WIDTH, gridItem.state.height ?? NEW_PANEL_HEIGHT); - getLayoutOrchestratorFor(this)?.startDraggingSync(evt, gridItem, this); + getLayoutOrchestratorFor(this)?.startDragging(evt, gridItem, this); } }) - ) + ); } this._subs.add( @@ -367,7 +368,11 @@ export class DefaultGridLayoutManager const panels: VizPanel[] = []; this.state.grid.forEachChild((child) => { - if (!(child instanceof DashboardGridItem) && !(child instanceof SceneGridRow) && !(child instanceof SceneGridPlaceholderItem)) { + if ( + !(child instanceof DashboardGridItem) && + !(child instanceof SceneGridRow) && + !(child instanceof SceneGridPlaceholderItem) + ) { throw new Error('Child is not a DashboardGridItem or SceneGridRow, invalid scene'); } @@ -566,34 +571,33 @@ 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, - }; + public onDragStart(sourceGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem) { + if (layoutItem instanceof DashboardGridItem) { + this._draggingGridItem = sourceGrid === this ? layoutItem : layoutItem.clone(); + } else { + const width = layoutItem instanceof DashboardGridItem ? layoutItem.state.width : NEW_PANEL_WIDTH; + const height = layoutItem instanceof DashboardGridItem ? layoutItem.state.height : NEW_PANEL_HEIGHT; - if (newState.isDragging !== this.state.grid.state.isDragging || newState.isOutsideDragging !== this.state.grid.state.isOutsideDragging) { - this.state.grid.setState(newState); + this._draggingGridItem = new DashboardGridItem({ + width: width ?? NEW_PANEL_WIDTH, + height: height ?? NEW_PANEL_HEIGHT, + body: layoutItem.getElementBody().clone(), + }); } + + this.state.grid.setPlaceholder(this._draggingGridItem); } - 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) { + public onDragStop(sourceGrid: DashboardLayoutGrid, targetGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem) { + if (sourceGrid !== this && targetGrid === this) { + const panel = layoutItem.getElementBody(); + panel.clearParent(); + this._draggingGridItem?.setElementBody(panel); + } else if (sourceGrid === this && targetGrid !== this) { this.state.grid.setState({ children: this.state.grid.state.children.filter((child) => child !== layoutItem) }); - } else if (!isSourceGrid && isTargetGrid) { - // From outside drag - } else { - // Inside drag } + + this._draggingGridItem = undefined; } public static createFromLayout(currentLayout: DashboardLayoutManager): DefaultGridLayoutManager { @@ -690,7 +694,10 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps +
{model.state.grid.Component && } {showCanvasActions && (
diff --git a/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts b/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts deleted file mode 100644 index 8e28d5be9f0..00000000000 --- a/public/app/features/dashboard-scene/scene/types/DashboardDropTarget.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SceneObject, SceneGridItemLike } from '@grafana/scenes'; - -export interface DashboardDropTarget extends SceneObject { - isDashboardDropTarget: Readonly; - setIsDropTarget?(isDropTarget: boolean): void; - draggedGridItemOutside?(gridItem: SceneGridItemLike): void; - draggedGridItemInside?(gridItem: SceneGridItemLike): void; -} - -export function isDashboardDropTarget(scene: SceneObject): scene is DashboardDropTarget { - return 'isDashboardDropTarget' in scene && scene.isDashboardDropTarget === true; -} diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts index b17eafe0af5..7ad8adcd0d0 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutGrid.ts @@ -16,15 +16,25 @@ export interface DashboardLayoutGrid extends DashboardLayoutManager { /** * Start the synchronization of the orchestrator with the grid drag */ - startOrchestratorSync?(): void; + onDragStart?(sourceGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem, evt: PointerEvent): void; - stopOrchestratorSync?(sourceGrid: DashboardLayoutGrid, targetGrid: DashboardLayoutGrid, layoutItem: DashboardLayoutItem): void; + onDragStop?( + sourceGrid: DashboardLayoutGrid, + targetGrid: DashboardLayoutGrid, + layoutItem: DashboardLayoutItem, + evt: PointerEvent + ): 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; + onDrag?( + sourceGrid: DashboardLayoutGrid, + targetGrid: DashboardLayoutGrid, + layoutItem: DashboardLayoutItem, + evt: PointerEvent + ): void; } export function isDashboardLayoutGrid(obj: DashboardLayoutManager): obj is DashboardLayoutGrid { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts index c7bac407208..5f62179e587 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts @@ -19,6 +19,11 @@ export interface DashboardLayoutItem extends SceneObject { * Change inner body / viz panel */ setElementBody(body: VizPanel): void; + + /** + * Access inner body / viz panel + */ + getElementBody(): VizPanel; } export function isDashboardLayoutItem(obj: SceneObject): obj is DashboardLayoutItem {