From ed6f4313b1452a2f8dfcede11608070249d076cf Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 21 Mar 2024 14:30:33 +0100 Subject: [PATCH 1/4] feat(react): add devtools example --- examples/react/src/App/routes.ts | 6 ++ .../ReactFlowDevTools/NodeInspector.tsx | 40 +++++++++++++ .../DevTools/ReactFlowDevTools/index.tsx | 22 +++++++ .../react/src/examples/DevTools/index.tsx | 59 +++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx create mode 100644 examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx create mode 100644 examples/react/src/examples/DevTools/index.tsx diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 7cd3eb13..1d68c858 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -50,6 +50,7 @@ import UseNodesInitialized from '../examples/UseNodesInit'; import UseNodesData from '../examples/UseNodesData'; import UseHandleConnections from '../examples/UseHandleConnections'; import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; +import DevTools from '../examples/DevTools'; export interface IRoute { name: string; @@ -113,6 +114,11 @@ const routes: IRoute[] = [ path: 'default-nodes', component: DefaultNodes, }, + { + name: 'DevTools', + path: 'devtools', + component: DevTools, + }, { name: 'Drag Handle', path: 'draghandle', diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx new file mode 100644 index 00000000..34ebc361 --- /dev/null +++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx @@ -0,0 +1,40 @@ +import { useNodes, ViewportPortal } from '@xyflow/react'; + +type NodeInspectorProps = { + color: string; +}; + +export default function NodeInspector({ color = '#aaa' }: NodeInspectorProps) { + const nodes = useNodes(); + + return ( + + {nodes.map((node) => { + const x = node.computed?.positionAbsolute?.x || 0; + const y = node.computed?.positionAbsolute?.y || 0; + const height = node.computed?.height || 0; + + return ( +
+
id: {node.id}
+
type: {node.type}
+
+ position: {x.toFixed(1)}, {y.toFixed(1)} +
+
data: {JSON.stringify(node.data, null, 2)}
+
+ ); + })} +
+ ); +} diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx new file mode 100644 index 00000000..8c521aa0 --- /dev/null +++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx @@ -0,0 +1,22 @@ +import { useState } from 'react'; +import { Panel, PanelPosition } from '@xyflow/react'; + +import NodeInspector from './NodeInspector'; + +type ReactFlowDevToolsProps = { + position?: PanelPosition; + color?: string; +}; + +export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) { + const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false); + + return ( + <> + + + + {isNodeInspectorActive && } + + ); +} diff --git a/examples/react/src/examples/DevTools/index.tsx b/examples/react/src/examples/DevTools/index.tsx new file mode 100644 index 00000000..153b0897 --- /dev/null +++ b/examples/react/src/examples/DevTools/index.tsx @@ -0,0 +1,59 @@ +import { useCallback } from 'react'; +import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react'; +import ReactFlowDevTools from './ReactFlowDevTools'; + +const initNodes: Node[] = [ + { + id: '1a', + type: 'input', + data: { label: 'Node 1' }, + position: { x: 250, y: 5 }, + className: 'light', + ariaLabel: 'Input Node 1', + }, + { + id: '2a', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + className: 'light', + ariaLabel: 'Default Node 2', + }, + { + id: '3a', + data: { label: 'Node 3' }, + position: { x: 400, y: 100 }, + className: 'light', + }, + { + id: '4a', + data: { label: 'Node 4' }, + position: { x: 400, y: 200 }, + className: 'light', + }, +]; + +const initEdges: Edge[] = [ + { id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined }, + { id: 'e1-3', source: '1a', target: '3a' }, +]; + +const BasicFlow = () => { + const [nodes, setNodes, onNodesChange] = useNodesState(initNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); + + const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + + return ( + + + + ); +}; + +export default BasicFlow; From eedff63399c8914e7a4edd0cfa7c0dccc98126c5 Mon Sep 17 00:00:00 2001 From: moklick Date: Sat, 23 Mar 2024 16:25:01 +0100 Subject: [PATCH 2/4] feat(devtools): add change logger --- .../ReactFlowDevTools/ChangeLogger.tsx | 82 ++++++++++++++++++ .../ReactFlowDevTools/NodeInspector.tsx | 86 +++++++++++++------ .../DevTools/ReactFlowDevTools/index.tsx | 4 + 3 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx new file mode 100644 index 00000000..7a137e33 --- /dev/null +++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx @@ -0,0 +1,82 @@ +import { useEffect, useRef, useState } from 'react'; +import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react'; + +type ChangeLoggerProps = { + color?: string; + limit?: number; +}; + +type ChangeInfoProps = { + change: NodeChange; +}; + +function ChangeInfo({ change, index }: ChangeInfoProps) { + const id = 'id' in change ? change.id : '-'; + const { type } = change; + + return ( +
+
node id: {id}
+
+ {type === 'add' ? JSON.stringify(change.item, null, 2) : null} + {type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null} + {type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null} + {type === 'remove' ? 'remove' : null} + {type === 'replace' ? JSON.stringify(change.item, null, 2) : null} + {type === 'select' ? (change.selected ? 'select' : 'unselect') : null} +
+
+ ); +} + +export default function ChangeLogger({ color = '#555', limit = 20 }: ChangeLoggerProps) { + const [changes, setChanges] = useState([]); + const onNodesChangeIntercepted = useRef(false); + const onNodesChange = useStore((s) => s.onNodesChange); + const store = useStoreApi(); + + useEffect(() => { + if (!onNodesChange || onNodesChangeIntercepted.current) { + return; + } + + onNodesChangeIntercepted.current = true; + const userOnNodesChange = onNodesChange; + + const onNodesChangeLogger: OnNodesChange = (changes) => { + userOnNodesChange(changes); + + requestAnimationFrame(() => { + setChanges((c) => { + changes.forEach((change) => { + if (c.length >= limit) { + c.pop(); + } + + c = [change, ...c]; + }); + return c; + }); + }); + }; + + store.setState({ onNodesChange: onNodesChangeLogger }); + }, [onNodesChange]); + + return ( +
+
+ Change Logger +
+
+ {changes.length === 0 ? ( + <>no changes triggered + ) : ( + changes.map((change, index) => ) + )} +
+
+ ); +} diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx index 34ebc361..ae07029a 100644 --- a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx +++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx @@ -1,40 +1,74 @@ import { useNodes, ViewportPortal } from '@xyflow/react'; +type NodeInfoProps = { + id: string; + type: string; + x: number; + y: number; + width?: number; + height?: number; + data: any; +}; + +function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) { + if (!width || !height) { + return
; + } + + return ( +
+
id: {id}
+
type: {type}
+
+ position: {x.toFixed(1)}, {y.toFixed(1)} +
+
+ dimensions: {width} × {height} +
+
data: {JSON.stringify(data, null, 2)}
+
+ ); +} + type NodeInspectorProps = { color: string; }; -export default function NodeInspector({ color = '#aaa' }: NodeInspectorProps) { +export default function NodeInspector({ color = '#555' }: NodeInspectorProps) { const nodes = useNodes(); return ( - {nodes.map((node) => { - const x = node.computed?.positionAbsolute?.x || 0; - const y = node.computed?.positionAbsolute?.y || 0; - const height = node.computed?.height || 0; +
+ {nodes.map((node) => { + const x = node.computed?.positionAbsolute?.x || 0; + const y = node.computed?.positionAbsolute?.y || 0; + const width = node.computed?.width || 0; + const height = node.computed?.height || 0; - return ( -
-
id: {node.id}
-
type: {node.type}
-
- position: {x.toFixed(1)}, {y.toFixed(1)} -
-
data: {JSON.stringify(node.data, null, 2)}
-
- ); - })} + return ( + + ); + })} +
); } diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx index 8c521aa0..2b488b7c 100644 --- a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx +++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Panel, PanelPosition } from '@xyflow/react'; import NodeInspector from './NodeInspector'; +import ChangeLogger from './ChangeLogger'; type ReactFlowDevToolsProps = { position?: PanelPosition; @@ -10,13 +11,16 @@ type ReactFlowDevToolsProps = { export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) { const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false); + const [isChangeLoggerActive, setIsChangeLoggerActive] = useState(false); return ( <> + {isNodeInspectorActive && } + {isChangeLoggerActive && } ); } From 79dbdc27f62443b785b6f806775f98ecbf868d68 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 27 Mar 2024 15:14:36 +0100 Subject: [PATCH 3/4] chore(react/devtools): cleanup --- .../ReactFlowDevTools/ChangeLogger.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx index 7a137e33..4593ba28 100644 --- a/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx +++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx @@ -10,7 +10,7 @@ type ChangeInfoProps = { change: NodeChange; }; -function ChangeInfo({ change, index }: ChangeInfoProps) { +function ChangeInfo({ change }: ChangeInfoProps) { const id = 'id' in change ? change.id : '-'; const { type } = change; @@ -46,17 +46,15 @@ export default function ChangeLogger({ color = '#555', limit = 20 }: ChangeLogge const onNodesChangeLogger: OnNodesChange = (changes) => { userOnNodesChange(changes); - requestAnimationFrame(() => { - setChanges((c) => { - changes.forEach((change) => { - if (c.length >= limit) { - c.pop(); - } + setChanges((c) => { + changes.forEach((change) => { + if (c.length >= limit) { + c.pop(); + } - c = [change, ...c]; - }); - return c; + c = [change, ...c]; }); + return c; }); }; From b302cb59e8b3134b6a77040ab5c763983f98fcab Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 3 Apr 2024 11:33:21 +0200 Subject: [PATCH 4/4] style(devtools): put styles in css file --- .../ChangeLogger.tsx | 22 +++---- .../NodeInspector.tsx | 14 ++--- .../src/examples/DevTools/DevTools/index.tsx | 42 +++++++++++++ .../src/examples/DevTools/DevTools/style.css | 63 +++++++++++++++++++ .../DevTools/ReactFlowDevTools/index.tsx | 26 -------- .../react/src/examples/DevTools/index.tsx | 15 ++--- 6 files changed, 123 insertions(+), 59 deletions(-) rename examples/react/src/examples/DevTools/{ReactFlowDevTools => DevTools}/ChangeLogger.tsx (77%) rename examples/react/src/examples/DevTools/{ReactFlowDevTools => DevTools}/NodeInspector.tsx (82%) create mode 100644 examples/react/src/examples/DevTools/DevTools/index.tsx create mode 100644 examples/react/src/examples/DevTools/DevTools/style.css delete mode 100644 examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx similarity index 77% rename from examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx rename to examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx index 4593ba28..d4fffcda 100644 --- a/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx +++ b/examples/react/src/examples/DevTools/DevTools/ChangeLogger.tsx @@ -29,7 +29,7 @@ function ChangeInfo({ change }: ChangeInfoProps) { ); } -export default function ChangeLogger({ color = '#555', limit = 20 }: ChangeLoggerProps) { +export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) { const [changes, setChanges] = useState([]); const onNodesChangeIntercepted = useRef(false); const onNodesChange = useStore((s) => s.onNodesChange); @@ -62,19 +62,13 @@ export default function ChangeLogger({ color = '#555', limit = 20 }: ChangeLogge }, [onNodesChange]); return ( -
-
- Change Logger -
-
- {changes.length === 0 ? ( - <>no changes triggered - ) : ( - changes.map((change, index) => ) - )} -
+
+
Change Logger
+ {changes.length === 0 ? ( + <>no changes triggered + ) : ( + changes.map((change, index) => ) + )}
); } diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx similarity index 82% rename from examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx rename to examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx index ae07029a..5d467514 100644 --- a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx +++ b/examples/react/src/examples/DevTools/DevTools/NodeInspector.tsx @@ -12,17 +12,15 @@ type NodeInfoProps = { function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) { if (!width || !height) { - return
; + return null; } return (
@@ -39,16 +37,12 @@ function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) { ); } -type NodeInspectorProps = { - color: string; -}; - -export default function NodeInspector({ color = '#555' }: NodeInspectorProps) { +export default function NodeInspector() { const nodes = useNodes(); return ( -
+
{nodes.map((node) => { const x = node.computed?.positionAbsolute?.x || 0; const y = node.computed?.positionAbsolute?.y || 0; diff --git a/examples/react/src/examples/DevTools/DevTools/index.tsx b/examples/react/src/examples/DevTools/DevTools/index.tsx new file mode 100644 index 00000000..107d0078 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/index.tsx @@ -0,0 +1,42 @@ +import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react'; +import { Panel } from '@xyflow/react'; + +import NodeInspector from './NodeInspector'; +import ChangeLogger from './ChangeLogger'; + +export default function ReactFlowDevTools() { + const [nodeInspectorActive, setNodeInspectorActive] = useState(false); + const [changeLoggerActive, setChangeLoggerActive] = useState(false); + + return ( +
+ + + Node Inspector + + + Change Logger + + + {changeLoggerActive && } + {nodeInspectorActive && } +
+ ); +} + +function DevToolButton({ + active, + setActive, + children, + ...rest +}: { + active: boolean; + setActive: Dispatch>; + children: ReactNode; +} & HTMLAttributes) { + return ( + + ); +} diff --git a/examples/react/src/examples/DevTools/DevTools/style.css b/examples/react/src/examples/DevTools/DevTools/style.css new file mode 100644 index 00000000..39c65969 --- /dev/null +++ b/examples/react/src/examples/DevTools/DevTools/style.css @@ -0,0 +1,63 @@ +.react-flow__devtools { + --border-radius: 4px; + --highlight-color: rgba(238, 58, 115, 1); + --font: monospace, sans-serif; + + border-radius: var(--border-radius); + font-size: 11px; + font-family: var(--font); +} + +.react-flow__devtools button { + background: white; + border: none; + padding: 5px 15px; + color: #222; + font-weight: bold; + font-size: 12px; + cursor: pointer; + font-family: var(--font); + background-color: #f4f4f4; +} + +.react-flow__devtools button:hover { + background: var(--highlight-color); + color: white; +} + +.react-flow__devtools button.active { + background: var(--highlight-color); + color: white; +} + +.react-flow__devtools button:first-child { + border-radius: var(--border-radius) 0 0 var(--border-radius); + border-right: 1px solid #ddd; +} + +.react-flow__devtools button:last-child { + border-radius: 0 var(--border-radius) var(--border-radius) 0; +} + +.react-flow__devtools-changelogger { + pointer-events: none; + position: relative; + top: 50px; + left: 20px; + font-family: var(--font); +} + +.react-flow__devtools-title { + font-weight: bold; + margin-bottom: 5px; +} + +.react-flow__devtools-nodeinspector { + pointer-events: none; + font-family: monospace, sans-serif; + font-size: 10px; +} + +.react-flow__devtools-nodeinfo { + top: 5px; +} diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx deleted file mode 100644 index 2b488b7c..00000000 --- a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useState } from 'react'; -import { Panel, PanelPosition } from '@xyflow/react'; - -import NodeInspector from './NodeInspector'; -import ChangeLogger from './ChangeLogger'; - -type ReactFlowDevToolsProps = { - position?: PanelPosition; - color?: string; -}; - -export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) { - const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false); - const [isChangeLoggerActive, setIsChangeLoggerActive] = useState(false); - - return ( - <> - - - - - {isNodeInspectorActive && } - {isChangeLoggerActive && } - - ); -} diff --git a/examples/react/src/examples/DevTools/index.tsx b/examples/react/src/examples/DevTools/index.tsx index 153b0897..5303dea6 100644 --- a/examples/react/src/examples/DevTools/index.tsx +++ b/examples/react/src/examples/DevTools/index.tsx @@ -1,6 +1,8 @@ import { useCallback } from 'react'; import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react'; -import ReactFlowDevTools from './ReactFlowDevTools'; + +import DevTools from './DevTools'; +import './Devtools/style.css'; const initNodes: Node[] = [ { @@ -8,32 +10,26 @@ const initNodes: Node[] = [ type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, - className: 'light', - ariaLabel: 'Input Node 1', }, { id: '2a', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, - className: 'light', - ariaLabel: 'Default Node 2', }, { id: '3a', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, - className: 'light', }, { id: '4a', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, - className: 'light', }, ]; const initEdges: Edge[] = [ - { id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined }, + { id: 'e1-2', source: '1a', target: '2a' }, { id: 'e1-3', source: '1a', target: '3a' }, ]; @@ -50,8 +46,9 @@ const BasicFlow = () => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} + fitView > - + ); };