Layout
Guidelines to create consistent layouts across Studio pages using a set of page components.
Build every Studio page with PageContainer, optional page chrome (Page Breadcrumbs, Page Nav), PageHeader when you need a title block, and PageSection. These components handle width, spacing, and content structure so pages feel consistent across the app.
Core rules
- Breadcrumbs first. Every page starts with a
bordered breadcrumb row (
PageBreadcrumbs) at the top. Place it as a sibling abovePageHeaderor page content — not insidePageHeader. - Sub navigation below breadcrumbs. When a
page has tabs or section links, place
PageNavdirectly under the breadcrumb row, aligned top left. Also a sibling — not insidePageHeader. -
Parent pages with sub navigation stay compact.
The parent omitsPageHeader(no title or description). Breadcrumbs name the parent; child routes render their ownPageHeaderwith meta. - Pick width by content, not page type.
| Width | Use when |
|---|---|
small | Settings, forms, and focused configuration (including child pages under a settings parent) |
default | Lists, tables, and detail pages that stay readable without full viewport width |
full | Dense horizontal content: logs, code, editors, charts, or tables that need the viewport |
A route can mix widths across child pages (for example, a full-width parent with a small settings tab and a full logs tab).
- Page header meta is optional. Add
PageHeaderwithPageHeaderMeta(icon, title, description, aside) only when that context helps the user. Skip it when the work area is self-explanatory (for example, logs with filters and a table). -
Put actions where the user is already looking.
| Situation | Where actions go |
|---|---|
| Parent with sub navigation | PageBreadcrumbsActions on the breadcrumb row |
Child page with PageHeaderMeta and no filter row | PageHeaderAside |
| Table or list with a filter/search row | Right side of that row (not header aside) |
| Simple list with no filter row | PageHeaderAside or PageSectionAside |
| Compact chrome, no meta (for example logs) | Breadcrumb row or in-page controls (filter bar, toolbar) |
- Section titles for multiple sections. When a
page has no
PageHeadertitle and the content is split into multiplePageSections, usePageSectionTitleandPageSectionDescriptionto label each section (see Page Section).
Patterns
Settings
Single-column configuration. Use size="small" or size="default" for both header and container. Group fields with PageSection.
import { zodResolver } from '@hookform/resolvers/zod'
import Link from 'next/link'
import { useForm } from 'react-hook-form'
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Card,
CardContent,
CardFooter,
Form,
FormControl,
FormField,
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
const RefreshTokenSchema = z.object({
REFRESH_TOKEN_ROTATION_ENABLED: z.boolean(),
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: z.coerce.number().min(0),
})
const UserSessionsSchema = z.object({
SESSIONS_TIMEBOX: z.coerce.number().min(0),
SESSIONS_INACTIVITY_TIMEOUT: z.coerce.number().min(0),
SESSIONS_SINGLE_PER_USER: z.boolean(),
})
function HoursOrNeverText({ value }: { value: number }) {
if (value === 0) {
return 'never'
} else if (value === 1) {
return 'hour'
} else {
return 'hours'
}
}
export function PageLayoutSettings() {
const refreshTokenForm = useForm<z.infer<typeof RefreshTokenSchema>>({
resolver: zodResolver(RefreshTokenSchema),
defaultValues: {
REFRESH_TOKEN_ROTATION_ENABLED: false,
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10,
},
})
const userSessionsForm = useForm<z.infer<typeof UserSessionsSchema>>({
resolver: zodResolver(UserSessionsSchema),
defaultValues: {
SESSIONS_TIMEBOX: 0,
SESSIONS_INACTIVITY_TIMEOUT: 0,
SESSIONS_SINGLE_PER_USER: false,
},
})
return (
<div className="w-full">
<PageBreadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/project/demo/auth">Authentication</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>User Sessions</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>User Sessions</PageHeaderTitle>
<PageHeaderDescription>
Configure settings for user sessions and refresh tokens
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Refresh Tokens</PageSectionTitle>
<PageSectionDescription>
Configure refresh token rotation and security settings.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...refreshTokenForm}>
<form className="space-y-4">
<Card>
<CardContent className="pt-6">
<FormField
control={refreshTokenForm.control}
name="REFRESH_TOKEN_ROTATION_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Detect and revoke potentially compromised refresh tokens"
description="Prevent replay attacks from potentially compromised refresh tokens."
>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={refreshTokenForm.control}
name="SECURITY_REFRESH_TOKEN_REUSE_INTERVAL"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Refresh token reuse interval"
description="Time interval where the same refresh token can be used multiple times to request for an access token. Recommendation: 10 seconds."
>
<FormControl>
<InputGroup>
<InputGroupAddon align="inline-end">
<InputGroupText>seconds</InputGroupText>
</InputGroupAddon>
<InputGroupInput type="number" min={0} {...field} />
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{refreshTokenForm.formState.isDirty && (
<Button type="default" onClick={() => refreshTokenForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!refreshTokenForm.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>User Sessions</PageSectionTitle>
<PageSectionDescription>
Configure session timeout and single session enforcement settings.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...userSessionsForm}>
<form className="space-y-4">
<Card>
<CardContent>
<FormField
control={userSessionsForm.control}
name="SESSIONS_SINGLE_PER_USER"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enforce single session per user"
description="If enabled, all but a user's most recently active session will be terminated."
>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={userSessionsForm.control}
name="SESSIONS_TIMEBOX"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Time-box user sessions"
description="The amount of time before a user is forced to sign in again. Use 0 for never."
>
<div className="flex items-center">
<FormControl>
<InputGroup>
<InputGroupAddon align="inline-end">
<InputGroupText>
<HoursOrNeverText value={field.value || 0} />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput type="number" min={0} {...field} />
</InputGroup>
</FormControl>
</div>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={userSessionsForm.control}
name="SESSIONS_INACTIVITY_TIMEOUT"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Inactivity timeout"
description="The amount of time a user needs to be inactive to be forced to sign in again. Use 0 for never."
>
<div className="flex items-center">
<FormControl>
<InputGroup>
<InputGroupAddon align="inline-end">
<InputGroupText>
<HoursOrNeverText value={field.value || 0} />
</InputGroupText>
</InputGroupAddon>
<InputGroupInput type="number" {...field} />
</InputGroup>
</FormControl>
</div>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{userSessionsForm.formState.isDirty && (
<Button type="default" onClick={() => userSessionsForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!userSessionsForm.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}Page sub navigation sits below page breadcrumbs.
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Card,
CardContent,
CardFooter,
Form,
FormControl,
FormField,
Input,
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
NavMenu,
NavMenuItem,
Switch,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageBreadcrumbs } 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,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
type SubPageId = 'templates' | 'smtp'
const subPages: { id: SubPageId; label: string }[] = [
{ id: 'templates', label: 'Templates' },
{ id: 'smtp', label: 'SMTP Settings' },
]
const AUTHENTICATION_TEMPLATES = [
{
title: 'Confirm signup',
purpose: 'Ask users to confirm their email address after signing up',
},
{
title: 'Invite user',
purpose: 'Invite users who do not yet have an account to join your application',
},
{
title: 'Magic link',
purpose: 'Allow users to sign in via a one-time link sent to their email',
},
{
title: 'Change email address',
purpose: 'Ask users to verify their new email address after changing it',
},
{
title: 'Reset password',
purpose: 'Allow users to reset their password if they forget it',
},
]
const SECURITY_TEMPLATES = [
{
id: 'PASSWORD_CHANGED',
title: 'Password changed',
purpose: 'Notify users when their password has been changed',
enabled: true,
},
{
id: 'EMAIL_CHANGED',
title: 'Email address changed',
purpose: 'Notify users when their email address has been changed',
enabled: true,
},
{
id: 'PHONE_CHANGED',
title: 'Phone number changed',
purpose: 'Notify users when their phone number has been changed',
enabled: false,
},
]
const NotificationsFormSchema = z.object({
MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED: z.boolean(),
MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED: z.boolean(),
MAILER_NOTIFICATIONS_PHONE_CHANGED_ENABLED: z.boolean(),
})
const SmtpFormSchema = z.object({
ENABLE_SMTP: z.boolean(),
SMTP_ADMIN_EMAIL: z.string().optional(),
SMTP_SENDER_NAME: z.string().optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional(),
SMTP_MAX_FREQUENCY: z.coerce.number().optional(),
SMTP_USER: z.string().optional(),
})
export function PageLayoutAuthEmails() {
const [activePage, setActivePage] = useState<SubPageId>('templates')
return (
<div className="w-full">
<PageBreadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/project/demo/auth">Authentication</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Emails</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageNav>
<NavMenu>
{subPages.map((page) => (
<NavMenuItem key={page.id} active={activePage === page.id}>
<button
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 === 'templates' && (
<TemplatesPage onNavigateToSmtp={() => setActivePage('smtp')} />
)}
{activePage === 'smtp' && <SmtpPage />}
</div>
)
}
function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
const notificationsForm = useForm<z.infer<typeof NotificationsFormSchema>>({
resolver: zodResolver(NotificationsFormSchema),
defaultValues: {
MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED: true,
MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED: true,
MAILER_NOTIFICATIONS_PHONE_CHANGED_ENABLED: false,
},
})
const handleSave = (values: z.infer<typeof NotificationsFormSchema>) => {
notificationsForm.reset(values)
notificationsForm.clearErrors()
}
return (
<>
<PageHeader size="small">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Templates</PageHeaderTitle>
<PageHeaderDescription>
Configure what emails your users receive and how they are sent
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="small" className="pb-16">
<PageSection>
<Admonition
type="warning"
title="Set up custom SMTP"
description="You're using the built-in email service. This service has rate limits and is not meant to be used for production apps."
layout="horizontal"
className="mb-4"
actions={
<Button type="default" size="tiny" onClick={onNavigateToSmtp}>
Set up SMTP
</Button>
}
/>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Authentication</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
{AUTHENTICATION_TEMPLATES.map((template) => (
<CardContent key={template.title} className="p-0">
<button
type="button"
className="flex w-full items-center justify-between px-6 py-4 text-left transition-colors hover:bg-surface-200"
>
<div className="flex flex-col">
<h3 className="text-sm text-foreground">{template.title}</h3>
<p className="text-sm text-foreground-lighter">{template.purpose}</p>
</div>
<ChevronRight size={16} className="text-foreground-muted" />
</button>
</CardContent>
))}
</Card>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Security</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...notificationsForm}>
<form className="space-y-4" onSubmit={notificationsForm.handleSubmit(handleSave)}>
<Card>
{SECURITY_TEMPLATES.map((template) => {
const fieldName =
`MAILER_NOTIFICATIONS_${template.id}_ENABLED` as keyof z.infer<
typeof NotificationsFormSchema
>
return (
<CardContent
key={template.id}
className="flex h-full w-full items-center justify-between p-0 transition-colors hover:bg-surface-200"
>
<button type="button" className="flex flex-1 flex-col px-6 py-4 text-left">
<h3 className="text-sm text-foreground">{template.title}</h3>
<p className="text-sm text-foreground-lighter">{template.purpose}</p>
</button>
<div className="relative flex h-full items-center gap-4 pl-2">
<FormField
control={notificationsForm.control}
name={fieldName}
render={({ field }) => (
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
)}
/>
<button type="button" className="py-6 pr-6" aria-label="Edit template">
<ChevronRight size={16} className="text-foreground-muted" />
</button>
</div>
</CardContent>
)
})}
<CardFooter className="justify-end space-x-2">
{notificationsForm.formState.isDirty && (
<Button type="default" onClick={() => notificationsForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!notificationsForm.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
</PageContainer>
</>
)
}
function SmtpPage() {
const form = useForm<z.infer<typeof SmtpFormSchema>>({
resolver: zodResolver(SmtpFormSchema),
defaultValues: {
ENABLE_SMTP: false,
SMTP_ADMIN_EMAIL: '',
SMTP_SENDER_NAME: '',
SMTP_HOST: '',
SMTP_PORT: undefined,
SMTP_MAX_FREQUENCY: 60,
SMTP_USER: '',
},
})
const enableSmtp = form.watch('ENABLE_SMTP')
return (
<>
<PageHeader size="small">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>SMTP Settings</PageHeaderTitle>
<PageHeaderDescription>
Configure a custom SMTP provider for sending authentication and security emails
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="small">
<PageSection>
<PageSectionContent>
<Form {...form}>
<form className="space-y-4" onSubmit={form.handleSubmit(() => undefined)}>
<Card>
<CardContent>
<FormField
control={form.control}
name="ENABLE_SMTP"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable custom SMTP"
description="Emails will be sent using your custom SMTP provider. Email rate limits can be adjusted in rate limits settings."
>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItemLayout>
)}
/>
{enableSmtp && (
<Admonition
type="warning"
title="All fields must be filled"
description="Each of the fields below must be filled before custom SMTP can be enabled."
className="mt-4 border-warning-400 bg-warning-200"
/>
)}
</CardContent>
{enableSmtp && (
<>
<CardContent className="py-6">
<div className="grid grid-cols-12 gap-6">
<div className="col-span-4">
<h3 className="mb-1 text-sm">Sender details</h3>
<p className="text-balance text-sm text-foreground-lighter">
Configure the sender information for your emails.
</p>
</div>
<div className="col-span-8 space-y-4">
<FormField
control={form.control}
name="SMTP_ADMIN_EMAIL"
render={({ field }) => (
<FormItemLayout
label="Sender email address"
description="The email address the emails are sent from."
>
<FormControl>
<Input {...field} placeholder="noreply@yourdomain.com" />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="SMTP_SENDER_NAME"
render={({ field }) => (
<FormItemLayout
label="Sender name"
description="Name displayed in the recipient's inbox."
>
<FormControl>
<Input {...field} placeholder="Your App" />
</FormControl>
</FormItemLayout>
)}
/>
</div>
</div>
</CardContent>
<CardContent className="py-6">
<div className="grid grid-cols-12 gap-6">
<div className="col-span-4">
<h3 className="mb-1 text-sm">SMTP provider settings</h3>
<p className="text-balance text-sm text-foreground-lighter">
Your SMTP credentials will always be encrypted in our database.
</p>
</div>
<div className="col-span-8 space-y-4">
<FormField
control={form.control}
name="SMTP_HOST"
render={({ field }) => (
<FormItemLayout
label="Host"
description="Hostname or IP address of your SMTP server."
>
<FormControl>
<Input {...field} placeholder="smtp.yourdomain.com" />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="SMTP_PORT"
render={({ field }) => (
<FormItemLayout
label="Port number"
description="Port used by your SMTP server. Common ports include 465 and 587."
>
<FormControl>
<Input
type="number"
value={field.value ?? ''}
onChange={(event) => field.onChange(event.target.value)}
placeholder="587"
/>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="SMTP_MAX_FREQUENCY"
render={({ field }) => (
<FormItemLayout
label="Minimum interval per user"
description="The minimum time in seconds between emails before another email can be sent to the same user."
>
<FormControl>
<InputGroup>
<InputGroupInput
type="number"
value={field.value ?? ''}
onChange={(event) => field.onChange(event.target.value)}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>seconds</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="SMTP_USER"
render={({ field }) => (
<FormItemLayout
label="Username"
description="Username for your SMTP server."
>
<FormControl>
<Input {...field} placeholder="SMTP Username" />
</FormControl>
</FormItemLayout>
)}
/>
</div>
</div>
</CardContent>
</>
)}
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
<Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
</PageContainer>
</>
)
}List
Collections of objects (tables, triggers, functions). Default to size="default"; use full when columns need the width.
import { Search } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
InputGroup,
InputGroupAddon,
InputGroupInput,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
export function PageLayoutList() {
const functions = [
{
id: 1,
name: 'get_user_profile',
arguments: 'user_id uuid',
return_type: 'jsonb',
security: 'Definer',
},
{
id: 2,
name: 'update_user_settings',
arguments: 'user_id uuid, settings jsonb',
return_type: 'void',
security: 'Invoker',
},
{
id: 3,
name: 'calculate_total',
arguments: 'amount numeric, tax_rate numeric',
return_type: 'numeric',
security: 'Definer',
},
]
return (
<div className="w-full">
<PageBreadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/project/demo/database">Database</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Functions</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Database Functions</PageHeaderTitle>
<PageHeaderDescription>Manage your database functions</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
<PageSection>
<PageSectionContent>
<div className="w-full space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2">
<InputGroup>
<InputGroupInput
placeholder="Search for a function"
size="tiny"
className="w-full lg:w-52"
/>
<InputGroupAddon>
<Search />
</InputGroupAddon>
</InputGroup>
</div>
<Button type="primary">Create a new function</Button>
</div>
<Card>
<Table className="table-fixed overflow-x-auto">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="table-cell">Arguments</TableHead>
<TableHead className="table-cell">Return type</TableHead>
<TableHead className="table-cell w-[100px]">Security</TableHead>
<TableHead className="w-1/6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{functions.map((fn) => (
<TableRow key={fn.id}>
<TableCell className="font-medium">{fn.name}</TableCell>
<TableCell>{fn.arguments}</TableCell>
<TableCell>{fn.return_type}</TableCell>
<TableCell>{fn.security}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" size="small">
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</div>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}import Link from 'next/link'
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderAside,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
export function PageLayoutListSimple() {
const items = [
{ id: 1, name: 'Project Alpha', status: 'Active', members: 12 },
{ id: 2, name: 'Project Beta', status: 'Active', members: 8 },
{ id: 3, name: 'Project Gamma', status: 'Inactive', members: 5 },
]
return (
<div className="w-full">
<PageBreadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/org/demo">Organization</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Projects</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Projects</PageHeaderTitle>
<PageHeaderDescription>
Manage and view all your projects in one place.
</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
<Button type="primary" size="small">
Create Project
</Button>
</PageHeaderAside>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
<PageSection>
<PageSectionContent>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Members</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.status}</TableCell>
<TableCell>{item.members}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" size="small">
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}Detail
Dense content split into sections. Use size="default" and PageSection with orientation="horizontal" where a summary sits beside content.
import Link from 'next/link'
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
Card,
CardContent,
} from 'ui'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
export function PageLayoutDetail() {
return (
<div className="w-full">
<PageBreadcrumbs>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/org/demo">Organization</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Billing</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Billing</PageHeaderTitle>
<PageHeaderDescription>
Manage your organization's billing and subscription settings.
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
<PageSection orientation="horizontal">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Subscription</PageSectionTitle>
<PageSectionDescription>
View and manage your current subscription plan and billing cycle.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<p className="text-xs text-foreground-lighter mb-1">Current Plan</p>
<p className="text-sm font-medium">Pro Plan</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Billing Cycle</p>
<p className="text-sm">Monthly</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Next Billing Date</p>
<p className="text-sm">March 15, 2024</p>
</div>
<div className="pt-2">
<Button type="default" size="small">
Change Plan
</Button>
</div>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
<PageSection orientation="horizontal">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Cost control</PageSectionTitle>
<PageSectionDescription>
Set spending limits and alerts to manage your organization's costs.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<p className="text-xs text-foreground-lighter mb-1">Monthly Spending Limit</p>
<p className="text-sm">$500.00</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Current Month Spend</p>
<p className="text-sm">$234.50</p>
</div>
<div className="pt-2">
<Button type="default" size="small">
Configure Limits
</Button>
</div>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
<PageSection orientation="horizontal">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Payment Methods</PageSectionTitle>
<PageSectionDescription>
Manage payment methods and billing information for your organization.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<p className="text-xs text-foreground-lighter mb-1">Primary Payment Method</p>
<p className="text-sm">•••• •••• •••• 4242</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Expires</p>
<p className="text-sm">12/2025</p>
</div>
<div className="pt-2">
<Button type="default" size="small">
Update Payment Method
</Button>
</div>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}Full width
Logs, code, editors, and other dense views. Use size="full". Keep the top compact: breadcrumbs, optional breadcrumb-row actions, then sub navigation when needed. Omit PageHeader when there is no title block. Page-level actions live on the breadcrumb row or in the content (filter bar, toolbar).
'use client'
import Link from 'next/link'
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
} from 'ui'
import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import { PageLayoutLogsContent } from './page-layout-logs-content'
export function PageLayoutFullWidth() {
return (
<div className="w-full">
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button type="default" size="tiny">
Docs
</Button>
</PageBreadcrumbsActions>
}
>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/project/demo">Project</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Logs</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</PageBreadcrumbs>
<PageContainer size="full" className="px-0 xl:px-0">
<PageLayoutLogsContent />
</PageContainer>
</div>
)
}Parent with mixed child widths
Some parents are full width because a child needs it. The parent chrome stays compact (breadcrumbs, actions, sub navigation). Each child picks its own container width: overview charts in full, settings in small, logs and code in full without extra outer padding.
'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: 'hsl(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 function PageLayoutEdgeFunction() {
const [activePage, setActivePage] = useState<PageId>('overview')
return (
<div className="w-full">
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button type="default" size="tiny">
Test
</Button>
<Button type="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
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}
type={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 type="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: 'hsl(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: 'hsl(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: 'hsl(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" type="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
type="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>
</>
)
}Components
- Page Breadcrumbs — full-width breadcrumb row
- Page Nav — full-width sub-navigation row
- Page Container — max-width and padding (
small,default,full, …) - Page Header — optional title block (meta, icon, aside)
- Page Section — titled content blocks and section-level actions