Merge pull request #4131 from xyflow/feat/react-devtools
Feat/react devtools example
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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 }: ChangeInfoProps) {
|
||||
const id = 'id' in change ? change.id : '-';
|
||||
const { type } = change;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<div>node id: {id}</div>
|
||||
<div>
|
||||
{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}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
|
||||
const [changes, setChanges] = useState<NodeChange[]>([]);
|
||||
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);
|
||||
|
||||
setChanges((c) => {
|
||||
changes.forEach((change) => {
|
||||
if (c.length >= limit) {
|
||||
c.pop();
|
||||
}
|
||||
|
||||
c = [change, ...c];
|
||||
});
|
||||
return c;
|
||||
});
|
||||
};
|
||||
|
||||
store.setState({ onNodesChange: onNodesChangeLogger });
|
||||
}, [onNodesChange]);
|
||||
|
||||
return (
|
||||
<div className="react-flow__devtools-changelogger">
|
||||
<div className="react-flow__devtools-title">Change Logger</div>
|
||||
{changes.length === 0 ? (
|
||||
<>no changes triggered</>
|
||||
) : (
|
||||
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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 null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__devtools-nodeinfo"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(${x}px, ${y + height}px)`,
|
||||
width: width * 2,
|
||||
}}
|
||||
>
|
||||
<div>id: {id}</div>
|
||||
<div>type: {type}</div>
|
||||
<div>
|
||||
position: {x.toFixed(1)}, {y.toFixed(1)}
|
||||
</div>
|
||||
<div>
|
||||
dimensions: {width} × {height}
|
||||
</div>
|
||||
<div>data: {JSON.stringify(data, null, 2)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NodeInspector() {
|
||||
const nodes = useNodes();
|
||||
|
||||
return (
|
||||
<ViewportPortal>
|
||||
<div className="react-flow__devtools-nodeinspector">
|
||||
{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 (
|
||||
<NodeInfo
|
||||
key={node.id}
|
||||
id={node.id}
|
||||
type={node.type || 'default'}
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
data={node.data}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ViewportPortal>
|
||||
);
|
||||
}
|
||||
42
examples/react/src/examples/DevTools/DevTools/index.tsx
Normal file
42
examples/react/src/examples/DevTools/DevTools/index.tsx
Normal file
@@ -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 (
|
||||
<div className="react-flow__devtools">
|
||||
<Panel position="top-left">
|
||||
<DevToolButton setActive={setNodeInspectorActive} active={nodeInspectorActive} title="Toggle Node Inspector">
|
||||
Node Inspector
|
||||
</DevToolButton>
|
||||
<DevToolButton setActive={setChangeLoggerActive} active={changeLoggerActive} title="Toggle Change Logger">
|
||||
Change Logger
|
||||
</DevToolButton>
|
||||
</Panel>
|
||||
{changeLoggerActive && <ChangeLogger />}
|
||||
{nodeInspectorActive && <NodeInspector />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DevToolButton({
|
||||
active,
|
||||
setActive,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
active: boolean;
|
||||
setActive: Dispatch<SetStateAction<boolean>>;
|
||||
children: ReactNode;
|
||||
} & HTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button onClick={() => setActive((a) => !a)} className={active ? 'active' : ''} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
63
examples/react/src/examples/DevTools/DevTools/style.css
Normal file
63
examples/react/src/examples/DevTools/DevTools/style.css
Normal file
@@ -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;
|
||||
}
|
||||
56
examples/react/src/examples/DevTools/index.tsx
Normal file
56
examples/react/src/examples/DevTools/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
|
||||
|
||||
import DevTools from './DevTools';
|
||||
import './Devtools/style.css';
|
||||
|
||||
const initNodes: Node[] = [
|
||||
{
|
||||
id: '1a',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3a',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
},
|
||||
];
|
||||
|
||||
const initEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1a', target: '2a' },
|
||||
{ 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 (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<DevTools />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicFlow;
|
||||
Reference in New Issue
Block a user