diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index 71c6a758504..5211259fc6a 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -129,7 +129,7 @@ const EdgeFunctionDetails = () => { return ( <> -
+
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx index 05079a0a20c..da0716f9c90 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet.tsx @@ -1,4 +1,7 @@ import { useState } from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm, useFieldArray } from 'react-hook-form' +import * as z from 'zod' import { Sheet, SheetContent, @@ -20,7 +23,13 @@ import { TabsTrigger_Shadcn_ as TabsTrigger, Badge, CodeBlock, + Form_Shadcn_, + FormField_Shadcn_, + FormItem_Shadcn_, + FormControl_Shadcn_, + FormMessage_Shadcn_, } from 'ui' +import { FormFieldWrapper } from 'components/ui/Forms' import { constructHeaders } from 'data/fetchers' import { useParams } from 'common' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' @@ -54,6 +63,28 @@ type ResponseData = { const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const type HttpMethod = (typeof HTTP_METHODS)[number] +const FormSchema = z.object({ + method: z.enum(HTTP_METHODS), + body: z + .string() + .optional() + .transform((str) => str || '{}'), + headers: z.array( + z.object({ + key: z.string(), + value: z.string(), + }) + ), + queryParams: z.array( + z.object({ + key: z.string(), + value: z.string(), + }) + ), +}) + +type FormValues = z.infer + const EdgeFunctionTesterSheet = ({ visible, onClose, @@ -61,10 +92,6 @@ const EdgeFunctionTesterSheet = ({ apiKey, }: EdgeFunctionTesterSheetProps) => { const { ref: projectRef } = useParams() - const [method, setMethod] = useState('POST') - const [body, setBody] = useState('{ "name": "Functions" }') - const [headers, setHeaders] = useState([{ key: '', value: '' }]) - const [queryParams, setQueryParams] = useState([{ key: '', value: '' }]) const [response, setResponse] = useState(null) const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) @@ -75,40 +102,51 @@ const EdgeFunctionTesterSheet = ({ const getImpersonatedRole = useGetImpersonatedRole() const { serviceKey } = getAPIKeys(settings) + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + method: 'POST', + body: '{ "name": "Functions" }', + headers: [{ key: '', value: '' }], + queryParams: [{ key: '', value: '' }], + }, + }) + + const { + fields: headerFields, + append: appendHeader, + remove: removeHeader, + } = useFieldArray({ + control: form.control, + name: 'headers', + }) + + const { + fields: queryParamFields, + append: appendQueryParam, + remove: removeQueryParam, + } = useFieldArray({ + control: form.control, + name: 'queryParams', + }) + const addKeyValuePair = (type: 'headers' | 'queryParams') => { if (type === 'headers') { - setHeaders([...headers, { key: '', value: '' }]) + appendHeader({ key: '', value: '' }) } else { - setQueryParams([...queryParams, { key: '', value: '' }]) + appendQueryParam({ key: '', value: '' }) } } const removeKeyValuePair = (index: number, type: 'headers' | 'queryParams') => { if (type === 'headers') { - setHeaders(headers.filter((_, i) => i !== index)) + removeHeader(index) } else { - setQueryParams(queryParams.filter((_, i) => i !== index)) + removeQueryParam(index) } } - const updateKeyValuePair = ( - index: number, - field: 'key' | 'value', - value: string, - type: 'headers' | 'queryParams' - ) => { - if (type === 'headers') { - const newHeaders = [...headers] - newHeaders[index][field] = value - setHeaders(newHeaders) - } else { - const newQueryParams = [...queryParams] - newQueryParams[index][field] = value - setQueryParams(newQueryParams) - } - } - - const sendRequest = async () => { + const onSubmit = async (values: FormValues) => { try { setIsLoading(true) setError(null) @@ -116,9 +154,10 @@ const EdgeFunctionTesterSheet = ({ let requestBody: any try { - requestBody = JSON.parse(body) + requestBody = JSON.parse(values.body) } catch (e) { - throw new Error('Invalid JSON in request body') + form.setError('body', { message: 'Must be a valid JSON string' }) + return } let testAuthorization: string | undefined @@ -138,21 +177,20 @@ const EdgeFunctionTesterSheet = ({ error: err.message, roleDetails: role, }) - // Don't throw, fall back to service key console.log('Falling back to service key') } } // Construct custom headers const customHeaders: Record = {} - headers.forEach(({ key, value }) => { + headerFields.forEach(({ key, value }) => { if (key && value) { customHeaders[key] = value } }) // Construct query parameters - const queryString = queryParams + const queryString = queryParamFields .filter(({ key, value }) => key && value) .map(({ key, value }) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&') @@ -168,7 +206,7 @@ const EdgeFunctionTesterSheet = ({ }, body: JSON.stringify({ url: finalUrl, - method, + method: values.method, body: requestBody, headers: { ...(accessToken && { @@ -194,14 +232,10 @@ const EdgeFunctionTesterSheet = ({ } } - const renderKeyValuePairs = ( - type: 'headers' | 'queryParams', - items: KeyValuePair[], - label: string - ) => ( + const renderKeyValuePairs = (type: 'headers' | 'queryParams', label: string) => (
- +
- {items.map((item, index) => ( -
- updateKeyValuePair(index, 'key', e.target.value, type)} - disabled={isLoading} - className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-l-0 border-r-1 border-t-0 border-b-0 border-border" + {(type === 'headers' ? headerFields : queryParamFields).map((field, index) => ( +
+ ( + + + + )} /> - updateKeyValuePair(index, 'value', e.target.value, type)} - disabled={isLoading} - className="h-auto py-2 font-mono rounded-none shadow-none bg-transparent border-none" + ( + + + + )} />
- {items.length > 1 && ( + {(type === 'headers' ? headerFields : queryParamFields).length > 1 && (