Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect } from 'react';4import { ScheduledFunction, EdgeFunction } from '@/lib/vfs/types';5import {6 Dialog,7 DialogContent,8 DialogDescription,9 DialogHeader,10 DialogTitle,11} from '@/components/ui/dialog';12import { Button } from '@/components/ui/button';13import { Input } from '@/components/ui/input';14import { Label } from '@/components/ui/label';15import { Textarea } from '@/components/ui/textarea';16import {17 Select,18 SelectContent,19 SelectItem,20 SelectTrigger,21 SelectValue,22} from '@/components/ui/select';23import { Loader2, AlertCircle, Info } from 'lucide-react';24 25interface ScheduledFunctionEditorProps {26 scheduledFunction: ScheduledFunction | null;27 edgeFunctions: EdgeFunction[];28 isOpen: boolean;29 onClose: () => void;30 onSave: (data: Partial<ScheduledFunction>) => Promise<void>;31}32 33export function ScheduledFunctionEditor({34 scheduledFunction: fn,35 edgeFunctions,36 isOpen,37 onClose,38 onSave,39}: ScheduledFunctionEditorProps) {40 const [name, setName] = useState(fn?.name || '');41 const [functionId, setFunctionId] = useState(fn?.functionId || '');42 const [cronExpression, setCronExpression] = useState(fn?.cronExpression || '');43 const [timezone, setTimezone] = useState(fn?.timezone || 'UTC');44 const [description, setDescription] = useState(fn?.description || '');45 const [config, setConfig] = useState(fn?.config ? JSON.stringify(fn.config, null, 2) : '{}');46 const [saving, setSaving] = useState(false);47 const [error, setError] = useState<string | null>(null);48 49 useEffect(() => {50 if (isOpen) {51 setName(fn?.name || '');52 setFunctionId(fn?.functionId || '');53 setCronExpression(fn?.cronExpression || '');54 setTimezone(fn?.timezone || 'UTC');55 setDescription(fn?.description || '');56 setConfig(fn?.config ? JSON.stringify(fn.config, null, 2) : '{}');57 setError(null);58 }59 }, [fn, isOpen]);60 61 const handleSave = async () => {62 setError(null);63 64 if (!name.trim()) {65 setError('Name is required');66 return;67 }68 if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {69 setError('Name must be lowercase letters, numbers, and hyphens only');70 return;71 }72 if (!functionId) {73 setError('Edge function selection is required');74 return;75 }76 if (!cronExpression.trim()) {77 setError('Cron expression is required');78 return;79 }80 81 let parsedConfig: Record<string, unknown> = {};82 if (config.trim()) {83 try {84 const parsed = JSON.parse(config);85 if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {86 setError('Config must be a JSON object');87 return;88 }89 parsedConfig = parsed;90 } catch {91 setError('Config must be valid JSON');92 return;93 }94 }95 96 setSaving(true);97 try {98 await onSave({99 name: name.trim(),100 functionId,101 cronExpression: cronExpression.trim(),102 timezone: timezone.trim() || 'UTC',103 description: description.trim() || undefined,104 config: parsedConfig,105 enabled: fn?.enabled ?? true,106 });107 } catch (err) {108 setError(err instanceof Error ? err.message : 'Failed to save scheduled function');109 } finally {110 setSaving(false);111 }112 };113 114 return (115 <Dialog open={isOpen} onOpenChange={onClose}>116 <DialogContent className="sm:max-w-lg max-h-[85vh] flex flex-col">117 <DialogHeader>118 <DialogTitle>119 {fn ? 'Edit Schedule' : 'Create Schedule'}120 </DialogTitle>121 <DialogDescription>122 Run an edge function on a cron schedule.123 </DialogDescription>124 </DialogHeader>125 126 <div className="flex-1 overflow-auto space-y-4">127 {/* Name */}128 <div className="space-y-2">129 <Label htmlFor="sched-name">Name</Label>130 <Input131 id="sched-name"132 value={name}133 onChange={e => setName(e.target.value.toLowerCase())}134 placeholder="daily-report"135 disabled={!!fn}136 />137 </div>138 139 {/* Edge Function */}140 <div className="space-y-2">141 <Label htmlFor="sched-function">Edge Function</Label>142 <Select value={functionId} onValueChange={setFunctionId}>143 <SelectTrigger>144 <SelectValue placeholder="Select a function..." />145 </SelectTrigger>146 <SelectContent>147 {edgeFunctions.map(ef => (148 <SelectItem key={ef.id} value={ef.id}>149 {ef.name}150 </SelectItem>151 ))}152 </SelectContent>153 </Select>154 {edgeFunctions.length === 0 && (155 <p className="text-xs text-muted-foreground">156 No edge functions available. Create one in the Functions tab first.157 </p>158 )}159 </div>160 161 {/* Cron Expression */}162 <div className="space-y-2">163 <Label htmlFor="sched-cron">Cron Expression</Label>164 <Input165 id="sched-cron"166 value={cronExpression}167 onChange={e => setCronExpression(e.target.value)}168 placeholder="0 8 * * *"169 className="font-mono"170 />171 </div>172 173 {/* Timezone */}174 <div className="space-y-2">175 <Label htmlFor="sched-tz">Timezone</Label>176 <Input177 id="sched-tz"178 value={timezone}179 onChange={e => setTimezone(e.target.value)}180 placeholder="UTC"181 />182 <p className="text-xs text-muted-foreground">183 e.g. UTC, America/New_York, Europe/London184 </p>185 </div>186 187 {/* Description */}188 <div className="space-y-2">189 <Label htmlFor="sched-desc">Description (optional)</Label>190 <Input191 id="sched-desc"192 value={description}193 onChange={e => setDescription(e.target.value)}194 placeholder="What does this schedule do?"195 />196 </div>197 198 {/* Config */}199 <div className="space-y-2">200 <Label htmlFor="sched-config">Config JSON (optional)</Label>201 <Textarea202 id="sched-config"203 value={config}204 onChange={e => setConfig(e.target.value)}205 placeholder="{}"206 className="font-mono text-sm h-20"207 />208 <p className="text-xs text-muted-foreground">209 Custom data passed as the request body to the edge function.210 </p>211 </div>212 213 {/* Cron Reference */}214 <div className="bg-muted/30 border rounded-lg p-4 space-y-2">215 <div className="flex items-center gap-2 text-sm font-medium">216 <Info className="h-4 w-4" />217 Cron Patterns <span className="font-normal text-muted-foreground">(minimum 5 min interval)</span>218 </div>219 <div className="grid gap-1 text-xs font-mono">220 <div><span className="text-muted-foreground">*/5 * * * *</span> Every 5 minutes</div>221 <div><span className="text-muted-foreground">0 * * * *</span> Every hour</div>222 <div><span className="text-muted-foreground">0 8 * * *</span> Daily at 8am</div>223 <div><span className="text-muted-foreground">0 0 * * 1</span> Every Monday at midnight</div>224 <div><span className="text-muted-foreground">0 0 1 * *</span> First of every month</div>225 </div>226 </div>227 228 {/* Error */}229 {error && (230 <div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 p-3 rounded-lg">231 <AlertCircle className="h-4 w-4 shrink-0" />232 {error}233 </div>234 )}235 </div>236 237 {/* Footer */}238 <div className="flex items-center justify-end gap-2 pt-4 border-t">239 <Button variant="outline" onClick={onClose} disabled={saving}>240 Cancel241 </Button>242 <Button onClick={handleSave} disabled={saving}>243 {saving ? (244 <>245 <Loader2 className="h-4 w-4 mr-2 animate-spin" />246 Saving...247 </>248 ) : (249 fn ? 'Save Changes' : 'Create Schedule'250 )}251 </Button>252 </div>253 </DialogContent>254 </Dialog>255 );256}257 