Files
elink-test-master/TestMasterUI/src/components/api-tester/DiagramArea.jsx
T
2025-01-21 13:19:21 +09:00

286 lines
8.4 KiB
React

import { Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, Position, Handle, ReactFlowProvider } from '@xyflow/react';
import React, { useCallback, memo, useMemo, useRef, useState, useEffect } from 'react';
import { Play } from 'lucide-react';
// Optimized node components with proper prop types and memoization
const StartNode = memo(({ data, isConnectable }) => {
// Memoize handles configuration
const handles = useMemo(() => ([
{ id: 'start-right', type: 'source', position: Position.Right }
]), []);
return (
<div className="bg-green-50 rounded-lg border-2 border-green-500 shadow-lg p-4 w-[100px]">
{handles.map(handle => (
<Handle
key={handle.id}
type={handle.type}
position={handle.position}
isConnectable={isConnectable}
className="w-3 h-3 bg-green-500"
/>
))}
<div className="flex items-center justify-center gap-2">
<Play className="h-4 w-4 text-green-600" />
<span className="font-semibold text-sm text-green-700">Start</span>
</div>
</div>
);
});
const ApiNode = memo(({ data, isConnectable }) => {
// Memoize handles and colors
const handles = useMemo(() => ([
{ id: `${data.id}-left`, type: 'target', position: Position.Left },
{ id: `${data.id}-right`, type: 'source', position: Position.Right }
]), [data.id]);
const methodColor = useMemo(() => {
const colors = {
GET: 'bg-green-500',
POST: 'bg-blue-500',
PUT: 'bg-yellow-500',
DELETE: 'bg-red-500',
default: 'bg-gray-500'
};
return colors[data.method] || colors.default;
}, [data.method]);
return (
<div className="bg-white rounded-lg border shadow-lg p-4 w-[200px]">
{handles.map(handle => (
<Handle
key={handle.id}
type={handle.type}
position={handle.position}
isConnectable={isConnectable}
className="w-3 h-3 bg-gray-500"
/>
))}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${methodColor}`} />
<span className="font-bold text-sm">{data.method}</span>
</div>
</div>
<div className="text-xs text-gray-500">{data.name}</div>
<div className="text-sm truncate text-gray-600 mb-2">
{data.path}
</div>
</div>
);
});
const ConditionNode = memo(({ data, isConnectable }) => {
// Memoize handle styles
const handleStyles = useMemo(() => ({
true: {
top: '30%',
backgroundColor: '#22c55e',
border: '2px solid #16a34a'
},
false: {
top: '70%',
backgroundColor: '#ef4444',
border: '2px solid #dc2626'
}
}), []);
return (
<div className="bg-white rounded-lg border shadow-lg p-4 w-[200px]">
<Handle
type="target"
position={Position.Left}
id="in"
isConnectable={isConnectable}
className="w-3 h-3"
/>
<Handle
type="source"
position={Position.Right}
id="true"
style={handleStyles.true}
isConnectable={isConnectable}
className="w-3 h-3 !opacity-100"
/>
<Handle
type="source"
position={Position.Right}
id="false"
style={handleStyles.false}
isConnectable={isConnectable}
className="w-3 h-3 !opacity-100"
/>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500" />
<span className="font-bold text-sm">Condition</span>
</div>
</div>
<div className="text-sm text-gray-600">
{data.condition || 'Add condition'}
</div>
</div>
);
});
const DelayNode = memo(({ data, isConnectable }) => {
const handles = useMemo(() => ([
{ id: `${data.id}-left`, type: 'target', position: Position.Left },
{ id: `${data.id}-right`, type: 'source', position: Position.Right }
]), [data.id]);
return (
<div className="bg-white rounded-lg border shadow-lg p-4 w-[200px]">
{handles.map(handle => (
<Handle
key={handle.id}
type={handle.type}
position={handle.position}
isConnectable={isConnectable}
className="w-3 h-3 bg-gray-500"
/>
))}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500" />
<span className="font-bold text-sm">Delay</span>
</div>
</div>
<div className="text-sm text-gray-600">
Delay: {data.duration || '1000'}ms
</div>
</div>
);
});
// Add display names for debugging
StartNode.displayName = 'StartNode';
ApiNode.displayName = 'ApiNode';
ConditionNode.displayName = 'ConditionNode';
DelayNode.displayName = 'DelayNode';
// Memoize initial nodes and edges
const initialNodes = [
{
id: 'start',
type: 'startNode',
position: { x: 100, y: 50 },
data: { id: 'start' }
}
];
const initialEdges = [];
// Memoize node types
const nodeTypes = {
startNode: StartNode,
apiNode: ApiNode,
conditionNode: ConditionNode,
delayNode: DelayNode
};
const DiagramArea = () => {
const reactFlowWrapper = useRef(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [selectedNodeId, setSelectedNodeId] = useState(null);
// Click handlers for node selection
const onNodeClick = useCallback((event, node) => {
event.stopPropagation();
setSelectedNodeId(node.id);
}, []);
const onPaneClick = useCallback(() => {
setSelectedNodeId(null);
}, []);
// Apply styles to nodes
useEffect(() => {
setNodes((nds) =>
nds.map((node) => ({
...node,
className: node.id === selectedNodeId ? 'border-2 border-primary shadow-md bg-primary/5' : null
}))
);
}, [selectedNodeId, setNodes]);
// Memoize callback functions
const onConnect = useCallback((params) => {
setEdges(prev => [...prev, { ...params, id: `e${prev.length + 1}` }]);
}, []);
const onInit = useCallback((instance) => {
reactFlowWrapper.current = instance;
}, []);
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}, []);
const onDrop = useCallback((event) => {
event.preventDefault();
if (!reactFlowWrapper.current) return;
try {
const apiData = JSON.parse(event.dataTransfer.getData('application/json'));
const position = reactFlowWrapper.current.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode = {
id: `api-${Date.now()}`,
type: 'apiNode',
position,
data: {
id: apiData.id,
name: apiData.name,
method: apiData.method,
path: apiData.path,
collectionId: apiData.collectionId
}
};
setNodes(prev => prev.concat(newNode));
} catch (error) {
console.error('Error adding new node:', error);
}
}, [setNodes]);
// Default edge options memoized
const defaultEdgeOptions = useMemo(() => ({ type: 'smoothstep' }), []);
return (
<div className="h-[50vh] border-b">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={onInit}
defaultEdgeOptions={defaultEdgeOptions}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
defaultZoom={1.0} // 초기 배율 설정 (1이 기본값)
proOptions={{ hideAttribution: true }}
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
</ReactFlowProvider>
</div>
);
};
export default memo(DiagramArea);