Files
supabase/apps/design-system/registry/default/example/page-layout-edge-function.tsx
Danny White 6f6badae51 fix(eslint): promote require-explicit-tabindex to error (#48170)
## What kind of change does this PR introduce?

Accessibility / lint hardening (Safari keyboard focus).

## What is the current behavior?

`supabase/require-explicit-tabindex` is `'warn'`. Studio’s ratchet was
at 0 but the rule was still ratcheted; www / docs / design-system still
had raw `<button>` / `role="button"` call sites without an explicit
`tabIndex`.

[DEPR-627](https://linear.app/supabase/issue/DEPR-627) · follow-up to
#47984 / #48040

## What is the new behavior?

- Shared config: `'supabase/require-explicit-tabindex': 'error'`
- Swept www / docs / design-system (+ Studio test fixtures the ratchet
skipped)
- Removed the rule from the Studio ratchet + baselines

## To test

Prefer **Safari**. This PR only adds explicit `tabIndex` to raw
`<button>` / `role="button"` call sites — not links, and not controls
that already go through `Button` from `ui`.

### Marketing (`www`) ([staging
link](https://zone-www-dot-com-git-danny-depr-627-promote-req-7ae43c-supabase.vercel.app/))

- [x] Homepage frameworks / dashboard feature tabs — Tab through each
tab button
- [x] Product pages (e.g. `/auth`, `/database`) — section tab switchers
- [x] Narrow viewport — open the hamburger; Tab through menu buttons
- [x] `/partners/catalog` — filter / view controls
- [x] Blog view toggle (list ↔ grid)

### Docs ([staging
link](https://docs-git-danny-depr-627-promote-require-explici-25e46d-supabase.vercel.app/))

- [x] **Desktop (≥ lg):** top-right **⋯ menu** (hamburger icon) — opens
a dropdown that includes Theme. Not a separate theme button.
- [x] **Mobile (< lg):** top-right **hamburger** opens the sheet; close
(X) is the raw button we tagged. Theme inside the sheet uses
`ThemeToggle` / `DropdownMenuTrigger` from `ui` (already supposed to set
`tabIndex`).
- [x] **Code blocks** — copy / language controls
- [x] **Is this helpful?** — X / check are `Button` from `ui` (should
already Tab). After voting **while signed in**, the follow-up “What went
well?” / “How can we improve?” text button is the raw one we tagged.
- [x] **AI Tools → Copy as Markdown** (right rail on a guide) — this is
the only GuidesSidebar control this PR changed. “On this page” TOC items
are **links**, not covered by this lint.
- [x] **Reference docs** (e.g. JS client reference) — section headers
that expand/collapse in the left nav (`Collapsible.Trigger`)
- [x] **Troubleshooting index** — type in the search field, then Tab to
the **clear (X)** control

### Dashboard (`studio`)

No production UI changes in this PR (tests + lint config only). Quick
Safari smoke that prior tabindex work still holds:

- [x] Project sidebar — Tab through primary nav links
- [x] Settings → General — Tab through inputs / buttons
- [x] Storage → Files — Tab a bucket row / file actions
2026-07-23 05:21:15 +10:00

824 lines
28 KiB
TypeScript

'use client'
import { Check, Clock, CornerDownLeft, ExternalLink, File, Plus } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useMemo, useState } from 'react'
import {
Badge,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Card,
CardContent,
cn,
flattenTree,
NavMenu,
NavMenuItem,
Tooltip,
TooltipContent,
TooltipTrigger,
TreeView,
TreeViewItem,
type ChartConfig,
type INodeRendererProps,
} from 'ui'
import {
Chart,
ChartActions,
ChartCard,
ChartContent,
ChartHeader,
ChartLine,
ChartLoadingState,
ChartMetric,
} from 'ui-patterns/Chart'
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageNav } from 'ui-patterns/PageNav'
import {
PageSection,
PageSectionAside,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { PageLayoutLogsContent } from './page-layout-logs-content'
type PageId = 'overview' | 'logs' | 'code' | 'settings'
const pages: { id: PageId; label: string }[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'logs', label: 'Logs' },
{ id: 'code', label: 'Code' },
{ id: 'settings', label: 'Settings' },
]
const CHART_INTERVALS = [
{ key: '15min', label: '15 min', format: 'MMM D, h:mm:ssa', minutes: 15 },
{ key: '1hr', label: '1 hour', format: 'MMM D, h:mma', minutes: 60 },
{ key: '3hr', label: '3 hours', format: 'MMM D, h:mma', minutes: 180 },
{ key: '1day', label: '1 day', format: 'MMM D, h:mma', minutes: 24 * 60 },
] as const
const FUNCTION_URL = 'https://demo.supabase.co/functions/v1/hello-world'
type MockInvocationDatum = {
timestamp: string
ok_count: number
warning_count: number
error_count: number
}
type MockMetricsDatum = MockInvocationDatum & {
avg_execution_time: number
max_execution_time: number
avg_cpu_time_used: number
max_cpu_time_used: number
avg_memory_used: number
avg_heap_memory_used: number
avg_external_memory_used: number
}
const pseudoNoise = (seed: number, amplitude = 1) => {
const value = Math.sin(seed * 12.9898) * 43758.5453
return (value - Math.floor(value)) * amplitude
}
const getIntervalMinutes = (intervalKey: string) =>
CHART_INTERVALS.find((item) => item.key === intervalKey)?.minutes ?? 60
const buildMockChartData = (intervalKey: string) => {
const minutes = getIntervalMinutes(intervalKey)
const end = new Date()
end.setSeconds(0, 0)
const invocation: MockInvocationDatum[] = Array.from({ length: minutes }, (_, index) => {
const timestamp = new Date(end)
timestamp.setMinutes(end.getMinutes() - (minutes - 1 - index))
const progress = minutes <= 1 ? 0 : index / (minutes - 1)
const baseLoad = 12 + Math.round(progress * 28)
const noise = Math.round(pseudoNoise(index + minutes * 0.1, 8))
const ok_count = baseLoad + noise
const warning_count = index % 17 === 0 ? 2 + (index % 3) : index % 11 === 0 ? 1 : 0
const error_count = index % 43 === 0 ? 3 + (index % 2) : 0
return {
timestamp: timestamp.toISOString(),
ok_count,
warning_count,
error_count,
}
})
const metrics: MockMetricsDatum[] = invocation.map((datum, index) => {
const avg_execution_time = Math.round(68 + pseudoNoise(index + 2, 24) + index * 0.15)
const max_execution_time = Math.round(
avg_execution_time * (1.25 + pseudoNoise(index + minutes, 0.35))
)
return {
...datum,
avg_execution_time,
max_execution_time,
avg_cpu_time_used: Math.round(8 + pseudoNoise(index + 4, 10) + index * 0.05),
max_cpu_time_used: Math.round(16 + pseudoNoise(index + 6, 18) + index * 0.1),
avg_memory_used: Number((40 + pseudoNoise(index + 8, 12) + index * 0.08).toFixed(1)),
avg_heap_memory_used: Number((24 + pseudoNoise(index + 10, 8) + index * 0.05).toFixed(1)),
avg_external_memory_used: Number((14 + pseudoNoise(index + 12, 6) + index * 0.04).toFixed(1)),
}
})
return { invocation, metrics }
}
const EXECUTION_TIME_CHART_CONFIG = {
avg_execution_time: {
label: 'Average Execution Time',
color: 'var(--foreground-default)',
},
max_execution_time: {
label: 'Max Execution Time',
color: 'hsl(var(--brand-default))',
},
} satisfies ChartConfig
const CPU_TIME_CHART_CONFIG = {
max_cpu_time_used: {
label: 'Max CPU Time',
color: 'hsl(var(--brand-default))',
},
} satisfies ChartConfig
const MEMORY_CHART_CONFIG = {
avg_memory_used: {
label: 'Memory Usage',
color: 'hsl(var(--brand-default))',
},
} satisfies ChartConfig
type MockFileData = {
id: number
name: string
content: string
state: 'new' | 'modified' | 'unchanged'
}
const MOCK_CODE_FILES: MockFileData[] = [
{
id: 1,
name: 'index.ts',
state: 'modified',
content: `import { serve } from 'https://deno.land/std/http/server.ts'
serve(async (req) => {
const { name = 'World' } = await req.json().catch(() => ({}))
return new Response(
JSON.stringify({ message: \`Hello \${name}\` }),
{ headers: { 'content-type': 'application/json' } }
)
})`,
},
{
id: 2,
name: 'deno.json',
state: 'unchanged',
content: JSON.stringify({ imports: {} }, null, '\t'),
},
]
const formatRate = (count: number, total: number) =>
new Intl.NumberFormat('en-US', {
style: 'percent',
maximumFractionDigits: 1,
}).format(total === 0 ? 0 : count / total)
const formatMetric = (value: number, unit?: string) => {
const formatted = unit === 'MB' ? value.toFixed(1) : Math.round(value).toLocaleString('en-US')
return unit ? `${formatted}${unit}` : formatted
}
const getSegmentedButtonClassName = (index: number, total: number) => {
if (index === 0) return 'rounded-tr-none rounded-br-none'
if (index === total - 1) return 'rounded-tl-none rounded-bl-none'
return 'rounded-none'
}
const sumBy = <T,>(items: T[], getValue: (item: T) => number) =>
items.reduce((total, item) => total + getValue(item), 0)
const meanBy = <T,>(items: T[], getValue: (item: T) => number) =>
items.length === 0 ? 0 : sumBy(items, getValue) / items.length
export default function PageLayoutEdgeFunction() {
const [activePage, setActivePage] = useState<PageId>('overview')
return (
<div className="w-full">
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button variant="default" size="tiny">
Test
</Button>
<Button variant="primary" size="tiny">
Deploy
</Button>
</PageBreadcrumbsActions>
}
>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/project/demo/functions">Edge Functions</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>hello-world</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageNav>
<NavMenu>
{pages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
tabIndex={0}
type="button"
aria-pressed={activePage === page.id}
className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
onClick={() => setActivePage(page.id)}
>
{page.label}
</button>
</NavMenuItem>
))}
</NavMenu>
</PageNav>
{activePage === 'overview' && <OverviewPage />}
{activePage === 'logs' && (
<PageContainer size="full" className="px-0 xl:px-0">
<PageLayoutLogsContent />
</PageContainer>
)}
{activePage === 'code' && (
<PageContainer size="full" className="px-0 xl:px-0">
<CodePage />
</PageContainer>
)}
{activePage === 'settings' && <SettingsPage />}
</div>
)
}
function OverviewPage() {
const [interval, setInterval] = useState<string>('1hr')
const selectedInterval =
CHART_INTERVALS.find((item) => item.key === interval) ?? CHART_INTERVALS[1]
const dateTimeFormat = selectedInterval.format
const { invocation: mockInvocationChartData, metrics: mockMetricsChartData } = useMemo(
() => buildMockChartData(interval),
[interval]
)
const { totalInvocationCount, totalWarningCount, totalErrorCount } = useMemo(() => {
const totalInvocationCount = sumBy(
mockInvocationChartData,
(datum) => datum.ok_count + datum.warning_count + datum.error_count
)
return {
totalInvocationCount,
totalWarningCount: sumBy(mockInvocationChartData, (datum) => datum.warning_count),
totalErrorCount: sumBy(mockInvocationChartData, (datum) => datum.error_count),
}
}, [mockInvocationChartData])
const { averageExecutionTime, maxExecutionTime } = useMemo(
() => ({
averageExecutionTime: meanBy(mockMetricsChartData, (datum) => datum.avg_execution_time),
maxExecutionTime: Math.max(...mockMetricsChartData.map((datum) => datum.max_execution_time)),
}),
[mockMetricsChartData]
)
const { averageCpuTime, maxCpuTime, averageMemoryUsage, totalHeapMemory, totalExternalMemory } =
useMemo(() => {
const totalHeapMemory = sumBy(mockMetricsChartData, (datum) => datum.avg_heap_memory_used)
const totalExternalMemory = sumBy(
mockMetricsChartData,
(datum) => datum.avg_external_memory_used
)
return {
averageCpuTime: meanBy(mockMetricsChartData, (datum) => datum.avg_cpu_time_used),
maxCpuTime: Math.max(...mockMetricsChartData.map((datum) => datum.max_cpu_time_used)),
averageMemoryUsage: meanBy(mockMetricsChartData, (datum) => datum.avg_memory_used),
totalHeapMemory,
totalExternalMemory,
}
}, [mockMetricsChartData])
const invocationActions = [
{
label: 'Open logs',
href: '#',
icon: <ExternalLink size={12} />,
},
]
return (
<>
<PageHeader size="small" className="pb-12">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>hello-world</PageHeaderTitle>
<PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 text-sm!">
<span>{FUNCTION_URL}</span>
<span className="flex items-center gap-2">
<Clock size={16} strokeWidth={1.5} className="text-foreground-lighter" />
<span>Last deployed 2 hours ago</span>
</span>
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageSection className="border-t bg-surface-100/50 border-b pb-8 pt-0">
<PageContainer size="full">
<div className="flex flex-col gap-5">
<PageSectionMeta className="items-center! pt-8">
<PageSectionSummary>
<div className="flex flex-wrap items-start gap-x-8 gap-y-4">
<ChartMetric
label="Total Invocations"
value={totalInvocationCount}
status="default"
tooltip="Total number of invocations"
/>
<ChartMetric
label="5xx Rate"
value={formatRate(totalErrorCount, totalInvocationCount)}
status="negative"
tooltip="Share of invocations that returned a 5xx status code"
/>
<ChartMetric
label="4xx Rate"
value={formatRate(totalWarningCount, totalInvocationCount)}
status="warning"
tooltip="Share of invocations that returned a 4xx status code"
/>
</div>
</PageSectionSummary>
<PageSectionAside className="flex-wrap @xl:self-center">
<div className="flex items-center">
{CHART_INTERVALS.map((item, index) => (
<Button
key={item.key}
variant={interval === item.key ? 'secondary' : 'default'}
onClick={() => setInterval(item.key)}
className={getSegmentedButtonClassName(index, CHART_INTERVALS.length)}
>
{item.label}
</Button>
))}
</div>
<ChartActions actions={invocationActions} />
</PageSectionAside>
</PageSectionMeta>
<Chart>
<div className="h-40">
<LogsBarChart
data={mockInvocationChartData}
DateTimeFormat={dateTimeFormat}
isFullHeight
/>
</div>
</Chart>
</div>
</PageContainer>
</PageSection>
<PageContainer size="small">
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Errors since last deploy</PageSectionTitle>
</PageSectionSummary>
<PageSectionAside>
<Button variant="default" size="tiny" icon={<ExternalLink size={14} />}>
View logs
</Button>
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent>
<div className="rounded-md border border-dashed px-5 py-6 text-sm text-foreground-light">
<div className="flex items-start gap-3">
<Check
size={16}
strokeWidth={1.5}
className="mt-0.5 shrink-0 text-brand"
aria-hidden="true"
/>
<div>
There have been <span className="text-foreground">847 invocations</span> since
last deploy and no errors.
</div>
</div>
</div>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Performance</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Chart>
<ChartCard>
<ChartHeader align="start">
<div className="flex flex-wrap gap-x-8 gap-y-4">
<ChartMetric
label="Average Execution Time"
value={formatMetric(averageExecutionTime, 'ms')}
tooltip="Average execution time of function invocations"
/>
<ChartMetric
label="Max Execution Time"
value={formatMetric(maxExecutionTime, 'ms')}
tooltip="Maximum execution time of function invocations"
/>
</div>
</ChartHeader>
<ChartContent loadingState={<ChartLoadingState />}>
<div className="h-40">
<ChartLine
data={mockMetricsChartData}
dataKey="max_execution_time"
dataKeys={['avg_execution_time', 'max_execution_time']}
DateTimeFormat={dateTimeFormat}
config={EXECUTION_TIME_CHART_CONFIG}
isFullHeight
showYAxis
referenceLines={[
{
y: averageExecutionTime,
label: 'average',
stroke: 'var(--foreground-default)',
strokeWidth: 1.5,
},
]}
YAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Math.round(value)}ms`,
}}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Usage</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<div className="flex flex-col gap-6">
<Chart>
<ChartCard>
<ChartHeader align="start">
<div className="flex flex-wrap gap-x-8 gap-y-4">
<ChartMetric
label="Average CPU Time"
value={formatMetric(averageCpuTime, 'ms')}
tooltip="Average CPU time usage for the function"
/>
<ChartMetric
label="Max CPU Time"
value={formatMetric(maxCpuTime, 'ms')}
tooltip="Maximum CPU time usage for the function"
/>
</div>
</ChartHeader>
<ChartContent loadingState={<ChartLoadingState />}>
<div className="h-40">
<ChartLine
data={mockMetricsChartData}
dataKey="max_cpu_time_used"
DateTimeFormat={dateTimeFormat}
config={CPU_TIME_CHART_CONFIG}
isFullHeight
showYAxis
referenceLines={[
{
y: averageCpuTime,
label: 'average',
stroke: 'var(--foreground-default)',
strokeWidth: 1.5,
},
]}
YAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Math.round(value)}ms`,
}}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
<Chart>
<ChartCard>
<ChartHeader align="start">
<div className="flex flex-wrap gap-x-8 gap-y-4">
<ChartMetric
label="Average Memory Usage"
value={formatMetric(averageMemoryUsage, 'MB')}
tooltip="Average memory usage for the function"
/>
<ChartMetric
label="Heap"
value={formatRate(totalHeapMemory, totalHeapMemory + totalExternalMemory)}
tooltip="Share of memory attributed to heap usage over the selected interval"
/>
<ChartMetric
label="External"
value={formatRate(
totalExternalMemory,
totalHeapMemory + totalExternalMemory
)}
tooltip="Share of memory attributed to external usage over the selected interval"
/>
</div>
</ChartHeader>
<ChartContent loadingState={<ChartLoadingState />}>
<div className="h-40">
<ChartLine
data={mockMetricsChartData}
dataKey="avg_memory_used"
DateTimeFormat={dateTimeFormat}
config={MEMORY_CHART_CONFIG}
isFullHeight
showYAxis
referenceLines={[
{
y: averageMemoryUsage,
label: 'average',
stroke: 'var(--foreground-default)',
strokeWidth: 1.5,
},
]}
YAxisProps={{
width: 64,
tickFormatter: (value: number) => `${Number(value).toFixed(1)}MB`,
}}
/>
</div>
</ChartContent>
</ChartCard>
</Chart>
</div>
</PageSectionContent>
</PageSection>
</PageContainer>
</>
)
}
function CodePage() {
const initialFiles = useMemo(() => MOCK_CODE_FILES, [])
const [files, setFiles] = useState<MockFileData[]>(initialFiles)
const [selectedFileId, setSelectedFileId] = useState(initialFiles[0]?.id ?? 1)
const selectedFile = files.find((file) => file.id === selectedFileId)
const treeData = useMemo(
() => ({
name: '',
children: files.map((file) => ({
id: file.id.toString(),
name: file.name,
metadata: { originalId: file.id, state: file.state },
})),
}),
[files]
)
const handleContentChange = (value: string) => {
setFiles((currentFiles) =>
currentFiles.map((file) => {
if (file.id !== selectedFileId) return file
const originalFile = initialFiles.find((item) => item.id === file.id)
if (!originalFile) return { ...file, content: value, state: 'new' }
if (originalFile.content !== value) return { ...file, content: value, state: 'modified' }
return { ...file, content: value, state: 'unchanged' }
})
)
}
const addNewFile = () => {
const newId = Math.max(0, ...files.map((file) => file.id)) + 1
const newFile: MockFileData = {
id: newId,
name: `file-${newId}.ts`,
content: '',
state: 'new',
}
setFiles((currentFiles) => [...currentFiles, newFile])
setSelectedFileId(newId)
}
const renderFileNode = useCallback(
({ element, isBranch, isExpanded, getNodeProps, level }: INodeRendererProps) => {
const originalId =
typeof element.metadata?.originalId === 'number' ? element.metadata.originalId : null
const state = element.metadata?.state as MockFileData['state']
return (
<TreeViewItem
{...getNodeProps()}
isExpanded={isExpanded}
isBranch={isBranch}
isSelected={originalId === selectedFileId}
level={level}
xPadding={16}
name={element.name}
className={cn(
state === 'new' ? 'text-brand-600' : state === 'modified' ? 'text-code_block-2' : ''
)}
icon={<File size={14} className="text-foreground-light shrink-0" />}
onClick={() => {
if (originalId !== null) setSelectedFileId(originalId)
}}
actions={
state !== 'unchanged' && (
<div className="flex items-center justify-center w-3">
<Tooltip>
<TooltipTrigger className="text-xs">{state === 'new' ? 'U' : 'M'}</TooltipTrigger>
<TooltipContent side="bottom">
{state === 'new' ? 'Unsaved' : 'Modified'}
</TooltipContent>
</Tooltip>
</div>
)
}
/>
)
},
[selectedFileId]
)
return (
<div className="flex min-h-[480px] flex-col">
<div className="flex flex-1 overflow-hidden bg-surface-100">
<div className="flex min-w-64 w-64 flex-col border-r bg-surface-200">
<div className="flex items-center justify-between border-b px-4 py-4">
<h3 className="text-sm font-normal font-mono uppercase text-lighter tracking-wide">
Files
</h3>
<Button size="tiny" variant="default" icon={<Plus size={14} />} onClick={addNewFile}>
Add File
</Button>
</div>
<div className="flex-1 overflow-y-auto py-2">
<TreeView
data={flattenTree(treeData)}
aria-label="files tree"
nodeRenderer={renderFileNode}
/>
</div>
</div>
<div className="min-w-0 grow">
{selectedFile ? (
<textarea
aria-label={`Edit ${selectedFile.name}`}
className="h-full min-h-[420px] w-full resize-none border-0 bg-transparent px-5 py-5 font-mono text-[13px] leading-6 text-foreground focus:outline-none"
spellCheck={false}
value={selectedFile.content}
onChange={(event) => handleContentChange(event.target.value)}
/>
) : (
<div className="flex h-full min-h-[420px] items-center justify-center text-sm text-foreground-light">
Select a file to edit
</div>
)}
</div>
</div>
<div className="flex shrink-0 items-center justify-end border-t bg-surface-100 p-4">
<Button
variant="primary"
size="medium"
iconRight={<CornerDownLeft size={10} strokeWidth={1.5} />}
>
Deploy updates
</Button>
</div>
</div>
)
}
function SettingsPage() {
return (
<>
<PageHeader size="small" className="pb-12">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Settings</PageHeaderTitle>
<PageHeaderDescription>
Configure function behavior, security, and deployment options.
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="small">
<PageSection className="pt-0">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Function configuration</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="grid gap-4 p-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Name</p>
<p className="text-sm text-foreground-light">
Your slug and endpoint URL will remain the same
</p>
</div>
<span className="font-mono text-sm">hello-world</span>
</div>
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Verify JWT with legacy secret</p>
<p className="text-sm text-foreground-light">
Require a JWT signed by the legacy secret in the Authorization header.
</p>
</div>
<Badge variant="default">Enabled</Badge>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Import map</PageSectionTitle>
<PageSectionDescription>
Control which import map this function uses at deploy time.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium">Import map</p>
<p className="text-sm text-foreground-light">Use the project import map.</p>
</div>
<Badge variant="default">Default</Badge>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
</PageContainer>
</>
)
}