Dynamic Dashboards: Change dragging to using grid items instead of viz panels. (#113343)
* Preserve grid item size and repeat options when dragging between grids * push gridItem usage all the way * do console.warn and log to faro instead
This commit is contained in:
@@ -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<VizPanel>;
|
||||
draggingGridItem?: SceneObjectRef<SceneGridItemLike>;
|
||||
}
|
||||
|
||||
export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayoutOrchestratorState> {
|
||||
@@ -32,11 +40,11 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
};
|
||||
}
|
||||
|
||||
public startDraggingSync(evt: ReactPointerEvent, panel: VizPanel): void {
|
||||
public startDraggingSync(evt: ReactPointerEvent, gridItem: SceneGridItemLike): void {
|
||||
this._pointerDistance.set(evt);
|
||||
this._isSelectedObject = false;
|
||||
|
||||
const dropTarget = sceneGraph.findObject(panel, isDashboardDropTarget);
|
||||
const dropTarget = sceneGraph.findObject(gridItem, isDashboardDropTarget);
|
||||
|
||||
if (!dropTarget || !isDashboardDropTarget(dropTarget)) {
|
||||
return;
|
||||
@@ -48,32 +56,42 @@ export class DashboardLayoutOrchestrator extends SceneObjectBase<DashboardLayout
|
||||
document.body.addEventListener('pointermove', this._onPointerMove);
|
||||
document.body.addEventListener('pointerup', this._stopDraggingSync);
|
||||
|
||||
this.setState({ draggingPanel: panel.getRef() });
|
||||
this.setState({ draggingGridItem: gridItem.getRef() });
|
||||
}
|
||||
|
||||
private _stopDraggingSync(_evt: PointerEvent) {
|
||||
const panel = this.state.draggingPanel?.resolve();
|
||||
const gridItem = this.state.draggingGridItem?.resolve();
|
||||
|
||||
if (this._sourceDropTarget !== this._lastDropTarget) {
|
||||
// Wrapped in setTimeout to ensure that any event handlers are called
|
||||
// Useful for allowing react-grid-layout to remove placeholders, etc.
|
||||
setTimeout(() => {
|
||||
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;
|
||||
|
||||
@@ -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<AutoGridLayoutState> 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<AutoGridLayoutState> 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<AutoGridLayoutState> 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<AutoGridLayoutState> 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
|
||||
|
||||
+35
-1
@@ -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<AutoGridLayoutManager
|
||||
|
||||
return layoutManager;
|
||||
}
|
||||
|
||||
public addGridItem(gridItem: SceneGridItemLike): void {
|
||||
if (!(gridItem instanceof AutoGridItem)) {
|
||||
// If it's a DashboardGridItem, convert it to AutoGridItem
|
||||
if (gridItem instanceof DashboardGridItem) {
|
||||
if (!(gridItem.state.body instanceof VizPanel)) {
|
||||
throw new Error('DashboardGridItem body is not a VizPanel');
|
||||
}
|
||||
const panel = gridItem.state.body;
|
||||
panel.clearParent();
|
||||
|
||||
const newGridItem = new AutoGridItem({
|
||||
body: panel,
|
||||
variableName: gridItem.state.variableName,
|
||||
});
|
||||
|
||||
this.state.layout.setState({ children: [...this.state.layout.state.children, newGridItem] });
|
||||
return;
|
||||
}
|
||||
throw new Error('Grid item must be an AutoGridItem or DashboardGridItem');
|
||||
}
|
||||
|
||||
// Clear parent before moving
|
||||
gridItem.clearParent();
|
||||
|
||||
this.state.layout.setState({ children: [...this.state.layout.state.children, gridItem] });
|
||||
}
|
||||
}
|
||||
|
||||
function AutoGridLayoutManagerRenderer({ model }: SceneComponentProps<AutoGridLayoutManager>) {
|
||||
|
||||
+51
-3
@@ -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)!;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() });
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SceneObject, VizPanel } from '@grafana/scenes';
|
||||
import { SceneObject, SceneGridItemLike } from '@grafana/scenes';
|
||||
|
||||
export interface DashboardDropTarget extends SceneObject {
|
||||
isDashboardDropTarget: Readonly<true>;
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user