Slight tuning of edge function page (#43798)

<img width="1543" height="981" alt="image"
src="https://github.com/user-attachments/assets/8e6c37c5-15c7-4bc4-be87-477e36a62a65"
/>


Switches from two to one column layout for details page and removes an
inconsistent background colour in header. Moves critical detail
information from page into header (url and deploy dates/counts)
This commit is contained in:
Saxon Fletcher authored and GitHub committed 2026-03-19 15:55:19 +10:00
1 parent 48b96e8a79
commit 8499d30e98
4 files changed
+272 -320

No files matched your search

@@ -1,3 +1,4 @@
import { IS_PLATFORM } from 'common'
import { Circle, Code, Minus, Plus, Wind } from 'lucide-react'
import Link from 'next/link'
import { basename } from 'path'
@@ -111,7 +112,7 @@ const FunctionDiff = ({
<CardHeader>
<CardTitle>
<Link
href={`/project/${currentBranchRef}/functions/${functionSlug}`}
href={`/project/${currentBranchRef}/functions/${functionSlug}${IS_PLATFORM ? '' : '/details'}`}
className="flex items-center gap-2"
>
<Code strokeWidth={1.5} size={16} className="text-foreground-muted" />
@@ -1,9 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { IS_PLATFORM, useParams } from 'common'
import dayjs from 'dayjs'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
@@ -29,7 +26,6 @@ import {
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
@@ -46,7 +42,6 @@ import z from 'zod'
import CommandRender from '../CommandRender'
import { INVOCATION_TABS } from './EdgeFunctionDetails.constants'
import { generateCLICommands } from './EdgeFunctionDetails.utils'
import AlertError from '@/components/ui/AlertError'
import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
@@ -54,7 +49,6 @@ import { useEdgeFunctionDeleteMutation } from '@/data/edge-functions/edge-functi
import { useEdgeFunctionUpdateMutation } from '@/data/edge-functions/edge-functions-update-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { DOCS_URL } from '@/lib/constants'
const FormSchema = z.object({
name: z.string().min(0, 'Name is required'),
@@ -87,13 +81,7 @@ export const EdgeFunctionDetails = () => {
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const {
data: selectedFunction,
error,
isPending: isLoading,
isError,
isSuccess,
} = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
const { data: endpoint } = useProjectApiUrl({ projectRef })
const functionUrl = `${endpoint}/functions/v1/${selectedFunction?.slug}`
@@ -114,10 +102,6 @@ export const EdgeFunctionDetails = () => {
const { anonKey, publishableKey } = getKeys(apiKeys)
const apiKey = publishableKey?.api_key ?? anonKey?.api_key ?? '[YOUR ANON KEY]'
const hasImportMap = useMemo(
() => selectedFunction?.import_map || selectedFunction?.import_map_path,
[selectedFunction]
)
const { managementCommands } = generateCLICommands({
selectedFunction,
functionUrl,
@@ -158,112 +142,58 @@ export const EdgeFunctionDetails = () => {
}, [selectedFunction])
return (
<PageContainer size="full">
<PageSection orientation="horizontal">
<PageSectionSummary className="gap-6 !self-start">
<PageSectionTitle>Details</PageSectionTitle>
{isLoading && <GenericSkeletonLoader />}
{isError && (
<AlertError
error={error}
subject="Failed to retrieve edge function details"
layout="vertical"
/>
)}
{isSuccess && (
<dl className="grid grid-cols-1 @xl:grid-cols-[auto_1fr] gap-y-2 [&>dd]:mb-3 @xl:[&>dd]:mb-0 @xl:gap-y-4 gap-x-10">
<dt className="text-sm text-foreground-light">Slug</dt>
<dd className="text-sm @lg:text-left">{selectedFunction?.slug}</dd>
<dt className="text-sm text-foreground-light">Endpoint URL</dt>
<dd className="text-sm @lg:text-left">
<Input
copy
readOnly
size="small"
className="font-mono input-mono"
value={functionUrl}
/>
</dd>
{IS_PLATFORM && (
<>
<dt className="text-sm text-foreground-light">Region</dt>
<dd className="text-sm @lg:text-left">All functions are deployed globally</dd>
</>
)}
<dt className="text-sm text-foreground-light">Created at</dt>
<dd className="text-sm @lg:text-left">
{dayjs(selectedFunction?.created_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')}
</dd>
<dt className="text-sm text-foreground-light">Last updated at</dt>
<dd className="text-sm @lg:text-left">
{dayjs(selectedFunction?.updated_at ?? 0).format('dddd, MMMM D, YYYY h:mm A')}
</dd>
<dt className="text-sm text-foreground-light">Deployments</dt>
<dd className="text-sm @lg:text-left">{selectedFunction?.version ?? 0}</dd>
<dt className="text-sm text-foreground-light">Import Maps</dt>
<dd className="text-sm @lg:text-left">
<p>
Import maps are{' '}
<span className={cn(hasImportMap ? 'text-brand' : 'text-amber-900')}>
{hasImportMap ? 'used' : 'not used'}
</span>{' '}
for this function
</p>
<p className="text-foreground-light mt-1">
Import maps allow the use of bare specifiers in functions instead of explicit
import URLs
</p>
<div className="mt-4">
<Button
asChild
type="default"
size="tiny"
icon={<ExternalLink strokeWidth={1.5} />}
>
<Link
href={`${DOCS_URL}/guides/functions/dependencies`}
target="_blank"
rel="noreferrer"
>
More about import maps
</Link>
</Button>
</div>
</dd>
</dl>
)}
</PageSectionSummary>
<PageContainer size="small">
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Function configuration</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<PageSection className="pt-0">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Function configuration</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
<Card>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onUpdateFunction)}>
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="name"
render={({ field }) => (
<FormItemLayout
label="Name"
layout="flex-row-reverse"
description="Your slug and endpoint URL will remain the same"
>
<FormControl_Shadcn_>
<Input {...field} className="w-64" disabled={!canUpdateEdgeFunction} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
{IS_PLATFORM && (
<>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="name"
name="verify_jwt"
render={({ field }) => (
<FormItemLayout
label="Name"
label="Verify JWT with legacy secret"
layout="flex-row-reverse"
description="Your slug and endpoint URL will remain the same"
description={
<>
Requires a JWT signed{' '}
<em className="text-brand not-italic">only by the legacy secret</em>{' '}
in the <code>Authorization</code> header. The <code>anon</code> key
satisfies this. Recommended: OFF with JWT and custom auth logic in
your function code.
</>
}
>
<FormControl_Shadcn_>
<Input
{...field}
className="w-64"
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateEdgeFunction}
/>
</FormControl_Shadcn_>
@@ -271,214 +201,178 @@ export const EdgeFunctionDetails = () => {
)}
/>
</CardContent>
{IS_PLATFORM && (
<>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="verify_jwt"
render={({ field }) => (
<FormItemLayout
label="Verify JWT with legacy secret"
layout="flex-row-reverse"
description={
<>
Requires that a JWT signed{' '}
<em className="text-brand not-italic">
only by the legacy JWT secret
</em>{' '}
is present in the <code>Authorization</code> header. The easy to
obtain <code>anon</code> key can be used to satisfy this
requirement. Recommendation: OFF with JWT and additional
authorization logic implemented inside your function's code.
</>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateEdgeFunction}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="flex justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdating}
disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</>
)}
</Card>
</form>
</Form_Shadcn_>
</PageSectionContent>
</PageSection>
<CardFooter className="flex justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdating}
disabled={!canUpdateEdgeFunction || !form.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</>
)}
</Card>
</form>
</Form_Shadcn_>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Invoke function</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="px-0">
<Tabs
className="w-full"
defaultValue="curl"
value={selectedTab}
onValueChange={setSelectedTab}
>
<TabsList className="flex flex-wrap gap-4 px-6">
{invocationTabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
{selectedTab === 'curl' && (
<Button
type="default"
className="ml-auto -translate-y-2 translate-x-3"
onClick={() => setShowKey(!showKey)}
>
{showKey ? 'Hide' : 'Show'} anon key
</Button>
)}
</TabsList>
{invocationTabs.map((tab) => {
const code = tab.code({
showKey,
functionUrl,
functionName: selectedFunction?.name ?? '',
apiKey,
})
return (
<TabsContent key={tab.id} value={tab.id} className="mt-4 px-6">
<CodeBlock
value={code}
className={cn(
'p-0 text-xs !mt-0 border-none [&>code]:!whitespace-pre-wrap',
showKey ? '[&>code]:break-all' : '[&>code]:break-words'
)}
language={tab.language}
wrapLines={true}
hideLineNumbers={tab.hideLineNumbers}
handleCopy={() => {
copyToClipboard(
tab.code({
showKey: true,
functionUrl,
functionName: selectedFunction?.name ?? '',
apiKey,
})
)
}}
/>
</TabsContent>
)
})}
</Tabs>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
{IS_PLATFORM && (
<>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Invoke function</PageSectionTitle>
<PageSectionTitle>Develop locally</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="px-0">
<Tabs
className="w-full"
defaultValue="curl"
value={selectedTab}
onValueChange={setSelectedTab}
>
<TabsList className="flex flex-wrap gap-4 px-6">
{invocationTabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
{selectedTab === 'curl' && (
<Button
type="default"
className="ml-auto -translate-y-2 translate-x-3"
onClick={() => setShowKey(!showKey)}
>
{showKey ? 'Hide' : 'Show'} anon key
</Button>
)}
</TabsList>
{invocationTabs.map((tab) => {
const code = tab.code({
showKey,
functionUrl,
functionName: selectedFunction?.name ?? '',
apiKey,
})
return (
<TabsContent key={tab.id} value={tab.id} className="mt-4 px-6">
<CodeBlock
value={code}
className={cn(
'p-0 text-xs !mt-0 border-none [&>code]:!whitespace-pre-wrap',
showKey ? '[&>code]:break-all' : '[&>code]:break-words'
)}
language={tab.language}
wrapLines={true}
hideLineNumbers={tab.hideLineNumbers}
handleCopy={() => {
copyToClipboard(
tab.code({
showKey: true,
functionUrl,
functionName: selectedFunction?.name ?? '',
apiKey,
})
)
}}
/>
</TabsContent>
)
})}
</Tabs>
</CardContent>
</Card>
<div className="rounded border bg-surface-100 px-6 py-4 drop-shadow-sm">
<div className="space-y-6">
<CommandRender
commands={[
{
command: `supabase functions download ${selectedFunction?.slug}`,
description: 'Download the function to your local machine',
jsx: () => (
<>
<span className="text-brand-600">supabase</span> functions download{' '}
{selectedFunction?.slug}
</>
),
comment: '1. Download the function',
},
]}
/>
<CommandRender commands={[managementCommands[0]]} />
<CommandRender commands={[managementCommands[1]]} />
</div>
</div>
</PageSectionContent>
</PageSection>
{IS_PLATFORM && (
<>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Develop locally</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="rounded border bg-surface-100 px-6 py-4 drop-shadow-sm">
<div className="space-y-6">
<CommandRender
commands={[
{
command: `supabase functions download ${selectedFunction?.slug}`,
description: 'Download the function to your local machine',
jsx: () => (
<>
<span className="text-brand-600">supabase</span> functions download{' '}
{selectedFunction?.slug}
</>
),
comment: '1. Download the function',
},
]}
/>
<CommandRender commands={[managementCommands[0]]} />
<CommandRender commands={[managementCommands[1]]} />
</div>
</div>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Delete function</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Alert_Shadcn_ variant="destructive">
<CriticalIcon />
<AlertTitle_Shadcn_>
Once your function is deleted, it can no longer be restored
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Make sure you have made a backup if you want to restore your edge function
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-3">
<Button
type="danger"
disabled={!canUpdateEdgeFunction}
loading={selectedFunction?.id === undefined}
onClick={() => setShowDeleteModal(true)}
>
Delete edge function
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</PageSectionContent>
</PageSection>
<ConfirmationModal
visible={showDeleteModal}
loading={isDeleting}
variant="destructive"
confirmLabel="Delete"
confirmLabelLoading="Deleting"
title={`Confirm to delete ${selectedFunction?.name}`}
onCancel={() => setShowDeleteModal(false)}
onConfirm={onConfirmDelete}
alert={{
base: { variant: 'destructive' },
title: 'This action cannot be undone',
description:
'Ensure that you have made a backup if you want to restore your edge function',
}}
/>
</>
)}
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Delete function</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Alert_Shadcn_ variant="destructive">
<CriticalIcon />
<AlertTitle_Shadcn_>
Once your function is deleted, it can no longer be restored
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Make sure you have made a backup if you want to restore your edge function
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-3">
<Button
type="danger"
disabled={!canUpdateEdgeFunction}
loading={selectedFunction?.id === undefined}
onClick={() => setShowDeleteModal(true)}
>
Delete edge function
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</PageSectionContent>
</PageSection>
<ConfirmationModal
visible={showDeleteModal}
loading={isDeleting}
variant="destructive"
confirmLabel="Delete"
confirmLabelLoading="Deleting"
title={`Confirm to delete ${selectedFunction?.name}`}
onCancel={() => setShowDeleteModal(false)}
onConfirm={onConfirmDelete}
alert={{
base: { variant: 'destructive' },
title: 'This action cannot be undone',
description:
'Ensure that you have made a backup if you want to restore your edge function',
}}
/>
</>
)}
</PageContainer>
)
}
@@ -6,14 +6,17 @@ import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFun
import { APIDocsButton } from 'components/ui/APIDocsButton'
import { DocsButton } from 'components/ui/DocsButton'
import NoPermission from 'components/ui/NoPermission'
import { useProjectApiUrl } from 'data/config/project-endpoint-query'
import { useEdgeFunctionBodyQuery } from 'data/edge-functions/edge-function-body-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { withAuth } from 'hooks/misc/withAuth'
import { DOCS_URL } from 'lib/constants'
import { Download, FileArchive, Send } from 'lucide-react'
import { Clock, Download, FileArchive, Send } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useEffect, useState, type PropsWithChildren } from 'react'
@@ -24,6 +27,9 @@ import {
BreadcrumbList_Shadcn_ as BreadcrumbList,
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
Button,
HoverCard,
HoverCardContent,
HoverCardTrigger,
NavMenu,
NavMenuItem,
Popover_Shadcn_,
@@ -36,6 +42,7 @@ import {
PageHeader,
PageHeaderAside,
PageHeaderBreadcrumb,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderNavigationTabs,
PageHeaderSummary,
@@ -45,6 +52,8 @@ import {
import { ProjectLayout } from '../ProjectLayout'
import EdgeFunctionsLayout from './EdgeFunctionsLayout'
dayjs.extend(relativeTime)
interface EdgeFunctionDetailsLayoutProps {
title: string
}
@@ -65,12 +74,14 @@ const EdgeFunctionDetailsLayout = ({
)
const [isOpen, setIsOpen] = useState(false)
const [isTimestampHoverCardOpen, setIsTimestampHoverCardOpen] = useState(false)
const {
data: selectedFunction,
error,
isError,
} = useEdgeFunctionQuery({ projectRef: ref, slug: functionSlug })
const { data: endpoint } = useProjectApiUrl({ projectRef: ref })
const { data: functionBody = { version: 0, files: [] }, error: filesError } =
useEdgeFunctionBodyQuery(
@@ -91,6 +102,14 @@ const EdgeFunctionDetailsLayout = ({
)
const name = selectedFunction?.name || ''
const functionUrl =
endpoint && selectedFunction?.slug ? `${endpoint}/functions/v1/${selectedFunction.slug}` : ''
const createdRelative = selectedFunction?.created_at
? dayjs(selectedFunction.created_at).fromNow()
: undefined
const updatedRelative = selectedFunction?.updated_at
? dayjs(selectedFunction.updated_at).fromNow()
: undefined
const browserTitle = {
entity: functionSlug ? name || functionSlug : undefined,
section: title,
@@ -125,14 +144,14 @@ const EdgeFunctionDetailsLayout = ({
},
]
: []),
{
label: 'Details',
href: `/project/${ref}/functions/${functionSlug}/details`,
},
{
label: 'Code',
href: `/project/${ref}/functions/${functionSlug}/code`,
},
{
label: 'Settings',
href: `/project/${ref}/functions/${functionSlug}/details`,
},
]
: []
@@ -228,7 +247,7 @@ const EdgeFunctionDetailsLayout = ({
return (
<EdgeFunctionsLayout title={title} browserTitle={browserTitle}>
<div className="w-full min-h-full flex flex-col items-stretch">
<PageHeader size="full" className="sticky top-0 z-10 bg-background">
<PageHeader size="full" className="sticky top-0 z-10 bg-surface-75">
{breadcrumbItems.length > 0 && (
<PageHeaderBreadcrumb>
<BreadcrumbList>
@@ -253,6 +272,44 @@ const EdgeFunctionDetailsLayout = ({
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>{functionSlug ? name : 'Edge Functions'}</PageHeaderTitle>
<PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 !text-sm">
<span className="flex items-center gap-2">{functionUrl}</span>
<HoverCard
openDelay={250}
closeDelay={100}
open={isTimestampHoverCardOpen}
onOpenChange={setIsTimestampHoverCardOpen}
>
<HoverCardTrigger asChild>
<button type="button" className="flex items-center gap-2 group">
<Clock size={16} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="transition text-foreground-light group-hover:text-foreground underline decoration-dotted decoration-foreground-muted underline-offset-4">
{updatedRelative ?? 'Deploy status unavailable'}
</span>
</button>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-[320px] p-0">
{createdRelative && (
<div className="px-4 py-2 space-y-1">
<h3 className="heading-meta text-foreground-light">Created</h3>
<p className="text-foreground">{createdRelative}</p>
</div>
)}
{updatedRelative && (
<div className="px-4 py-2 space-y-1">
<h3 className="heading-meta text-foreground-light">Last deployed</h3>
<p className="text-foreground">{updatedRelative}</p>
</div>
)}
{selectedFunction?.version !== undefined && (
<div className="px-4 py-2 space-y-1">
<h3 className="heading-meta text-foreground-light">Deployments</h3>
<p className="text-foreground">{selectedFunction.version}</p>
</div>
)}
</HoverCardContent>
</HoverCard>
</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
@@ -7,7 +7,7 @@ const PageLayout: NextPageWithLayout = () => <EdgeFunctionDetails />
PageLayout.getLayout = (page) => (
<DefaultLayout>
<EdgeFunctionDetailsLayout title="Details">{page}</EdgeFunctionDetailsLayout>
<EdgeFunctionDetailsLayout title="Settings">{page}</EdgeFunctionDetailsLayout>
</DefaultLayout>
)