Canvas: Context menu (#48909)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
pull/49357/head
Adela Almasan 3 years ago committed by GitHub
parent dea6fb4c1b
commit b3b650be1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      public/app/features/canvas/runtime/frame.tsx
  2. 9
      public/app/features/canvas/runtime/root.tsx
  3. 18
      public/app/features/canvas/runtime/scene.tsx
  4. 143
      public/app/plugins/panel/canvas/CanvasContextMenu.tsx

@ -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;
`,
}));

@ -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…
Cancel
Save