Spaces:
Sleeping
Sleeping
| import { useCallback, useEffect, useRef, useState } from 'react'; | |
| import { Background, Controls, Panel, ReactFlow, addEdge, useEdgesState, useNodesState } from '@xyflow/react'; | |
| import '@xyflow/react/dist/style.css'; | |
| import { MarkerType } from '@xyflow/react'; | |
| import WorkflowEdge from './WorkflowEdge'; | |
| import WorkflowMinimap from './WorkflowMinimap'; | |
| import WorkflowNode from './WorkflowNode'; | |
| import WorkflowToolbar from './WorkflowToolbar'; | |
| const nodeTypes = { workflowNode: WorkflowNode }; | |
| const edgeTypes = { workflowEdge: WorkflowEdge, default: WorkflowEdge }; | |
| export const NODE_TEMPLATES = [ | |
| { type: 'start', label: 'Start / Trigger', iconName: 'Play', color: '#10B981', category: 'Flow' }, | |
| { type: 'process', label: 'Process', iconName: 'Cpu', color: '#3B82F6', category: 'Flow' }, | |
| { type: 'decision', label: 'Decision', iconName: 'GitBranch', color: '#F59E0B', category: 'Flow' }, | |
| { type: 'database', label: 'Database', iconName: 'Database', color: '#10B981', category: 'Infrastructure' }, | |
| { type: 'api', label: 'API Call', iconName: 'Globe', color: '#06B6D4', category: 'Integration' }, | |
| { type: 'auth', label: 'Auth Check', iconName: 'Lock', color: '#EF4444', category: 'Security' }, | |
| { type: 'notification', label: 'Notification', iconName: 'Bell', color: '#8B5CF6', category: 'Output' }, | |
| { type: 'code', label: 'Code Module', iconName: 'Code', color: '#84CC16', category: 'Code' }, | |
| { type: 'service', label: 'Microservice', iconName: 'Box', color: '#EC4899', category: 'Infrastructure' }, | |
| { type: 'queue', label: 'Message Queue', iconName: 'Layers', color: '#F97316', category: 'Infrastructure' }, | |
| { type: 'cloud', label: 'Cloud Service', iconName: 'Cloud', color: '#14B8A6', category: 'Infrastructure' }, | |
| ]; | |
| let nodeIdCounter = 1; | |
| export default function WorkflowDesigner({ initialNodes = [], initialEdges = [] }) { | |
| const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); | |
| const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); | |
| const [isDraggingOver, setIsDraggingOver] = useState(false); | |
| const [reactFlowInstance, setReactFlowInstance] = useState(null); | |
| const wrapperRef = useRef(null); | |
| useEffect(() => { | |
| setNodes(initialNodes); | |
| setEdges(initialEdges); | |
| }, [initialEdges, initialNodes, setEdges, setNodes]); | |
| const onConnect = useCallback( | |
| (params) => | |
| setEdges((current) => | |
| addEdge( | |
| { | |
| ...params, | |
| type: 'workflowEdge', | |
| markerEnd: { type: MarkerType.ArrowClosed, color: '#38BDF8' }, | |
| animated: true, | |
| }, | |
| current, | |
| ), | |
| ), | |
| [setEdges], | |
| ); | |
| const onDragStart = (event, template) => { | |
| event.dataTransfer.setData('application/workflow-node', JSON.stringify(template)); | |
| event.dataTransfer.effectAllowed = 'move'; | |
| }; | |
| const onDragOver = useCallback((event) => { | |
| event.preventDefault(); | |
| event.dataTransfer.dropEffect = 'move'; | |
| setIsDraggingOver(true); | |
| }, []); | |
| const onDrop = useCallback( | |
| (event) => { | |
| event.preventDefault(); | |
| setIsDraggingOver(false); | |
| const raw = event.dataTransfer.getData('application/workflow-node'); | |
| if (!raw || !reactFlowInstance) return; | |
| const template = JSON.parse(raw); | |
| const position = reactFlowInstance.screenToFlowPosition({ | |
| x: event.clientX, | |
| y: event.clientY, | |
| }); | |
| setNodes((current) => | |
| current.concat({ | |
| id: `node_${nodeIdCounter++}`, | |
| type: 'workflowNode', | |
| position, | |
| data: { ...template, nodeType: template.type, description: '' }, | |
| }), | |
| ); | |
| }, | |
| [reactFlowInstance, setNodes], | |
| ); | |
| const addNode = (template = NODE_TEMPLATES[0]) => { | |
| setNodes((current) => | |
| current.concat({ | |
| id: `node_${nodeIdCounter++}`, | |
| type: 'workflowNode', | |
| position: { x: 120 + current.length * 35, y: 120 + current.length * 24 }, | |
| data: { ...template, nodeType: template.type, description: '' }, | |
| }), | |
| ); | |
| }; | |
| const deleteSelected = () => { | |
| setNodes((current) => current.filter((node) => !node.selected)); | |
| setEdges((current) => current.filter((edge) => !edge.selected)); | |
| }; | |
| const exportWorkflow = () => { | |
| const blob = new Blob([JSON.stringify({ nodes, edges, timestamp: new Date().toISOString() }, null, 2)], { | |
| type: 'application/json', | |
| }); | |
| const url = URL.createObjectURL(blob); | |
| const anchor = document.createElement('a'); | |
| anchor.href = url; | |
| anchor.download = 'codeatlas-workflow.json'; | |
| anchor.click(); | |
| URL.revokeObjectURL(url); | |
| }; | |
| return ( | |
| <div className="flex h-full min-h-[720px] overflow-hidden rounded-lg border border-white/10"> | |
| <WorkflowToolbar templates={NODE_TEMPLATES} onDragStart={onDragStart} /> | |
| <div className="relative flex-1" ref={wrapperRef}> | |
| <ReactFlow | |
| nodes={nodes} | |
| edges={edges} | |
| onNodesChange={onNodesChange} | |
| onEdgesChange={onEdgesChange} | |
| onConnect={onConnect} | |
| onInit={setReactFlowInstance} | |
| onDrop={onDrop} | |
| onDragOver={onDragOver} | |
| onDragLeave={() => setIsDraggingOver(false)} | |
| nodeTypes={nodeTypes} | |
| edgeTypes={edgeTypes} | |
| fitView | |
| deleteKeyCode="Delete" | |
| style={{ background: isDraggingOver ? 'rgba(56,189,248,0.05)' : '#080b14' }} | |
| > | |
| <Background color="#263144" gap={28} size={1} /> | |
| <Controls /> | |
| <WorkflowMinimap /> | |
| <Panel position="top-right"> | |
| <div className="flex gap-2"> | |
| <button onClick={() => addNode()} className="rounded-lg border border-cyan-400/25 bg-cyan-500/12 px-3 py-2 text-xs font-medium text-cyan-200"> | |
| Add Node | |
| </button> | |
| <button onClick={deleteSelected} className="rounded-lg border border-red-400/25 bg-red-500/12 px-3 py-2 text-xs font-medium text-red-200"> | |
| Delete | |
| </button> | |
| <button onClick={exportWorkflow} className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs font-medium text-slate-200"> | |
| Export | |
| </button> | |
| </div> | |
| </Panel> | |
| </ReactFlow> | |
| </div> | |
| </div> | |
| ); | |
| } | |