EdgeFunctionSection.tsx209 lines · main
1import { useParams } from 'common/hooks'
2import { Check, ChevronsUpDown } from 'lucide-react'
3import Link from 'next/link'
4import { useEffect, useId, useMemo, useState } from 'react'
5import { UseFormReturn } from 'react-hook-form'
6import {
7 Button,
8 cn,
9 Command,
10 CommandEmpty,
11 CommandGroup,
12 CommandInput,
13 CommandItem,
14 CommandList,
15 FormControl,
16 FormField,
17 FormItem,
18 FormLabel,
19 FormMessage,
20 InputGroup,
21 InputGroupAddon,
22 InputGroupInput,
23 InputGroupText,
24 Popover,
25 PopoverContent,
26 PopoverTrigger,
27 ScrollArea,
28 Select,
29 SelectContent,
30 SelectItem,
31 SelectTrigger,
32 SelectValue,
33 SheetSection,
34} from 'ui'
35import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
36
37import { CreateCronJobForm } from './CreateCronJobSheet/CreateCronJobSheet.constants'
38import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
39import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
40
41interface HTTPRequestFieldsProps {
42 form: UseFormReturn<CreateCronJobForm>
43}
44
45const buildFunctionUrl = (slug: string, projectRef: string, restUrl?: string) => {
46 const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
47 const functionUrl = `https://${projectRef}.briven.${restUrlTld}/functions/v1/${slug}`
48 return functionUrl
49}
50
51export const EdgeFunctionSection = ({ form }: HTTPRequestFieldsProps) => {
52 const { ref } = useParams()
53 const { data: selectedProject } = useSelectedProjectQuery()
54 const {
55 data: functions,
56 isSuccess,
57 isPending: isLoading,
58 } = useEdgeFunctionsQuery({ projectRef: ref })
59 const [open, setOpen] = useState(false)
60 const listboxId = useId()
61 const edgeFunctions = useMemo(
62 () =>
63 functions?.map((fn) => ({
64 ...fn,
65 url: buildFunctionUrl(fn.slug, selectedProject?.ref || '', selectedProject?.restUrl),
66 })) ?? [],
67 [functions, selectedProject]
68 )
69
70 // Only set a default value if the field is empty
71 useEffect(() => {
72 if (isSuccess && edgeFunctions.length > 0 && !form.getValues('values.edgeFunctionName')) {
73 const fn = edgeFunctions[0]
74 form.setValue('values.edgeFunctionName', fn.url)
75 }
76 }, [edgeFunctions, form, isSuccess, selectedProject?.ref, selectedProject?.restUrl])
77
78 return (
79 <SheetSection className="flex flex-col gap-6">
80 <FormField
81 control={form.control}
82 name="values.method"
83 render={({ field }) => (
84 <FormItem>
85 <FormLabel>Method</FormLabel>
86 <Select onValueChange={field.onChange} value={field.value}>
87 <FormControl>
88 <SelectTrigger>
89 <SelectValue placeholder="Select a method for the API call" />
90 </SelectTrigger>
91 </FormControl>
92 <SelectContent>
93 <SelectItem value="GET">GET</SelectItem>
94 <SelectItem value="POST">POST</SelectItem>
95 </SelectContent>
96 </Select>
97 <FormMessage />
98 </FormItem>
99 )}
100 />
101
102 {edgeFunctions.length === 0 ? (
103 <div className="space-y-1">
104 <p className="text-sm text-foreground-light">Select which edge function to trigger</p>
105 {isLoading ? (
106 <Button type="default" className="justify-start" block size="small" loading>
107 Loading edge functions...
108 </Button>
109 ) : (
110 <div className="px-4 py-4 border rounded-sm bg-surface-300 border-strong flex items-center justify-between space-x-4">
111 <p className="text-sm">No edge functions created yet</p>
112 <Button asChild>
113 <Link href={`/project/${ref}/functions`}>Create an edge function</Link>
114 </Button>
115 </div>
116 )}
117 </div>
118 ) : edgeFunctions.length > 0 ? (
119 <FormField
120 control={form.control}
121 name="values.edgeFunctionName"
122 render={({ field }) => {
123 const selectedFunction = edgeFunctions.find((fn) => fn.url === field.value)
124
125 return (
126 <FormItem>
127 <FormLabel>Edge Function</FormLabel>
128 <Popover open={open} onOpenChange={setOpen}>
129 <PopoverTrigger asChild>
130 <FormControl>
131 <Button
132 type="default"
133 role="combobox"
134 aria-expanded={open}
135 aria-controls={listboxId}
136 className={cn(
137 'w-full justify-between',
138 !field.value && 'text-muted-foreground'
139 )}
140 size="small"
141 iconRight={
142 <ChevronsUpDown
143 className="ml-2 h-4 w-4 shrink-0 opacity-50"
144 strokeWidth={1}
145 />
146 }
147 >
148 {selectedFunction
149 ? selectedFunction.name
150 : 'Select which edge function to trigger'}
151 </Button>
152 </FormControl>
153 </PopoverTrigger>
154 <PopoverContent id={listboxId} className="p-0" sameWidthAsTrigger>
155 <Command>
156 <CommandInput placeholder="Search edge functions..." />
157 <CommandList>
158 <CommandEmpty>No edge function found.</CommandEmpty>
159 <CommandGroup>
160 <ScrollArea className={edgeFunctions.length > 7 ? 'h-[210px]' : ''}>
161 {edgeFunctions.map((fn) => {
162 return (
163 <CommandItem
164 value={fn.name}
165 key={fn.id}
166 onSelect={() => {
167 field.onChange(fn.url === field.value ? '' : fn.url)
168 setOpen(false)
169 }}
170 >
171 <Check
172 className={cn(
173 'mr-2 h-4 w-4',
174 fn.url === field.value ? 'opacity-100' : 'opacity-0'
175 )}
176 />
177 {fn.name}
178 </CommandItem>
179 )
180 })}
181 </ScrollArea>
182 </CommandGroup>
183 </CommandList>
184 </Command>
185 </PopoverContent>
186 </Popover>
187 <FormMessage />
188 </FormItem>
189 )
190 }}
191 />
192 ) : null}
193 <FormField
194 control={form.control}
195 name="values.timeoutMs"
196 render={({ field: { ref, ...rest } }) => (
197 <FormItemLayout label="Timeout" layout="vertical" className="gap-1">
198 <InputGroup>
199 <InputGroupInput {...rest} type="number" placeholder="1000" />
200 <InputGroupAddon align="inline-end">
201 <InputGroupText> ms</InputGroupText>
202 </InputGroupAddon>
203 </InputGroup>
204 </FormItemLayout>
205 )}
206 />
207 </SheetSection>
208 )
209}