Skip to content

Commit

Permalink
grafana: Canvas: Add support for basic arrows (#57561)
Browse files Browse the repository at this point in the history
Commit: b1a24232e44f860d3effff0ac7216ad9c3a51d3a
  • Loading branch information
Nathan Marrs authored and sourcegraph-bot committed Jan 13, 2023
1 parent 922ceb4 commit f7cfa16
Show file tree
Hide file tree
Showing 7 changed files with 625 additions and 1 deletion.
20 changes: 20 additions & 0 deletions grafana/public/app/features/canvas/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ export interface CanvasElementOptions<TConfig = any> {
placement?: Placement;
background?: BackgroundConfig;
border?: LineConfig;
connections?: CanvasConnection[];
}

// Unit is percentage from the middle of the element
// 0, 0 middle; -1, -1 bottom left; 1, 1 top right
export interface ConnectionCoordinates {
x: number;
y: number;
}

export enum ConnectionPath {
Straight = 'straight',
}

export interface CanvasConnection {
source: ConnectionCoordinates;
target: ConnectionCoordinates;
targetName?: string;
path: ConnectionPath;
// See https://github.com/anseki/leader-line#options for more examples of more properties
}

export interface CanvasElementProps<TConfig = any, TData = any> {
Expand Down
6 changes: 5 additions & 1 deletion grafana/public/app/features/canvas/runtime/element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,11 @@ export class ElementState implements LayerElement {
const isSelected = div && scene && scene.selecto && scene.selecto.getSelectedTargets().includes(div);

return (
<div key={this.UID} ref={this.initElement}>
<div
key={this.UID}
ref={this.initElement}
onMouseEnter={!isSelected ? scene?.connections.handleMouseEnter : undefined}
>
<item.display
key={`${this.UID}/${this.revId}`}
config={this.options.config}
Expand Down
7 changes: 7 additions & 0 deletions grafana/public/app/features/canvas/runtime/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,20 @@ export class FrameState extends ElementState {
opts.placement = placement;
}

// Clear connections on duplicate
opts.connections = undefined;

const copy = new ElementState(element.item, opts, this);
copy.updateData(this.scene.context);
if (updateName) {
copy.options.name = this.scene.getNextElementName();
}
this.elements.push(copy);
this.scene.byName.set(copy.options.name, copy);

// Update scene byName map for original element (to avoid stale references (e.g. for connections))
this.scene.byName.set(element.options.name, element);

this.scene.save();
this.reinitializeMoveable();

Expand Down
22 changes: 22 additions & 0 deletions grafana/public/app/features/canvas/runtime/scene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
getTextDimensionFromData,
} from 'app/features/dimensions/utils';
import { CanvasContextMenu } from 'app/plugins/panel/canvas/CanvasContextMenu';
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/ConnectionAnchors';
import { Connections } from 'app/plugins/panel/canvas/Connections';
import { AnchorPoint, LayerActionID } from 'app/plugins/panel/canvas/types';

import appEvents from '../../../core/app_events';
Expand Down Expand Up @@ -59,6 +61,7 @@ export class Scene {
selecto?: Selecto;
moveable?: Moveable;
div?: HTMLDivElement;
connections: Connections;
currentLayer?: FrameState;
isEditingEnabled?: boolean;
shouldShowAdvancedTypes?: boolean;
Expand Down Expand Up @@ -93,6 +96,7 @@ export class Scene {
});

this.panel = panel;
this.connections = new Connections(this);
}

getNextElementName = (isFrame = false) => {
Expand Down Expand Up @@ -304,6 +308,11 @@ export class Scene {
this.selecto.setSelectedTargets(selection.targets);
this.updateSelection(selection);
this.editModeEnabled.next(false);

// Hide connection anchors on programmatic select
if (this.connections.connectionAnchorDiv) {
this.connections.connectionAnchorDiv.style.display = 'none';
}
}
};

Expand Down Expand Up @@ -463,6 +472,13 @@ export class Scene {
this.selecto!.on('dragStart', (event) => {
const selectedTarget = event.inputEvent.target;

// If selected target is a connection control, eject to handle connection event
if (selectedTarget.id === CONNECTION_ANCHOR_DIV_ID) {
this.connections.handleConnectionDragStart(selectedTarget, event.inputEvent.clientX, event.inputEvent.clientY);
event.stop();
return;
}

const isTargetMoveableElement =
this.moveable!.isMoveableElement(selectedTarget) ||
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
Expand All @@ -488,6 +504,11 @@ export class Scene {
})
.on('select', () => {
this.editModeEnabled.next(false);

// Hide connection anchors on select
if (this.connections.connectionAnchorDiv) {
this.connections.connectionAnchorDiv.style.display = 'none';
}
})
.on('selectEnd', (event) => {
targets = event.selected;
Expand Down Expand Up @@ -582,6 +603,7 @@ export class Scene {

return (
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.setRef}>
{this.connections.render()}
{this.root.render()}
{canShowContextMenu && (
<Portal>
Expand Down
134 changes: 134 additions & 0 deletions grafana/public/app/plugins/panel/canvas/ConnectionAnchors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { css } from '@emotion/css';
import React, { useRef } from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { ConnectionCoordinates } from 'app/features/canvas';

type Props = {
setRef: (anchorElement: HTMLDivElement) => void;
handleMouseLeave: (event: React.MouseEvent<Element, MouseEvent> | React.FocusEvent<HTMLDivElement, Element>) => void;
};

export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl';

export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => {
const highlightEllipseRef = useRef<HTMLDivElement>(null);
const styles = useStyles2(getStyles);
const halfSize = 2.5;
const halfSizeHighlightEllipse = 5.5;
const anchorImage =
'data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI1cHgiIGhlaWdodD0iNXB4IiB2ZXJzaW9uPSIxLjEiPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2Utb3BhY2l0eTowLjQiIHN0cm9rZT0iI2ZmZmZmZiIvPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2U9IiMyOWI2ZjIiLz48L3N2Zz4=';

const onMouseEnterAnchor = (event: React.MouseEvent) => {
if (!(event.target instanceof HTMLImageElement)) {
return;
}

if (highlightEllipseRef.current && event.target.style) {
highlightEllipseRef.current.style.display = 'block';
highlightEllipseRef.current.style.top = `calc(${event.target.style.top} - ${halfSizeHighlightEllipse}px)`;
highlightEllipseRef.current.style.left = `calc(${event.target.style.left} - ${halfSizeHighlightEllipse}px)`;
}
};

const onMouseLeaveHighlightElement = () => {
if (highlightEllipseRef.current) {
highlightEllipseRef.current.style.display = 'none';
}
};

const connectionAnchorAlt = 'connection anchor';

// Unit is percentage from the middle of the element
// 0, 0 middle; -1, -1 bottom left; 1, 1 top right
const ANCHORS = [
{ x: -1, y: 1 },
{ x: -0.5, y: 1 },
{ x: 0, y: 1 },
{ x: 0.5, y: 1 },
{ x: 1, y: 1 },
{ x: 1, y: 0.5 },
{ x: 1, y: 0 },
{ x: 1, y: -0.5 },
{ x: 1, y: -1 },
{ x: 0.5, y: -1 },
{ x: 0, y: -1 },
{ x: -0.5, y: -1 },
{ x: -1, y: -1 },
{ x: -1, y: -0.5 },
{ x: -1, y: 0 },
{ x: -1, y: 0.5 },
];

const generateAnchors = (anchors: ConnectionCoordinates[] = ANCHORS) => {
return anchors.map((anchor) => {
const id = `${anchor.x},${anchor.y}`;

// Convert anchor coords to relative percentage
const style = {
top: `calc(${-anchor.y * 50 + 50}% - ${halfSize}px)`,
left: `calc(${anchor.x * 50 + 50}% - ${halfSize}px)`,
};

return (
<img
id={id}
key={id}
alt={connectionAnchorAlt}
className={styles.anchor}
style={style}
src={anchorImage}
onMouseEnter={onMouseEnterAnchor}
/>
);
});
};

return (
<div className={styles.root} ref={setRef}>
<div className={styles.mouseoutDiv} onMouseOut={handleMouseLeave} onBlur={handleMouseLeave} />
<div
id={CONNECTION_ANCHOR_DIV_ID}
ref={highlightEllipseRef}
className={styles.highlightElement}
onMouseLeave={onMouseLeaveHighlightElement}
/>
{generateAnchors()}
</div>
);
};

const getStyles = (theme: GrafanaTheme2) => ({
root: css`
position: absolute;
display: none;
`,
mouseoutDiv: css`
position: absolute;
margin: -30px;
width: calc(100% + 60px);
height: calc(100% + 60px);
`,
anchor: css`
position: absolute;
cursor: cursor;
width: 5px;
height: 5px;
z-index: 100;
pointer-events: auto !important;
`,
highlightElement: css`
background-color: #00ff00;
opacity: 0.3;
position: absolute;
cursor: cursor;
position: absolute;
pointer-events: auto;
width: 16px;
height: 16px;
border-radius: 50%;
display: none;
z-index: 110;
`,
});
Loading

0 comments on commit f7cfa16

Please sign in to comment.