FormContents.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import type { PGTrigger } from '@supabase/pg-meta'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import Image from 'next/legacy/image'
  5. import { useEffect } from 'react'
  6. import { UseFormReturn } from 'react-hook-form'
  7. import {
  8. Checkbox,
  9. FormControl,
  10. FormField,
  11. Input,
  12. Label,
  13. RadioGroupStacked,
  14. RadioGroupStackedItem,
  15. Select,
  16. SelectContent,
  17. SelectItem,
  18. SelectTrigger,
  19. SelectValue,
  20. SidePanel,
  21. useWatch,
  22. } from 'ui'
  23. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  24. import { isEdgeFunction } from './EditHookPanel'
  25. import { WebhookFormValues } from './EditHookPanel.constants'
  26. import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants'
  27. import { HTTPHeaders } from './HTTPHeaders'
  28. import { HTTPParameters } from './HTTPParameters'
  29. import { HTTPRequestConfig } from './HTTPRequestConfig'
  30. import {
  31. FormSection,
  32. FormSectionContent,
  33. FormSectionLabel,
  34. } from '@/components/ui/Forms/FormSection'
  35. import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
  36. import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
  37. import { useTableNamesQuery } from '@/data/tables/table-names-query'
  38. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  39. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  40. import { uuidv4 } from '@/lib/helpers'
  41. export interface FormContentsProps {
  42. form: UseFormReturn<WebhookFormValues>
  43. selectedHook?: PGTrigger
  44. }
  45. export const FormContents = ({ form, selectedHook }: FormContentsProps) => {
  46. const { ref } = useParams()
  47. const { data: project } = useSelectedProjectQuery()
  48. const restUrl = project?.restUrl
  49. const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
  50. const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
  51. const { data: keys = [] } = useAPIKeysQuery(
  52. { projectRef: ref, reveal: true },
  53. { enabled: canReadAPIKeys }
  54. )
  55. const { data: functions = [], isSuccess: isSuccessEdgeFunctions } = useEdgeFunctionsQuery({
  56. projectRef: ref,
  57. })
  58. const legacyServiceRole = keys.find((x) => x.name === 'service_role')?.api_key ?? '[YOUR API KEY]'
  59. const httpUrl = useWatch({ control: form.control, name: 'http_url' })
  60. const httpHeaders = useWatch({ control: form.control, name: 'httpHeaders' })
  61. const { data: tables = [] } = useTableNamesQuery({
  62. projectRef: project?.ref,
  63. connectionString: project?.connectionString,
  64. })
  65. // Handle auth header auto-add for edge functions
  66. useEffect(() => {
  67. if (!isSuccessEdgeFunctions) return
  68. const isEdgeFunctionSelected = isEdgeFunction({ ref, restUrlTld, url: httpUrl })
  69. if (httpUrl && isEdgeFunctionSelected) {
  70. const fnSlug = httpUrl.split('/').at(-1)
  71. const fn = functions.find((x) => x.slug === fnSlug)
  72. const authorizationHeader = httpHeaders.find((x) => x.name === 'Authorization')
  73. const edgeFunctionAuthHeaderVal = `Bearer ${legacyServiceRole}`
  74. if (fn?.verify_jwt && authorizationHeader == null) {
  75. const newAuthHeader = {
  76. id: uuidv4(),
  77. name: 'Authorization',
  78. value: edgeFunctionAuthHeaderVal,
  79. }
  80. form.setValue('httpHeaders', [...httpHeaders, newAuthHeader])
  81. } else if (fn?.verify_jwt && authorizationHeader?.value !== edgeFunctionAuthHeaderVal) {
  82. const updatedHttpHeaders = httpHeaders.map((x) => {
  83. if (x.name === 'Authorization') return { ...x, value: edgeFunctionAuthHeaderVal }
  84. else return x
  85. })
  86. form.setValue('httpHeaders', updatedHttpHeaders)
  87. }
  88. }
  89. // eslint-disable-next-line react-hooks/exhaustive-deps
  90. }, [httpUrl, isSuccessEdgeFunctions])
  91. return (
  92. <div>
  93. <FormSection header={<FormSectionLabel className="lg:col-span-4!">General</FormSectionLabel>}>
  94. <FormSectionContent loading={false} className="lg:col-span-8!">
  95. <FormField
  96. control={form.control}
  97. name="name"
  98. render={({ field }) => (
  99. <FormItemLayout label="Name" layout="vertical" className="gap-1">
  100. <FormControl>
  101. <Input {...field} placeholder="my_webhook" />
  102. </FormControl>
  103. <p className="mt-2 text-xs text-foreground-lighter">
  104. Do not use spaces/whitespaces
  105. </p>
  106. </FormItemLayout>
  107. )}
  108. />
  109. </FormSectionContent>
  110. </FormSection>
  111. <SidePanel.Separator />
  112. <FormSection
  113. header={
  114. <FormSectionLabel
  115. className="lg:col-span-4!"
  116. description={
  117. <p className="text-sm text-foreground-light">
  118. Select which table and events will trigger your webhook
  119. </p>
  120. }
  121. >
  122. Conditions to fire webhook
  123. </FormSectionLabel>
  124. }
  125. >
  126. <FormSectionContent loading={false} className="lg:col-span-8!">
  127. <FormField
  128. control={form.control}
  129. name="table_id"
  130. render={({ field }) => (
  131. <FormItemLayout
  132. label="Table"
  133. layout="vertical"
  134. className="gap-1"
  135. description="This is the table the trigger will watch for changes. You can only select 1 table for a trigger."
  136. >
  137. <Select value={field.value} onValueChange={field.onChange}>
  138. <FormControl>
  139. <SelectTrigger>
  140. <SelectValue placeholder="Select a table" />
  141. </SelectTrigger>
  142. </FormControl>
  143. <SelectContent>
  144. {tables.map((table) => (
  145. <SelectItem key={table.id} value={table.id.toString()}>
  146. <div className="flex items-center space-x-2">
  147. <span className="text-foreground-light">{table.schema}</span>
  148. <span className="text-foreground">{table.name}</span>
  149. </div>
  150. </SelectItem>
  151. ))}
  152. </SelectContent>
  153. </Select>
  154. </FormItemLayout>
  155. )}
  156. />
  157. <FormField
  158. control={form.control}
  159. name="events"
  160. render={({ field }) => (
  161. <FormItemLayout
  162. label="Events"
  163. layout="vertical"
  164. className="gap-1"
  165. description="These are the events that are watched by the webhook, only the events selected above will fire the webhook on the table you've selected."
  166. >
  167. <div className="space-y-3">
  168. {HOOK_EVENTS.map((event) => (
  169. <div key={event.value} className="flex items-start space-x-3">
  170. <Checkbox
  171. id={`event-${event.value}`}
  172. checked={field.value.includes(event.value)}
  173. onCheckedChange={(checked) => {
  174. if (checked) {
  175. field.onChange([...field.value, event.value])
  176. } else {
  177. field.onChange(field.value.filter((v) => v !== event.value))
  178. }
  179. }}
  180. />
  181. <div className="grid gap-1.5 leading-none">
  182. <Label
  183. htmlFor={`event-${event.value}`}
  184. className="text-sm font-normal cursor-pointer"
  185. >
  186. {event.label}
  187. </Label>
  188. <p className="text-xs text-foreground-lighter">{event.description}</p>
  189. </div>
  190. </div>
  191. ))}
  192. </div>
  193. </FormItemLayout>
  194. )}
  195. />
  196. </FormSectionContent>
  197. </FormSection>
  198. <SidePanel.Separator />
  199. <FormSection
  200. header={
  201. <FormSectionLabel className="lg:col-span-4!">Webhook configuration</FormSectionLabel>
  202. }
  203. >
  204. <FormSectionContent loading={false} className="lg:col-span-8!">
  205. <FormField
  206. control={form.control}
  207. name="function_type"
  208. render={({ field }) => (
  209. <FormItemLayout label="Type of webhook" layout="vertical" className="gap-1">
  210. <FormControl>
  211. <RadioGroupStacked
  212. value={field.value}
  213. onValueChange={(functionType) => {
  214. if (functionType === 'http_request') {
  215. if (selectedHook !== undefined) {
  216. const [url] = selectedHook.function_args
  217. form.setValue('http_url', url, { shouldDirty: false })
  218. } else {
  219. form.setValue('http_url', '', { shouldDirty: false })
  220. }
  221. } else if (functionType === 'briven_function') {
  222. // Default to first edge function in the list
  223. const fnSlug = functions[0]?.slug
  224. const defaultFunctionUrl = `https://${ref}.briven.${restUrlTld}/functions/v1/${fnSlug}`
  225. const currentUrl = form.getValues('http_url')
  226. if (!isEdgeFunction({ ref, restUrlTld, url: currentUrl })) {
  227. form.setValue('http_url', defaultFunctionUrl, { shouldDirty: false })
  228. }
  229. }
  230. field.onChange(functionType)
  231. }}
  232. >
  233. {AVAILABLE_WEBHOOK_TYPES.map((webhook) => (
  234. <RadioGroupStackedItem
  235. key={webhook.value}
  236. id={webhook.value}
  237. value={webhook.value}
  238. label=""
  239. showIndicator={false}
  240. >
  241. <div className="flex items-center space-x-5">
  242. <Image
  243. alt={webhook.label}
  244. src={webhook.icon}
  245. layout="fixed"
  246. width="32"
  247. height="32"
  248. />
  249. <div className="flex-col space-y-0">
  250. <div className="flex space-x-2">
  251. <p className="text-foreground">{webhook.label}</p>
  252. </div>
  253. <p className="text-foreground-light">{webhook.description}</p>
  254. </div>
  255. </div>
  256. </RadioGroupStackedItem>
  257. ))}
  258. </RadioGroupStacked>
  259. </FormControl>
  260. </FormItemLayout>
  261. )}
  262. />
  263. </FormSectionContent>
  264. </FormSection>
  265. <SidePanel.Separator />
  266. <HTTPRequestConfig form={form} />
  267. <SidePanel.Separator />
  268. <HTTPHeaders form={form} />
  269. <SidePanel.Separator />
  270. <HTTPParameters form={form} />
  271. </div>
  272. )
  273. }