// ======================================== // Flow Toolbar Component // ======================================== // Toolbar for flow operations: Save, Load, Import Template, Export, Run, Monitor import { useState, useCallback, useEffect } from 'react'; import { useIntl } from 'react-intl'; import { Save, FolderOpen, Download, Trash2, Copy, Workflow, Loader2, ChevronDown, Library, Play, Activity, Maximize2, Minimize2, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { useFlowStore, toast } from '@/stores'; import { useExecutionStore } from '@/stores/executionStore'; import { useExecuteFlow } from '@/hooks/useFlows'; import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore'; import type { Flow } from '@/types/flow'; interface FlowToolbarProps { className?: string; onOpenTemplateLibrary?: () => void; } export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarProps) { const { formatMessage } = useIntl(); const [isFlowListOpen, setIsFlowListOpen] = useState(false); const [flowName, setFlowName] = useState(''); const [isSaving, setIsSaving] = useState(false); // Immersive mode state const isImmersiveMode = useAppStore(selectIsImmersiveMode); const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode); // Flow store const currentFlow = useFlowStore((state) => state.currentFlow); const isModified = useFlowStore((state) => state.isModified); const flows = useFlowStore((state) => state.flows); const isLoadingFlows = useFlowStore((state) => state.isLoadingFlows); const saveFlow = useFlowStore((state) => state.saveFlow); const loadFlow = useFlowStore((state) => state.loadFlow); const deleteFlow = useFlowStore((state) => state.deleteFlow); const duplicateFlow = useFlowStore((state) => state.duplicateFlow); const fetchFlows = useFlowStore((state) => state.fetchFlows); // Execution store const currentExecution = useExecutionStore((state) => state.currentExecution); const isMonitorPanelOpen = useExecutionStore((state) => state.isMonitorPanelOpen); const setMonitorPanelOpen = useExecutionStore((state) => state.setMonitorPanelOpen); const startExecution = useExecutionStore((state) => state.startExecution); // Mutations const executeFlow = useExecuteFlow(); const isExecuting = currentExecution?.status === 'running'; const isPaused = currentExecution?.status === 'paused'; // Load flows on mount useEffect(() => { fetchFlows(); }, [fetchFlows]); // Sync flow name with current flow useEffect(() => { setFlowName(currentFlow?.name || ''); }, [currentFlow?.name]); // Handle save const handleSave = useCallback(async () => { setIsSaving(true); try { const name = flowName.trim() || formatMessage({ id: 'orchestrator.toolbar.placeholder' }); // Auto-create a new flow if none exists if (!currentFlow) { const now = new Date().toISOString(); const newFlow: Flow = { id: `flow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name, version: 1, created_at: now, updated_at: now, nodes: useFlowStore.getState().nodes, edges: useFlowStore.getState().edges, variables: {}, metadata: {}, }; useFlowStore.setState({ currentFlow: newFlow }); } else if (flowName && flowName !== currentFlow.name) { // Update flow name if changed useFlowStore.setState((state) => ({ currentFlow: state.currentFlow ? { ...state.currentFlow, name } : null, })); } const saved = await saveFlow(); if (saved) { toast.success(formatMessage({ id: 'orchestrator.notifications.flowSaved' }), formatMessage({ id: 'orchestrator.notifications.savedSuccessfully' }, { name })); } else { toast.error(formatMessage({ id: 'orchestrator.notifications.saveFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotSave' })); } } catch (err) { toast.error(formatMessage({ id: 'orchestrator.notifications.saveFailed' }), formatMessage({ id: 'orchestrator.notifications.saveError' })); } finally { setIsSaving(false); } }, [currentFlow, flowName, saveFlow, formatMessage]); // Handle load const handleLoad = useCallback( async (flow: Flow) => { const loaded = await loadFlow(flow.id); if (loaded) { setIsFlowListOpen(false); toast.success(formatMessage({ id: 'orchestrator.notifications.flowLoaded' }), formatMessage({ id: 'orchestrator.notifications.loadedSuccessfully' }, { name: flow.name })); } else { toast.error(formatMessage({ id: 'orchestrator.notifications.loadFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotLoad' })); } }, [loadFlow] ); // Handle delete const handleDelete = useCallback( async (flow: Flow, e: React.MouseEvent) => { e.stopPropagation(); if (!confirm(formatMessage({ id: 'orchestrator.notifications.confirmDelete' }, { name: flow.name }))) return; const deleted = await deleteFlow(flow.id); if (deleted) { toast.success(formatMessage({ id: 'orchestrator.notifications.flowDeleted' }), formatMessage({ id: 'orchestrator.notifications.deletedSuccessfully' }, { name: flow.name })); } else { toast.error(formatMessage({ id: 'orchestrator.notifications.deleteFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotDelete' })); } }, [deleteFlow] ); // Handle duplicate const handleDuplicate = useCallback( async (flow: Flow, e: React.MouseEvent) => { e.stopPropagation(); const duplicated = await duplicateFlow(flow.id); if (duplicated) { toast.success(formatMessage({ id: 'orchestrator.notifications.flowDuplicated' }), formatMessage({ id: 'orchestrator.notifications.duplicatedSuccessfully' }, { name: duplicated.name })); } else { toast.error(formatMessage({ id: 'orchestrator.notifications.duplicateFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotDuplicate' })); } }, [duplicateFlow] ); // Handle export const handleExport = useCallback(() => { if (!currentFlow) { toast.error(formatMessage({ id: 'orchestrator.notifications.noFlow' }), formatMessage({ id: 'orchestrator.notifications.noFlowToExport' })); return; } const nodes = useFlowStore.getState().nodes; const edges = useFlowStore.getState().edges; const exportData = { ...currentFlow, nodes, edges, }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${currentFlow.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(formatMessage({ id: 'orchestrator.notifications.flowExported' }), formatMessage({ id: 'orchestrator.notifications.flowExported' })); }, [currentFlow]); // Handle run workflow const handleRun = useCallback(async () => { if (!currentFlow) return; try { // Open monitor panel automatically setMonitorPanelOpen(true); const result = await executeFlow.mutateAsync(currentFlow.id); startExecution(result.execId, currentFlow.id); } catch (error) { console.error('Failed to execute flow:', error); toast.error(formatMessage({ id: 'orchestrator.notifications.executionFailed' }), formatMessage({ id: 'orchestrator.notifications.couldNotExecute' })); } }, [currentFlow, executeFlow, startExecution, setMonitorPanelOpen]); // Handle monitor toggle const handleToggleMonitor = useCallback(() => { setMonitorPanelOpen(!isMonitorPanelOpen); }, [isMonitorPanelOpen, setMonitorPanelOpen]); return (
{/* Flow Icon and Name */}
setFlowName(e.target.value)} placeholder={formatMessage({ id: 'orchestrator.toolbar.placeholder' })} className="max-w-[200px] h-8 text-sm" /> {isModified && ( {formatMessage({ id: 'orchestrator.toolbar.unsavedChanges' })} )}
{/* Action Buttons */}
{/* Save & Load Group */} {/* Flow List Dropdown */}
{isFlowListOpen && ( <> {/* Backdrop */}
setIsFlowListOpen(false)} /> {/* Dropdown */}
{formatMessage({ id: 'orchestrator.toolbar.savedFlows' }, { count: flows.length })}
{isLoadingFlows ? (
{formatMessage({ id: 'orchestrator.toolbar.loading' })}
) : flows.length === 0 ? (
{formatMessage({ id: 'orchestrator.toolbar.noSavedFlows' })}
) : ( flows.map((flow) => (
handleLoad(flow)} className={cn( 'flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors', currentFlow?.id === flow.id && 'bg-primary/10' )} >
{flow.name}
{new Date(flow.updated_at).toLocaleDateString()}
)) )}
)}
{/* Import & Export Group */}
{/* Run & Monitor Group */}
{/* Fullscreen Toggle */}
); } export default FlowToolbar;