/** * SchedulerPanel - ZCLAW Scheduler UI * * Displays scheduled jobs, event triggers, workflows, and run history. * * Design based on ZCLAW Dashboard v0.4.0 */ import { useState, useEffect, useCallback } from 'react'; import { useHandStore } from '../store/handStore'; import { useWorkflowStore, type Workflow } from '../store/workflowStore'; import { useAgentStore } from '../store/agentStore'; import { useConfigStore } from '../store/configStore'; import { WorkflowEditor } from './WorkflowEditor'; import { WorkflowHistory } from './WorkflowHistory'; import { TriggersPanel } from './TriggersPanel'; import { Clock, Zap, History, Plus, RefreshCw, Loader2, Calendar, X, AlertCircle, CheckCircle, GitBranch, Play, } from 'lucide-react'; // === Tab Types === type TabType = 'scheduled' | 'triggers' | 'workflows' | 'history'; // === Schedule Type === type ScheduleType = 'cron' | 'interval' | 'once'; type TargetType = 'agent' | 'hand' | 'workflow'; // === Form State Interface === interface JobFormData { name: string; scheduleType: ScheduleType; cronExpression: string; intervalValue: number; intervalUnit: 'minutes' | 'hours' | 'days'; runOnceDate: string; runOnceTime: string; targetType: TargetType; targetId: string; description: string; enabled: boolean; } const initialFormData: JobFormData = { name: '', scheduleType: 'cron', cronExpression: '', intervalValue: 30, intervalUnit: 'minutes', runOnceDate: '', runOnceTime: '', targetType: 'hand', targetId: '', description: '', enabled: true, }; // === Tab Button Component === function TabButton({ active, onClick, icon: Icon, label, }: { active: boolean; onClick: () => void; icon: React.ComponentType<{ className?: string }>; label: string; }) { return ( ); } // === Empty State Component === function EmptyState({ icon: Icon, title, description, actionLabel, onAction, }: { icon: React.ComponentType<{ className?: string }>; title: string; description: string; actionLabel?: string; onAction?: () => void; }) { return (

{title}

{description}

{actionLabel && onAction && ( )}
); } // === Create Job Modal Component === interface CreateJobModalProps { isOpen: boolean; onClose: () => void; onSuccess: () => void; } function CreateJobModal({ isOpen, onClose, onSuccess }: CreateJobModalProps) { // Store state - use domain stores const hands = useHandStore((s) => s.hands); const workflows = useWorkflowStore((s) => s.workflows); const clones = useAgentStore((s) => s.clones); const createScheduledTask = useConfigStore((s) => s.createScheduledTask); const loadHands = useHandStore((s) => s.loadHands); const loadWorkflows = useWorkflowStore((s) => s.loadWorkflows); const loadClones = useAgentStore((s) => s.loadClones); const [formData, setFormData] = useState(initialFormData); const [errors, setErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [errorMessage, setErrorMessage] = useState(''); // Load available targets on mount useEffect(() => { if (isOpen) { loadHands(); loadWorkflows(); loadClones(); } }, [isOpen, loadHands, loadWorkflows, loadClones]); // Reset form when modal opens useEffect(() => { if (isOpen) { setFormData(initialFormData); setErrors({}); setSubmitStatus('idle'); setErrorMessage(''); } }, [isOpen]); // Generate schedule string based on type const generateScheduleString = useCallback((): string => { switch (formData.scheduleType) { case 'cron': return formData.cronExpression; case 'interval': { const unitMap = { minutes: 'm', hours: 'h', days: 'd' }; return `every ${formData.intervalValue}${unitMap[formData.intervalUnit]}`; } case 'once': { if (formData.runOnceDate && formData.runOnceTime) { return `once ${formData.runOnceDate}T${formData.runOnceTime}`; } return ''; } default: return ''; } }, [formData]); // Validate form const validateForm = useCallback((): boolean => { const newErrors: Record = {}; if (!formData.name.trim()) { newErrors.name = '任务名称不能为空'; } switch (formData.scheduleType) { case 'cron': if (!formData.cronExpression.trim()) { newErrors.cronExpression = 'Cron 表达式不能为空'; } else if (!isValidCron(formData.cronExpression)) { newErrors.cronExpression = 'Cron 表达式格式无效'; } break; case 'interval': if (formData.intervalValue <= 0) { newErrors.intervalValue = '间隔时间必须大于 0'; } break; case 'once': if (!formData.runOnceDate) { newErrors.runOnceDate = '请选择执行日期'; } if (!formData.runOnceTime) { newErrors.runOnceTime = '请选择执行时间'; } break; } if (!formData.targetId) { newErrors.targetId = '请选择要执行的目标'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }, [formData]); // Simple cron expression validator const isValidCron = (expression: string): boolean => { // Basic validation: 5 or 6 fields separated by spaces const parts = expression.trim().split(/\s+/); return parts.length >= 5 && parts.length <= 6; }; // Handle form submission const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { return; } setIsSubmitting(true); setSubmitStatus('idle'); setErrorMessage(''); try { const schedule = generateScheduleString(); await createScheduledTask({ name: formData.name.trim(), schedule, scheduleType: formData.scheduleType, target: { type: formData.targetType, id: formData.targetId, }, description: formData.description.trim() || undefined, enabled: formData.enabled, }); setSubmitStatus('success'); setTimeout(() => { onSuccess(); onClose(); }, 1500); } catch (err) { setSubmitStatus('error'); setErrorMessage(err instanceof Error ? err.message : '创建任务失败'); } finally { setIsSubmitting(false); } }; // Update form field const updateField = (field: K, value: JobFormData[K]) => { setFormData(prev => ({ ...prev, [field]: value })); // Clear error when field is updated if (errors[field]) { setErrors(prev => { const newErrors = { ...prev }; delete newErrors[field]; return newErrors; }); } }; // Get available targets based on type const getAvailableTargets = (): Array<{ id: string; name?: string }> => { switch (formData.targetType) { case 'agent': return clones.map(c => ({ id: c.id, name: c.name })); case 'hand': return hands.map(h => ({ id: h.id, name: h.name })); case 'workflow': return workflows.map(w => ({ id: w.id, name: w.name })); default: return []; } }; if (!isOpen) return null; return (
{/* Backdrop */}
{/* Modal */}
{/* Header */}

