2023-01-13 10:38:00 +08:00
|
|
|
import { css } from '@emotion/css';
|
2023-04-26 01:31:45 +08:00
|
|
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
|
|
import { useStyles2 } from '@grafana/ui';
|
2023-02-04 00:50:36 +08:00
|
|
|
import { config } from 'app/core/config';
|
2023-01-13 10:38:00 +08:00
|
|
|
import { Scene } from 'app/features/canvas/runtime/scene';
|
|
|
|
|
2023-05-25 00:32:36 +08:00
|
|
|
import { ConnectionState } from '../../types';
|
2024-01-03 03:52:21 +08:00
|
|
|
import { calculateCoordinates, getConnectionStyles, getParentBoundingClientRect } from '../../utils';
|
2023-01-31 05:50:10 +08:00
|
|
|
|
2023-01-13 10:38:00 +08:00
|
|
|
type Props = {
|
|
|
|
setSVGRef: (anchorElement: SVGSVGElement) => void;
|
|
|
|
setLineRef: (anchorElement: SVGLineElement) => void;
|
|
|
|
scene: Scene;
|
|
|
|
};
|
|
|
|
|
2023-01-24 00:51:55 +08:00
|
|
|
let idCounter = 0;
|
2023-04-26 01:31:45 +08:00
|
|
|
const htmlElementTypes = ['input', 'textarea'];
|
|
|
|
|
2023-01-13 10:38:00 +08:00
|
|
|
export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
|
|
|
const styles = useStyles2(getStyles);
|
|
|
|
|
2023-01-24 00:51:55 +08:00
|
|
|
const headId = Date.now() + '_' + idCounter++;
|
2023-04-26 01:31:45 +08:00
|
|
|
const CONNECTION_LINE_ID = useMemo(() => `connectionLineId-${headId}`, [headId]);
|
2023-01-24 00:51:55 +08:00
|
|
|
const EDITOR_HEAD_ID = useMemo(() => `editorHead-${headId}`, [headId]);
|
2023-02-04 00:50:36 +08:00
|
|
|
const defaultArrowColor = config.theme2.colors.text.primary;
|
2023-04-26 01:31:45 +08:00
|
|
|
const defaultArrowSize = 2;
|
2023-01-13 10:38:00 +08:00
|
|
|
|
2023-04-26 01:31:45 +08:00
|
|
|
const [selectedConnection, setSelectedConnection] = useState<ConnectionState | undefined>(undefined);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
// Need to use ref to ensure state is not stale in event handler
|
|
|
|
const selectedConnectionRef = useRef(selectedConnection);
|
|
|
|
useEffect(() => {
|
|
|
|
selectedConnectionRef.current = selectedConnection;
|
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-04-26 01:31:45 +08:00
|
|
|
if (scene.panel.context.instanceState?.selectedConnection) {
|
|
|
|
setSelectedConnection(scene.panel.context.instanceState?.selectedConnection);
|
|
|
|
}
|
|
|
|
}, [scene.panel.context.instanceState?.selectedConnection]);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
const onKeyUp = (e: KeyboardEvent) => {
|
2023-04-26 01:31:45 +08:00
|
|
|
const target = e.target;
|
|
|
|
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (htmlElementTypes.indexOf(target.nodeName.toLowerCase()) > -1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-01-13 10:38:00 +08:00
|
|
|
// Backspace (8) or delete (46)
|
|
|
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
2023-04-26 01:31:45 +08:00
|
|
|
if (selectedConnectionRef.current && selectedConnectionRef.current.source) {
|
|
|
|
selectedConnectionRef.current.source.options.connections =
|
|
|
|
selectedConnectionRef.current.source.options.connections?.filter(
|
|
|
|
(connection) => connection !== selectedConnectionRef.current?.info
|
2023-01-13 10:38:00 +08:00
|
|
|
);
|
2023-04-26 01:31:45 +08:00
|
|
|
selectedConnectionRef.current.source.onChange(selectedConnectionRef.current.source.options);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
setSelectedConnection(undefined);
|
2023-04-26 01:31:45 +08:00
|
|
|
scene.connections.select(undefined);
|
|
|
|
scene.connections.updateState();
|
|
|
|
scene.save();
|
2023-01-13 10:38:00 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Prevent removing event listener if key is not delete
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
document.removeEventListener('keyup', onKeyUp);
|
|
|
|
scene.selecto!.rootContainer!.removeEventListener('click', clearSelectedConnection);
|
|
|
|
};
|
|
|
|
|
|
|
|
const clearSelectedConnection = (event: MouseEvent) => {
|
|
|
|
const eventTarget = event.target;
|
|
|
|
|
|
|
|
const shouldResetSelectedConnection = !(
|
|
|
|
eventTarget instanceof SVGLineElement && eventTarget.id === CONNECTION_LINE_ID
|
|
|
|
);
|
|
|
|
|
|
|
|
if (shouldResetSelectedConnection) {
|
|
|
|
setSelectedConnection(undefined);
|
2023-04-26 01:31:45 +08:00
|
|
|
scene.connections.select(undefined);
|
2023-01-13 10:38:00 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-04-26 01:31:45 +08:00
|
|
|
const selectConnection = (connection: ConnectionState) => {
|
2023-01-13 10:38:00 +08:00
|
|
|
if (scene.isEditingEnabled) {
|
|
|
|
setSelectedConnection(connection);
|
2023-04-26 01:31:45 +08:00
|
|
|
scene.connections.select(connection);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
document.addEventListener('keyup', onKeyUp);
|
|
|
|
scene.selecto!.rootContainer!.addEventListener('click', clearSelectedConnection);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Figure out target and then target's relative coordinates drawing (if no target do parent)
|
|
|
|
const renderConnections = () => {
|
2023-04-26 01:31:45 +08:00
|
|
|
return scene.connections.state.map((v, idx) => {
|
2023-01-13 10:38:00 +08:00
|
|
|
const { source, target, info } = v;
|
|
|
|
const sourceRect = source.div?.getBoundingClientRect();
|
|
|
|
const parent = source.div?.parentElement;
|
2024-01-03 03:52:21 +08:00
|
|
|
const transformScale = scene.scale;
|
|
|
|
const parentRect = getParentBoundingClientRect(scene);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
if (!sourceRect || !parent || !parentRect) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-01-03 03:52:21 +08:00
|
|
|
const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
2023-11-03 01:09:07 +08:00
|
|
|
const { strokeColor, strokeWidth } = getConnectionStyles(info, scene, defaultArrowSize);
|
2023-01-13 10:38:00 +08:00
|
|
|
|
2023-04-26 01:31:45 +08:00
|
|
|
const isSelected = selectedConnection === v && scene.panel.context.instanceState.selectedConnection;
|
|
|
|
|
2023-01-13 10:38:00 +08:00
|
|
|
const connectionCursorStyle = scene.isEditingEnabled ? 'grab' : '';
|
2023-04-26 01:31:45 +08:00
|
|
|
const selectedStyles = { stroke: '#44aaff', strokeOpacity: 0.6, strokeWidth: strokeWidth + 5 };
|
|
|
|
|
|
|
|
const CONNECTION_HEAD_ID = `connectionHead-${headId + Math.random()}`;
|
2023-01-13 10:38:00 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<svg className={styles.connection} key={idx}>
|
2023-04-26 01:31:45 +08:00
|
|
|
<g onClick={() => selectConnection(v)}>
|
2023-01-13 10:38:00 +08:00
|
|
|
<defs>
|
|
|
|
<marker
|
|
|
|
id={CONNECTION_HEAD_ID}
|
|
|
|
markerWidth="10"
|
|
|
|
markerHeight="7"
|
|
|
|
refX="10"
|
|
|
|
refY="3.5"
|
|
|
|
orient="auto"
|
2023-04-26 01:31:45 +08:00
|
|
|
stroke={strokeColor}
|
2023-01-13 10:38:00 +08:00
|
|
|
>
|
2023-04-26 01:31:45 +08:00
|
|
|
<polygon points="0 0, 10 3.5, 0 7" fill={strokeColor} />
|
2023-01-13 10:38:00 +08:00
|
|
|
</marker>
|
|
|
|
</defs>
|
|
|
|
<line
|
|
|
|
id={`${CONNECTION_LINE_ID}_transparent`}
|
|
|
|
cursor={connectionCursorStyle}
|
|
|
|
pointerEvents="auto"
|
2023-04-26 01:31:45 +08:00
|
|
|
stroke="transparent"
|
2023-01-13 10:38:00 +08:00
|
|
|
strokeWidth={15}
|
2023-04-26 01:31:45 +08:00
|
|
|
style={isSelected ? selectedStyles : {}}
|
2023-01-13 10:38:00 +08:00
|
|
|
x1={x1}
|
|
|
|
y1={y1}
|
|
|
|
x2={x2}
|
|
|
|
y2={y2}
|
|
|
|
/>
|
|
|
|
<line
|
|
|
|
id={CONNECTION_LINE_ID}
|
2023-04-26 01:31:45 +08:00
|
|
|
stroke={strokeColor}
|
2023-01-13 10:38:00 +08:00
|
|
|
pointerEvents="auto"
|
2023-04-26 01:31:45 +08:00
|
|
|
strokeWidth={strokeWidth}
|
2023-01-13 10:38:00 +08:00
|
|
|
markerEnd={`url(#${CONNECTION_HEAD_ID})`}
|
|
|
|
x1={x1}
|
|
|
|
y1={y1}
|
|
|
|
x2={x2}
|
|
|
|
y2={y2}
|
|
|
|
cursor={connectionCursorStyle}
|
|
|
|
/>
|
|
|
|
</g>
|
|
|
|
</svg>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<svg ref={setSVGRef} className={styles.editorSVG}>
|
|
|
|
<defs>
|
|
|
|
<marker
|
2023-01-24 00:51:55 +08:00
|
|
|
id={EDITOR_HEAD_ID}
|
2023-01-13 10:38:00 +08:00
|
|
|
markerWidth="10"
|
|
|
|
markerHeight="7"
|
|
|
|
refX="10"
|
|
|
|
refY="3.5"
|
|
|
|
orient="auto"
|
2023-02-04 00:50:36 +08:00
|
|
|
stroke={defaultArrowColor}
|
2023-01-13 10:38:00 +08:00
|
|
|
>
|
2023-02-04 00:50:36 +08:00
|
|
|
<polygon points="0 0, 10 3.5, 0 7" fill={defaultArrowColor} />
|
2023-01-13 10:38:00 +08:00
|
|
|
</marker>
|
|
|
|
</defs>
|
2023-02-04 00:50:36 +08:00
|
|
|
<line ref={setLineRef} stroke={defaultArrowColor} strokeWidth={2} markerEnd={`url(#${EDITOR_HEAD_ID})`} />
|
2023-01-13 10:38:00 +08:00
|
|
|
</svg>
|
|
|
|
{renderConnections()}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
2023-11-02 00:25:26 +08:00
|
|
|
editorSVG: css({
|
|
|
|
position: 'absolute',
|
|
|
|
pointerEvents: 'none',
|
|
|
|
width: '100%',
|
|
|
|
height: '100%',
|
|
|
|
zIndex: 1000,
|
|
|
|
display: 'none',
|
|
|
|
}),
|
|
|
|
connection: css({
|
|
|
|
position: 'absolute',
|
|
|
|
width: '100%',
|
|
|
|
height: '100%',
|
|
|
|
zIndex: 1000,
|
|
|
|
pointerEvents: 'none',
|
|
|
|
}),
|
2023-01-13 10:38:00 +08:00
|
|
|
});
|