mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Context menu (#48909)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
parent
dea6fb4c1b
commit
b3b650be1f
@ -1,7 +1,7 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import { CanvasFrameOptions, canvasElementRegistry } from 'app/features/canvas';
|
||||
import { canvasElementRegistry, CanvasFrameOptions } from 'app/features/canvas';
|
||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||
import { DimensionContext } from 'app/features/dimensions';
|
||||
import { LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
@ -74,6 +74,18 @@ export class FrameState extends ElementState {
|
||||
this.reinitializeMoveable();
|
||||
}
|
||||
|
||||
doMove(child: ElementState, action: LayerActionID) {
|
||||
const vals = this.elements.filter((v) => v !== child);
|
||||
if (action === LayerActionID.MoveBottom) {
|
||||
vals.unshift(child);
|
||||
} else {
|
||||
vals.push(child);
|
||||
}
|
||||
this.elements = vals;
|
||||
this.scene.save();
|
||||
this.reinitializeMoveable();
|
||||
}
|
||||
|
||||
reinitializeMoveable() {
|
||||
// Need to first clear current selection and then re-init moveable with slight delay
|
||||
this.scene.clearCurrentSelection();
|
||||
@ -151,6 +163,11 @@ export class FrameState extends ElementState {
|
||||
this.scene.save();
|
||||
this.reinitializeMoveable();
|
||||
break;
|
||||
case LayerActionID.MoveTop:
|
||||
case LayerActionID.MoveBottom:
|
||||
element.parent?.doMove(element, action);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('DO action', action, element);
|
||||
return;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CanvasFrameOptions, CanvasElementOptions } from 'app/features/canvas';
|
||||
import { CanvasElementOptions, CanvasFrameOptions } from 'app/features/canvas';
|
||||
|
||||
import { FrameState } from './frame';
|
||||
import { Scene } from './scene';
|
||||
@ -41,7 +41,12 @@ export class RootElement extends FrameState {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div key={this.UID} ref={this.setRootRef} style={{ ...this.sizeStyle, ...this.dataStyle }}>
|
||||
<div
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
key={this.UID}
|
||||
ref={this.setRootRef}
|
||||
style={{ ...this.sizeStyle, ...this.dataStyle }}
|
||||
>
|
||||
{this.elements.map((v) => v.render())}
|
||||
</div>
|
||||
);
|
||||
|
@ -6,7 +6,8 @@ import { first } from 'rxjs/operators';
|
||||
import Selecto from 'selecto';
|
||||
|
||||
import { GrafanaTheme2, PanelData } from '@grafana/data';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { locationService } from '@grafana/runtime/src';
|
||||
import { Portal, stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { CanvasFrameOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
|
||||
import {
|
||||
@ -24,6 +25,7 @@ import {
|
||||
getScaleDimensionFromData,
|
||||
getTextDimensionFromData,
|
||||
} from 'app/features/dimensions/utils';
|
||||
import { CanvasContextMenu } from 'app/plugins/panel/canvas/CanvasContextMenu';
|
||||
import { LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
|
||||
import { Placement } from '../types';
|
||||
@ -43,6 +45,7 @@ export class Scene {
|
||||
readonly selection = new ReplaySubject<ElementState[]>(1);
|
||||
readonly moved = new Subject<number>(); // called after resize/drag for editor updates
|
||||
readonly byName = new Map<string, ElementState>();
|
||||
|
||||
root: RootElement;
|
||||
|
||||
revId = 0;
|
||||
@ -58,6 +61,8 @@ export class Scene {
|
||||
isEditingEnabled?: boolean;
|
||||
skipNextSelectionBroadcast = false;
|
||||
|
||||
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
|
||||
|
||||
constructor(cfg: CanvasFrameOptions, enableEditing: boolean, public onSave: (cfg: CanvasFrameOptions) => void) {
|
||||
this.root = this.load(cfg, enableEditing);
|
||||
}
|
||||
@ -314,6 +319,7 @@ export class Scene {
|
||||
constraintViewable: allowChanges,
|
||||
},
|
||||
origin: false,
|
||||
className: this.styles.selected,
|
||||
})
|
||||
.on('clickGroup', (event) => {
|
||||
this.selecto!.clickTarget(event.inputEvent, event.inputTarget);
|
||||
@ -389,9 +395,16 @@ export class Scene {
|
||||
};
|
||||
|
||||
render() {
|
||||
const canShowContextMenu = this.isPanelEditing || (!this.isPanelEditing && this.isEditingEnabled);
|
||||
|
||||
return (
|
||||
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.setRef}>
|
||||
{this.root.render()}
|
||||
{canShowContextMenu && (
|
||||
<Portal>
|
||||
<CanvasContextMenu scene={this} />
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -402,4 +415,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
`,
|
||||
selected: css`
|
||||
z-index: 999 !important;
|
||||
`,
|
||||
}));
|
||||
|
143
public/app/plugins/panel/canvas/CanvasContextMenu.tsx
Normal file
143
public/app/plugins/panel/canvas/CanvasContextMenu.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { ContextMenu, MenuItem } from '@grafana/ui';
|
||||
|
||||
import { Scene } from '../../../features/canvas/runtime/scene';
|
||||
|
||||
import { LayerActionID } from './types';
|
||||
|
||||
type Props = {
|
||||
scene: Scene;
|
||||
};
|
||||
|
||||
type AnchorPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
|
||||
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
|
||||
|
||||
const styles = getStyles();
|
||||
|
||||
const selectedElements = scene.selecto?.getSelectedTargets();
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if (event.currentTarget) {
|
||||
scene.select({ targets: [event.currentTarget as HTMLElement | SVGElement] });
|
||||
}
|
||||
setAnchorPoint({ x: event.pageX, y: event.pageY });
|
||||
setIsMenuVisible(true);
|
||||
},
|
||||
[scene]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedElements && selectedElements.length === 1) {
|
||||
const element = selectedElements[0];
|
||||
element.addEventListener('contextmenu', handleContextMenu);
|
||||
}
|
||||
}, [selectedElements, handleContextMenu]);
|
||||
|
||||
if (!selectedElements) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const closeContextMenu = () => {
|
||||
setIsMenuVisible(false);
|
||||
};
|
||||
|
||||
const renderMenuItems = () => {
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
label="Delete"
|
||||
onClick={() => {
|
||||
contextMenuAction(LayerActionID.Delete);
|
||||
closeContextMenu();
|
||||
}}
|
||||
className={styles.menuItem}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Duplicate"
|
||||
onClick={() => {
|
||||
contextMenuAction(LayerActionID.Duplicate);
|
||||
closeContextMenu();
|
||||
}}
|
||||
className={styles.menuItem}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Bring to front"
|
||||
onClick={() => {
|
||||
contextMenuAction(LayerActionID.MoveTop);
|
||||
closeContextMenu();
|
||||
}}
|
||||
className={styles.menuItem}
|
||||
/>
|
||||
<MenuItem
|
||||
label="Send to back"
|
||||
onClick={() => {
|
||||
contextMenuAction(LayerActionID.MoveBottom);
|
||||
closeContextMenu();
|
||||
}}
|
||||
className={styles.menuItem}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const contextMenuAction = (actionType: string) => {
|
||||
scene.selection.pipe(first()).subscribe((currentSelectedElements) => {
|
||||
const currentSelectedElement = currentSelectedElements[0];
|
||||
const currentLayer = currentSelectedElement.parent!;
|
||||
|
||||
switch (actionType) {
|
||||
case LayerActionID.Delete:
|
||||
currentLayer.doAction(LayerActionID.Delete, currentSelectedElement);
|
||||
break;
|
||||
case LayerActionID.Duplicate:
|
||||
currentLayer.doAction(LayerActionID.Duplicate, currentSelectedElement);
|
||||
break;
|
||||
case LayerActionID.MoveTop:
|
||||
currentLayer.doAction(LayerActionID.MoveTop, currentSelectedElement);
|
||||
break;
|
||||
case LayerActionID.MoveBottom:
|
||||
currentLayer.doAction(LayerActionID.MoveBottom, currentSelectedElement);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (isMenuVisible) {
|
||||
return (
|
||||
<div
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
closeContextMenu();
|
||||
}}
|
||||
>
|
||||
<ContextMenu
|
||||
x={anchorPoint.x}
|
||||
y={anchorPoint.y}
|
||||
onClose={closeContextMenu}
|
||||
renderMenuItems={renderMenuItems}
|
||||
focusOnOpen={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
menuItem: css`
|
||||
max-width: 60ch;
|
||||
overflow: hidden;
|
||||
`,
|
||||
});
|
Loading…
Reference in New Issue
Block a user