builder / components /database-manager /scheduled-function-editor.tsx
Leon4gr45's picture
Upload folder using huggingface_hub
eeb9404 verified
Raw
History Blame Contribute Delete
8.58 kB
'use client';
import React, { useState, useEffect } from 'react';
import { ScheduledFunction, EdgeFunction } from '@/lib/vfs/types';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Loader2, AlertCircle, Info } from 'lucide-react';
interface ScheduledFunctionEditorProps {
scheduledFunction: ScheduledFunction | null;
edgeFunctions: EdgeFunction[];
isOpen: boolean;
onClose: () => void;
onSave: (data: Partial<ScheduledFunction>) => Promise<void>;
}
export function ScheduledFunctionEditor({
scheduledFunction: fn,
edgeFunctions,
isOpen,
onClose,
onSave,
}: ScheduledFunctionEditorProps) {
const [name, setName] = useState(fn?.name || '');
const [functionId, setFunctionId] = useState(fn?.functionId || '');
const [cronExpression, setCronExpression] = useState(fn?.cronExpression || '');
const [timezone, setTimezone] = useState(fn?.timezone || 'UTC');
const [description, setDescription] = useState(fn?.description || '');
const [config, setConfig] = useState(fn?.config ? JSON.stringify(fn.config, null, 2) : '{}');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
setName(fn?.name || '');
setFunctionId(fn?.functionId || '');
setCronExpression(fn?.cronExpression || '');
setTimezone(fn?.timezone || 'UTC');
setDescription(fn?.description || '');
setConfig(fn?.config ? JSON.stringify(fn.config, null, 2) : '{}');
setError(null);
}
}, [fn, isOpen]);
const handleSave = async () => {
setError(null);
if (!name.trim()) {
setError('Name is required');
return;
}
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
setError('Name must be lowercase letters, numbers, and hyphens only');
return;
}
if (!functionId) {
setError('Edge function selection is required');
return;
}
if (!cronExpression.trim()) {
setError('Cron expression is required');
return;
}
let parsedConfig: Record<string, unknown> = {};
if (config.trim()) {
try {
const parsed = JSON.parse(config);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
setError('Config must be a JSON object');
return;
}
parsedConfig = parsed;
} catch {
setError('Config must be valid JSON');
return;
}
}
setSaving(true);
try {
await onSave({
name: name.trim(),
functionId,
cronExpression: cronExpression.trim(),
timezone: timezone.trim() || 'UTC',
description: description.trim() || undefined,
config: parsedConfig,
enabled: fn?.enabled ?? true,
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save scheduled function');
} finally {
setSaving(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle>
{fn ? 'Edit Schedule' : 'Create Schedule'}
</DialogTitle>
<DialogDescription>
Run an edge function on a cron schedule.
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-auto space-y-4">
{/* Name */}
<div className="space-y-2">
<Label htmlFor="sched-name">Name</Label>
<Input
id="sched-name"
value={name}
onChange={e => setName(e.target.value.toLowerCase())}
placeholder="daily-report"
disabled={!!fn}
/>
</div>
{/* Edge Function */}
<div className="space-y-2">
<Label htmlFor="sched-function">Edge Function</Label>
<Select value={functionId} onValueChange={setFunctionId}>
<SelectTrigger>
<SelectValue placeholder="Select a function..." />
</SelectTrigger>
<SelectContent>
{edgeFunctions.map(ef => (
<SelectItem key={ef.id} value={ef.id}>
{ef.name}
</SelectItem>
))}
</SelectContent>
</Select>
{edgeFunctions.length === 0 && (
<p className="text-xs text-muted-foreground">
No edge functions available. Create one in the Functions tab first.
</p>
)}
</div>
{/* Cron Expression */}
<div className="space-y-2">
<Label htmlFor="sched-cron">Cron Expression</Label>
<Input
id="sched-cron"
value={cronExpression}
onChange={e => setCronExpression(e.target.value)}
placeholder="0 8 * * *"
className="font-mono"
/>
</div>
{/* Timezone */}
<div className="space-y-2">
<Label htmlFor="sched-tz">Timezone</Label>
<Input
id="sched-tz"
value={timezone}
onChange={e => setTimezone(e.target.value)}
placeholder="UTC"
/>
<p className="text-xs text-muted-foreground">
e.g. UTC, America/New_York, Europe/London
</p>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="sched-desc">Description (optional)</Label>
<Input
id="sched-desc"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="What does this schedule do?"
/>
</div>
{/* Config */}
<div className="space-y-2">
<Label htmlFor="sched-config">Config JSON (optional)</Label>
<Textarea
id="sched-config"
value={config}
onChange={e => setConfig(e.target.value)}
placeholder="{}"
className="font-mono text-sm h-20"
/>
<p className="text-xs text-muted-foreground">
Custom data passed as the request body to the edge function.
</p>
</div>
{/* Cron Reference */}
<div className="bg-muted/30 border rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
<Info className="h-4 w-4" />
Cron Patterns <span className="font-normal text-muted-foreground">(minimum 5 min interval)</span>
</div>
<div className="grid gap-1 text-xs font-mono">
<div><span className="text-muted-foreground">*/5 * * * *</span> Every 5 minutes</div>
<div><span className="text-muted-foreground">0 * * * *</span> Every hour</div>
<div><span className="text-muted-foreground">0 8 * * *</span> Daily at 8am</div>
<div><span className="text-muted-foreground">0 0 * * 1</span> Every Monday at midnight</div>
<div><span className="text-muted-foreground">0 0 1 * *</span> First of every month</div>
</div>
</div>
{/* Error */}
{error && (
<div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 p-3 rounded-lg">
<AlertCircle className="h-4 w-4 shrink-0" />
{error}
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={onClose} disabled={saving}>
Cancel
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Saving...
</>
) : (
fn ? 'Save Changes' : 'Create Schedule'
)}
</Button>
</div>
</DialogContent>
</Dialog>
);
}