From 5dea949433349a345985354f18846cfba4b6e2bf Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 23 Apr 2024 15:26:53 -0600 Subject: [PATCH] Canvas: Match connection anchor points to elements (#85421) Co-authored-by: Ihor Yeromin --- public/app/features/canvas/element.ts | 6 +++ public/app/features/canvas/elements/cloud.tsx | 18 +++++++ .../app/features/canvas/elements/ellipse.tsx | 18 +++++++ .../canvas/elements/parallelogram.tsx | 19 +++++++ .../app/features/canvas/elements/triangle.tsx | 23 ++++++++ .../connections/ConnectionAnchors.tsx | 54 +++++++++---------- .../components/connections/Connections.tsx | 39 +++++++++++++- 7 files changed, 148 insertions(+), 29 deletions(-) diff --git a/public/app/features/canvas/element.ts b/public/app/features/canvas/element.ts index 371cc46ec43..61d7061eaaf 100644 --- a/public/app/features/canvas/element.ts +++ b/public/app/features/canvas/element.ts @@ -101,6 +101,12 @@ export interface CanvasElementItem extends RegistryI /** Optional config to customize what standard element editor options are available for the item */ standardEditorConfig?: StandardEditorConfig; + + /** Custom connection anchor coordinates, like for svg elements such as triangle, cloud, etc */ + customConnectionAnchors?: Array<{ + x: number; + y: number; + }>; } export const defaultBgColor = '#D9D9D9'; diff --git a/public/app/features/canvas/elements/cloud.tsx b/public/app/features/canvas/elements/cloud.tsx index 4f1bd04648c..1d48f43a35c 100644 --- a/public/app/features/canvas/elements/cloud.tsx +++ b/public/app/features/canvas/elements/cloud.tsx @@ -184,6 +184,24 @@ export const cloudItem: CanvasElementItem = { }, }); }, + + customConnectionAnchors: [ + { x: -0.58, y: 0.63 }, // Top Left + { x: -0.22, y: 0.99 }, // Top Middle + { x: 0.235, y: 0.75 }, // Top Right + + { x: 0.8, y: 0.6 }, // Right Top + { x: 0.785, y: 0.06 }, // Right Middle + { x: 0.91, y: -0.51 }, // Right Bottom + + { x: 0.62, y: -0.635 }, // Bottom Right + { x: 0.05, y: -0.98 }, // Bottom Middle + { x: -0.45, y: -0.635 }, // Bottom Left + + { x: -0.8, y: -0.58 }, // Left Bottom + { x: -0.78, y: -0.06 }, // Left Middle + { x: -0.9, y: 0.48 }, // Left Top + ], }; const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index 8cab935e0ec..b15b5908610 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -190,6 +190,24 @@ export const ellipseItem: CanvasElementItem { diff --git a/public/app/features/canvas/elements/parallelogram.tsx b/public/app/features/canvas/elements/parallelogram.tsx index 4caec1e776a..61c4b8531fa 100644 --- a/public/app/features/canvas/elements/parallelogram.tsx +++ b/public/app/features/canvas/elements/parallelogram.tsx @@ -184,6 +184,25 @@ export const parallelogramItem: CanvasElementItem = { }, }); }, + + customConnectionAnchors: [ + { x: -0.6, y: 1 }, // Angled Top Left + { x: -0.1, y: 1 }, // Top Middle + { x: 0.5, y: 1 }, // Angled Top Right + { x: 1, y: 1 }, // Top Right + { x: 0.925, y: 0.6 }, // Angled Right Top + { x: 0.84, y: 0.2 }, // Right Middle + { x: 0.76, y: -0.2 }, // Angled Right Bottom + { x: 0.675, y: -0.6 }, // Bottom Right + { x: -0.5, y: -1 }, // Angled Bottom Right + { x: 0.1, y: -1 }, // Bottom Middle + { x: 0.6, y: -1 }, // Angled Bottom Left + { x: -1, y: -1 }, // Bottom Left + { x: -0.925, y: -0.6 }, // Angled Left Bottom + { x: -0.84, y: -0.2 }, // Left Middle + { x: -0.76, y: 0.2 }, // Angled Left Top + { x: -0.675, y: 0.6 }, // Top Left 2 + ], }; const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { diff --git a/public/app/features/canvas/elements/triangle.tsx b/public/app/features/canvas/elements/triangle.tsx index 6af74d68775..9a46e7f3dd5 100644 --- a/public/app/features/canvas/elements/triangle.tsx +++ b/public/app/features/canvas/elements/triangle.tsx @@ -185,6 +185,29 @@ export const triangleItem: CanvasElementItem = { }, }); }, + + customConnectionAnchors: [ + // points along the left edge + { x: -1, y: -1 }, // bottom left + { x: -0.8, y: -0.6 }, + { x: -0.6, y: -0.2 }, + { x: -0.4, y: 0.2 }, + { x: -0.2, y: 0.6 }, + { x: 0, y: 1 }, // top + + // points along the right edge + { x: 0.2, y: 0.6 }, + { x: 0.4, y: 0.2 }, + { x: 0.6, y: -0.2 }, + { x: 0.8, y: -0.6 }, + { x: 1, y: -1 }, // bottom right + + // points along the bottom edge + { x: 0.6, y: -1 }, + { x: 0.2, y: -1 }, + { x: -0.2, y: -1 }, + { x: -0.6, y: -1 }, + ], }; const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { diff --git a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx index cbd13b41e24..ed9903b30c0 100644 --- a/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx +++ b/public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx @@ -7,6 +7,7 @@ import { ConnectionCoordinates } from 'app/features/canvas'; type Props = { setRef: (anchorElement: HTMLDivElement) => void; + setAnchorsRef: (anchorsElement: HTMLDivElement) => void; handleMouseLeave: ( event: React.MouseEvent | React.FocusEvent ) => boolean; @@ -15,13 +16,33 @@ type Props = { export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl'; export const CONNECTION_ANCHOR_ALT = 'connection anchor'; export const CONNECTION_ANCHOR_HIGHLIGHT_OFFSET = 8; +// Unit is percentage from the middle of the element +// 0, 0 middle; -1, -1 bottom left; 1, 1 top right +export 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 ANCHOR_PADDING = 3; +export const ANCHOR_PADDING = 3; +export const HALF_SIZE = 2.5; -export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { +export const ConnectionAnchors = ({ setRef, setAnchorsRef, handleMouseLeave }: Props) => { const highlightEllipseRef = useRef(null); const styles = useStyles2(getStyles); - const halfSize = 2.5; const halfSizeHighlightEllipse = 5.5; const anchorImage = 'data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI1cHgiIGhlaWdodD0iNXB4IiB2ZXJzaW9uPSIxLjEiPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2Utb3BhY2l0eTowLjQiIHN0cm9rZT0iI2ZmZmZmZiIvPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2U9IiMyOWI2ZjIiLz48L3N2Zz4='; @@ -54,35 +75,14 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { } }; - // 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 - ${ANCHOR_PADDING}px)`, - left: `calc(${anchor.x * 50 + 50}% - ${halfSize}px - ${ANCHOR_PADDING}px)`, + top: `calc(${-anchor.y * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`, + left: `calc(${anchor.x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`, }; return ( @@ -114,7 +114,7 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => { className={styles.highlightElement} onMouseLeave={onMouseLeaveHighlightElement} /> - {generateAnchors()} +
{generateAnchors()}
); }; diff --git a/public/app/plugins/panel/canvas/components/connections/Connections.tsx b/public/app/plugins/panel/canvas/components/connections/Connections.tsx index b0e794ff8c1..2c066bcea63 100644 --- a/public/app/plugins/panel/canvas/components/connections/Connections.tsx +++ b/public/app/plugins/panel/canvas/components/connections/Connections.tsx @@ -16,7 +16,14 @@ import { isConnectionTarget, } from '../../utils'; -import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors'; +import { + CONNECTION_ANCHOR_ALT, + ConnectionAnchors, + CONNECTION_ANCHOR_HIGHLIGHT_OFFSET, + ANCHORS, + ANCHOR_PADDING, + HALF_SIZE, +} from './ConnectionAnchors'; import { ConnectionSVG } from './ConnectionSVG'; export const CONNECTION_VERTEX_ID = 'vertex'; @@ -27,6 +34,7 @@ const CONNECTION_VERTEX_SNAP_TOLERANCE = (5 / 180) * Math.PI; // Multi-segment s export class Connections { scene: Scene; connectionAnchorDiv?: HTMLDivElement; + anchorsDiv?: HTMLDivElement; connectionSVG?: SVGElement; connectionLine?: SVGLineElement; connectionSVGVertex?: SVGElement; @@ -70,6 +78,10 @@ export class Connections { this.connectionAnchorDiv = anchorElement; }; + setAnchorsRef = (anchorsElement: HTMLDivElement) => { + this.anchorsDiv = anchorsElement; + }; + setConnectionSVGRef = (connectionSVG: SVGSVGElement) => { this.connectionSVG = connectionSVG; }; @@ -130,6 +142,25 @@ export class Connections { } } + const customElementAnchors = element?.item.customConnectionAnchors || ANCHORS; + // This type cast is necessary as TS doesn't understand that `Element` is an `HTMLElement` + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const anchors = Array.from(this.anchorsDiv?.children as HTMLCollectionOf); + const anchorsAmount = customElementAnchors.length; + + // re-calculate the position of the existing anchors on hover + // and hide the rest of the anchors if there are more than the custom ones + anchors.forEach((anchor, index) => { + if (index >= anchorsAmount) { + anchor.style.display = 'none'; + } else { + const { x, y } = customElementAnchors[index]; + anchor.style.top = `calc(${-y * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`; + anchor.style.left = `calc(${x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`; + anchor.style.display = 'block'; + } + }); + const elementBoundingRect = element.div!.getBoundingClientRect(); const transformScale = this.scene.scale; const parentBoundingRect = getParentBoundingClientRect(this.scene); @@ -648,7 +679,11 @@ export class Connections { render() { return ( <> - +