mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-28 15:54:56 +00:00
Merge pull request #169 from johndoe6345789/codex/refactor-workfloweditor-into-separate-modules
Refactor workflow editor into modular components
This commit is contained in:
@@ -1,243 +1,56 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
TextField,
|
||||
Typography,
|
||||
Chip,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Delete as DeleteIcon,
|
||||
FlashOn as LightningIcon,
|
||||
Code as CodeIcon,
|
||||
AccountTree as GitBranchIcon,
|
||||
ArrowForward as ArrowRightIcon,
|
||||
PlayArrow as PlayIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Cancel as XCircleIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { toast } from 'sonner'
|
||||
import { createWorkflowEngine, type WorkflowExecutionResult as WFExecResult } from '@/lib/workflow/engine/workflow-engine'
|
||||
import type { Workflow, WorkflowNode, WorkflowEdge, LuaScript } from '@/lib/level-types'
|
||||
import { Input, Label, Badge, Button, Textarea, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui'
|
||||
import { CardTitle, CardDescription } from '@/components/ui'
|
||||
|
||||
interface WorkflowEditorProps {
|
||||
workflows: Workflow[]
|
||||
onWorkflowsChange: (workflows: Workflow[]) => void
|
||||
scripts?: LuaScript[]
|
||||
}
|
||||
import { Card, CardContent, CardHeader } from '@mui/material'
|
||||
import { PlayArrow as PlayIcon } from '@mui/icons-material'
|
||||
import { CardDescription, CardTitle } from '@/components/ui'
|
||||
import { WorkflowSidebar } from './editor/WorkflowSidebar'
|
||||
import { WorkflowDetailsPanel } from './editor/WorkflowDetailsPanel'
|
||||
import { WorkflowNodesPanel } from './editor/WorkflowNodesPanel'
|
||||
import { WorkflowTester } from './editor/WorkflowTester'
|
||||
import { useWorkflowState } from './editor/useWorkflowState'
|
||||
import { createActionHandlers } from './editor/createActionHandlers'
|
||||
import type { WorkflowEditorProps } from './editor/types'
|
||||
|
||||
export function WorkflowEditor({ workflows, onWorkflowsChange, scripts = [] }: WorkflowEditorProps) {
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(
|
||||
workflows.length > 0 ? workflows[0].id : null
|
||||
)
|
||||
const [testData, setTestData] = useState<string>('{"example": "data"}')
|
||||
const [testOutput, setTestOutput] = useState<WFExecResult | null>(null)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const state = useWorkflowState(workflows)
|
||||
const {
|
||||
currentWorkflow,
|
||||
selectedWorkflowId,
|
||||
setSelectedWorkflowId,
|
||||
testData,
|
||||
setTestData,
|
||||
testOutput,
|
||||
setTestOutput,
|
||||
isExecuting,
|
||||
setIsExecuting,
|
||||
} = state
|
||||
|
||||
const currentWorkflow = workflows.find(w => w.id === selectedWorkflow)
|
||||
|
||||
const handleAddWorkflow = () => {
|
||||
const newWorkflow: Workflow = {
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: 'New Workflow',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
enabled: true,
|
||||
}
|
||||
onWorkflowsChange([...workflows, newWorkflow])
|
||||
setSelectedWorkflow(newWorkflow.id)
|
||||
toast.success('Workflow created')
|
||||
}
|
||||
|
||||
const handleDeleteWorkflow = (workflowId: string) => {
|
||||
onWorkflowsChange(workflows.filter(w => w.id !== workflowId))
|
||||
if (selectedWorkflow === workflowId) {
|
||||
setSelectedWorkflow(workflows.length > 1 ? workflows[0].id : null)
|
||||
}
|
||||
toast.success('Workflow deleted')
|
||||
}
|
||||
|
||||
const handleUpdateWorkflow = (updates: Partial<Workflow>) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
onWorkflowsChange(
|
||||
workflows.map(w => w.id === selectedWorkflow ? { ...w, ...updates } : w)
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddNode = (type: WorkflowNode['type']) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
const newNode: WorkflowNode = {
|
||||
id: `node_${Date.now()}`,
|
||||
type,
|
||||
label: `${type.charAt(0).toUpperCase() + type.slice(1)} Node`,
|
||||
config: {},
|
||||
position: { x: 100, y: currentWorkflow.nodes.length * 100 + 100 },
|
||||
}
|
||||
|
||||
handleUpdateWorkflow({
|
||||
nodes: [...currentWorkflow.nodes, newNode],
|
||||
})
|
||||
toast.success('Node added')
|
||||
}
|
||||
|
||||
const handleDeleteNode = (nodeId: string) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
handleUpdateWorkflow({
|
||||
nodes: currentWorkflow.nodes.filter(n => n.id !== nodeId),
|
||||
edges: currentWorkflow.edges.filter(e => e.source !== nodeId && e.target !== nodeId),
|
||||
})
|
||||
toast.success('Node deleted')
|
||||
}
|
||||
|
||||
const handleUpdateNode = (nodeId: string, updates: Partial<WorkflowNode>) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
handleUpdateWorkflow({
|
||||
nodes: currentWorkflow.nodes.map(n => n.id === nodeId ? { ...n, ...updates } : n),
|
||||
})
|
||||
}
|
||||
|
||||
const handleTestWorkflow = async () => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
setIsExecuting(true)
|
||||
setTestOutput(null)
|
||||
|
||||
try {
|
||||
let parsedData: any
|
||||
try {
|
||||
parsedData = JSON.parse(testData)
|
||||
} catch {
|
||||
parsedData = testData
|
||||
}
|
||||
|
||||
const engine = createWorkflowEngine()
|
||||
const result = await engine.executeWorkflow(currentWorkflow, {
|
||||
data: parsedData,
|
||||
user: { username: 'test_user', role: 'god' },
|
||||
scripts,
|
||||
})
|
||||
|
||||
setTestOutput(result)
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Workflow executed successfully')
|
||||
} else {
|
||||
toast.error('Workflow execution failed')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error)))
|
||||
setTestOutput({
|
||||
success: false,
|
||||
outputs: {},
|
||||
logs: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeIcon = (type: WorkflowNode['type']) => {
|
||||
switch (type) {
|
||||
case 'trigger':
|
||||
return <LightningIcon fontSize="small" />
|
||||
case 'action':
|
||||
return <ArrowRightIcon fontSize="small" />
|
||||
case 'condition':
|
||||
return <GitBranchIcon fontSize="small" />
|
||||
case 'lua':
|
||||
return <CodeIcon fontSize="small" />
|
||||
case 'transform':
|
||||
return <ArrowRightIcon fontSize="small" />
|
||||
default:
|
||||
return <ArrowRightIcon fontSize="small" />
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeColor = (type: WorkflowNode['type']): string => {
|
||||
switch (type) {
|
||||
case 'trigger':
|
||||
return 'success.main'
|
||||
case 'action':
|
||||
return 'primary.main'
|
||||
case 'condition':
|
||||
return 'warning.main'
|
||||
case 'lua':
|
||||
return 'secondary.main'
|
||||
case 'transform':
|
||||
return 'info.main'
|
||||
default:
|
||||
return 'grey.500'
|
||||
}
|
||||
}
|
||||
const {
|
||||
handleAddWorkflow,
|
||||
handleDeleteWorkflow,
|
||||
handleUpdateWorkflow,
|
||||
handleAddNode,
|
||||
handleDeleteNode,
|
||||
handleUpdateNode,
|
||||
handleTestWorkflow,
|
||||
} = createActionHandlers({
|
||||
workflows,
|
||||
currentWorkflow,
|
||||
onWorkflowsChange,
|
||||
setSelectedWorkflowId,
|
||||
setTestOutput,
|
||||
setIsExecuting,
|
||||
scripts,
|
||||
testData,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-3 gap-6 h-full">
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Workflows</CardTitle>
|
||||
<Button size="sm" onClick={handleAddWorkflow}>
|
||||
<AddIcon sx={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>Automation workflows</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{workflows.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No workflows yet. Create one to start.
|
||||
</p>
|
||||
) : (
|
||||
workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedWorkflow === workflow.id
|
||||
? 'bg-accent border-accent-foreground'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
onClick={() => setSelectedWorkflow(workflow.id)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{workflow.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{workflow.nodes.length} nodes
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={workflow.enabled ? 'default' : 'secondary'} className="text-xs">
|
||||
{workflow.enabled ? 'On' : 'Off'}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteWorkflow(workflow.id)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon sx={{ fontSize: 14 }} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<WorkflowSidebar
|
||||
workflows={workflows}
|
||||
selectedWorkflowId={selectedWorkflowId}
|
||||
onSelectWorkflow={setSelectedWorkflowId}
|
||||
onAddWorkflow={handleAddWorkflow}
|
||||
onDeleteWorkflow={handleDeleteWorkflow}
|
||||
/>
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
{!currentWorkflow ? (
|
||||
@@ -254,250 +67,34 @@ export function WorkflowEditor({ workflows, onWorkflowsChange, scripts = [] }: W
|
||||
<CardTitle>Edit Workflow: {currentWorkflow.name}</CardTitle>
|
||||
<CardDescription>Configure workflow nodes and connections</CardDescription>
|
||||
</div>
|
||||
<Button onClick={handleTestWorkflow} disabled={isExecuting}>
|
||||
<PlayIcon sx={{ fontSize: 16, mr: 1 }} />
|
||||
<button
|
||||
className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm"
|
||||
onClick={handleTestWorkflow}
|
||||
disabled={isExecuting}
|
||||
>
|
||||
<PlayIcon sx={{ fontSize: 16 }} />
|
||||
{isExecuting ? 'Running...' : 'Test Workflow'}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Workflow Name</Label>
|
||||
<Input
|
||||
value={currentWorkflow.name}
|
||||
onChange={(e) => handleUpdateWorkflow({ name: e.target.value })}
|
||||
placeholder="My Workflow"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
value={currentWorkflow.description || ''}
|
||||
onChange={(e) => handleUpdateWorkflow({ description: e.target.value })}
|
||||
placeholder="What this workflow does..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<WorkflowDetailsPanel workflow={currentWorkflow} onUpdate={handleUpdateWorkflow} />
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-base">Nodes</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('trigger')}>
|
||||
<LightningIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Trigger
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('action')}>
|
||||
<ArrowRightIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Action
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('condition')}>
|
||||
<GitBranchIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Condition
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('lua')}>
|
||||
<CodeIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Lua
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{currentWorkflow.nodes.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg">
|
||||
No nodes yet. Add nodes to build your workflow.
|
||||
</p>
|
||||
) : (
|
||||
currentWorkflow.nodes.map((node, index) => (
|
||||
<Card key={node.id} className="border-2">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${getNodeColor(node.type)} flex items-center justify-center text-white shrink-0`}>
|
||||
{getNodeIcon(node.type)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Node Label</Label>
|
||||
<Input
|
||||
value={node.label}
|
||||
onChange={(e) =>
|
||||
handleUpdateNode(node.id, { label: e.target.value })
|
||||
}
|
||||
placeholder="Node name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Node Type</Label>
|
||||
<Select
|
||||
value={node.type}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateNode(node.id, { type: value as WorkflowNode['type'] })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="trigger">Trigger</SelectItem>
|
||||
<SelectItem value="action">Action</SelectItem>
|
||||
<SelectItem value="condition">Condition</SelectItem>
|
||||
<SelectItem value="lua">Lua Script</SelectItem>
|
||||
<SelectItem value="transform">Transform</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{node.type === 'lua' && scripts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Lua Script</Label>
|
||||
<Select
|
||||
value={node.config.scriptId || ''}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateNode(node.id, {
|
||||
config: { ...node.config, scriptId: value }
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a script" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{scripts.map((script) => (
|
||||
<SelectItem key={script.id} value={script.id}>
|
||||
{script.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.type === 'condition' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Condition Expression</Label>
|
||||
<Input
|
||||
value={node.config.condition || ''}
|
||||
onChange={(e) =>
|
||||
handleUpdateNode(node.id, {
|
||||
config: { ...node.config, condition: e.target.value }
|
||||
})
|
||||
}
|
||||
placeholder="data.value > 10"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.type === 'transform' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Transform Expression</Label>
|
||||
<Input
|
||||
value={node.config.transform || ''}
|
||||
onChange={(e) =>
|
||||
handleUpdateNode(node.id, {
|
||||
config: { ...node.config, transform: e.target.value }
|
||||
})
|
||||
}
|
||||
placeholder="{ result: data.value * 2 }"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Step {index + 1}
|
||||
</Badge>
|
||||
{index < currentWorkflow.nodes.length - 1 && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<ArrowRightIcon sx={{ fontSize: 12 }} />
|
||||
<span>Next</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteNode(node.id)}
|
||||
>
|
||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<WorkflowNodesPanel
|
||||
workflow={currentWorkflow}
|
||||
scripts={scripts}
|
||||
onAddNode={handleAddNode}
|
||||
onDeleteNode={handleDeleteNode}
|
||||
onUpdateNode={handleUpdateNode}
|
||||
/>
|
||||
|
||||
{currentWorkflow.nodes.length > 0 && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Test Input Data (JSON)</Label>
|
||||
<Textarea
|
||||
value={testData}
|
||||
onChange={(e) => setTestData(e.target.value)}
|
||||
className="font-mono text-sm min-h-[100px]"
|
||||
placeholder='{"example": "data"}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{testOutput && (
|
||||
<Card className={testOutput.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
{testOutput.success ? (
|
||||
<CheckCircleIcon sx={{ fontSize: 20, color: 'success.main' }} />
|
||||
) : (
|
||||
<XCircleIcon sx={{ fontSize: 20, color: 'error.main' }} />
|
||||
)}
|
||||
<CardTitle className="text-sm">
|
||||
{testOutput.success ? 'Workflow Execution Successful' : 'Workflow Execution Failed'}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{testOutput.error && (
|
||||
<div>
|
||||
<Label className="text-xs text-red-600 mb-1">Error</Label>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap text-red-700 bg-red-100 p-2 rounded">
|
||||
{testOutput.error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testOutput.logs.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-xs mb-1">Execution Logs</Label>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded max-h-[200px] overflow-y-auto">
|
||||
{testOutput.logs.join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(testOutput.outputs).length > 0 && (
|
||||
<div>
|
||||
<Label className="text-xs mb-1">Node Outputs</Label>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded max-h-[200px] overflow-y-auto">
|
||||
{JSON.stringify(testOutput.outputs, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 border border-dashed">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LightningIcon sx={{ fontSize: 16 }} />
|
||||
<span>Workflow execution: {currentWorkflow.nodes.map(n => n.label).join(' → ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<WorkflowTester
|
||||
workflow={currentWorkflow}
|
||||
testData={testData}
|
||||
testOutput={testOutput}
|
||||
onTestDataChange={setTestData}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Input, Label } from '@/components/ui'
|
||||
import type { WorkflowDetailsPanelProps } from './types'
|
||||
|
||||
export const WorkflowDetailsPanel = ({ workflow, onUpdate }: WorkflowDetailsPanelProps) => (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Workflow Name</Label>
|
||||
<Input
|
||||
value={workflow.name}
|
||||
onChange={(e) => onUpdate({ name: e.target.value })}
|
||||
placeholder="My Workflow"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
value={workflow.description || ''}
|
||||
onChange={(e) => onUpdate({ description: e.target.value })}
|
||||
placeholder="What this workflow does..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Badge, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
|
||||
import { Card, CardContent } from '@mui/material'
|
||||
import { ArrowForward as ArrowRightIcon, Delete as DeleteIcon } from '@mui/icons-material'
|
||||
import type { WorkflowNode, LuaScript } from '@/lib/level-types'
|
||||
import { NODE_TYPE_COLORS, NODE_TYPE_ICONS, NODE_TYPE_OPTIONS } from './constants'
|
||||
|
||||
interface WorkflowNodeCardProps {
|
||||
node: WorkflowNode
|
||||
index: number
|
||||
totalNodes: number
|
||||
scripts: LuaScript[]
|
||||
onDeleteNode: (nodeId: string) => void
|
||||
onUpdateNode: (nodeId: string, updates: Partial<WorkflowNode>) => void
|
||||
}
|
||||
|
||||
const getNodeIcon = (type: WorkflowNode['type']) => NODE_TYPE_ICONS[type] || <ArrowRightIcon fontSize="small" />
|
||||
const getNodeColor = (type: WorkflowNode['type']) => NODE_TYPE_COLORS[type] || 'grey.500'
|
||||
|
||||
export const WorkflowNodeCard = ({
|
||||
node,
|
||||
index,
|
||||
totalNodes,
|
||||
scripts,
|
||||
onDeleteNode,
|
||||
onUpdateNode,
|
||||
}: WorkflowNodeCardProps) => (
|
||||
<Card className="border-2">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${getNodeColor(node.type)} flex items-center justify-center text-white shrink-0`}>
|
||||
{getNodeIcon(node.type)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Node Label</Label>
|
||||
<Input
|
||||
value={node.label}
|
||||
onChange={(e) => onUpdateNode(node.id, { label: e.target.value })}
|
||||
placeholder="Node name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Node Type</Label>
|
||||
<Select
|
||||
value={node.type}
|
||||
onValueChange={(value) => onUpdateNode(node.id, { type: value as WorkflowNode['type'] })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{NODE_TYPE_OPTIONS.map(({ type, label }) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{node.type === 'lua' && scripts.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Lua Script</Label>
|
||||
<Select
|
||||
value={node.config.scriptId || ''}
|
||||
onValueChange={(value) =>
|
||||
onUpdateNode(node.id, {
|
||||
config: { ...node.config, scriptId: value },
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a script" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{scripts.map((script) => (
|
||||
<SelectItem key={script.id} value={script.id}>
|
||||
{script.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.type === 'condition' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Condition Expression</Label>
|
||||
<Input
|
||||
value={node.config.condition || ''}
|
||||
onChange={(e) =>
|
||||
onUpdateNode(node.id, {
|
||||
config: { ...node.config, condition: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder="data.value > 10"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.type === 'transform' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Transform Expression</Label>
|
||||
<Input
|
||||
value={node.config.transform || ''}
|
||||
onChange={(e) =>
|
||||
onUpdateNode(node.id, {
|
||||
config: { ...node.config, transform: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder="{ result: data.value * 2 }"
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Step {index + 1}
|
||||
</Badge>
|
||||
{index < totalNodes - 1 && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<ArrowRightIcon sx={{ fontSize: 12 }} />
|
||||
<span>Next</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onDeleteNode(node.id)}>
|
||||
<DeleteIcon sx={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Button, Label } from '@/components/ui'
|
||||
import {
|
||||
AccountTree as GitBranchIcon,
|
||||
ArrowForward as ArrowRightIcon,
|
||||
Code as CodeIcon,
|
||||
FlashOn as LightningIcon,
|
||||
} from '@mui/icons-material'
|
||||
import type { WorkflowNodesPanelProps } from './types'
|
||||
import { WorkflowNodeCard } from './WorkflowNodeCard'
|
||||
|
||||
export const WorkflowNodesPanel = ({
|
||||
workflow,
|
||||
scripts,
|
||||
onAddNode,
|
||||
onDeleteNode,
|
||||
onUpdateNode,
|
||||
}: WorkflowNodesPanelProps) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-base">Nodes</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => onAddNode('trigger')}>
|
||||
<LightningIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Trigger
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => onAddNode('action')}>
|
||||
<ArrowRightIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Action
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => onAddNode('condition')}>
|
||||
<GitBranchIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Condition
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => onAddNode('lua')}>
|
||||
<CodeIcon sx={{ fontSize: 14, mr: 1 }} />
|
||||
Lua
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{workflow.nodes.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg">
|
||||
No nodes yet. Add nodes to build your workflow.
|
||||
</p>
|
||||
) : (
|
||||
workflow.nodes.map((node, index) => (
|
||||
<WorkflowNodeCard
|
||||
key={node.id}
|
||||
node={node}
|
||||
index={index}
|
||||
totalNodes={workflow.nodes.length}
|
||||
scripts={scripts}
|
||||
onDeleteNode={onDeleteNode}
|
||||
onUpdateNode={onUpdateNode}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Badge, Button } from '@/components/ui'
|
||||
import { Card, CardContent, CardHeader } from '@mui/material'
|
||||
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'
|
||||
import type { WorkflowSidebarProps } from './types'
|
||||
|
||||
export const WorkflowSidebar = ({
|
||||
workflows,
|
||||
selectedWorkflowId,
|
||||
onSelectWorkflow,
|
||||
onAddWorkflow,
|
||||
onDeleteWorkflow,
|
||||
}: WorkflowSidebarProps) => (
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-lg font-semibold">Workflows</div>
|
||||
<Button size="sm" onClick={onAddWorkflow}>
|
||||
<AddIcon sx={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Automation workflows</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{workflows.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No workflows yet. Create one to start.
|
||||
</p>
|
||||
) : (
|
||||
workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedWorkflowId === workflow.id
|
||||
? 'bg-accent border-accent-foreground'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
onClick={() => onSelectWorkflow(workflow.id)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{workflow.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{workflow.nodes.length} nodes
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={workflow.enabled ? 'default' : 'secondary'} className="text-xs">
|
||||
{workflow.enabled ? 'On' : 'Off'}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDeleteWorkflow(workflow.id)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon sx={{ fontSize: 14 }} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Card, CardContent, CardHeader } from '@mui/material'
|
||||
import { CardTitle } from '@/components/ui'
|
||||
import { CheckCircle as CheckCircleIcon, Cancel as XCircleIcon, FlashOn as LightningIcon } from '@mui/icons-material'
|
||||
import { Label, Textarea } from '@/components/ui'
|
||||
import type { WorkflowTesterProps } from './types'
|
||||
|
||||
export const WorkflowTester = ({
|
||||
workflow,
|
||||
testData,
|
||||
testOutput,
|
||||
onTestDataChange,
|
||||
}: WorkflowTesterProps) => (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Test Input Data (JSON)</Label>
|
||||
<Textarea
|
||||
value={testData}
|
||||
onChange={(e) => onTestDataChange(e.target.value)}
|
||||
className="font-mono text-sm min-h-[100px]"
|
||||
placeholder='{"example": "data"}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{testOutput && (
|
||||
<Card className={testOutput.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
{testOutput.success ? (
|
||||
<CheckCircleIcon sx={{ fontSize: 20, color: 'success.main' }} />
|
||||
) : (
|
||||
<XCircleIcon sx={{ fontSize: 20, color: 'error.main' }} />
|
||||
)}
|
||||
<CardTitle className="text-sm">
|
||||
{testOutput.success ? 'Workflow Execution Successful' : 'Workflow Execution Failed'}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{testOutput.error && (
|
||||
<div>
|
||||
<Label className="text-xs text-red-600 mb-1">Error</Label>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap text-red-700 bg-red-100 p-2 rounded">
|
||||
{testOutput.error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{testOutput.logs.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-xs mb-1">Execution Logs</Label>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded max-h-[200px] overflow-y-auto">
|
||||
{testOutput.logs.join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(testOutput.outputs).length > 0 && (
|
||||
<div>
|
||||
<Label className="text-xs mb-1">Node Outputs</Label>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded max-h-[200px] overflow-y-auto">
|
||||
{JSON.stringify(testOutput.outputs, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 border border-dashed">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<LightningIcon sx={{ fontSize: 16 }} />
|
||||
<span>Workflow execution: {workflow.nodes.map(n => n.label).join(' → ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
35
frontends/nextjs/src/components/workflow/editor/constants.ts
Normal file
35
frontends/nextjs/src/components/workflow/editor/constants.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
AccountTree as GitBranchIcon,
|
||||
ArrowForward as ArrowRightIcon,
|
||||
Code as CodeIcon,
|
||||
FlashOn as LightningIcon,
|
||||
} from '@mui/icons-material'
|
||||
import type { NodeTypeOption } from './types'
|
||||
import type { WorkflowNode } from '@/lib/level-types'
|
||||
|
||||
export const DEFAULT_TEST_DATA = '{"example": "data"}'
|
||||
|
||||
export const NODE_TYPE_OPTIONS: NodeTypeOption[] = [
|
||||
{ type: 'trigger', label: 'Trigger' },
|
||||
{ type: 'action', label: 'Action' },
|
||||
{ type: 'condition', label: 'Condition' },
|
||||
{ type: 'lua', label: 'Lua Script' },
|
||||
{ type: 'transform', label: 'Transform' },
|
||||
]
|
||||
|
||||
export const NODE_TYPE_ICONS: Record<WorkflowNode['type'], JSX.Element> = {
|
||||
trigger: <LightningIcon fontSize="small" />,
|
||||
action: <ArrowRightIcon fontSize="small" />,
|
||||
condition: <GitBranchIcon fontSize="small" />,
|
||||
lua: <CodeIcon fontSize="small" />,
|
||||
transform: <ArrowRightIcon fontSize="small" />,
|
||||
}
|
||||
|
||||
export const NODE_TYPE_COLORS: Record<WorkflowNode['type'], string> = {
|
||||
trigger: 'success.main',
|
||||
action: 'primary.main',
|
||||
condition: 'warning.main',
|
||||
lua: 'secondary.main',
|
||||
transform: 'info.main',
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { toast } from 'sonner'
|
||||
import { createWorkflowEngine } from '@/lib/workflow/engine/workflow-engine'
|
||||
import type { LuaScript, Workflow, WorkflowNode } from '@/lib/level-types'
|
||||
import type { WorkflowActionHandlers, WorkflowStateSetters } from './types'
|
||||
import type { WorkflowExecutionResult } from '@/lib/workflow/engine/workflow-engine'
|
||||
|
||||
interface WorkflowActionDependencies extends WorkflowStateSetters {
|
||||
workflows: Workflow[]
|
||||
currentWorkflow: Workflow | undefined
|
||||
onWorkflowsChange: (workflows: Workflow[]) => void
|
||||
scripts: LuaScript[]
|
||||
testData: string
|
||||
}
|
||||
|
||||
export const createActionHandlers = ({
|
||||
workflows,
|
||||
currentWorkflow,
|
||||
onWorkflowsChange,
|
||||
setSelectedWorkflowId,
|
||||
setTestOutput,
|
||||
setIsExecuting,
|
||||
scripts,
|
||||
testData,
|
||||
}: WorkflowActionDependencies): WorkflowActionHandlers => {
|
||||
const handleAddWorkflow = () => {
|
||||
const newWorkflow: Workflow = {
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: 'New Workflow',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
enabled: true,
|
||||
}
|
||||
onWorkflowsChange([...workflows, newWorkflow])
|
||||
setSelectedWorkflowId(newWorkflow.id)
|
||||
toast.success('Workflow created')
|
||||
}
|
||||
|
||||
const handleDeleteWorkflow = (workflowId: string) => {
|
||||
onWorkflowsChange(workflows.filter(w => w.id !== workflowId))
|
||||
if (currentWorkflow?.id === workflowId) {
|
||||
setSelectedWorkflowId(workflows.length > 1 ? workflows[0].id : null)
|
||||
}
|
||||
toast.success('Workflow deleted')
|
||||
}
|
||||
|
||||
const handleUpdateWorkflow = (updates: Partial<Workflow>) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
onWorkflowsChange(workflows.map(w => w.id === currentWorkflow.id ? { ...w, ...updates } : w))
|
||||
}
|
||||
|
||||
const handleAddNode = (type: WorkflowNode['type']) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
const newNode: WorkflowNode = {
|
||||
id: `node_${Date.now()}`,
|
||||
type,
|
||||
label: `${type.charAt(0).toUpperCase() + type.slice(1)} Node`,
|
||||
config: {},
|
||||
position: { x: 100, y: currentWorkflow.nodes.length * 100 + 100 },
|
||||
}
|
||||
|
||||
handleUpdateWorkflow({ nodes: [...currentWorkflow.nodes, newNode] })
|
||||
toast.success('Node added')
|
||||
}
|
||||
|
||||
const handleDeleteNode = (nodeId: string) => {
|
||||
if (!currentWorkflow) return
|
||||
handleUpdateWorkflow({
|
||||
nodes: currentWorkflow.nodes.filter(n => n.id !== nodeId),
|
||||
edges: currentWorkflow.edges.filter(e => e.source !== nodeId && e.target !== nodeId),
|
||||
})
|
||||
toast.success('Node deleted')
|
||||
}
|
||||
|
||||
const handleUpdateNode = (nodeId: string, updates: Partial<WorkflowNode>) => {
|
||||
if (!currentWorkflow) return
|
||||
handleUpdateWorkflow({
|
||||
nodes: currentWorkflow.nodes.map(n => n.id === nodeId ? { ...n, ...updates } : n),
|
||||
})
|
||||
}
|
||||
|
||||
const handleTestWorkflow = async () => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
setIsExecuting(true)
|
||||
setTestOutput(null)
|
||||
|
||||
try {
|
||||
let parsedData: any
|
||||
try {
|
||||
parsedData = JSON.parse(testData)
|
||||
} catch {
|
||||
parsedData = testData
|
||||
}
|
||||
|
||||
const engine = createWorkflowEngine()
|
||||
const result = await engine.executeWorkflow(currentWorkflow, {
|
||||
data: parsedData,
|
||||
user: { username: 'test_user', role: 'god' },
|
||||
scripts,
|
||||
}) as WorkflowExecutionResult
|
||||
|
||||
setTestOutput(result)
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Workflow executed successfully')
|
||||
} else {
|
||||
toast.error('Workflow execution failed')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error)))
|
||||
setTestOutput({
|
||||
success: false,
|
||||
outputs: {},
|
||||
logs: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleAddWorkflow,
|
||||
handleDeleteWorkflow,
|
||||
handleUpdateWorkflow,
|
||||
handleAddNode,
|
||||
handleDeleteNode,
|
||||
handleUpdateNode,
|
||||
handleTestWorkflow,
|
||||
}
|
||||
}
|
||||
69
frontends/nextjs/src/components/workflow/editor/types.ts
Normal file
69
frontends/nextjs/src/components/workflow/editor/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { WorkflowExecutionResult } from '@/lib/workflow/engine/workflow-engine'
|
||||
import type { LuaScript, Workflow, WorkflowNode } from '@/lib/level-types'
|
||||
|
||||
export interface WorkflowEditorProps {
|
||||
workflows: Workflow[]
|
||||
onWorkflowsChange: (workflows: Workflow[]) => void
|
||||
scripts?: LuaScript[]
|
||||
}
|
||||
|
||||
export interface WorkflowState {
|
||||
selectedWorkflowId: string | null
|
||||
testData: string
|
||||
testOutput: WorkflowExecutionResult | null
|
||||
isExecuting: boolean
|
||||
}
|
||||
|
||||
export interface WorkflowStateSetters {
|
||||
setSelectedWorkflowId: (id: string | null) => void
|
||||
setTestData: (data: string) => void
|
||||
setTestOutput: (result: WorkflowExecutionResult | null) => void
|
||||
setIsExecuting: (isExecuting: boolean) => void
|
||||
}
|
||||
|
||||
export interface WorkflowSelection {
|
||||
currentWorkflow: Workflow | undefined
|
||||
}
|
||||
|
||||
export interface WorkflowActionHandlers {
|
||||
handleAddWorkflow: () => void
|
||||
handleDeleteWorkflow: (workflowId: string) => void
|
||||
handleUpdateWorkflow: (updates: Partial<Workflow>) => void
|
||||
handleAddNode: (type: WorkflowNode['type']) => void
|
||||
handleDeleteNode: (nodeId: string) => void
|
||||
handleUpdateNode: (nodeId: string, updates: Partial<WorkflowNode>) => void
|
||||
handleTestWorkflow: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface WorkflowSidebarProps {
|
||||
workflows: Workflow[]
|
||||
selectedWorkflowId: string | null
|
||||
onSelectWorkflow: (id: string) => void
|
||||
onAddWorkflow: () => void
|
||||
onDeleteWorkflow: (id: string) => void
|
||||
}
|
||||
|
||||
export interface WorkflowDetailsPanelProps {
|
||||
workflow: Workflow
|
||||
onUpdate: (updates: Partial<Workflow>) => void
|
||||
}
|
||||
|
||||
export interface WorkflowNodesPanelProps {
|
||||
workflow: Workflow
|
||||
scripts: LuaScript[]
|
||||
onAddNode: (type: WorkflowNode['type']) => void
|
||||
onDeleteNode: (nodeId: string) => void
|
||||
onUpdateNode: (nodeId: string, updates: Partial<WorkflowNode>) => void
|
||||
}
|
||||
|
||||
export interface WorkflowTesterProps {
|
||||
workflow: Workflow
|
||||
testData: string
|
||||
testOutput: WorkflowExecutionResult | null
|
||||
onTestDataChange: (value: string) => void
|
||||
}
|
||||
|
||||
export interface NodeTypeOption {
|
||||
type: WorkflowNode['type']
|
||||
label: string
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { Workflow } from '@/lib/level-types'
|
||||
import type { WorkflowSelection, WorkflowState, WorkflowStateSetters } from './types'
|
||||
import { DEFAULT_TEST_DATA } from './constants'
|
||||
import type { WorkflowExecutionResult } from '@/lib/workflow/engine/workflow-engine'
|
||||
|
||||
export const useWorkflowState = (
|
||||
workflows: Workflow[]
|
||||
): WorkflowState & WorkflowStateSetters & WorkflowSelection => {
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(
|
||||
workflows.length > 0 ? workflows[0].id : null
|
||||
)
|
||||
const [testData, setTestData] = useState<string>(DEFAULT_TEST_DATA)
|
||||
const [testOutput, setTestOutput] = useState<WorkflowExecutionResult | null>(null)
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
|
||||
const currentWorkflow = useMemo(
|
||||
() => workflows.find(({ id }) => id === selectedWorkflowId),
|
||||
[workflows, selectedWorkflowId]
|
||||
)
|
||||
|
||||
return {
|
||||
selectedWorkflowId,
|
||||
setSelectedWorkflowId,
|
||||
testData,
|
||||
setTestData,
|
||||
testOutput,
|
||||
setTestOutput,
|
||||
isExecuting,
|
||||
setIsExecuting,
|
||||
currentWorkflow,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user