新建定时任务

创建一个定时执行的任务

{/* Form */}
{/* Task Name */}
updateField('name', e.target.value)} placeholder="例如: 每日数据同步" className={`w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 ${ errors.name ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' }`} /> {errors.name && (

{errors.name}

)}
{/* Schedule Type */}
{[ { value: 'cron', label: 'Cron 表达式' }, { value: 'interval', label: '固定间隔' }, { value: 'once', label: '执行一次' }, ].map((option) => ( ))}
{/* Cron Expression */} {formData.scheduleType === 'cron' && (
updateField('cronExpression', e.target.value)} placeholder="0 9 * * * (每天 9:00)" className={`w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono ${ errors.cronExpression ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' }`} /> {errors.cronExpression && (

{errors.cronExpression}

)}

格式: 分 时 日 月 周 (例如: 0 9 * * * 表示每天 9:00)

)} {/* Interval Settings */} {formData.scheduleType === 'interval' && (
updateField('intervalValue', parseInt(e.target.value) || 0)} className={`w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 ${ errors.intervalValue ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' }`} /> {errors.intervalValue && (

{errors.intervalValue}

)}
)} {/* Run Once Settings */} {formData.scheduleType === 'once' && (
updateField('runOnceDate', e.target.value)} className={`w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 ${ errors.runOnceDate ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' }`} /> {errors.runOnceDate && (

{errors.runOnceDate}

)}
updateField('runOnceTime', e.target.value)} className={`w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 ${ errors.runOnceTime ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' }`} /> {errors.runOnceTime && (

{errors.runOnceTime}

)}
)} {/* Target Selection */}
{[ { value: 'hand', label: 'Hand' }, { value: 'workflow', label: 'Workflow' }, { value: 'agent', label: 'Agent' }, ].map((option) => ( ))}
{/* Target Selection Dropdown */}
{errors.targetId && (

{errors.targetId}

)} {getAvailableTargets().length === 0 && (

当前没有可用的 {formData.targetType === 'hand' ? 'Hands' : formData.targetType === 'workflow' ? 'Workflows' : 'Agents'}

)}
{/* Description */}