Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect } from 'react';4import MonacoEditor from '@monaco-editor/react';5import { EdgeFunction } from '@/lib/vfs/types';6import {7 Dialog,8 DialogContent,9 DialogDescription,10 DialogHeader,11 DialogTitle,12} from '@/components/ui/dialog';13import { Button } from '@/components/ui/button';14import { Input } from '@/components/ui/input';15import { Label } from '@/components/ui/label';16import {17 Select,18 SelectContent,19 SelectItem,20 SelectTrigger,21 SelectValue,22} from '@/components/ui/select';23import { Loader2, AlertCircle, Info } from 'lucide-react';24import { useTheme } from 'next-themes';25 26interface FunctionEditorProps {27 deploymentId: string;28 function: EdgeFunction | null;29 isOpen: boolean;30 onClose: () => void;31 onSave: (data: Partial<EdgeFunction>) => Promise<void>;32}33 34const DEFAULT_CODE = `// Access the request object35// request.method - HTTP method36// request.body - Parsed request body37// request.query - Query string parameters38// request.headers - Request headers39 40// Use the database41// db.query(sql, params) - Execute SELECT query42// db.run(sql, params) - Execute INSERT/UPDATE/DELETE43// db.all(sql, params) - Alias for query44 45// Return a response46// Response.json(data, status) - Return JSON47// Response.text(text, status) - Return text48// Response.error(message, status) - Return error49 50// Example: List items51const items = db.all('SELECT * FROM items LIMIT 10');52Response.json({ items });53`;54 55export function FunctionEditor({56 deploymentId,57 function: fn,58 isOpen,59 onClose,60 onSave,61}: FunctionEditorProps) {62 const [name, setName] = useState(fn?.name || '');63 const [description, setDescription] = useState(fn?.description || '');64 const [method, setMethod] = useState<EdgeFunction['method']>(fn?.method || 'ANY');65 const [code, setCode] = useState(fn?.code || DEFAULT_CODE);66 const [timeoutMs, setTimeoutMs] = useState(fn?.timeoutMs || 5000);67 const [saving, setSaving] = useState(false);68 const [error, setError] = useState<string | null>(null);69 const { resolvedTheme } = useTheme();70 const [mounted, setMounted] = useState(false);71 72 useEffect(() => {73 setMounted(true);74 }, []);75 76 useEffect(() => {77 if (isOpen) {78 setName(fn?.name || '');79 setDescription(fn?.description || '');80 setMethod(fn?.method || 'ANY');81 setCode(fn?.code || DEFAULT_CODE);82 setTimeoutMs(fn?.timeoutMs || 5000);83 setError(null);84 }85 }, [fn, isOpen]);86 87 const handleSave = async () => {88 setError(null);89 90 // Basic validation91 if (!name.trim()) {92 setError('Function name is required');93 return;94 }95 if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {96 setError('Name must be lowercase letters, numbers, and hyphens only');97 return;98 }99 if (!code.trim()) {100 setError('Function code is required');101 return;102 }103 104 setSaving(true);105 try {106 await onSave({107 name: name.trim(),108 description: description.trim() || undefined,109 method,110 code,111 timeoutMs,112 enabled: fn?.enabled ?? true,113 });114 } catch (err) {115 setError(err instanceof Error ? err.message : 'Failed to save function');116 } finally {117 setSaving(false);118 }119 };120 121 if (!mounted) return null;122 123 return (124 <Dialog open={isOpen} onOpenChange={onClose}>125 <DialogContent className="sm:max-w-3xl h-[85vh] flex flex-col">126 <DialogHeader>127 <DialogTitle>128 {fn ? 'Edit Function' : 'Create Function'}129 </DialogTitle>130 <DialogDescription>131 Define an HTTP endpoint that can access your deployment database.132 </DialogDescription>133 </DialogHeader>134 135 <div className="flex-1 overflow-auto space-y-4">136 {/* Name & Method */}137 <div className="grid grid-cols-3 gap-4">138 <div className="col-span-2 space-y-2">139 <Label htmlFor="name">Function Name</Label>140 <Input141 id="name"142 value={name}143 onChange={e => setName(e.target.value.toLowerCase())}144 placeholder="my-function"145 disabled={!!fn}146 />147 {deploymentId && (148 <p className="text-xs text-muted-foreground">149 URL: /api/deployments/{deploymentId}/functions/<span className="font-mono">{name || 'name'}</span>150 </p>151 )}152 </div>153 <div className="space-y-2">154 <Label htmlFor="method">HTTP Method</Label>155 <Select value={method} onValueChange={v => setMethod(v as EdgeFunction['method'])}>156 <SelectTrigger>157 <SelectValue />158 </SelectTrigger>159 <SelectContent>160 <SelectItem value="ANY">ANY</SelectItem>161 <SelectItem value="GET">GET</SelectItem>162 <SelectItem value="POST">POST</SelectItem>163 <SelectItem value="PUT">PUT</SelectItem>164 <SelectItem value="DELETE">DELETE</SelectItem>165 </SelectContent>166 </Select>167 </div>168 </div>169 170 {/* Description */}171 <div className="space-y-2">172 <Label htmlFor="description">Description (optional)</Label>173 <Input174 id="description"175 value={description}176 onChange={e => setDescription(e.target.value)}177 placeholder="What does this function do?"178 />179 </div>180 181 {/* Timeout */}182 <div className="space-y-2">183 <Label htmlFor="timeout">Timeout (seconds)</Label>184 <div className="flex items-center gap-2">185 <Input186 id="timeout"187 type="number"188 min={1}189 max={30}190 value={timeoutMs / 1000}191 onChange={e => setTimeoutMs(Math.min(30, Math.max(1, parseInt(e.target.value) || 5)) * 1000)}192 className="w-24"193 />194 <span className="text-sm text-muted-foreground">1-30 seconds</span>195 </div>196 </div>197 198 {/* Code Editor */}199 <div className="space-y-2">200 <Label>Function Code</Label>201 <div className="h-64 border rounded-lg overflow-hidden">202 <MonacoEditor203 language="javascript"204 theme={resolvedTheme === 'dark' ? 'vs-dark' : 'light'}205 value={code}206 onChange={value => setCode(value || '')}207 options={{208 minimap: { enabled: false },209 fontSize: 13,210 scrollBeyondLastLine: false,211 automaticLayout: true,212 tabSize: 2,213 }}214 />215 </div>216 </div>217 218 {/* API Reference */}219 <div className="bg-muted/30 border rounded-lg p-4 space-y-2">220 <div className="flex items-center gap-2 text-sm font-medium">221 <Info className="h-4 w-4" />222 Available APIs223 </div>224 <div className="grid gap-2 text-xs font-mono">225 <div><span className="text-blue-500">request</span>.method, .body, .query, .headers, .params, .path</div>226 <div><span className="text-green-500">db</span>.query(sql, params), .run(sql, params), .all(sql, params)</div>227 <div><span className="text-purple-500">Response</span>.json(data, status), .text(text, status), .error(msg, status)</div>228 <div><span className="text-yellow-500">fetch</span>(url, options) - External HTTP requests</div>229 </div>230 </div>231 232 {/* Error */}233 {error && (234 <div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 p-3 rounded-lg">235 <AlertCircle className="h-4 w-4" />236 {error}237 </div>238 )}239 </div>240 241 {/* Footer */}242 <div className="flex items-center justify-end gap-2 pt-4 border-t">243 <Button variant="outline" onClick={onClose} disabled={saving}>244 Cancel245 </Button>246 <Button onClick={handleSave} disabled={saving}>247 {saving ? (248 <>249 <Loader2 className="h-4 w-4 mr-2 animate-spin" />250 Saving...251 </>252 ) : (253 fn ? 'Save Changes' : 'Create Function'254 )}255 </Button>256 </div>257 </DialogContent>258 </Dialog>259 );260}261 