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