mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Match connection anchor points to elements (#85421)
Co-authored-by: Ihor Yeromin <yeryomin.igor@gmail.com>
This commit is contained in:
parent
fc5007b0d5
commit
5dea949433
@ -101,6 +101,12 @@ export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryI
|
|||||||
|
|
||||||
/** Optional config to customize what standard element editor options are available for the item */
|
/** Optional config to customize what standard element editor options are available for the item */
|
||||||
standardEditorConfig?: StandardEditorConfig;
|
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';
|
export const defaultBgColor = '#D9D9D9';
|
||||||
|
@ -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) => {
|
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {
|
||||||
|
@ -190,6 +190,24 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
customConnectionAnchors: [
|
||||||
|
// points along the left edge
|
||||||
|
{ x: -1, y: 0 }, // middle left
|
||||||
|
{ x: -0.7, y: 0.7 },
|
||||||
|
|
||||||
|
// points along the top edge
|
||||||
|
{ x: 0, y: 1 }, // top
|
||||||
|
{ x: 0.7, y: 0.7 },
|
||||||
|
|
||||||
|
// points along the right edge
|
||||||
|
{ x: 1, y: 0 }, // middle right
|
||||||
|
{ x: 0.7, y: -0.7 },
|
||||||
|
|
||||||
|
// points along the bottom edge
|
||||||
|
{ x: 0, y: -1 }, // bottom
|
||||||
|
{ x: -0.7, y: -0.7 },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {
|
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {
|
||||||
|
@ -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) => {
|
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {
|
||||||
|
@ -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) => {
|
const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => {
|
||||||
|
@ -7,6 +7,7 @@ import { ConnectionCoordinates } from 'app/features/canvas';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setRef: (anchorElement: HTMLDivElement) => void;
|
setRef: (anchorElement: HTMLDivElement) => void;
|
||||||
|
setAnchorsRef: (anchorsElement: HTMLDivElement) => void;
|
||||||
handleMouseLeave: (
|
handleMouseLeave: (
|
||||||
event: React.MouseEvent<Element, MouseEvent> | React.FocusEvent<HTMLDivElement, Element>
|
event: React.MouseEvent<Element, MouseEvent> | React.FocusEvent<HTMLDivElement, Element>
|
||||||
) => boolean;
|
) => boolean;
|
||||||
@ -15,13 +16,33 @@ type Props = {
|
|||||||
export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl';
|
export const CONNECTION_ANCHOR_DIV_ID = 'connectionControl';
|
||||||
export const CONNECTION_ANCHOR_ALT = 'connection anchor';
|
export const CONNECTION_ANCHOR_ALT = 'connection anchor';
|
||||||
export const CONNECTION_ANCHOR_HIGHLIGHT_OFFSET = 8;
|
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<HTMLDivElement>(null);
|
const highlightEllipseRef = useRef<HTMLDivElement>(null);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const halfSize = 2.5;
|
|
||||||
const halfSizeHighlightEllipse = 5.5;
|
const halfSizeHighlightEllipse = 5.5;
|
||||||
const anchorImage =
|
const anchorImage =
|
||||||
'data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI1cHgiIGhlaWdodD0iNXB4IiB2ZXJzaW9uPSIxLjEiPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2Utd2lkdGg9IjIiIHN0eWxlPSJzdHJva2Utb3BhY2l0eTowLjQiIHN0cm9rZT0iI2ZmZmZmZiIvPjxwYXRoIGQ9Im0gMCAwIEwgNSA1IE0gMCA1IEwgNSAwIiBzdHJva2U9IiMyOWI2ZjIiLz48L3N2Zz4=';
|
'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) => {
|
const generateAnchors = (anchors: ConnectionCoordinates[] = ANCHORS) => {
|
||||||
return anchors.map((anchor) => {
|
return anchors.map((anchor) => {
|
||||||
const id = `${anchor.x},${anchor.y}`;
|
const id = `${anchor.x},${anchor.y}`;
|
||||||
|
|
||||||
// Convert anchor coords to relative percentage
|
// Convert anchor coords to relative percentage
|
||||||
const style = {
|
const style = {
|
||||||
top: `calc(${-anchor.y * 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}% - ${halfSize}px - ${ANCHOR_PADDING}px)`,
|
left: `calc(${anchor.x * 50 + 50}% - ${HALF_SIZE}px - ${ANCHOR_PADDING}px)`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -114,7 +114,7 @@ export const ConnectionAnchors = ({ setRef, handleMouseLeave }: Props) => {
|
|||||||
className={styles.highlightElement}
|
className={styles.highlightElement}
|
||||||
onMouseLeave={onMouseLeaveHighlightElement}
|
onMouseLeave={onMouseLeaveHighlightElement}
|
||||||
/>
|
/>
|
||||||
{generateAnchors()}
|
<div ref={setAnchorsRef}>{generateAnchors()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,14 @@ import {
|
|||||||
isConnectionTarget,
|
isConnectionTarget,
|
||||||
} from '../../utils';
|
} 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';
|
import { ConnectionSVG } from './ConnectionSVG';
|
||||||
|
|
||||||
export const CONNECTION_VERTEX_ID = 'vertex';
|
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 {
|
export class Connections {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
connectionAnchorDiv?: HTMLDivElement;
|
connectionAnchorDiv?: HTMLDivElement;
|
||||||
|
anchorsDiv?: HTMLDivElement;
|
||||||
connectionSVG?: SVGElement;
|
connectionSVG?: SVGElement;
|
||||||
connectionLine?: SVGLineElement;
|
connectionLine?: SVGLineElement;
|
||||||
connectionSVGVertex?: SVGElement;
|
connectionSVGVertex?: SVGElement;
|
||||||
@ -70,6 +78,10 @@ export class Connections {
|
|||||||
this.connectionAnchorDiv = anchorElement;
|
this.connectionAnchorDiv = anchorElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setAnchorsRef = (anchorsElement: HTMLDivElement) => {
|
||||||
|
this.anchorsDiv = anchorsElement;
|
||||||
|
};
|
||||||
|
|
||||||
setConnectionSVGRef = (connectionSVG: SVGSVGElement) => {
|
setConnectionSVGRef = (connectionSVG: SVGSVGElement) => {
|
||||||
this.connectionSVG = connectionSVG;
|
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<HTMLElement>);
|
||||||
|
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 elementBoundingRect = element.div!.getBoundingClientRect();
|
||||||
const transformScale = this.scene.scale;
|
const transformScale = this.scene.scale;
|
||||||
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
||||||
@ -648,7 +679,11 @@ export class Connections {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConnectionAnchors setRef={this.setConnectionAnchorRef} handleMouseLeave={this.handleMouseLeave} />
|
<ConnectionAnchors
|
||||||
|
setRef={this.setConnectionAnchorRef}
|
||||||
|
setAnchorsRef={this.setAnchorsRef}
|
||||||
|
handleMouseLeave={this.handleMouseLeave}
|
||||||
|
/>
|
||||||
<ConnectionSVG
|
<ConnectionSVG
|
||||||
setSVGRef={this.setConnectionSVGRef}
|
setSVGRef={this.setConnectionSVGRef}
|
||||||
setLineRef={this.setConnectionLineRef}
|
setLineRef={this.setConnectionLineRef}
|
||||||
|
Loading…
Reference in New Issue
Block a user