mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 14:49:10 +03:00
improvements
This commit is contained in:
1 parent
43f2334dfc
commit
b22d80d40e
5 files changed
+375
-151
No files matched your search
+1
-1
@@ -129,7 +129,7 @@ const EdgeFunctionDetails = () => {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title="Function Configuration" />
|
||||
<div className="mx-auto flex flex-col xl:flex-row gap-8">
|
||||
<div className="mx-auto flex flex-col xl:flex-row gap-8 pb-8">
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
|
||||
|
||||
+207
-148
@@ -1,4 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm, useFieldArray } from 'react-hook-form'
|
||||
import * as z from 'zod'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -20,7 +23,13 @@ import {
|
||||
TabsTrigger_Shadcn_ as TabsTrigger,
|
||||
Badge,
|
||||
CodeBlock,
|
||||
Form_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormMessage_Shadcn_,
|
||||
} from 'ui'
|
||||
import { FormFieldWrapper } from 'components/ui/Forms'
|
||||
import { constructHeaders } from 'data/fetchers'
|
||||
import { useParams } from 'common'
|
||||
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
|
||||
@@ -54,6 +63,28 @@ type ResponseData = {
|
||||
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const
|
||||
type HttpMethod = (typeof HTTP_METHODS)[number]
|
||||
|
||||
const FormSchema = z.object({
|
||||
method: z.enum(HTTP_METHODS),
|
||||
body: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((str) => str || '{}'),
|
||||
headers: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
queryParams: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof FormSchema>
|
||||
|
||||
const EdgeFunctionTesterSheet = ({
|
||||
visible,
|
||||
onClose,
|
||||
@@ -61,10 +92,6 @@ const EdgeFunctionTesterSheet = ({
|
||||
apiKey,
|
||||
}: EdgeFunctionTesterSheetProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [method, setMethod] = useState<HttpMethod>('POST')
|
||||
const [body, setBody] = useState('{ "name": "Functions" }')
|
||||
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: '', value: '' }])
|
||||
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: '', value: '' }])
|
||||
const [response, setResponse] = useState<ResponseData | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -75,40 +102,51 @@ const EdgeFunctionTesterSheet = ({
|
||||
const getImpersonatedRole = useGetImpersonatedRole()
|
||||
const { serviceKey } = getAPIKeys(settings)
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
method: 'POST',
|
||||
body: '{ "name": "Functions" }',
|
||||
headers: [{ key: '', value: '' }],
|
||||
queryParams: [{ key: '', value: '' }],
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
fields: headerFields,
|
||||
append: appendHeader,
|
||||
remove: removeHeader,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'headers',
|
||||
})
|
||||
|
||||
const {
|
||||
fields: queryParamFields,
|
||||
append: appendQueryParam,
|
||||
remove: removeQueryParam,
|
||||
} = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'queryParams',
|
||||
})
|
||||
|
||||
const addKeyValuePair = (type: 'headers' | 'queryParams') => {
|
||||
if (type === 'headers') {
|
||||
setHeaders([...headers, { key: '', value: '' }])
|
||||
appendHeader({ key: '', value: '' })
|
||||
} else {
|
||||
setQueryParams([...queryParams, { key: '', value: '' }])
|
||||
appendQueryParam({ key: '', value: '' })
|
||||
}
|
||||
}
|
||||
|
||||
const removeKeyValuePair = (index: number, type: 'headers' | 'queryParams') => {
|
||||
if (type === 'headers') {
|
||||
setHeaders(headers.filter((_, i) => i !== index))
|
||||
removeHeader(index)
|
||||
} else {
|
||||
setQueryParams(queryParams.filter((_, i) => i !== index))
|
||||
removeQueryParam(index)
|
||||
}
|
||||
}
|
||||
|
||||
const updateKeyValuePair = (
|
||||
index: number,
|
||||
field: 'key' | 'value',
|
||||
value: string,
|
||||
type: 'headers' | 'queryParams'
|
||||
) => {
|
||||
if (type === 'headers') {
|
||||
const newHeaders = [...headers]
|
||||
newHeaders[index][field] = value
|
||||
setHeaders(newHeaders)
|
||||
} else {
|
||||
const newQueryParams = [...queryParams]
|
||||
newQueryParams[index][field] = value
|
||||
setQueryParams(newQueryParams)
|
||||
}
|
||||
}
|
||||
|
||||
const sendRequest = async () => {
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
@@ -116,9 +154,10 @@ const EdgeFunctionTesterSheet = ({
|
||||
|
||||
let requestBody: any
|
||||
try {
|
||||
requestBody = JSON.parse(body)
|
||||
requestBody = JSON.parse(values.body)
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON in request body')
|
||||
form.setError('body', { message: 'Must be a valid JSON string' })
|
||||
return
|
||||
}
|
||||
|
||||
let testAuthorization: string | undefined
|
||||
@@ -138,21 +177,20 @@ const EdgeFunctionTesterSheet = ({
|
||||
error: err.message,
|
||||
roleDetails: role,
|
||||
})
|
||||
// Don't throw, fall back to service key
|
||||
console.log('Falling back to service key')
|
||||
}
|
||||
}
|
||||
|
||||
// Construct custom headers
|
||||
const customHeaders: Record<string, string> = {}
|
||||
headers.forEach(({ key, value }) => {
|
||||
headerFields.forEach(({ key, value }) => {
|
||||
if (key && value) {
|
||||
customHeaders[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
// Construct query parameters
|
||||
const queryString = queryParams
|
||||
const queryString = queryParamFields
|
||||
.filter(({ key, value }) => key && value)
|
||||
.map(({ key, value }) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join('&')
|
||||
@@ -168,7 +206,7 @@ const EdgeFunctionTesterSheet = ({
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: finalUrl,
|
||||
method,
|
||||
method: values.method,
|
||||
body: requestBody,
|
||||
headers: {
|
||||
...(accessToken && {
|
||||
@@ -194,14 +232,10 @@ const EdgeFunctionTesterSheet = ({
|
||||
}
|
||||
}
|
||||
|
||||
const renderKeyValuePairs = (
|
||||
type: 'headers' | 'queryParams',
|
||||
items: KeyValuePair[],
|
||||
label: string
|
||||
) => (
|
||||
const renderKeyValuePairs = (type: 'headers' | 'queryParams', label: string) => (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground-light text-sm">{label}</Label>
|
||||
<Label className="text-foreground text-sm">{label}</Label>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
@@ -212,26 +246,40 @@ const EdgeFunctionTesterSheet = ({
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border rounded-md bg-surface-200">
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className="grid grid-cols-[1fr,1fr,32px] border-b last:border-b-0">
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Enter key..."
|
||||
value={item.key}
|
||||
onChange={(e) => updateKeyValuePair(index, 'key', e.target.value, type)}
|
||||
disabled={isLoading}
|
||||
className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-l-0 border-r-1 border-t-0 border-b-0 border-border"
|
||||
{(type === 'headers' ? headerFields : queryParamFields).map((field, index) => (
|
||||
<div key={field.id} className="grid grid-cols-[1fr,1fr,32px] border-b last:border-b-0">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name={`${type}.${index}.key`}
|
||||
render={({ field }) => (
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
size="tiny"
|
||||
placeholder="Enter key..."
|
||||
disabled={isLoading}
|
||||
className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-l-0 border-r-1 border-t-0 border-b-0 border-border"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
size="tiny"
|
||||
placeholder="Enter value..."
|
||||
value={item.value}
|
||||
onChange={(e) => updateKeyValuePair(index, 'value', e.target.value, type)}
|
||||
disabled={isLoading}
|
||||
className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-none"
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name={`${type}.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormControl_Shadcn_>
|
||||
<Input
|
||||
{...field}
|
||||
size="tiny"
|
||||
placeholder="Enter value..."
|
||||
disabled={isLoading}
|
||||
className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-none"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center justify-center">
|
||||
{items.length > 1 && (
|
||||
{(type === 'headers' ? headerFields : queryParamFields).length > 1 && (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
@@ -254,104 +302,115 @@ const EdgeFunctionTesterSheet = ({
|
||||
<SheetTitle>Test Edge Function</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto flex flex-col">
|
||||
<div className="space-y-4 p-5">
|
||||
<div>
|
||||
<Label className="text-foreground-light mb-2 block text-sm">Method</Label>
|
||||
<Select
|
||||
value={method}
|
||||
onValueChange={(value) => setMethod(value as HttpMethod)}
|
||||
disabled={isLoading}
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex-1 overflow-y-auto flex flex-col"
|
||||
>
|
||||
<div className="space-y-4 p-5">
|
||||
<FormFieldWrapper
|
||||
control={form.control}
|
||||
name="method"
|
||||
label="Method"
|
||||
description="Select the HTTP method for your request"
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{HTTP_METHODS.map((m) => (
|
||||
<SelectItem key={m} value={m}>
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(field) => (
|
||||
<Select value={field.value} onValueChange={field.onChange} disabled={isLoading}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{HTTP_METHODS.map((m) => (
|
||||
<SelectItem key={m} value={m}>
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</FormFieldWrapper>
|
||||
<FormFieldWrapper
|
||||
control={form.control}
|
||||
name="body"
|
||||
label="Body"
|
||||
description="Enter the JSON request body"
|
||||
>
|
||||
{(field) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
placeholder="Request body (JSON)"
|
||||
rows={3}
|
||||
disabled={isLoading}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
)}
|
||||
</FormFieldWrapper>
|
||||
|
||||
{renderKeyValuePairs('headers', 'Headers')}
|
||||
{renderKeyValuePairs('queryParams', 'Query Parameters')}
|
||||
</div>
|
||||
|
||||
{renderKeyValuePairs('headers', headers, 'Headers')}
|
||||
{renderKeyValuePairs('queryParams', queryParams, 'Query Parameters')}
|
||||
|
||||
<div>
|
||||
<Label className="text-foreground-light mb-2 block text-sm">Body</Label>
|
||||
<Textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
placeholder="Request body (JSON)"
|
||||
rows={3}
|
||||
disabled={isLoading}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full bg-surface-100 border-t flex-1 flex flex-col overflow-hidden">
|
||||
{response ? (
|
||||
<div className="h-full bg-surface-100 flex flex-col overflow-hidden">
|
||||
<div className="flex gap-2 items-center p-5 text-sm pb-3">
|
||||
Function responded with
|
||||
<Badge variant={response.status >= 400 ? 'destructive' : 'success'}>
|
||||
{response.status}
|
||||
</Badge>
|
||||
<div className="h-full bg-surface-100 border-t flex-1 flex flex-col overflow-hidden">
|
||||
{response ? (
|
||||
<div className="h-full bg-surface-100 flex flex-col overflow-hidden">
|
||||
<div className="flex gap-2 items-center p-5 text-sm pb-3">
|
||||
Function responded with
|
||||
<Badge variant={response.status >= 400 ? 'destructive' : 'success'}>
|
||||
{response.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<Tabs defaultValue="body" className="h-full flex-1 flex flex-col overflow-hidden">
|
||||
<TabsList className="gap-4 px-5">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<TabsTrigger className="text-sm" value="body">
|
||||
Body
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="text-sm" value="headers">
|
||||
Headers
|
||||
</TabsTrigger>
|
||||
</div>
|
||||
</TabsList>
|
||||
<TabsContent value="body" className="mt-0 px-5 pb-4 flex-1 overflow-auto">
|
||||
<CodeBlock
|
||||
language="json"
|
||||
hideLineNumbers
|
||||
className="rounded-md !border-none !px-4 !py-3 h-full"
|
||||
value={prettifyJSON(response.body)}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="headers" className="mt-0 px-4 pb-4 flex-1 overflow-auto">
|
||||
<CodeBlock
|
||||
language="json"
|
||||
hideLineNumbers
|
||||
className="rounded-md !border-none !px-4 !py-3 h-full"
|
||||
value={prettifyJSON(JSON.stringify(response.headers, null, 2))}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<Tabs defaultValue="body" className="h-full flex-1 flex flex-col overflow-hidden">
|
||||
<TabsList className="gap-4 px-5">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<TabsTrigger className="text-sm" value="body">
|
||||
Body
|
||||
</TabsTrigger>
|
||||
<TabsTrigger className="text-sm" value="headers">
|
||||
Headers
|
||||
</TabsTrigger>
|
||||
</div>
|
||||
</TabsList>
|
||||
<TabsContent value="body" className="mt-0 px-5 pb-4 flex-1 overflow-auto">
|
||||
<CodeBlock
|
||||
language="json"
|
||||
hideLineNumbers
|
||||
className="rounded-md !border-none !px-4 !py-3 h-full"
|
||||
value={prettifyJSON(response.body)}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="headers" className="mt-0 px-4 pb-4 flex-1 overflow-auto">
|
||||
<CodeBlock
|
||||
language="json"
|
||||
hideLineNumbers
|
||||
className="rounded-md !border-none !px-4 !py-3 h-full"
|
||||
value={prettifyJSON(JSON.stringify(response.headers, null, 2))}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-5 space-y-2">
|
||||
<Label>Error</Label>
|
||||
<p className="text-sm text-foreground-light">{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-2">
|
||||
<Send size={24} className="text-foreground-muted" />
|
||||
<p className="text-sm text-foreground-light">Send your first test request</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-5 space-y-2">
|
||||
<Label>Error</Label>
|
||||
<p className="text-sm text-foreground-light">{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-2">
|
||||
<Send size={24} className="text-foreground-muted" />
|
||||
<p className="text-sm text-foreground-light">Send your first test request</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SheetFooter className="px-5 py-3 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<RoleImpersonationPopover />
|
||||
<Button type="primary" onClick={sendRequest} loading={isLoading} disabled={isLoading}>
|
||||
Send Request
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
<SheetFooter className="px-5 py-3 border-t">
|
||||
<div className="flex items-center gap-2">
|
||||
<RoleImpersonationPopover />
|
||||
<Button type="primary" htmlType="submit" loading={isLoading} disabled={isLoading}>
|
||||
Send Request
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
|
||||
@@ -110,6 +110,10 @@ const FunctionsLayout = ({ title, children }: PropsWithChildren<FunctionsLayoutP
|
||||
label: 'Logs',
|
||||
href: `/project/${ref}/functions/${functionSlug}/logs`,
|
||||
},
|
||||
{
|
||||
label: 'Code',
|
||||
href: `/project/${ref}/functions/${functionSlug}/code`,
|
||||
},
|
||||
{
|
||||
label: 'Details',
|
||||
href: `/project/${ref}/functions/${functionSlug}/details`,
|
||||
|
||||
@@ -151,7 +151,7 @@ export const MarkdownPre = ({ children }: { children: any }) => {
|
||||
|
||||
const language = children[0].props.className?.replace('language-', '') || 'sql'
|
||||
const rawContent = children[0].props.children[0]
|
||||
const propsMatch = rawContent.match(/--\s*props:\s*(\{[^}]+\})/)
|
||||
const propsMatch = rawContent.match(/(?:--|\/\/)\s*props:\s*(\{[^}]+\})/)
|
||||
|
||||
const snippetProps: AssistantSnippetProps = useMemo(
|
||||
() => (propsMatch ? JSON.parse(propsMatch[1]) : {}),
|
||||
@@ -164,7 +164,7 @@ export const MarkdownPre = ({ children }: { children: any }) => {
|
||||
const runQuery = snippetProps.runQuery === 'true'
|
||||
|
||||
// Strip props from the content for both SQL and edge functions
|
||||
const cleanContent = rawContent.replace(/--\s*props:\s*\{[^}]+\}/, '').trim()
|
||||
const cleanContent = rawContent.replace(/(?:--|\/\/)\s*props:\s*\{[^}]+\}/, '').trim()
|
||||
|
||||
const isDraggableToReports =
|
||||
supportSQLBlocks && canCreateSQLSnippet && router.pathname.endsWith('/reports/[id]')
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import { CornerDownLeft, Loader2, Book, Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Button, Input_Shadcn_, Label_Shadcn_, cn } from 'ui'
|
||||
import { AiIconAnimation } from 'ui'
|
||||
import AIEditor from 'components/ui/AIEditor'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useEdgeFunctionDeployMutation } from 'data/edge-functions/edge-functions-deploy-mutation'
|
||||
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useParams } from 'common'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout'
|
||||
import { PageLayout } from 'components/layouts/PageLayout'
|
||||
|
||||
const CodePage = () => {
|
||||
const router = useRouter()
|
||||
const { ref, functionSlug } = useParams()
|
||||
const project = useSelectedProject()
|
||||
const isOptedInToAI = useOrgOptedIntoAi()
|
||||
const includeSchemaMetadata = isOptedInToAI || !IS_PLATFORM
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
|
||||
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
|
||||
const [currentValue, setCurrentValue] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial code value when function is loaded
|
||||
if (selectedFunction) {
|
||||
// For now, we'll use a default template. In the future, we should fetch the actual code
|
||||
setCurrentValue(`// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||
|
||||
console.info('server started');
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const data = {
|
||||
message: 'Hello from Supabase Edge Functions!',
|
||||
};
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(data),
|
||||
{ headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' }}
|
||||
);
|
||||
});`)
|
||||
}
|
||||
}, [selectedFunction])
|
||||
|
||||
const { mutateAsync: deployFunction, isLoading: isDeploying } = useEdgeFunctionDeployMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Successfully updated edge function')
|
||||
if (ref && functionSlug) {
|
||||
router.push(`/project/${ref}/functions/${functionSlug}/details`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setCurrentValue(value)
|
||||
}
|
||||
|
||||
const onUpdate = async () => {
|
||||
if (!currentValue || isDeploying || !ref || !functionSlug || !selectedFunction) return
|
||||
|
||||
try {
|
||||
await deployFunction({
|
||||
projectRef: ref,
|
||||
metadata: {
|
||||
name: selectedFunction.name,
|
||||
verify_jwt: selectedFunction.verify_jwt,
|
||||
entrypoint_path: 'index.ts',
|
||||
},
|
||||
files: [{ name: 'index.ts', content: currentValue }],
|
||||
})
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to update function: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChat = () => {
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
sqlSnippets: currentValue ? [currentValue] : [],
|
||||
initialInput: 'Help me understand and improve this edge function...',
|
||||
suggestions: {
|
||||
title:
|
||||
'I can help you understand and improve your edge function. Here are a few example prompts to get you started:',
|
||||
prompts: [
|
||||
'Explain what this function does...',
|
||||
'Help me optimize this function...',
|
||||
'Show me how to add more features...',
|
||||
'Help me handle errors better...',
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 min-h-0 bg-surface-200">
|
||||
<AIEditor
|
||||
language="typescript"
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
aiEndpoint={`${BASE_PATH}/api/ai/edge-function/complete`}
|
||||
aiMetadata={{
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
includeSchemaMetadata,
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
fontSize: 12,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on',
|
||||
lineNumbers: 'on',
|
||||
folding: false,
|
||||
padding: { top: 20, bottom: 20 },
|
||||
lineNumbersMinChars: 3,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center bg-background-muted justify-end p-4 border-t bg-surface-100 shrink-0">
|
||||
<Button
|
||||
loading={isDeploying}
|
||||
size="medium"
|
||||
disabled={!currentValue}
|
||||
onClick={onUpdate}
|
||||
iconRight={
|
||||
isDeploying ? (
|
||||
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
<CornerDownLeft size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
Deploy updates
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
CodePage.getLayout = (page: React.ReactNode) => {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<FunctionsLayout>{page}</FunctionsLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodePage
|
||||
Reference in new issue
Block a user