Skip to content

Commit 2a0224f

Browse files
committed
Initial yaml
1 parent 6cb15a6 commit 2a0224f

File tree

3 files changed

+848
-0
lines changed

3 files changed

+848
-0
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
'use client'
2+
3+
import { useState, useRef } from 'react'
4+
import { Upload, FileText, Plus, AlertCircle, CheckCircle } from 'lucide-react'
5+
import { Button } from '@/components/ui/button'
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuTrigger,
11+
} from '@/components/ui/dropdown-menu'
12+
import {
13+
Dialog,
14+
DialogContent,
15+
DialogDescription,
16+
DialogFooter,
17+
DialogHeader,
18+
DialogTitle,
19+
} from '@/components/ui/dialog'
20+
import { Textarea } from '@/components/ui/textarea'
21+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
22+
import { Alert, AlertDescription } from '@/components/ui/alert'
23+
import { createLogger } from '@/lib/logs/console-logger'
24+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
25+
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
26+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
27+
import { importWorkflowFromYaml, parseWorkflowYaml } from '@/stores/workflows/yaml/importer'
28+
import { useRouter } from 'next/navigation'
29+
import { useParams } from 'next/navigation'
30+
31+
const logger = createLogger('ImportControls')
32+
33+
interface ImportControlsProps {
34+
disabled?: boolean
35+
}
36+
37+
export function ImportControls({ disabled = false }: ImportControlsProps) {
38+
const [isImporting, setIsImporting] = useState(false)
39+
const [showYamlDialog, setShowYamlDialog] = useState(false)
40+
const [yamlContent, setYamlContent] = useState('')
41+
const [importResult, setImportResult] = useState<{
42+
success: boolean
43+
errors: string[]
44+
warnings: string[]
45+
summary?: string
46+
} | null>(null)
47+
48+
const fileInputRef = useRef<HTMLInputElement>(null)
49+
const router = useRouter()
50+
const params = useParams()
51+
const workspaceId = params.workspaceId as string
52+
53+
// Stores and hooks
54+
const { createWorkflow } = useWorkflowRegistry()
55+
const { collaborativeAddBlock, collaborativeAddEdge } = useCollaborativeWorkflow()
56+
const subBlockStore = useSubBlockStore()
57+
58+
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
59+
const file = event.target.files?.[0]
60+
if (!file) return
61+
62+
try {
63+
const content = await file.text()
64+
setYamlContent(content)
65+
setShowYamlDialog(true)
66+
} catch (error) {
67+
logger.error('Failed to read file:', error)
68+
setImportResult({
69+
success: false,
70+
errors: [`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`],
71+
warnings: []
72+
})
73+
}
74+
75+
// Reset file input
76+
if (fileInputRef.current) {
77+
fileInputRef.current.value = ''
78+
}
79+
}
80+
81+
const handleYamlImport = async () => {
82+
if (!yamlContent.trim()) {
83+
setImportResult({
84+
success: false,
85+
errors: ['YAML content is required'],
86+
warnings: []
87+
})
88+
return
89+
}
90+
91+
setIsImporting(true)
92+
setImportResult(null)
93+
94+
try {
95+
// First validate the YAML without importing
96+
const { data: yamlWorkflow, errors: parseErrors } = parseWorkflowYaml(yamlContent)
97+
98+
if (!yamlWorkflow || parseErrors.length > 0) {
99+
setImportResult({
100+
success: false,
101+
errors: parseErrors,
102+
warnings: []
103+
})
104+
return
105+
}
106+
107+
// Create a new workflow
108+
logger.info('Creating new workflow for YAML import')
109+
const newWorkflowId = await createWorkflow({
110+
name: `Imported Workflow - ${new Date().toLocaleString()}`,
111+
description: 'Workflow imported from YAML',
112+
workspaceId,
113+
})
114+
115+
// Navigate to the new workflow
116+
router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`)
117+
118+
// Small delay to ensure navigation and workflow initialization
119+
await new Promise(resolve => setTimeout(resolve, 1000))
120+
121+
// Import the YAML into the new workflow
122+
const result = await importWorkflowFromYaml(yamlContent, {
123+
addBlock: collaborativeAddBlock,
124+
addEdge: collaborativeAddEdge,
125+
applyAutoLayout: () => {
126+
// Trigger auto layout
127+
window.dispatchEvent(new CustomEvent('trigger-auto-layout'))
128+
},
129+
setSubBlockValue: (blockId: string, subBlockId: string, value: any) => {
130+
subBlockStore.setValue(blockId, subBlockId, value)
131+
},
132+
getExistingBlocks: () => {
133+
// This will be called after navigation, so we need to get blocks from the store
134+
const { useWorkflowStore } = require('@/stores/workflows/workflow/store')
135+
return useWorkflowStore.getState().blocks
136+
}
137+
})
138+
139+
setImportResult(result)
140+
141+
if (result.success) {
142+
// Close dialog on success
143+
setTimeout(() => {
144+
setShowYamlDialog(false)
145+
setYamlContent('')
146+
setImportResult(null)
147+
}, 2000)
148+
}
149+
150+
} catch (error) {
151+
logger.error('Failed to import YAML workflow:', error)
152+
setImportResult({
153+
success: false,
154+
errors: [`Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
155+
warnings: []
156+
})
157+
} finally {
158+
setIsImporting(false)
159+
}
160+
}
161+
162+
const handleOpenYamlDialog = () => {
163+
setYamlContent('')
164+
setImportResult(null)
165+
setShowYamlDialog(true)
166+
}
167+
168+
const isDisabled = disabled || isImporting
169+
170+
return (
171+
<>
172+
<Tooltip>
173+
<TooltipTrigger asChild>
174+
<DropdownMenu>
175+
<DropdownMenuTrigger asChild>
176+
{isDisabled ? (
177+
<div className='inline-flex h-10 w-10 cursor-not-allowed items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm opacity-50 ring-offset-background transition-colors [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0'>
178+
<Plus className='h-4 w-4' />
179+
</div>
180+
) : (
181+
<Button
182+
variant='ghost'
183+
size='icon'
184+
className='hover:text-primary'
185+
disabled={isDisabled}
186+
>
187+
<Plus className='h-4 w-4' />
188+
<span className='sr-only'>Import Workflow</span>
189+
</Button>
190+
)}
191+
</DropdownMenuTrigger>
192+
<DropdownMenuContent align='end' className='w-64'>
193+
<DropdownMenuItem
194+
onClick={() => fileInputRef.current?.click()}
195+
disabled={isDisabled}
196+
className='flex cursor-pointer items-center gap-2'
197+
>
198+
<Upload className='h-4 w-4' />
199+
<div className='flex flex-col'>
200+
<span>Upload YAML File</span>
201+
<span className='text-muted-foreground text-xs'>Import from .yaml or .yml file</span>
202+
</div>
203+
</DropdownMenuItem>
204+
205+
<DropdownMenuItem
206+
onClick={handleOpenYamlDialog}
207+
disabled={isDisabled}
208+
className='flex cursor-pointer items-center gap-2'
209+
>
210+
<FileText className='h-4 w-4' />
211+
<div className='flex flex-col'>
212+
<span>Paste YAML</span>
213+
<span className='text-muted-foreground text-xs'>Import from YAML text</span>
214+
</div>
215+
</DropdownMenuItem>
216+
</DropdownMenuContent>
217+
</DropdownMenu>
218+
</TooltipTrigger>
219+
<TooltipContent>
220+
{isDisabled
221+
? isImporting
222+
? 'Importing workflow...'
223+
: 'Cannot import workflow'
224+
: 'Import Workflow from YAML'}
225+
</TooltipContent>
226+
</Tooltip>
227+
228+
{/* Hidden file input */}
229+
<input
230+
ref={fileInputRef}
231+
type='file'
232+
accept='.yaml,.yml'
233+
onChange={handleFileUpload}
234+
className='hidden'
235+
/>
236+
237+
{/* YAML Import Dialog */}
238+
<Dialog open={showYamlDialog} onOpenChange={setShowYamlDialog}>
239+
<DialogContent className='max-w-4xl max-h-[80vh] flex flex-col'>
240+
<DialogHeader>
241+
<DialogTitle>Import Workflow from YAML</DialogTitle>
242+
<DialogDescription>
243+
Paste your workflow YAML content below. This will create a new workflow with the blocks and connections defined in the YAML.
244+
</DialogDescription>
245+
</DialogHeader>
246+
247+
<div className='flex-1 space-y-4 overflow-hidden'>
248+
<Textarea
249+
placeholder={`version: "1.0"
250+
blocks:
251+
start:
252+
type: "starter"
253+
name: "Start"
254+
inputs:
255+
startWorkflow: "manual"
256+
following:
257+
- "process"
258+
259+
process:
260+
type: "agent"
261+
name: "Process Data"
262+
inputs:
263+
systemPrompt: "You are a helpful assistant"
264+
userPrompt: "Process the data"
265+
model: "gpt-4"
266+
preceding:
267+
- "start"`}
268+
value={yamlContent}
269+
onChange={(e) => setYamlContent(e.target.value)}
270+
className='min-h-[300px] font-mono text-sm'
271+
disabled={isImporting}
272+
/>
273+
274+
{/* Import Result */}
275+
{importResult && (
276+
<div className='space-y-2'>
277+
{importResult.success ? (
278+
<Alert>
279+
<CheckCircle className='h-4 w-4' />
280+
<AlertDescription>
281+
<div className='font-medium text-green-700'>Import Successful!</div>
282+
{importResult.summary && (
283+
<div className='mt-1 text-sm'>{importResult.summary}</div>
284+
)}
285+
{importResult.warnings.length > 0 && (
286+
<div className='mt-2'>
287+
<div className='text-sm font-medium'>Warnings:</div>
288+
<ul className='mt-1 space-y-1 text-sm'>
289+
{importResult.warnings.map((warning, index) => (
290+
<li key={index} className='text-yellow-700'>{warning}</li>
291+
))}
292+
</ul>
293+
</div>
294+
)}
295+
</AlertDescription>
296+
</Alert>
297+
) : (
298+
<Alert variant='destructive'>
299+
<AlertCircle className='h-4 w-4' />
300+
<AlertDescription>
301+
<div className='font-medium'>Import Failed</div>
302+
{importResult.errors.length > 0 && (
303+
<ul className='mt-2 space-y-1 text-sm'>
304+
{importResult.errors.map((error, index) => (
305+
<li key={index}>{error}</li>
306+
))}
307+
</ul>
308+
)}
309+
</AlertDescription>
310+
</Alert>
311+
)}
312+
</div>
313+
)}
314+
</div>
315+
316+
<DialogFooter>
317+
<Button
318+
variant='outline'
319+
onClick={() => setShowYamlDialog(false)}
320+
disabled={isImporting}
321+
>
322+
Cancel
323+
</Button>
324+
<Button
325+
onClick={handleYamlImport}
326+
disabled={isImporting || !yamlContent.trim()}
327+
>
328+
{isImporting ? 'Importing...' : 'Import Workflow'}
329+
</Button>
330+
</DialogFooter>
331+
</DialogContent>
332+
</Dialog>
333+
</>
334+
)
335+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
5858
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
5959
import { ExportControls } from './components/export-controls/export-controls'
60+
import { ImportControls } from './components/import-controls/import-controls'
6061
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
6162
import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal'
6263
import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item'
@@ -1288,6 +1289,7 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
12881289
{renderDuplicateButton()}
12891290
{renderAutoLayoutButton()}
12901291
{renderDebugModeToggle()}
1292+
<ImportControls disabled={!userPermissions.canEdit} />
12911293
<ExportControls disabled={!userPermissions.canRead} />
12921294
{/* {renderPublishButton()} */}
12931295
{renderDeployButton()}

0 commit comments

Comments
 (0)