diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx
index 11f6df3e22c..c46ee161d1b 100644
--- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx
@@ -4,14 +4,14 @@ import { ExternalLink, Maximize2, Minimize2, Terminal } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
+import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
+import { object, string, boolean } from 'yup'
+import { yupResolver } from '@hookform/resolvers/yup'
import { useParams } from 'common'
-import { ButtonTooltip } from 'components/ui/ButtonTooltip'
-import { FormActions } from 'components/ui/Forms/FormActions'
-import { FormHeader } from 'components/ui/Forms/FormHeader'
-import { FormPanel } from 'components/ui/Forms/FormPanel'
-import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
+import { FormFieldWrapper } from 'components/ui/Forms'
+import { SectionHeader } from 'components/layouts/PageLayout'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
@@ -23,16 +23,27 @@ import {
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
CriticalIcon,
- Form,
+ Form_Shadcn_,
Input,
Modal,
+ Switch,
Toggle,
cn,
} from 'ui'
import CommandRender from '../CommandRender'
import { generateCLICommands } from './EdgeFunctionDetails.utils'
+const schema = object({
+ name: string().required('Name is required'),
+ verify_jwt: boolean().required(),
+})
+
const EdgeFunctionDetails = () => {
const router = useRouter()
const { ref: projectRef, functionSlug } = useParams()
@@ -50,7 +61,6 @@ const EdgeFunctionDetails = () => {
},
})
- const formId = 'edge-function-update-form'
const canUpdateEdgeFunction = useCheckPermissions(PermissionAction.FUNCTIONS_WRITE, '*')
const { anonKey } = getAPIKeys(settings)
@@ -69,7 +79,24 @@ const EdgeFunctionDetails = () => {
anonKey: apiKey,
})
- const onUpdateFunction = async (values: any, { resetForm }: any) => {
+ const form = useForm({
+ resolver: yupResolver(schema),
+ defaultValues: {
+ name: '',
+ verify_jwt: false,
+ },
+ })
+
+ useEffect(() => {
+ if (selectedFunction) {
+ form.reset({
+ name: selectedFunction.name,
+ verify_jwt: selectedFunction.verify_jwt,
+ })
+ }
+ }, [selectedFunction])
+
+ const onUpdateFunction = async (values: any) => {
if (!projectRef) return console.error('Project ref is required')
if (selectedFunction === undefined) return console.error('No edge function selected')
@@ -81,7 +108,6 @@ const EdgeFunctionDetails = () => {
},
{
onSuccess: () => {
- resetForm({ values, initialValues: values })
toast.success(`Successfully updated edge function`)
},
}
@@ -101,173 +127,183 @@ const EdgeFunctionDetails = () => {
return (
<>
-
-
- }
- >
- Function Details}>
-
-
-
-
-
-
-
-
-
-
- Function Configuration}>
-
-
+
+
+
+
+ )}
+
+
+
+ {form.formState.isDirty && (
+ form.reset()}>
+ Cancel
+
+ )}
+
+ Save changes
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
Command line access
+
+
setShowInstructions(!showInstructions)}
+ >
+ {showInstructions ? (
+
+ ) : (
+
+ )}
-
Command line access
-
-
setShowInstructions(!showInstructions)}>
- {showInstructions ? (
-
- ) : (
-
- )}
+
+
Deployment management
+
+
Invoke
+
+
Secrets management
+
-
Deployment management
-
-
Invoke
-
-
Secrets management
-
+
+
+
+
+ Once your function is deleted, it can no longer be restored
+
+
+ Make sure you have made a backup if you want to restore your edge function
+
+
+ setShowDeleteModal(true)}
+ >
+ Delete edge function
+
+
+
+
-
-
-
-
-
- Once your function is deleted, it can no longer be restored
-
-
- Make sure you have made a backup if you want to restore your edge function
-
-
- setShowDeleteModal(true)}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canUpdateEdgeFunction
- ? 'You need additional permissions to delete edge functions'
- : undefined,
- },
- }}
- >
- Delete edge function
-
-
-
+
+
+
+ Details
+
+
+
+ Slug
+ {selectedFunction?.slug}
+
+ Endpoint URL
+
+
+
+
+ Region
+ All functions are deployed globally
+
+ Created at
+
+ {dayjs(selectedFunction?.created_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')}
+
+
+ Last updated at
+
+ {dayjs(selectedFunction?.updated_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')}
+
+
+ Deployments
+ {selectedFunction?.version ?? 0}
+
+ Import Maps
+
+
+ Import maps are{' '}
+
+ {hasImportMap ? 'used' : 'not used'}
+ {' '}
+ for this function
+
+
+ Import maps allow the use of bare specifiers in functions instead of explicit
+ import URLs
+
+
+ }
+ >
+
+ More about import maps
+
+
+
+
+
+
+
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTester.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTester.tsx
new file mode 100644
index 00000000000..b7c729a4456
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTester.tsx
@@ -0,0 +1,239 @@
+'use client'
+
+import { useState } from 'react'
+import {
+ Button,
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+ Label_Shadcn_ as Label,
+ Select_Shadcn_ as Select,
+ SelectContent_Shadcn_ as SelectContent,
+ SelectItem_Shadcn_ as SelectItem,
+ SelectTrigger_Shadcn_ as SelectTrigger,
+ SelectValue_Shadcn_ as SelectValue,
+ TextArea_Shadcn_ as Textarea,
+ Tabs_Shadcn_ as Tabs,
+ TabsContent_Shadcn_ as TabsContent,
+ TabsList_Shadcn_ as TabsList,
+ TabsTrigger_Shadcn_ as TabsTrigger,
+ Badge,
+ CodeBlock,
+} from 'ui'
+import { constructHeaders } from 'data/fetchers'
+import { useParams } from 'common'
+import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
+import { getRoleImpersonationJWT } from 'lib/role-impersonation'
+import { useGetImpersonatedRole } from 'state/role-impersonation-state'
+import {
+ RoleImpersonationPopover,
+ RoleImpersonationSelector,
+} from 'components/interfaces/RoleImpersonationSelector'
+import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
+import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
+import { IS_PLATFORM } from 'lib/constants'
+import { prettifyJSON } from 'lib/helpers'
+
+type ResponseData = {
+ status: number
+ headers: Record
+ body: string
+}
+
+interface EdgeFunctionTesterProps {
+ url: string
+ anonKey: string
+}
+
+const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const
+type HttpMethod = (typeof HTTP_METHODS)[number]
+
+export default function EdgeFunctionTester({ url, anonKey }: EdgeFunctionTesterProps) {
+ const { ref: projectRef } = useParams()
+ const [method, setMethod] = useState('POST')
+ const [body, setBody] = useState('{ "name": "Functions" }')
+ const [response, setResponse] = useState(null)
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+
+ const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
+ const { data: settings } = useProjectSettingsV2Query({ projectRef })
+ const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
+ const getImpersonatedRole = useGetImpersonatedRole()
+ const { serviceKey } = getAPIKeys(settings)
+
+ const sendRequest = async () => {
+ try {
+ setIsLoading(true)
+ setError(null)
+ setResponse(null)
+
+ let requestBody: any
+ try {
+ requestBody = JSON.parse(body)
+ } catch (e) {
+ throw new Error('Invalid JSON in request body')
+ }
+
+ let testAuthorization: string | undefined
+ const role = getImpersonatedRole()
+
+ console.log('Role impersonation details:', {
+ role,
+ projectRef,
+ hasJwtSecret: !!config?.jwt_secret,
+ hasServiceKey: !!serviceKey?.api_key,
+ hasAccessToken: !!accessToken,
+ })
+
+ if (
+ projectRef !== undefined &&
+ config?.jwt_secret !== undefined &&
+ role !== undefined &&
+ role.type === 'postgrest'
+ ) {
+ try {
+ console.log('Generating JWT for role:', {
+ roleDetails: role,
+ type: role.type,
+ })
+
+ const token = await getRoleImpersonationJWT(projectRef, config.jwt_secret, role)
+ testAuthorization = 'Bearer ' + token
+ console.log('Successfully generated JWT token for role')
+ } catch (err: any) {
+ console.error('Failed to generate JWT:', {
+ error: err.message,
+ roleDetails: role,
+ })
+ // Don't throw, fall back to service key
+ console.log('Falling back to service key')
+ }
+ }
+
+ console.log('serviceKey', serviceKey)
+
+ const headers = await constructHeaders()
+ const res = await fetch('/api/edge-functions/test', {
+ method: 'POST',
+ headers: {
+ ...headers,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ url,
+ method,
+ body: requestBody,
+ headers: {
+ ...(accessToken && {
+ Authorization: `Bearer ${accessToken}`,
+ }),
+ 'x-test-authorization': testAuthorization ?? `Bearer ${serviceKey?.api_key}`,
+ 'Content-Type': 'application/json',
+ },
+ }),
+ })
+
+ const data = await res.json()
+ if (!res.ok) {
+ throw new Error(data.error?.message || 'Failed to test edge function')
+ }
+
+ setResponse(data)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'An unknown error occurred')
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <>
+
+
+ Method
+ setMethod(value as HttpMethod)}
+ disabled={isLoading}
+ >
+
+
+
+
+ {HTTP_METHODS.map((m) => (
+
+ {m}
+
+ ))}
+
+
+
+
+
+ Body
+
+
+
+
+ Send Request
+
+
+
+
+ {error && (
+
+ )}
+
+ {response && (
+
+
+
+
+
+ Body
+
+
+ Headers
+
+
+ = 400 ? 'destructive' : 'success'}>
+ {response.status}
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionPanel.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionPanel.tsx
index eb2f75444df..b417910a2d0 100644
--- a/apps/studio/components/interfaces/Functions/EdgeFunctionPanel.tsx
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionPanel.tsx
@@ -50,6 +50,36 @@ Deno.serve(async (req: Request) => {
{ headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' }}
);
});`,
+ },
+ {
+ name: 'Supabase Database Access',
+ description: 'Example using Supabase client to query your database',
+ content: `// Setup type definitions for built-in Supabase Runtime APIs
+import "jsr:@supabase/functions-js/edge-runtime.d.ts";
+import { createClient } from 'jsr:@supabase/supabase-js@2'
+
+Deno.serve(async (req) => {
+ try {
+ const supabase = createClient(
+ Deno.env.get('SUPABASE_URL') ?? '',
+ Deno.env.get('SUPABASE_ANON_KEY') ?? '',
+ { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
+ )
+
+ const { data, error } = await supabase.from('countries').select('*')
+
+ if (error) {
+ throw error
+ }
+
+ return new Response(JSON.stringify({ data }), {
+ headers: { 'Content-Type': 'application/json' },
+ status: 200,
+ })
+ } catch (err) {
+ return new Response(String(err?.message ?? err), { status: 500 })
+ }
+})`,
},
{
name: 'Node Built-in API Example',
diff --git a/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx b/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx
index 6d8d276014f..91fbb511a29 100644
--- a/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx
+++ b/apps/studio/components/layouts/FunctionsLayout/FunctionsLayout.tsx
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
-import { useEffect, type PropsWithChildren } from 'react'
+import { useEffect, type PropsWithChildren, useState } from 'react'
import { useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
@@ -11,19 +11,90 @@ import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { withAuth } from 'hooks/misc/withAuth'
-import { Code } from 'lucide-react'
+import { Code, Code2, Play, Send, Terminal, TestTubeDiagonal } from 'lucide-react'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
-import { Button, cn } from 'ui'
+import { Button, cn, CodeBlock } from 'ui'
+import { Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui'
+import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import FunctionsNav from '../../interfaces/Functions/FunctionsNav'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { PageLayout } from 'components/layouts/PageLayout'
import EdgeFunctionsLayout from '../EdgeFunctionsLayout/EdgeFunctionsLayout'
+import EdgeFunctionTester from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTester'
interface FunctionsLayoutProps {
title?: string
}
+const InvokePopover = ({
+ functionName,
+ url,
+ apiKey,
+}: {
+ functionName: string
+ url: string
+ apiKey: string
+}) => {
+ return (
+
+
+ }>
+ Invoke
+
+
+
+
+
Invoke via CLI
+
+
+
+
Invoke via supabase-js
+
+
+
+
+ )
+}
+
+const TestPopover = ({ url, apiKey }: { url: string; apiKey: string }) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+
+ }>
+ Test
+
+
+
+
+
+
Send test requests to your edge function
+
+
+
+
+
+ )
+}
+
const FunctionsLayout = ({ title, children }: PropsWithChildren) => {
const router = useRouter()
const { functionSlug, ref } = useParams()
@@ -35,11 +106,17 @@ const FunctionsLayout = ({ title, children }: PropsWithChildren 0
+ const { anonKey } = getAPIKeys(settings)
+ const apiKey = anonKey?.api_key ?? '[YOUR ANON KEY]'
+ const protocol = settings?.app_config?.protocol ?? 'https'
+ const endpoint = settings?.app_config?.endpoint ?? ''
+ const functionUrl = `${protocol}://${endpoint}/functions/v1/${functionSlug}`
useEffect(() => {
let cancel = false
@@ -92,42 +169,37 @@ const FunctionsLayout = ({ title, children }: PropsWithChildren
-
+ {isNewAPIDocsEnabled && (
+
+ )}
+
+ {functionSlug && (
+ <>
+
+
+ >
+ )}
+
+ }
>
-
-
- Manage secrets
-
- {isNewAPIDocsEnabled && (
-
- )}
-
-
- }
- >
- {children}
-
-
+ {children}
+
)
}
diff --git a/apps/studio/components/layouts/PageLayout/PageHeader.tsx b/apps/studio/components/layouts/PageLayout/PageHeader.tsx
index 706f106d578..220e31a5d14 100644
--- a/apps/studio/components/layouts/PageLayout/PageHeader.tsx
+++ b/apps/studio/components/layouts/PageLayout/PageHeader.tsx
@@ -1,4 +1,5 @@
import { useParams } from 'common'
+import { ChevronLeft } from 'lucide-react'
import { Fragment, ReactNode } from 'react'
import { cn } from 'ui'
import {
@@ -47,11 +48,18 @@ export const PageHeader = ({
{item.href ? (
-
+
+ {breadcrumbs.length === 1 && }
{item.label}
) : (
- {item.label}
+
+ {breadcrumbs.length === 1 && }
+ {item.label}
+
)}
{index < breadcrumbs.length - 1 && }
diff --git a/apps/studio/components/layouts/PageLayout/PageLayout.tsx b/apps/studio/components/layouts/PageLayout/PageLayout.tsx
index 021f3fe4c07..a895baee4b5 100644
--- a/apps/studio/components/layouts/PageLayout/PageLayout.tsx
+++ b/apps/studio/components/layouts/PageLayout/PageLayout.tsx
@@ -72,7 +72,7 @@ const PageLayout = ({
const { ref } = useParams()
return (
-
+
{item.icon && {item.icon} }
@@ -121,9 +124,8 @@ const PageLayout = ({
)}
-
{/* Content section */}
-
{children}
+ {children}
)
}
diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts
index fc0fa936ff0..a8832103248 100644
--- a/apps/studio/middleware.ts
+++ b/apps/studio/middleware.ts
@@ -13,6 +13,7 @@ const HOSTED_SUPPORTED_API_URLS = [
'/ai/onboarding/design',
'/ai/sql/complete',
'/ai/edge-function/complete',
+ '/edge-functions/test',
'/ai/sql/title',
'/ai/sql/debug',
'/ai/sql/cron',
diff --git a/apps/studio/pages/api/edge-functions/test.ts b/apps/studio/pages/api/edge-functions/test.ts
new file mode 100644
index 00000000000..1f47578867b
--- /dev/null
+++ b/apps/studio/pages/api/edge-functions/test.ts
@@ -0,0 +1,87 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import { constructHeaders, handleError } from 'data/fetchers'
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'POST') {
+ return res.status(405).json({
+ error: { message: `Method ${req.method} Not Allowed` },
+ })
+ }
+
+ try {
+ const { url, method, body, headers: customHeaders } = req.body
+
+ console.log('Edge function test request:', {
+ url,
+ method,
+ body,
+ customHeaders,
+ })
+
+ // Convert incoming headers to HeadersInit format
+ const incomingHeaders = Object.entries(req.headers).reduce(
+ (acc, [key, value]) => {
+ if (value) acc[key] = Array.isArray(value) ? value.join(', ') : value
+ return acc
+ },
+ {} as Record
+ )
+
+ // Get auth headers from the request to forward them
+ const headers = await constructHeaders(incomingHeaders)
+ const requestHeaders = {
+ ...headers,
+ 'Content-Type': 'application/json',
+ ...customHeaders,
+ }
+
+ // Use the test authorization header if provided, otherwise use the default authorization
+ if (customHeaders['x-test-authorization']) {
+ requestHeaders['Authorization'] = customHeaders['x-test-authorization']
+ console.log('Using test authorization header')
+ }
+
+ console.log('Forwarding request with headers:', requestHeaders)
+
+ const response = await fetch(url, {
+ method,
+ headers: requestHeaders,
+ body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
+ })
+
+ if (!response.ok) {
+ const error = await response.json()
+ return handleError(error)
+ }
+
+ console.log('Edge function response status:', response.status)
+
+ const responseHeaders: Record = {}
+ response.headers.forEach((value, key) => {
+ responseHeaders[key] = value
+ })
+
+ const responseBody = await response.text()
+
+ console.log('Edge function response:', {
+ status: response.status,
+ headers: responseHeaders,
+ body: responseBody,
+ })
+
+ return res.status(response.status).json({
+ status: response.status,
+ headers: responseHeaders,
+ body: responseBody,
+ })
+ } catch (error: any) {
+ console.error('Edge function test error:', {
+ message: error.message,
+ stack: error.stack,
+ cause: error.cause,
+ })
+ return res.status(500).json({
+ error: error.message || 'Failed to test edge function',
+ })
+ }
+}
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx
index e2c0057de39..bb0478a71d0 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx
@@ -5,9 +5,9 @@ import { PageContainer } from 'components/layouts/PageLayout'
import type { NextPageWithLayout } from 'types'
const PageLayout: NextPageWithLayout = () => (
-
+
-
+
)
PageLayout.getLayout = (page) => (
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
index f63c19adbe4..67896ca882a 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
@@ -159,7 +159,7 @@ const PageLayout: NextPageWithLayout = () => {
return
}
return (
-
+
@@ -336,7 +336,7 @@ const PageLayout: NextPageWithLayout = () => {
-
+
)
}
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx
index 5c8476bf659..5d7f597b4db 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/invocations.tsx
@@ -20,15 +20,14 @@ export const LogPage: NextPageWithLayout = () => {
if (selectedFunction === undefined || isLoading) return null
return (
-
+
-
+
)
}
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx
index ff4a8b4a1c1..cbb2df134e0 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/logs.tsx
@@ -17,15 +17,14 @@ export const LogPage: NextPageWithLayout = () => {
if (selectedFunction === undefined || isLoading) return null
return (
-
+
-
+
)
}
diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx
index 79dfa0bb127..4bc70c37029 100644
--- a/apps/studio/pages/project/[ref]/functions/index.tsx
+++ b/apps/studio/pages/project/[ref]/functions/index.tsx
@@ -78,14 +78,18 @@ const FunctionsPage: NextPageWithLayout = () => {
return (
-
- {isLoading && }
+
+ {isLoading && (
+
+
+
+ )}
{isError && }
diff --git a/apps/studio/pages/project/[ref]/functions/secrets.tsx b/apps/studio/pages/project/[ref]/functions/secrets.tsx
index f2b9b485901..6bb4819eb44 100644
--- a/apps/studio/pages/project/[ref]/functions/secrets.tsx
+++ b/apps/studio/pages/project/[ref]/functions/secrets.tsx
@@ -9,7 +9,7 @@ import { DocsButton } from 'components/ui/DocsButton'
const SecretsPage: NextPageWithLayout = () => {
return (
-
+
)
@@ -20,6 +20,7 @@ SecretsPage.getLayout = (page) => {