This commit is contained in:
Siddharth Ganesan
2025-07-08 20:54:21 -07:00
parent 2a0224f6ae
commit bacb6f3831
3 changed files with 132 additions and 123 deletions

View File

@@ -1,14 +1,10 @@
'use client'
import { useState, useRef } from 'react'
import { Upload, FileText, Plus, AlertCircle, CheckCircle } from 'lucide-react'
import { useRef, useState } from 'react'
import { AlertCircle, CheckCircle, FileText, Plus, Upload } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Dialog,
DialogContent,
@@ -17,16 +13,19 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Textarea } from '@/components/ui/textarea'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { createLogger } from '@/lib/logs/console-logger'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { importWorkflowFromYaml, parseWorkflowYaml } from '@/stores/workflows/yaml/importer'
import { useRouter } from 'next/navigation'
import { useParams } from 'next/navigation'
const logger = createLogger('ImportControls')
@@ -44,12 +43,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
warnings: string[]
summary?: string
} | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const router = useRouter()
const params = useParams()
const workspaceId = params.workspaceId as string
// Stores and hooks
const { createWorkflow } = useWorkflowRegistry()
const { collaborativeAddBlock, collaborativeAddEdge } = useCollaborativeWorkflow()
@@ -67,8 +66,10 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
logger.error('Failed to read file:', error)
setImportResult({
success: false,
errors: [`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`],
warnings: []
errors: [
`Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
],
warnings: [],
})
}
@@ -83,7 +84,7 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
setImportResult({
success: false,
errors: ['YAML content is required'],
warnings: []
warnings: [],
})
return
}
@@ -94,12 +95,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
try {
// First validate the YAML without importing
const { data: yamlWorkflow, errors: parseErrors } = parseWorkflowYaml(yamlContent)
if (!yamlWorkflow || parseErrors.length > 0) {
setImportResult({
success: false,
errors: parseErrors,
warnings: []
warnings: [],
})
return
}
@@ -116,9 +117,9 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
router.push(`/workspace/${workspaceId}/w/${newWorkflowId}`)
// Small delay to ensure navigation and workflow initialization
await new Promise(resolve => setTimeout(resolve, 1000))
await new Promise((resolve) => setTimeout(resolve, 1000))
// Import the YAML into the new workflow
// Import the YAML into the new workflow
const result = await importWorkflowFromYaml(yamlContent, {
addBlock: collaborativeAddBlock,
addEdge: collaborativeAddEdge,
@@ -133,7 +134,7 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
// This will be called after navigation, so we need to get blocks from the store
const { useWorkflowStore } = require('@/stores/workflows/workflow/store')
return useWorkflowStore.getState().blocks
}
},
})
setImportResult(result)
@@ -146,13 +147,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
setImportResult(null)
}, 2000)
}
} catch (error) {
logger.error('Failed to import YAML workflow:', error)
setImportResult({
success: false,
errors: [`Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`],
warnings: []
warnings: [],
})
} finally {
setIsImporting(false)
@@ -198,10 +198,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
<Upload className='h-4 w-4' />
<div className='flex flex-col'>
<span>Upload YAML File</span>
<span className='text-muted-foreground text-xs'>Import from .yaml or .yml file</span>
<span className='text-muted-foreground text-xs'>
Import from .yaml or .yml file
</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleOpenYamlDialog}
disabled={isDisabled}
@@ -236,11 +238,12 @@ export function ImportControls({ disabled = false }: ImportControlsProps) {
{/* YAML Import Dialog */}
<Dialog open={showYamlDialog} onOpenChange={setShowYamlDialog}>
<DialogContent className='max-w-4xl max-h-[80vh] flex flex-col'>
<DialogContent className='flex max-h-[80vh] max-w-4xl flex-col'>
<DialogHeader>
<DialogTitle>Import Workflow from YAML</DialogTitle>
<DialogDescription>
Paste your workflow YAML content below. This will create a new workflow with the blocks and connections defined in the YAML.
Paste your workflow YAML content below. This will create a new workflow with the
blocks and connections defined in the YAML.
</DialogDescription>
</DialogHeader>
@@ -284,10 +287,12 @@ blocks:
)}
{importResult.warnings.length > 0 && (
<div className='mt-2'>
<div className='text-sm font-medium'>Warnings:</div>
<div className='font-medium text-sm'>Warnings:</div>
<ul className='mt-1 space-y-1 text-sm'>
{importResult.warnings.map((warning, index) => (
<li key={index} className='text-yellow-700'> {warning}</li>
<li key={index} className='text-yellow-700'>
{warning}
</li>
))}
</ul>
</div>
@@ -321,10 +326,7 @@ blocks:
>
Cancel
</Button>
<Button
onClick={handleYamlImport}
disabled={isImporting || !yamlContent.trim()}
>
<Button onClick={handleYamlImport} disabled={isImporting || !yamlContent.trim()}>
{isImporting ? 'Importing...' : 'Import Workflow'}
</Button>
</DialogFooter>
@@ -332,4 +334,4 @@ blocks:
</Dialog>
</>
)
}
}

View File

@@ -57,8 +57,8 @@ import {
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
import { ExportControls } from './components/export-controls/export-controls'
import { ImportControls } from './components/import-controls/import-controls'
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
import { ImportControls } from './components/import-controls/import-controls'
import { MarketplaceModal } from './components/marketplace-modal/marketplace-modal'
import { NotificationDropdownItem } from './components/notification-dropdown-item/notification-dropdown-item'
import { UserAvatarStack } from './components/user-avatar-stack/user-avatar-stack'

View File

@@ -1,7 +1,6 @@
import { load as yamlParse } from 'js-yaml'
import { createLogger } from '@/lib/logs/console-logger'
import { getBlock } from '@/blocks'
import type { BlockConfig } from '@/blocks/types'
const logger = createLogger('WorkflowYamlImporter')
@@ -48,59 +47,62 @@ interface ImportResult {
/**
* Parse YAML content and validate its structure
*/
export function parseWorkflowYaml(yamlContent: string): { data: YamlWorkflow | null; errors: string[] } {
export function parseWorkflowYaml(yamlContent: string): {
data: YamlWorkflow | null
errors: string[]
} {
const errors: string[] = []
try {
const data = yamlParse(yamlContent) as any
// Validate top-level structure
if (!data || typeof data !== 'object') {
errors.push('Invalid YAML: Root must be an object')
return { data: null, errors }
}
if (!data.version) {
errors.push('Missing required field: version')
}
if (!data.blocks || typeof data.blocks !== 'object') {
errors.push('Missing or invalid field: blocks')
return { data: null, errors }
}
// Validate blocks structure
Object.entries(data.blocks).forEach(([blockId, block]: [string, any]) => {
if (!block || typeof block !== 'object') {
errors.push(`Invalid block definition for '${blockId}': must be an object`)
return
}
if (!block.type || typeof block.type !== 'string') {
errors.push(`Invalid block '${blockId}': missing or invalid 'type' field`)
}
if (!block.name || typeof block.name !== 'string') {
errors.push(`Invalid block '${blockId}': missing or invalid 'name' field`)
}
if (block.inputs && typeof block.inputs !== 'object') {
errors.push(`Invalid block '${blockId}': 'inputs' must be an object`)
}
if (block.preceding && !Array.isArray(block.preceding)) {
errors.push(`Invalid block '${blockId}': 'preceding' must be an array`)
}
if (block.following && !Array.isArray(block.following)) {
errors.push(`Invalid block '${blockId}': 'following' must be an array`)
}
})
if (errors.length > 0) {
return { data: null, errors }
}
return { data: data as YamlWorkflow, errors: [] }
} catch (error) {
errors.push(`YAML parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`)
@@ -114,63 +116,65 @@ export function parseWorkflowYaml(yamlContent: string): { data: YamlWorkflow | n
function validateBlockReferences(yamlWorkflow: YamlWorkflow): string[] {
const errors: string[] = []
const blockIds = new Set(Object.keys(yamlWorkflow.blocks))
Object.entries(yamlWorkflow.blocks).forEach(([blockId, block]) => {
// Check preceding references
if (block.preceding) {
block.preceding.forEach(precedingId => {
block.preceding.forEach((precedingId) => {
if (!blockIds.has(precedingId)) {
errors.push(`Block '${blockId}' references non-existent preceding block '${precedingId}'`)
}
})
}
// Check following references
if (block.following) {
block.following.forEach(followingId => {
block.following.forEach((followingId) => {
if (!blockIds.has(followingId)) {
errors.push(`Block '${blockId}' references non-existent following block '${followingId}'`)
}
})
}
})
return errors
}
/**
* Validate that block types exist and are valid
*/
function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[], warnings: string[] } {
function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; warnings: string[] } {
const errors: string[] = []
const warnings: string[] = []
Object.entries(yamlWorkflow.blocks).forEach(([blockId, block]) => {
// Check if block type exists
const blockConfig = getBlock(block.type)
// Special handling for container blocks
if (block.type === 'loop' || block.type === 'parallel') {
// These are valid container types
return
}
if (!blockConfig) {
errors.push(`Unknown block type '${block.type}' for block '${blockId}'`)
return
}
// Validate inputs against block configuration
if (block.inputs && blockConfig.subBlocks) {
Object.keys(block.inputs).forEach(inputKey => {
const subBlockConfig = blockConfig.subBlocks.find(sb => sb.id === inputKey)
Object.keys(block.inputs).forEach((inputKey) => {
const subBlockConfig = blockConfig.subBlocks.find((sb) => sb.id === inputKey)
if (!subBlockConfig) {
warnings.push(`Block '${blockId}' has unknown input '${inputKey}' for type '${block.type}'`)
warnings.push(
`Block '${blockId}' has unknown input '${inputKey}' for type '${block.type}'`
)
}
})
}
})
return { errors, warnings }
}
@@ -178,75 +182,77 @@ function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[], war
* Calculate positions for blocks based on their connections
* Uses a simple layered approach similar to the auto-layout algorithm
*/
function calculateBlockPositions(yamlWorkflow: YamlWorkflow): Record<string, { x: number; y: number }> {
function calculateBlockPositions(
yamlWorkflow: YamlWorkflow
): Record<string, { x: number; y: number }> {
const positions: Record<string, { x: number; y: number }> = {}
const blockIds = Object.keys(yamlWorkflow.blocks)
// Find starter blocks (no preceding connections)
const starterBlocks = blockIds.filter(id => {
const starterBlocks = blockIds.filter((id) => {
const block = yamlWorkflow.blocks[id]
return !block.preceding || block.preceding.length === 0
})
// If no starter blocks found, use first block as starter
if (starterBlocks.length === 0 && blockIds.length > 0) {
starterBlocks.push(blockIds[0])
}
// Build layers
const layers: string[][] = []
const visited = new Set<string>()
const queue = [...starterBlocks]
// BFS to organize blocks into layers
while (queue.length > 0) {
const currentLayer: string[] = []
const currentLayerSize = queue.length
for (let i = 0; i < currentLayerSize; i++) {
const blockId = queue.shift()!
if (visited.has(blockId)) continue
visited.add(blockId)
currentLayer.push(blockId)
// Add following blocks to queue
const block = yamlWorkflow.blocks[blockId]
if (block.following) {
block.following.forEach(followingId => {
block.following.forEach((followingId) => {
if (!visited.has(followingId)) {
queue.push(followingId)
}
})
}
}
if (currentLayer.length > 0) {
layers.push(currentLayer)
}
}
// Add any remaining blocks as isolated layer
const remainingBlocks = blockIds.filter(id => !visited.has(id))
const remainingBlocks = blockIds.filter((id) => !visited.has(id))
if (remainingBlocks.length > 0) {
layers.push(remainingBlocks)
}
// Calculate positions
const horizontalSpacing = 600
const verticalSpacing = 200
const startX = 150
const startY = 300
layers.forEach((layer, layerIndex) => {
const layerX = startX + layerIndex * horizontalSpacing
layer.forEach((blockId, blockIndex) => {
const blockY = startY + (blockIndex - layer.length / 2) * verticalSpacing
positions[blockId] = { x: layerX, y: blockY }
})
})
return positions
}
@@ -258,27 +264,27 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
const warnings: string[] = []
const blocks: ImportedBlock[] = []
const edges: ImportedEdge[] = []
// Validate block references
const referenceErrors = validateBlockReferences(yamlWorkflow)
errors.push(...referenceErrors)
// Validate block types
const { errors: typeErrors, warnings: typeWarnings } = validateBlockTypes(yamlWorkflow)
errors.push(...typeErrors)
warnings.push(...typeWarnings)
if (errors.length > 0) {
return { blocks: [], edges: [], errors, warnings }
}
// Calculate positions
const positions = calculateBlockPositions(yamlWorkflow)
// Convert blocks
Object.entries(yamlWorkflow.blocks).forEach(([blockId, yamlBlock]) => {
const position = positions[blockId] || { x: 100, y: 100 }
const importedBlock: ImportedBlock = {
id: blockId,
type: yamlBlock.type,
@@ -286,7 +292,7 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
inputs: yamlBlock.inputs || {},
position,
}
// Add container-specific data
if (yamlBlock.type === 'loop' || yamlBlock.type === 'parallel') {
importedBlock.data = {
@@ -295,16 +301,16 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
type: yamlBlock.type === 'loop' ? 'loopNode' : 'parallelNode',
}
}
blocks.push(importedBlock)
})
// Convert edges from connections
Object.entries(yamlWorkflow.blocks).forEach(([blockId, yamlBlock]) => {
if (yamlBlock.following) {
yamlBlock.following.forEach(targetId => {
yamlBlock.following.forEach((targetId) => {
const edgeId = `${blockId}-${targetId}-${Date.now()}`
const edge: ImportedEdge = {
id: edgeId,
source: blockId,
@@ -313,12 +319,12 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
targetHandle: 'target',
type: 'workflowEdge',
}
edges.push(edge)
})
}
})
return { blocks, edges, errors, warnings }
}
@@ -344,31 +350,33 @@ export async function importWorkflowFromYaml(
}
): Promise<{ success: boolean; errors: string[]; warnings: string[]; summary?: string }> {
logger.info('Starting YAML workflow import')
try {
// Parse YAML
const { data: yamlWorkflow, errors: parseErrors } = parseWorkflowYaml(yamlContent)
if (!yamlWorkflow || parseErrors.length > 0) {
return { success: false, errors: parseErrors, warnings: [] }
}
// Convert to importable format
const { blocks, edges, errors, warnings } = convertYamlToWorkflow(yamlWorkflow)
if (errors.length > 0) {
return { success: false, errors, warnings }
}
logger.info(`Importing ${blocks.length} blocks and ${edges.length} edges`)
// Check for existing blocks (new workflows already have a starter block)
const existingBlocks = workflowActions.getExistingBlocks()
const existingStarterBlocks = Object.values(existingBlocks).filter((block: any) => block.type === 'starter')
const existingStarterBlocks = Object.values(existingBlocks).filter(
(block: any) => block.type === 'starter'
)
let actualBlocksCreated = 0
let starterBlockId: string | null = null
// Create blocks, but handle starter blocks specially
for (const block of blocks) {
if (block.type === 'starter') {
@@ -377,7 +385,7 @@ export async function importWorkflowFromYaml(
const existingStarter = existingStarterBlocks[0] as any
starterBlockId = existingStarter.id
logger.debug(`Using existing starter block: ${starterBlockId}`)
// Update the starter block's inputs if needed
Object.entries(block.inputs).forEach(([inputKey, inputValue]) => {
if (inputValue !== undefined && inputValue !== null && starterBlockId !== null) {
@@ -415,9 +423,9 @@ export async function importWorkflowFromYaml(
block.extent
)
actualBlocksCreated++
// Update edges to use the new generated ID
edges.forEach(edge => {
edges.forEach((edge) => {
if (edge.source === block.id) {
edge.source = generatedId
}
@@ -425,15 +433,15 @@ export async function importWorkflowFromYaml(
edge.target = generatedId
}
})
// Store mapping for setting inputs later
block.id = generatedId
}
// Update edges to use the starter block ID
if (block.type === 'starter' && starterBlockId !== null) {
const starterId = starterBlockId // TypeScript now knows this is string
edges.forEach(edge => {
edges.forEach((edge) => {
if (edge.source === block.id) {
edge.source = starterId
}
@@ -443,10 +451,10 @@ export async function importWorkflowFromYaml(
})
}
}
// Small delay to ensure blocks are created before adding edges
await new Promise(resolve => setTimeout(resolve, 200))
await new Promise((resolve) => setTimeout(resolve, 200))
// Create edges
let edgesCreated = 0
for (const edge of edges) {
@@ -454,7 +462,7 @@ export async function importWorkflowFromYaml(
logger.debug(`Creating edge: ${edge.source} -> ${edge.target}`)
workflowActions.addEdge({
...edge,
id: crypto.randomUUID() // Generate unique edge ID
id: crypto.randomUUID(), // Generate unique edge ID
})
edgesCreated++
} catch (error) {
@@ -462,10 +470,10 @@ export async function importWorkflowFromYaml(
warnings.push(`Failed to create connection from ${edge.source} to ${edge.target}`)
}
}
// Small delay before setting input values
await new Promise(resolve => setTimeout(resolve, 200))
await new Promise((resolve) => setTimeout(resolve, 200))
// Set input values for non-starter blocks
for (const block of blocks) {
if (block.type !== 'starter') {
@@ -482,30 +490,29 @@ export async function importWorkflowFromYaml(
})
}
}
// Apply auto layout after a delay
setTimeout(() => {
logger.debug('Applying auto layout')
workflowActions.applyAutoLayout()
}, 800)
const summary = `Successfully imported ${actualBlocksCreated} new blocks and ${edgesCreated} connections`
logger.info(summary)
return {
success: true,
errors: [],
warnings,
summary
summary,
}
} catch (error) {
const errorMessage = `Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`
logger.error(errorMessage, error)
return {
success: false,
errors: [errorMessage],
warnings: []
warnings: [],
}
}
}
}