EdgeFunctionSection.tsx 7.4 KB

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