mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Edge functions empty state (#33802)
* page components * page component changes * settings but broken saving * rvert * use sheet for provider * styling * remove things * Some refactoring and fixing, added JSDocs to layouts * Smol refactor * Fix * Update JSDocs * updated scaffolding * update edge functions layout * remove params * Clean up * Spelling * Clean up FormFieldWrappers * One last clean up * create edge functon * use component * flag * fix merge errors * fix imports * fix merge issues * fix import * fix flag * remove unused * simple empty * empty state improvements * move templates * re-add flag * use templates * flag * copy functions * feature flag it * Fix issues and clean up * Smol fix * Add comment --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
a3fee4e76a
commit
593625c574
13 files changed
+422
-107
No files matched your search
+1
-3
@@ -52,7 +52,7 @@ const FormSchema = z.object({
|
||||
verify_jwt: z.boolean(),
|
||||
})
|
||||
|
||||
const EdgeFunctionDetails = () => {
|
||||
export const EdgeFunctionDetails = () => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef, functionSlug } = useParams()
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
@@ -365,5 +365,3 @@ const EdgeFunctionDetails = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EdgeFunctionDetails
|
||||
@@ -14,7 +14,7 @@ interface EdgeFunctionsListItemProps {
|
||||
function: EdgeFunctionsResponse
|
||||
}
|
||||
|
||||
const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) => {
|
||||
export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
@@ -46,7 +46,9 @@ const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) =
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<div className="text-xs text-foreground-light flex gap-2 items-center truncate">
|
||||
<p className="font-mono truncate hidden md:inline">{endpoint}</p>
|
||||
<p title={endpoint} className="font-mono truncate hidden md:inline max-w-[30rem]">
|
||||
{endpoint}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-foreground-lighter hover:text-foreground transition"
|
||||
@@ -99,5 +101,3 @@ const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) =
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default EdgeFunctionsListItem
|
||||
@@ -52,6 +52,37 @@ Deno.serve(async (req) => {
|
||||
} catch (err) {
|
||||
return new Response(String(err?.message ?? err), { status: 500 })
|
||||
}
|
||||
})`,
|
||||
},
|
||||
{
|
||||
value: 'storage-upload',
|
||||
name: 'Supabase Storage Upload',
|
||||
description: 'Upload files to Supabase Storage',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { createClient } from 'jsr:@supabase/supabase-js@2'
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
|
||||
)
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file')
|
||||
const { data, error } = await supabase
|
||||
.storage
|
||||
.from('your-bucket')
|
||||
.upload(
|
||||
\`files/\${crypto.randomUUID()}\`,
|
||||
file,
|
||||
{ contentType: file.type }
|
||||
)
|
||||
if (error) throw error
|
||||
return new Response(
|
||||
JSON.stringify({ data }),
|
||||
{ headers: { 'Content-Type': 'application/json' }}
|
||||
)
|
||||
})`,
|
||||
},
|
||||
{
|
||||
@@ -95,4 +126,177 @@ app.get(/(.*)/, (req, res) => {
|
||||
|
||||
app.listen(8000);`,
|
||||
},
|
||||
{
|
||||
value: 'openai-completion',
|
||||
name: 'OpenAI Text Completion',
|
||||
description: 'Generate text completions using OpenAI GPT-3',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { Configuration, OpenAIApi } from 'npm:openai@3.3.0'
|
||||
|
||||
const openAi = new OpenAIApi(
|
||||
new Configuration({
|
||||
apiKey: Deno.env.get('OPENAI_API_KEY')
|
||||
})
|
||||
)
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { prompt } = await req.json()
|
||||
const completion = await openAi.createCompletion({
|
||||
model: 'text-davinci-003',
|
||||
prompt,
|
||||
max_tokens: 200
|
||||
})
|
||||
return new Response(
|
||||
JSON.stringify({ text: completion.data.choices[0].text }),
|
||||
{ headers: { 'Content-Type': 'application/json' }}
|
||||
)
|
||||
})`,
|
||||
},
|
||||
{
|
||||
value: 'stripe-webhook',
|
||||
name: 'Stripe Webhook Example',
|
||||
description: 'Handle Stripe webhook events securely',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import Stripe from 'npm:stripe@12.0.0'
|
||||
|
||||
const stripe = new Stripe(Deno.env.get('STRIPE_API_KEY')!)
|
||||
const endpointSecret = Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const signature = req.headers.get('stripe-signature')
|
||||
try {
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
await req.text(),
|
||||
signature!,
|
||||
endpointSecret
|
||||
)
|
||||
switch (event.type) {
|
||||
case 'payment_intent.succeeded':
|
||||
// Handle successful payment
|
||||
break
|
||||
}
|
||||
return new Response(JSON.stringify({ received: true }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
} catch (err) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: err.message }),
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
})`,
|
||||
},
|
||||
{
|
||||
value: 'resend-email',
|
||||
name: 'Send Emails',
|
||||
description: 'Send emails using the Resend API',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
|
||||
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { to, subject, html } = await req.json()
|
||||
const res = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: \`Bearer \${RESEND_API_KEY}\`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: 'you@example.com',
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
})`,
|
||||
},
|
||||
{
|
||||
value: 'image-transform',
|
||||
name: 'Image Transformation',
|
||||
description: 'Transform images using ImageMagick WASM',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import {
|
||||
ImageMagick,
|
||||
initializeImageMagick,
|
||||
} from "npm:@imagemagick/magick-wasm@0.0.30"
|
||||
|
||||
await initializeImageMagick()
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const formData = await req.formData()
|
||||
const file = formData.get('file')
|
||||
const content = await file.arrayBuffer()
|
||||
const result = await ImageMagick.read(new Uint8Array(content), (img) => {
|
||||
img.resize(500, 300)
|
||||
img.blur(60, 5)
|
||||
return img.write(data => data)
|
||||
})
|
||||
return new Response(
|
||||
result,
|
||||
{ headers: { 'Content-Type': 'image/png' }}
|
||||
)
|
||||
})`,
|
||||
},
|
||||
{
|
||||
value: 'discord-bot',
|
||||
name: 'Discord Bot Example',
|
||||
description: 'Build a Slash Command Discord Bot',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { verifyDiscordRequest } from './_shared/discord.ts'
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { valid } = await verifyDiscordRequest(req)
|
||||
if (!valid) {
|
||||
return new Response('Invalid request', { status: 401 })
|
||||
}
|
||||
const message = await req.json()
|
||||
if (message.type === 1) {
|
||||
return new Response(
|
||||
JSON.stringify({ type: 1 }),
|
||||
{ headers: { 'Content-Type': 'application/json' }}
|
||||
)
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
type: 4,
|
||||
data: { content: 'Hello from Supabase Edge Functions!' }
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'application/json' }}
|
||||
)
|
||||
})`,
|
||||
},
|
||||
{
|
||||
value: 'websocket-server',
|
||||
name: 'Websocket Server Example',
|
||||
description: 'Create a real-time WebSocket server',
|
||||
content: `// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
|
||||
Deno.serve((req) => {
|
||||
const upgrade = req.headers.get("upgrade") || ""
|
||||
if (upgrade.toLowerCase() != "websocket") {
|
||||
return new Response("request isn't trying to upgrade to websocket.")
|
||||
}
|
||||
const { socket, response } = Deno.upgradeWebSocket(req)
|
||||
socket.onopen = () => {
|
||||
console.log("client connected!")
|
||||
socket.send('Welcome to Supabase Edge Functions!')
|
||||
}
|
||||
socket.onmessage = (e) => {
|
||||
console.log("client sent message:", e.data)
|
||||
socket.send(new Date().toString())
|
||||
}
|
||||
return response
|
||||
})`,
|
||||
},
|
||||
]
|
||||
@@ -1,19 +1,132 @@
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { useParams } from 'common'
|
||||
import { ScaffoldSectionTitle } from 'components/layouts/Scaffold'
|
||||
import { ResourceItem } from 'components/ui/Resource/ResourceItem'
|
||||
import { ResourceList } from 'components/ui/Resource/ResourceList'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { Code, Terminal } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import {
|
||||
AiIconAnimation,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogSection,
|
||||
DialogTrigger,
|
||||
} from 'ui'
|
||||
import { EDGE_FUNCTION_TEMPLATES } from './Functions.templates'
|
||||
import { TerminalInstructions } from './TerminalInstructions'
|
||||
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { Button } from 'ui'
|
||||
import TerminalInstructions from './TerminalInstructions'
|
||||
export const FunctionsEmptyState = () => {
|
||||
const { ref } = useParams()
|
||||
const router = useRouter()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const edgeFunctionCreate = useFlag('edgeFunctionCreate')
|
||||
|
||||
const FunctionsEmptyState = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full mt-8">
|
||||
<div className="col-span-8 bg-surface-100 px-5 py-4 border rounded-md">
|
||||
<TerminalInstructions />
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create your first edge function</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 grid grid-cols-[repeat(auto-fit,minmax(240px,1fr))] divide-y md:divide-y-0 md:divide-x divide-default items-stretch">
|
||||
{/* CLI Option */}
|
||||
<div className="p-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal strokeWidth={1.5} size={20} />
|
||||
<h4 className="text-base text-foreground">Via CLI</h4>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light mb-4 mt-1">
|
||||
Create and deploy functions using the Supabase CLI. Ideal for local development and
|
||||
version control.
|
||||
</p>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="default">View CLI Instructions</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent size="large">
|
||||
<DialogSection padding="small">
|
||||
<TerminalInstructions />
|
||||
</DialogSection>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* AI Assistant Option */}
|
||||
<div className="p-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<AiIconAnimation size={20} />
|
||||
<h4 className="text-base text-foreground">AI Assistant</h4>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light mb-4 mt-1">
|
||||
Let our AI assistant help you create functions. Perfect for kickstarting a function.
|
||||
</p>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() =>
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
initialInput: 'Create a new edge function that ...',
|
||||
suggestions: {
|
||||
title:
|
||||
'I can help you create a new edge function. Here are a few example prompts to get you started:',
|
||||
prompts: [
|
||||
'Create a new edge function that processes payments with Stripe',
|
||||
'Create a new edge function that sends emails with Resend',
|
||||
'Create a new edge function that generates PDFs from HTML templates',
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Open Assistant
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Editor Option */}
|
||||
{edgeFunctionCreate && (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code strokeWidth={1.5} size={20} />
|
||||
<h4 className="text-base text-foreground">Via Editor</h4>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-light mb-4 mt-1">
|
||||
Create and edit functions directly in the browser. Download to local at any time.
|
||||
</p>
|
||||
<Button type="default" onClick={() => router.push(`/project/${ref}/functions/new`)}>
|
||||
Open Editor
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{edgeFunctionCreate && (
|
||||
<>
|
||||
<ScaffoldSectionTitle className="text-xl mb-4 mt-12">
|
||||
Start with a template
|
||||
</ScaffoldSectionTitle>
|
||||
<ResourceList>
|
||||
{EDGE_FUNCTION_TEMPLATES.map((template) => (
|
||||
<ResourceItem
|
||||
key={template.name}
|
||||
media={<Code strokeWidth={1.5} size={16} className="-translate-y-[9px]" />}
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Link href={`/project/${ref}/functions/new?template=${template.value}`}>
|
||||
<p>{template.name}</p>
|
||||
<p className="text-sm text-foreground-lighter">{template.description}</p>
|
||||
</Link>
|
||||
</ResourceItem>
|
||||
))}
|
||||
</ResourceList>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FunctionsEmptyState
|
||||
@@ -21,7 +21,7 @@ interface TerminalInstructionsProps extends ComponentPropsWithoutRef<typeof Coll
|
||||
removeBorder?: boolean
|
||||
}
|
||||
|
||||
const TerminalInstructions = forwardRef<
|
||||
export const TerminalInstructions = forwardRef<
|
||||
ElementRef<typeof Collapsible_Shadcn_>,
|
||||
TerminalInstructionsProps
|
||||
>(({ closable = false, removeBorder = false, ...props }, ref) => {
|
||||
@@ -158,5 +158,3 @@ const TerminalInstructions = forwardRef<
|
||||
})
|
||||
|
||||
TerminalInstructions.displayName = 'TerminalInstructions'
|
||||
|
||||
export default TerminalInstructions
|
||||
@@ -1,4 +0,0 @@
|
||||
export { default as EdgeFunctionDetails } from './EdgeFunctionDetails/EdgeFunctionDetails'
|
||||
export { default as EdgeFunctionsListItem } from './EdgeFunctionsListItem'
|
||||
export { default as FunctionsEmptyState } from './FunctionsEmptyState'
|
||||
export { default as TerminalInstructions } from './TerminalInstructions'
|
||||
@@ -13,9 +13,7 @@ const EdgeFunctionsProductMenu = () => {
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
name: 'Edge Functions',
|
||||
key: 'functions',
|
||||
url: `/project/${projectRef}/functions`,
|
||||
title: 'Manage',
|
||||
items: [
|
||||
{
|
||||
name: 'Functions',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useParams } from 'common'
|
||||
import { ChevronLeft } from 'lucide-react'
|
||||
import { Fragment, ReactNode } from 'react'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { cn } from 'ui'
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -59,14 +60,13 @@ export const PageHeader = ({
|
||||
{item.element ? (
|
||||
item.element
|
||||
) : item.href ? (
|
||||
<BreadcrumbLink
|
||||
className="flex items-center gap-2"
|
||||
href={!!ref ? item.href.replace('[ref]', ref) : item.href}
|
||||
>
|
||||
{breadcrumbs.length === 1 && !isCompact && (
|
||||
<ChevronLeft size={16} strokeWidth={1.5} />
|
||||
)}
|
||||
{item.label}
|
||||
<BreadcrumbLink asChild className="flex items-center gap-2">
|
||||
<Link href={!!ref ? item.href.replace('[ref]', ref) : item.href}>
|
||||
{breadcrumbs.length === 1 && !isCompact && (
|
||||
<ChevronLeft size={16} strokeWidth={1.5} />
|
||||
)}
|
||||
{item.label}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbPageItem className="flex items-center gap-2">
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react'
|
||||
import { Edit, File, Plus, Trash } from 'lucide-react'
|
||||
import { Button } from 'ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import AIEditor from 'components/ui/AIEditor'
|
||||
import {
|
||||
TreeView,
|
||||
TreeViewItem,
|
||||
flattenTree,
|
||||
Button,
|
||||
ContextMenu_Shadcn_,
|
||||
ContextMenuTrigger_Shadcn_,
|
||||
ContextMenuContent_Shadcn_,
|
||||
ContextMenuItem_Shadcn_,
|
||||
ContextMenuSeparator_Shadcn_,
|
||||
ContextMenuTrigger_Shadcn_,
|
||||
flattenTree,
|
||||
TreeView,
|
||||
TreeViewItem,
|
||||
} from 'ui'
|
||||
|
||||
interface FileData {
|
||||
@@ -61,6 +62,18 @@ const FileExplorerAndEditor = ({
|
||||
}: FileExplorerAndEditorProps) => {
|
||||
const selectedFile = files.find((f) => f.selected) ?? files[0]
|
||||
|
||||
const [treeData, setTreeData] = useState({
|
||||
name: '',
|
||||
children: files.map((file) => ({
|
||||
id: file.id.toString(),
|
||||
name: file.name,
|
||||
metadata: {
|
||||
isEditing: false,
|
||||
originalId: file.id,
|
||||
},
|
||||
})),
|
||||
})
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
const updatedFiles = files.map((file) =>
|
||||
file.id === selectedFile.id ? { ...file, content: value } : file
|
||||
@@ -132,20 +145,8 @@ const FileExplorerAndEditor = ({
|
||||
setTreeData(updatedTreeData)
|
||||
}
|
||||
|
||||
const [treeData, setTreeData] = React.useState({
|
||||
name: '',
|
||||
children: files.map((file) => ({
|
||||
id: file.id.toString(),
|
||||
name: file.name,
|
||||
metadata: {
|
||||
isEditing: false,
|
||||
originalId: file.id,
|
||||
},
|
||||
})),
|
||||
})
|
||||
|
||||
// Update treeData when files change
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setTreeData({
|
||||
name: '',
|
||||
children: files.map((file) => ({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { edgeFunctionsKeys } from './keys'
|
||||
import { components } from 'api-types'
|
||||
|
||||
export type EdgeFunctionsDeployVariables = {
|
||||
projectRef: string
|
||||
@@ -61,7 +61,7 @@ export const useEdgeFunctionDeployMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await Promise.all([queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef))])
|
||||
await queryClient.invalidateQueries(edgeFunctionsKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EdgeFunctionDetails } from 'components/interfaces/Functions'
|
||||
import { EdgeFunctionDetails } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
@@ -2,15 +2,13 @@ import { ChevronDown, Code, ExternalLink, Terminal } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import {
|
||||
EdgeFunctionsListItem,
|
||||
FunctionsEmptyState,
|
||||
TerminalInstructions,
|
||||
} from 'components/interfaces/Functions'
|
||||
import { EdgeFunctionsListItem } from 'components/interfaces/Functions/EdgeFunctionsListItem'
|
||||
import { FunctionsEmptyState } from 'components/interfaces/Functions/FunctionsEmptyState'
|
||||
import { TerminalInstructions } from 'components/interfaces/Functions/TerminalInstructions'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout'
|
||||
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
|
||||
import { ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
@@ -93,20 +91,16 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
|
||||
)
|
||||
|
||||
const secondaryActions = [
|
||||
...(!hasFunctions
|
||||
? [
|
||||
<Button asChild key="edge-function-examples" type="default" icon={<ExternalLink />}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions"
|
||||
>
|
||||
Examples
|
||||
</a>
|
||||
</Button>,
|
||||
]
|
||||
: []),
|
||||
<DocsButton key="docs" href="https://supabase.com/docs/guides/functions" />,
|
||||
<Button asChild key="edge-function-examples" type="default" icon={<ExternalLink />}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions"
|
||||
>
|
||||
Examples
|
||||
</a>
|
||||
</Button>,
|
||||
<ButtonTooltip
|
||||
key="edge-function-create"
|
||||
type="default"
|
||||
@@ -145,18 +139,14 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
|
||||
secondaryActions={secondaryActions}
|
||||
>
|
||||
<ScaffoldContainer size="large">
|
||||
{isLoading && (
|
||||
<div className="pt-8">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)}
|
||||
<ScaffoldSection isFullWidth>
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve edge functions" />}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve edge functions" />}
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
{hasFunctions ? (
|
||||
<div className="py-6 space-y-4">
|
||||
{isSuccess && (
|
||||
<>
|
||||
{hasFunctions ? (
|
||||
<Table
|
||||
head={
|
||||
<>
|
||||
@@ -176,12 +166,12 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<FunctionsEmptyState />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<FunctionsEmptyState />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { AlertCircle, Book, Check } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import * as yup from 'yup'
|
||||
import { toast } from 'sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { EDGE_FUNCTION_TEMPLATES } from 'components/interfaces/Functions/Functions.templates'
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
|
||||
// Array of adjectives and nouns for random function name generation
|
||||
const ADJECTIVES = [
|
||||
@@ -73,11 +72,11 @@ const NOUNS = [
|
||||
const FUNCTION_NAME_REGEX = /^[A-Za-z0-9_-]+$/
|
||||
|
||||
// Define form schema with yup
|
||||
const formSchema = yup.object({
|
||||
functionName: yup
|
||||
const FormSchema = z.object({
|
||||
functionName: z
|
||||
.string()
|
||||
.required('Function name is required')
|
||||
.matches(FUNCTION_NAME_REGEX, 'Only letters, numbers, hyphens, and underscores allowed'),
|
||||
.min(1, 'Function name is required')
|
||||
.regex(FUNCTION_NAME_REGEX, 'Only letters, numbers, hyphens, and underscores allowed'),
|
||||
})
|
||||
|
||||
// Generate a random function name
|
||||
@@ -94,11 +93,11 @@ const sanitizeFunctionName = (name: string): string => {
|
||||
}
|
||||
|
||||
// Type for the form values
|
||||
type FormValues = yup.InferType<typeof formSchema>
|
||||
type FormValues = z.infer<typeof FormSchema>
|
||||
|
||||
const NewFunctionPage = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { ref, template } = useParams()
|
||||
const project = useSelectedProject()
|
||||
const isOptedInToAI = useOrgOptedIntoAi()
|
||||
const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM
|
||||
@@ -119,15 +118,15 @@ const NewFunctionPage = () => {
|
||||
const [isPreviewingTemplate, setIsPreviewingTemplate] = useState(false)
|
||||
const [savedCode, setSavedCode] = useState<string>('')
|
||||
|
||||
// Setup form with react-hook-form and yup resolver
|
||||
const form = useForm<FormValues>({
|
||||
resolver: yupResolver(formSchema),
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
functionName: generateRandomFunctionName(),
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: deployFunction, isLoading: isDeploying } = useEdgeFunctionDeployMutation({
|
||||
// [Joshen] To investigate: For some reason, the invalidation for list of edge functions isn't triggering
|
||||
onSuccess: () => {
|
||||
toast.success('Successfully deployed edge function')
|
||||
const functionName = form.getValues('functionName')
|
||||
@@ -218,6 +217,24 @@ const NewFunctionPage = () => {
|
||||
}
|
||||
}, [edgeFunctionCreate, ref, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (template) {
|
||||
const templateMeta = EDGE_FUNCTION_TEMPLATES.find((x) => x.value === template)
|
||||
if (templateMeta) {
|
||||
form.reset({ functionName: template })
|
||||
setFiles([
|
||||
{
|
||||
id: 1,
|
||||
name: 'index.ts',
|
||||
selected: true,
|
||||
content: templateMeta.content,
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [template])
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
size="full"
|
||||
@@ -243,7 +260,7 @@ const NewFunctionPage = () => {
|
||||
Templates
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="w-[300px] p-0">
|
||||
<PopoverContent_Shadcn_ className="w-[300px] p-0" align="end">
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search templates..." />
|
||||
<CommandList_Shadcn_>
|
||||
|
||||
Reference in new issue
Block a user