/usr/share/grafana/public/app/plugins/panel/nodeGraph
import { useEffect, useRef, RefObject, useState, useMemo } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; import usePrevious from 'react-use/lib/usePrevious'; import { Bounds } from './utils'; export interface State { isPanning: boolean; position: { x: number; y: number; }; } interface Options { scale?: number; bounds?: Bounds; focus?: { x: number; y: number; }; } /** * Based on https://github.com/streamich/react-use/blob/master/src/useSlider.ts * Returns position x/y coordinates which can be directly used in transform: translate(). * @param scale - Can be used when we want to scale the movement if we are moving a scaled element. We need to do it * here because we don't want to change the pos when scale changes. * @param bounds - If set the panning cannot go outside of those bounds. * @param focus - Position to focus on. */ export function usePanning<T extends Element>({ scale = 1, bounds, focus }: Options = {}): { state: State; ref: RefObject<T>; } { const isMounted = useMountedState(); const isPanning = useRef(false); const frame = useRef(0); const panRef = useRef<T>(null); const initial = { x: 0, y: 0 }; // As we return a diff of the view port to be applied we need as translate coordinates we have to invert the // bounds of the content to get the bounds of the view port diff. const viewBounds = useMemo( () => ({ right: bounds ? -bounds.left : Infinity, left: bounds ? -bounds.right : -Infinity, bottom: bounds ? -bounds.top : -Infinity, top: bounds ? -bounds.bottom : Infinity, }), [bounds] ); // We need to keep some state so we can compute the position diff and add that to the previous position. const startMousePosition = useRef(initial); const prevPosition = useRef(initial); // We cannot use the state as that would rerun the effect on each state change which we don't want so we have to keep // separate variable for the state that won't cause useEffect eval const currentPosition = useRef(initial); const [state, setState] = useState<State>({ isPanning: false, position: initial, }); useEffect(() => { const startPanning = (event: Event) => { if (!isPanning.current && isMounted()) { isPanning.current = true; // Snapshot the current position of both mouse pointer and the element startMousePosition.current = getEventXY(event); prevPosition.current = { ...currentPosition.current }; setState((state) => ({ ...state, isPanning: true })); bindEvents(); } }; const stopPanning = () => { if (isPanning.current && isMounted()) { isPanning.current = false; setState((state) => ({ ...state, isPanning: false })); unbindEvents(); } }; const onPanStart = (event: Event) => { startPanning(event); onPan(event); }; const bindEvents = () => { document.addEventListener('mousemove', onPan); document.addEventListener('mouseup', stopPanning); document.addEventListener('touchmove', onPan); document.addEventListener('touchend', stopPanning); }; const unbindEvents = () => { document.removeEventListener('mousemove', onPan); document.removeEventListener('mouseup', stopPanning); document.removeEventListener('touchmove', onPan); document.removeEventListener('touchend', stopPanning); }; const onPan = (event: Event) => { cancelAnimationFrame(frame.current); const pos = getEventXY(event); frame.current = requestAnimationFrame(() => { if (isMounted() && panRef.current) { // Get the diff by which we moved the mouse. let xDiff = pos.x - startMousePosition.current.x; let yDiff = pos.y - startMousePosition.current.y; // Add the diff to the position from the moment we started panning. currentPosition.current = { x: inBounds(prevPosition.current.x + xDiff / scale, viewBounds.left, viewBounds.right), y: inBounds(prevPosition.current.y + yDiff / scale, viewBounds.top, viewBounds.bottom), }; setState((state) => ({ ...state, position: { ...currentPosition.current, }, })); } }); }; const ref = panRef.current; if (ref) { ref.addEventListener('mousedown', onPanStart); ref.addEventListener('touchstart', onPanStart); } return () => { if (ref) { ref.removeEventListener('mousedown', onPanStart); ref.removeEventListener('touchstart', onPanStart); } }; }, [scale, viewBounds, isMounted]); const previousFocus = usePrevious(focus); // We need to update the state in case need to focus on something but we want to do it only once when the focus // changes to something new. useEffect(() => { if (focus && previousFocus?.x !== focus.x && previousFocus?.y !== focus.y) { const position = { x: inBounds(focus.x, viewBounds.left, viewBounds.right), y: inBounds(focus.y, viewBounds.top, viewBounds.bottom), }; setState({ position, isPanning: false, }); currentPosition.current = position; prevPosition.current = position; } }, [focus, previousFocus, viewBounds, currentPosition, prevPosition]); let position = state.position; // This part prevents an ugly jump from initial position to the focused one as the set state in the effects is after // initial render. if (focus && previousFocus?.x !== focus.x && previousFocus?.y !== focus.y) { position = focus; } return { state: { ...state, position: { x: inBounds(position.x, viewBounds.left, viewBounds.right), y: inBounds(position.y, viewBounds.top, viewBounds.bottom), }, }, ref: panRef, }; } function inBounds(value: number, min: number | undefined, max: number | undefined) { return Math.min(Math.max(value, min ?? -Infinity), max ?? Infinity); } // The issue here is that TouchEvent is undefined while using instanceof in Firefox and Safari // which will throw an exception but if it's event.changedTouches it will be undefined // and the if check will fail so it will go to the else but will not throw an exception function getEventXY(event: Event): { x: number; y: number } { if ('changedTouches' in event && event instanceof TouchEvent) { return { x: event.changedTouches[0].clientX, y: event.changedTouches[0].clientY }; } else if (event instanceof MouseEvent) { return { x: event.clientX, y: event.clientY }; } return { x: 0, y: 0 }; }
.
Edit
..
Edit
Edge.tsx
Edit
EdgeArrowMarker.tsx
Edit
EdgeLabel.tsx
Edit
Legend.test.tsx
Edit
Legend.tsx
Edit
Marker.tsx
Edit
Node.test.tsx
Edit
Node.tsx
Edit
NodeGraph.test.tsx
Edit
NodeGraph.tsx
Edit
NodeGraphPanel.tsx
Edit
README.md
Edit
ViewControls.tsx
Edit
createLayoutWorker.ts
Edit
editor
Edit
forceLayout.js
Edit
img
Edit
layeredLayout.js
Edit
layeredLayout.test.ts
Edit
layeredLayout.worker.js
Edit
layout.test.ts
Edit
layout.ts
Edit
layout.worker.js
Edit
module.tsx
Edit
panelcfg.cue
Edit
panelcfg.gen.ts
Edit
plugin.json
Edit
suggestions.ts
Edit
types.ts
Edit
useCategorizeFrames.ts
Edit
useContextMenu.tsx
Edit
useFocusPositionOnLayout.ts
Edit
useHighlight.ts
Edit
useNodeLimit.ts
Edit
usePanning.ts
Edit
useZoom.ts
Edit
utils.test.ts
Edit
utils.ts
Edit