Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect } from 'react';4import { EdgeFunction } from '@/lib/vfs/types';5import {6 Plus, Loader2, AlertCircle, Code2, MoreVertical, Pencil, Trash2,7 ToggleLeft, ToggleRight, Copy, ExternalLink, CheckCircle28} from 'lucide-react';9import { Button } from '@/components/ui/button';10import {11 DropdownMenu,12 DropdownMenuContent,13 DropdownMenuItem,14 DropdownMenuTrigger,15} from '@/components/ui/dropdown-menu';16import { FunctionEditor } from './function-editor';17import { cn } from '@/lib/utils';18import type { FunctionsDataProvider } from './data-providers';19 20interface FunctionsManagerProps {21 deploymentId?: string;22 dataProvider?: FunctionsDataProvider;23 hideRuntimeFeatures?: boolean;24 workspaceId?: string;25}26 27export function FunctionsManager({ deploymentId, dataProvider, hideRuntimeFeatures, workspaceId }: FunctionsManagerProps) {28 const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api';29 const [functions, setFunctions] = useState<EdgeFunction[]>([]);30 const [loading, setLoading] = useState(true);31 const [error, setError] = useState<string | null>(null);32 const [editingFunction, setEditingFunction] = useState<EdgeFunction | null>(null);33 const [isCreating, setIsCreating] = useState(false);34 const [copiedUrl, setCopiedUrl] = useState<string | null>(null);35 36 useEffect(() => {37 loadFunctions();38 }, [deploymentId, dataProvider]);39 40 const loadFunctions = async () => {41 try {42 setLoading(true);43 setError(null);44 if (dataProvider) {45 setFunctions(await dataProvider.list());46 } else if (deploymentId) {47 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/functions`);48 if (!res.ok) {49 const data = await res.json();50 throw new Error(data.error || 'Failed to load functions');51 }52 const data = await res.json();53 setFunctions(data.functions);54 }55 } catch (err) {56 setError(err instanceof Error ? err.message : 'Failed to load functions');57 } finally {58 setLoading(false);59 }60 };61 62 const toggleEnabled = async (fn: EdgeFunction) => {63 try {64 if (dataProvider) {65 await dataProvider.toggle(fn.id, !fn.enabled);66 } else if (deploymentId) {67 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/functions/${fn.id}`, {68 method: 'PUT',69 headers: { 'Content-Type': 'application/json' },70 body: JSON.stringify({ enabled: !fn.enabled }),71 });72 if (!res.ok) throw new Error('Failed to update function');73 } else {74 return;75 }76 await loadFunctions();77 } catch (err) {78 console.error('Failed to toggle function:', err);79 }80 };81 82 const deleteFunction = async (fn: EdgeFunction) => {83 if (!confirm(`Delete function "${fn.name}"? This cannot be undone.`)) return;84 85 try {86 if (dataProvider) {87 await dataProvider.remove(fn.id);88 } else if (deploymentId) {89 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/functions/${fn.id}`, {90 method: 'DELETE',91 });92 if (!res.ok) throw new Error('Failed to delete function');93 } else {94 return;95 }96 await loadFunctions();97 } catch (err) {98 console.error('Failed to delete function:', err);99 }100 };101 102 const copyUrl = (fn: EdgeFunction) => {103 if (!deploymentId) return;104 const url = `${window.location.origin}${apiBase}/deployments/${deploymentId}/functions/${fn.name}`;105 navigator.clipboard.writeText(url);106 setCopiedUrl(fn.id);107 setTimeout(() => setCopiedUrl(null), 2000);108 };109 110 const handleSave = async (data: Partial<EdgeFunction>) => {111 try {112 if (dataProvider) {113 await dataProvider.save(editingFunction?.id || null, data);114 } else if (!deploymentId) {115 throw new Error('No deployment ID available');116 } else if (editingFunction) {117 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/functions/${editingFunction.id}`, {118 method: 'PUT',119 headers: { 'Content-Type': 'application/json' },120 body: JSON.stringify(data),121 });122 if (!res.ok) {123 const err = await res.json();124 throw new Error(err.error || 'Failed to update function');125 }126 } else {127 const res = await fetch(`${apiBase}/admin/deployments/${deploymentId}/functions`, {128 method: 'POST',129 headers: { 'Content-Type': 'application/json' },130 body: JSON.stringify(data),131 });132 if (!res.ok) {133 const err = await res.json();134 throw new Error(err.error || 'Failed to create function');135 }136 }137 138 setEditingFunction(null);139 setIsCreating(false);140 await loadFunctions();141 } catch (err) {142 throw err;143 }144 };145 146 if (loading) {147 return (148 <div className="flex items-center justify-center h-full">149 <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />150 </div>151 );152 }153 154 if (error) {155 return (156 <div className="flex flex-col items-center justify-center h-full gap-4">157 <AlertCircle className="h-8 w-8 text-destructive" />158 <p className="text-sm text-muted-foreground">{error}</p>159 <Button variant="outline" onClick={loadFunctions}>160 Retry161 </Button>162 </div>163 );164 }165 166 return (167 <div className="h-full flex flex-col">168 <div className="flex items-center justify-between mb-4">169 <h3 className="text-sm font-medium">Edge Functions</h3>170 <Button size="sm" onClick={() => setIsCreating(true)}>171 <Plus className="h-4 w-4 mr-1" />172 New Function173 </Button>174 </div>175 176 <div className="flex-1 overflow-auto">177 {functions.length === 0 ? (178 <div className="flex flex-col items-center justify-center h-full p-8 text-center border rounded-lg">179 <Code2 className="h-8 w-8 text-muted-foreground mb-2" />180 <p className="text-sm text-muted-foreground">No edge functions yet</p>181 <p className="text-xs text-muted-foreground mt-1 mb-4">182 Create your first API endpoint183 </p>184 <Button size="sm" onClick={() => setIsCreating(true)}>185 <Plus className="h-4 w-4 mr-1" />186 Create Function187 </Button>188 </div>189 ) : (190 <div className="grid gap-3">191 {functions.map(fn => (192 <div193 key={fn.id}194 className={cn(195 "border rounded-lg p-4 transition-colors",196 !fn.enabled && "opacity-60 bg-muted/30"197 )}198 >199 <div className="flex items-start justify-between gap-2">200 <div className="flex-1 min-w-0 overflow-hidden">201 <div className="flex items-center gap-2 flex-wrap">202 <Code2 className="h-4 w-4 text-blue-500 shrink-0" />203 <span className="font-mono font-medium truncate">{fn.name}</span>204 <span className={cn(205 "text-xs px-1.5 py-0.5 rounded shrink-0",206 fn.method === 'ANY' ? "bg-purple-500/20 text-purple-600" :207 fn.method === 'GET' ? "bg-green-500/20 text-green-600" :208 fn.method === 'POST' ? "bg-blue-500/20 text-blue-600" :209 fn.method === 'PUT' ? "bg-yellow-500/20 text-yellow-600" :210 "bg-red-500/20 text-red-600"211 )}>212 {fn.method}213 </span>214 {!fn.enabled && (215 <span className="text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">disabled</span>216 )}217 </div>218 {fn.description && (219 <p className="text-sm text-muted-foreground mt-1 truncate">220 {fn.description}221 </p>222 )}223 <div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">224 <span className="shrink-0">Timeout: {fn.timeoutMs / 1000}s</span>225 {!hideRuntimeFeatures && deploymentId && (226 <button227 onClick={() => copyUrl(fn)}228 className="flex items-center gap-1 hover:text-foreground transition-colors shrink-0"229 >230 {copiedUrl === fn.id ? (231 <>232 <CheckCircle2 className="h-3 w-3 text-green-500" />233 Copied!234 </>235 ) : (236 <>237 <Copy className="h-3 w-3" />238 Copy URL239 </>240 )}241 </button>242 )}243 </div>244 </div>245 246 <DropdownMenu>247 <DropdownMenuTrigger asChild>248 <Button variant="ghost" size="sm">249 <MoreVertical className="h-4 w-4" />250 </Button>251 </DropdownMenuTrigger>252 <DropdownMenuContent align="end">253 <DropdownMenuItem onClick={() => setEditingFunction(fn)}>254 <Pencil className="h-4 w-4 mr-2" />255 Edit256 </DropdownMenuItem>257 <DropdownMenuItem onClick={() => toggleEnabled(fn)}>258 {fn.enabled ? (259 <>260 <ToggleLeft className="h-4 w-4 mr-2" />261 Disable262 </>263 ) : (264 <>265 <ToggleRight className="h-4 w-4 mr-2" />266 Enable267 </>268 )}269 </DropdownMenuItem>270 {!hideRuntimeFeatures && deploymentId && (271 <DropdownMenuItem272 onClick={() => window.open(`${apiBase}/deployments/${deploymentId}/functions/${fn.name}`, '_blank')}273 >274 <ExternalLink className="h-4 w-4 mr-2" />275 Open in Browser276 </DropdownMenuItem>277 )}278 <DropdownMenuItem279 onClick={() => deleteFunction(fn)}280 className="text-destructive"281 >282 <Trash2 className="h-4 w-4 mr-2" />283 Delete284 </DropdownMenuItem>285 </DropdownMenuContent>286 </DropdownMenu>287 </div>288 </div>289 ))}290 </div>291 )}292 </div>293 294 {/* Function Editor Dialog */}295 {(isCreating || editingFunction) && (296 <FunctionEditor297 deploymentId={deploymentId || ''}298 function={editingFunction}299 isOpen={true}300 onClose={() => {301 setIsCreating(false);302 setEditingFunction(null);303 }}304 onSave={handleSave}305 />306 )}307 </div>308 );309}310 