CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
functions-manager.tsx310 linesDownload Raw Back to database-manager
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