feat(devtools): add change logger

This commit is contained in:
moklick
2024-03-23 16:25:01 +01:00
parent ed6f4313b1
commit eedff63399
3 changed files with 146 additions and 26 deletions
@@ -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 (
<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({ color = '#555', 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);
requestAnimationFrame(() => {
setChanges((c) => {
changes.forEach((change) => {
if (c.length >= limit) {
c.pop();
}
c = [change, ...c];
});
return c;
});
});
};
store.setState({ onNodesChange: onNodesChangeLogger });
}, [onNodesChange]);
return (
<div
style={{ fontFamily: 'monospace', fontSize: 10, position: 'relative', top: 45, left: 15, width: '100%', color }}
>
<div style={{ margin: '5px 0' }}>
<strong>Change Logger</strong>
</div>
<div>
{changes.length === 0 ? (
<>no changes triggered</>
) : (
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
)}
</div>
</div>
);
}
@@ -1,40 +1,74 @@
import { useNodes, ViewportPortal } from '@xyflow/react'; 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 <div></div>;
}
return (
<div
style={{
position: 'absolute',
zIndex: 9999,
transform: `translate(${x}px, ${y + height}px)`,
padding: 5,
boxSizing: 'border-box',
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>
);
}
type NodeInspectorProps = { type NodeInspectorProps = {
color: string; color: string;
}; };
export default function NodeInspector({ color = '#aaa' }: NodeInspectorProps) { export default function NodeInspector({ color = '#555' }: NodeInspectorProps) {
const nodes = useNodes(); const nodes = useNodes();
return ( return (
<ViewportPortal> <ViewportPortal>
{nodes.map((node) => { <div style={{ color, fontSize: 10, fontFamily: 'monospace' }}>
const x = node.computed?.positionAbsolute?.x || 0; {nodes.map((node) => {
const y = node.computed?.positionAbsolute?.y || 0; const x = node.computed?.positionAbsolute?.x || 0;
const height = node.computed?.height || 0; const y = node.computed?.positionAbsolute?.y || 0;
const width = node.computed?.width || 0;
const height = node.computed?.height || 0;
return ( return (
<div <NodeInfo
key={node.id} key={node.id}
style={{ id={node.id}
position: 'absolute', type={node.type || 'default'}
fontSize: 10, x={x}
color: color, y={y}
pointerEvents: 'none', width={width}
zIndex: 9999, height={height}
transform: `translate(${x}px, ${y + height}px)`, data={node.data}
}} />
> );
<div>id: {node.id}</div> })}
<div>type: {node.type}</div> </div>
<div>
position: {x.toFixed(1)}, {y.toFixed(1)}
</div>
<div>data: {JSON.stringify(node.data, null, 2)}</div>
</div>
);
})}
</ViewportPortal> </ViewportPortal>
); );
} }
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Panel, PanelPosition } from '@xyflow/react'; import { Panel, PanelPosition } from '@xyflow/react';
import NodeInspector from './NodeInspector'; import NodeInspector from './NodeInspector';
import ChangeLogger from './ChangeLogger';
type ReactFlowDevToolsProps = { type ReactFlowDevToolsProps = {
position?: PanelPosition; position?: PanelPosition;
@@ -10,13 +11,16 @@ type ReactFlowDevToolsProps = {
export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) { export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) {
const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false); const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false);
const [isChangeLoggerActive, setIsChangeLoggerActive] = useState(false);
return ( return (
<> <>
<Panel position={position}> <Panel position={position}>
<button onClick={() => setIsNodeInspectorActive((a) => !a)}>node inspector</button> <button onClick={() => setIsNodeInspectorActive((a) => !a)}>node inspector</button>
<button onClick={() => setIsChangeLoggerActive((a) => !a)}>change logger</button>
</Panel> </Panel>
{isNodeInspectorActive && <NodeInspector color={color} />} {isNodeInspectorActive && <NodeInspector color={color} />}
{isChangeLoggerActive && <ChangeLogger color={color} />}
</> </>
); );
} }