EdgeFunctionDetails.tsx388 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { IS_PLATFORM, useParams } from 'common'
4import { useRouter } from 'next/router'
5import { useEffect, useMemo, useState } from 'react'
6import { SubmitHandler, useForm } from 'react-hook-form'
7import { toast } from 'sonner'
8import {
9 Alert,
10 AlertDescription,
11 AlertTitle,
12 Button,
13 Card,
14 CardContent,
15 CardFooter,
16 cn,
17 copyToClipboard,
18 CriticalIcon,
19 Form,
20 FormControl,
21 FormField,
22 Switch,
23 Tabs_Shadcn_ as Tabs,
24 TabsContent_Shadcn_ as TabsContent,
25 TabsList_Shadcn_ as TabsList,
26 TabsTrigger_Shadcn_ as TabsTrigger,
27} from 'ui'
28import { CodeBlock } from 'ui-patterns/CodeBlock'
29import { Input } from 'ui-patterns/DataInputs/Input'
30import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
31import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
32import { PageContainer } from 'ui-patterns/PageContainer'
33import {
34 PageSection,
35 PageSectionContent,
36 PageSectionMeta,
37 PageSectionSummary,
38 PageSectionTitle,
39} from 'ui-patterns/PageSection'
40import z from 'zod'
41
42import CommandRender from '../CommandRender'
43import { INVOCATION_TABS } from './EdgeFunctionDetails.constants'
44import { generateCLICommands } from './EdgeFunctionDetails.utils'
45import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
46import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
47import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
48import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functions-delete-mutation'
49import { useEdgeFunctionUpdateMutation } from '@/data/edge-functions/edge-functions-update-mutation'
50import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
51import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
52
53const FormSchema = z.object({
54 name: z.string().min(0, 'Name is required'),
55 verify_jwt: z.boolean(),
56})
57
58export const EdgeFunctionDetails = () => {
59 const router = useRouter()
60 const { ref: projectRef, functionSlug } = useParams()
61
62 const showAllEdgeFunctionInvocationExamples = useIsFeatureEnabled(
63 'edge_functions:show_all_edge_function_invocation_examples'
64 )
65 const invocationTabs = useMemo(() => {
66 if (showAllEdgeFunctionInvocationExamples) return INVOCATION_TABS
67 return INVOCATION_TABS.filter((tab) => tab.id === 'curl' || tab.id === 'briven-js')
68 }, [showAllEdgeFunctionInvocationExamples])
69
70 const [showKey, setShowKey] = useState(false)
71 const [selectedTab, setSelectedTab] = useState(invocationTabs[0].id)
72 const [showDeleteModal, setShowDeleteModal] = useState(false)
73
74 const { can: canUpdateEdgeFunctionPermission } = useAsyncCheckPermissions(
75 PermissionAction.FUNCTIONS_WRITE,
76 '*'
77 )
78
79 const canUpdateEdgeFunction = IS_PLATFORM && canUpdateEdgeFunctionPermission
80
81 const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
82 const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
83
84 const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
85
86 const { data: endpoint } = useProjectApiUrl({ projectRef })
87 const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}`
88
89 const { mutate: updateEdgeFunction, isPending: isUpdating } = useEdgeFunctionUpdateMutation()
90 const { mutate: deleteEdgeFunction, isPending: isDeleting } = useEdgeFunctionDeleteMutation({
91 onSuccess: () => {
92 toast.success(`Successfully deleted "${selectedFunction?.name}"`)
93 router.push(`/project/${projectRef}/functions`)
94 },
95 })
96
97 const form = useForm({
98 resolver: zodResolver(FormSchema as any),
99 defaultValues: { name: '', verify_jwt: false },
100 })
101
102 const { anonKey, publishableKey } = getKeys(apiKeys)
103 const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
104
105 const { managementCommands } = generateCLICommands({
106 selectedFunction,
107 functionUrl,
108 anonKey: apiKey,
109 })
110
111 const onUpdateFunction: SubmitHandler<z.infer<typeof FormSchema>> = async (values: any) => {
112 if (!projectRef) return console.error('Project ref is required')
113 if (selectedFunction === undefined) return console.error('No edge function selected')
114
115 updateEdgeFunction(
116 {
117 projectRef,
118 slug: selectedFunction.slug,
119 payload: values,
120 },
121 {
122 onSuccess: () => {
123 toast.success(`Successfully updated edge function`)
124 },
125 }
126 )
127 }
128
129 const onConfirmDelete = async () => {
130 if (!projectRef) return console.error('Project ref is required')
131 if (selectedFunction === undefined) return console.error('No edge function selected')
132 deleteEdgeFunction({ projectRef, slug: selectedFunction.slug })
133 }
134
135 useEffect(() => {
136 if (selectedFunction) {
137 form.reset({
138 name: selectedFunction.name,
139 verify_jwt: selectedFunction.verify_jwt,
140 })
141 }
142 }, [selectedFunction])
143
144 return (
145 <PageContainer size="small">
146 <PageSection>
147 <PageSectionMeta>
148 <PageSectionSummary>
149 <PageSectionTitle>Function configuration</PageSectionTitle>
150 </PageSectionSummary>
151 </PageSectionMeta>
152 <PageSectionContent>
153 <Form {...form}>
154 <form onSubmit={form.handleSubmit(onUpdateFunction)}>
155 <Card>
156 <CardContent>
157 <FormField
158 control={form.control}
159 name="name"
160 render={({ field }) => (
161 <FormItemLayout
162 label="Name"
163 layout="flex-row-reverse"
164 description="Your slug and endpoint URL will remain the same"
165 >
166 <FormControl>
167 <Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
168 </FormControl>
169 </FormItemLayout>
170 )}
171 />
172 </CardContent>
173 {IS_PLATFORM && (
174 <>
175 <CardContent>
176 <FormField
177 control={form.control}
178 name="verify_jwt"
179 render={({ field }) => (
180 <FormItemLayout
181 label="Verify JWT with legacy secret"
182 layout="flex-row-reverse"
183 description={
184 <>
185 <p className="mb-2">
186 Requires a JWT signed{' '}
187 <em className="text-foreground not-italic">
188 only by the legacy secret
189 </em>{' '}
190 in the{' '}
191 <code className="text-code-inline break-keep!">
192 Authorization
193 </code>{' '}
194 header. The <code className="text-code-inline">anon</code> key
195 satisfies this.
196 </p>
197 <p>
198 Recommended: OFF with JWT and custom auth logic in your function
199 code.
200 </p>
201 </>
202 }
203 >
204 <FormControl>
205 <Switch
206 checked={field.value}
207 onCheckedChange={field.onChange}
208 disabled={!canUpdateEdgeFunction}
209 />
210 </FormControl>
211 </FormItemLayout>
212 )}
213 />
214 </CardContent>
215
216 <CardFooter className="flex justify-end space-x-2">
217 {form.formState.isDirty && (
218 <Button type="default" onClick={() => form.reset()}>
219 Cancel
220 </Button>
221 )}
222 <Button
223 type="primary"
224 htmlType="submit"
225 loading={isUpdating}
226 disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
227 >
228 Save changes
229 </Button>
230 </CardFooter>
231 </>
232 )}
233 </Card>
234 </form>
235 </Form>
236 </PageSectionContent>
237 </PageSection>
238
239 <PageSection>
240 <PageSectionMeta>
241 <PageSectionSummary>
242 <PageSectionTitle>Invoke function</PageSectionTitle>
243 </PageSectionSummary>
244 </PageSectionMeta>
245 <PageSectionContent>
246 <Card>
247 <CardContent className="px-0">
248 <Tabs
249 className="w-full"
250 defaultValue="curl"
251 value={selectedTab}
252 onValueChange={setSelectedTab}
253 >
254 <TabsList className="flex flex-wrap gap-4 px-6">
255 {invocationTabs.map((tab) => (
256 <TabsTrigger key={tab.id} value={tab.id}>
257 {tab.label}
258 </TabsTrigger>
259 ))}
260 {selectedTab === 'curl' && (
261 <Button
262 type="default"
263 className="ml-auto -translate-y-2 translate-x-3"
264 onClick={() => setShowKey(!showKey)}
265 >
266 {showKey ? 'Hide' : 'Show'} anon key
267 </Button>
268 )}
269 </TabsList>
270 {invocationTabs.map((tab) => {
271 const code = tab.code({
272 showKey,
273 functionUrl,
274 functionName: selectedFunction?.name ?? '',
275 apiKey,
276 })
277
278 return (
279 <TabsContent key={tab.id} value={tab.id}>
280 <CodeBlock
281 value={code}
282 wrapperClassName="[&>div]:top-0 [&>div]:right-3 px-6"
283 className={cn(
284 'p-0 text-xs mt-0! border-none ',
285 showKey ? '[&>code]:break-all' : '[&>code]:wrap-break-word'
286 )}
287 language={tab.language}
288 wrapLines={false}
289 hideLineNumbers={tab.hideLineNumbers}
290 handleCopy={() => {
291 copyToClipboard(
292 tab.code({
293 showKey: true,
294 functionUrl,
295 functionName: selectedFunction?.name ?? '',
296 apiKey,
297 })
298 )
299 }}
300 />
301 </TabsContent>
302 )
303 })}
304 </Tabs>
305 </CardContent>
306 </Card>
307 </PageSectionContent>
308 </PageSection>
309
310 {IS_PLATFORM && (
311 <>
312 <PageSection>
313 <PageSectionMeta>
314 <PageSectionSummary>
315 <PageSectionTitle>Develop locally</PageSectionTitle>
316 </PageSectionSummary>
317 </PageSectionMeta>
318 <PageSectionContent>
319 <div className="rounded-sm border bg-surface-100 px-6 py-4 drop-shadow-xs">
320 <div className="space-y-6">
321 <CommandRender
322 commands={[
323 {
324 command: `briven functions download ${selectedFunction?.slug}`,
325 description: 'Download the function to your local machine',
326 jsx: () => (
327 <>
328 <span className="text-brand">briven</span> functions download{' '}
329 {selectedFunction?.slug}
330 </>
331 ),
332 comment: '1. Download the function',
333 },
334 ]}
335 />
336 <CommandRender commands={[managementCommands[0]]} />
337 <CommandRender commands={[managementCommands[1]]} />
338 </div>
339 </div>
340 </PageSectionContent>
341 </PageSection>
342 <PageSection>
343 <PageSectionMeta>
344 <PageSectionSummary>
345 <PageSectionTitle>Delete function</PageSectionTitle>
346 </PageSectionSummary>
347 </PageSectionMeta>
348 <PageSectionContent>
349 <Alert variant="destructive">
350 <CriticalIcon />
351 <AlertTitle>Once your function is deleted, it can no longer be restored</AlertTitle>
352 <AlertDescription>
353 Make sure you have made a backup if you want to restore your edge function
354 </AlertDescription>
355 <AlertDescription className="mt-3">
356 <Button
357 type="danger"
358 disabled={!canUpdateEdgeFunction}
359 loading={selectedFunction?.id === undefined}
360 onClick={() => setShowDeleteModal(true)}
361 >
362 Delete edge function
363 </Button>
364 </AlertDescription>
365 </Alert>
366 </PageSectionContent>
367 </PageSection>
368 <ConfirmationModal
369 visible={showDeleteModal}
370 loading={isDeleting}
371 variant="destructive"
372 confirmLabel="Delete"
373 confirmLabelLoading="Deleting"
374 title={`Confirm to delete ${selectedFunction?.name}`}
375 onCancel={() => setShowDeleteModal(false)}
376 onConfirm={onConfirmDelete}
377 alert={{
378 base: { variant: 'destructive' },
379 title: 'This action cannot be undone',
380 description:
381 'Ensure that you have made a backup if you want to restore your edge function',
382 }}
383 />
384 </>
385 )}
386 </PageContainer>
387 )
388}