Skip to content
55 changes: 55 additions & 0 deletions spec/openapi.infra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,17 @@ components:
items:
$ref: '#/components/schemas/SandboxLogEntry'

SandboxLogsV2Response:
required:
- logs
properties:
logs:
default: []
description: Sandbox logs structured
type: array
items:
$ref: "#/components/schemas/SandboxLogEntry"

SandboxMetric:
description: Metric entry with timestamp and line
required:
Expand Down Expand Up @@ -1914,6 +1925,50 @@ paths:
'500':
$ref: '#/components/responses/500'

/v2/sandboxes/{sandboxID}/logs:
get:
description: Get sandbox logs (v2)
tags: [sandboxes]
security:
- ApiKeyAuth: []
- Supabase1TokenAuth: []
Supabase2TeamAuth: []
parameters:
- $ref: "#/components/parameters/sandboxID"
- in: query
name: cursor
schema:
type: integer
format: int64
minimum: 0
description: Starting timestamp of the logs that should be returned in milliseconds
- in: query
name: limit
schema:
default: 1000
type: integer
format: int32
minimum: 0
maximum: 1000
description: Maximum number of logs that should be returned
- in: query
name: direction
schema:
$ref: "#/components/schemas/LogsDirection"
responses:
"200":
description: Successfully returned the sandbox logs
content:
application/json:
schema:
$ref: "#/components/schemas/SandboxLogsV2Response"
"401":
$ref: "#/components/responses/401"
"404":
$ref: "#/components/responses/404"
"500":
$ref: "#/components/responses/500"

/sandboxes/{sandboxID}:
get:
description: Get a sandbox by id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SandboxProvider } from '@/features/dashboard/sandbox/context'
import SandboxDetailsControls from '@/features/dashboard/sandbox/header/controls'
import SandboxDetailsHeader from '@/features/dashboard/sandbox/header/header'
import SandboxLayoutClient from '@/features/dashboard/sandbox/layout'
import { getSandboxDetails } from '@/server/sandboxes/get-sandbox-details'
Expand Down Expand Up @@ -30,12 +31,8 @@ export default async function SandboxLayout({
<SandboxProvider serverSandboxInfo={res?.data} isRunning={exists}>
<SandboxLayoutClient
teamIdOrSlug={teamIdOrSlug}
header={
<SandboxDetailsHeader
teamIdOrSlug={teamIdOrSlug}
state={exists ? 'running' : 'paused'}
/>
}
tabsHeaderAccessory={<SandboxDetailsControls />}
header={<SandboxDetailsHeader state={exists ? 'running' : 'paused'} />}
>
{children}
</SandboxLayoutClient>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/features/dashboard/loading-layout'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SandboxLogsView from '@/features/dashboard/sandbox/logs/view'

export default async function SandboxLogsPage({
params,
}: {
params: Promise<{ teamIdOrSlug: string; sandboxId: string }>
}) {
const { teamIdOrSlug, sandboxId } = await params

return <SandboxLogsView teamIdOrSlug={teamIdOrSlug} sandboxId={sandboxId} />
}
4 changes: 2 additions & 2 deletions src/app/sbx/new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ export const GET = async (req: NextRequest) => {
},
})

const inspectUrl = PROTECTED_URLS.SANDBOX_INSPECT(
const filesystemUrl = PROTECTED_URLS.SANDBOX_FILESYSTEM(
defaultTeam.slug,
sbx.sandboxId
)

return NextResponse.redirect(new URL(inspectUrl, req.url))
return NextResponse.redirect(new URL(filesystemUrl, req.url))
} catch (error) {
l.warn(
{
Expand Down
24 changes: 20 additions & 4 deletions src/configs/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface TitleSegment {
export interface DashboardLayoutConfig {
title: string | TitleSegment[]
type: 'default' | 'custom'
copyValue?: string
custom?: {
includeHeaderBottomStyles: boolean
}
Expand All @@ -27,10 +28,24 @@ const DASHBOARD_LAYOUT_CONFIGS: Record<
title: 'Sandboxes',
type: 'custom',
}),
'/dashboard/*/sandboxes/**/*': () => ({
title: 'Sandbox',
type: 'custom',
}),
'/dashboard/*/sandboxes/*/*': (pathname) => {
const parts = pathname.split('/')
const teamIdOrSlug = parts[2]!
const sandboxId = parts[4]!
const sandboxIdSliced = `${sandboxId.slice(0, 6)}...${sandboxId.slice(-6)}`

return {
title: [
{
label: 'Sandboxes',
href: PROTECTED_URLS.SANDBOXES_LIST(teamIdOrSlug),
},
{ label: sandboxIdSliced },
],
type: 'custom',
copyValue: sandboxId,
}
},
'/dashboard/*/templates': () => ({
title: 'Templates',
type: 'custom',
Expand All @@ -50,6 +65,7 @@ const DASHBOARD_LAYOUT_CONFIGS: Record<
{ label: `Build ${buildIdSliced}` },
],
type: 'custom',
copyValue: buildId,
custom: {
includeHeaderBottomStyles: true,
},
Expand Down
6 changes: 4 additions & 2 deletions src/configs/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ export const PROTECTED_URLS = {

SANDBOX: (teamIdOrSlug: string, sandboxId: string) =>
`/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}`,
SANDBOX_INSPECT: (teamIdOrSlug: string, sandboxId: string) =>
`/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/inspect`,
SANDBOX_FILESYSTEM: (teamIdOrSlug: string, sandboxId: string) =>
`/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/filesystem`,
SANDBOX_LOGS: (teamIdOrSlug: string, sandboxId: string) =>
`/dashboard/${teamIdOrSlug}/sandboxes/${sandboxId}/logs`,

WEBHOOKS: (teamIdOrSlug: string) => `/dashboard/${teamIdOrSlug}/webhooks`,

Expand Down
31 changes: 23 additions & 8 deletions src/features/dashboard/layouts/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { getDashboardLayoutConfig, TitleSegment } from '@/configs/layout'
import { cn } from '@/lib/utils'
import ClientOnly from '@/ui/client-only'
import CopyButton from '@/ui/copy-button'
import { SidebarTrigger } from '@/ui/primitives/sidebar'
import { ThemeSwitcher } from '@/ui/theme-switcher'
import Link from 'next/link'
Expand All @@ -20,6 +21,7 @@ export default function DashboardLayoutHeader({
}: DashboardLayoutHeaderProps) {
const pathname = usePathname()
const config = getDashboardLayoutConfig(pathname)
const copyableValue = config.copyValue ?? null

return (
<div
Expand All @@ -39,18 +41,29 @@ export default function DashboardLayoutHeader({
<div className="flex items-center w-full relative min-h-6 gap-2">
<SidebarTrigger className="w-7 h-7 md:hidden -translate-x-1 shrink-0" />

<h1 className="truncate min-w-0 flex-1">
<HeaderTitle title={config.title} />
</h1>

{/* Ghost element - reserves width but not height */}
<div className="h-0 overflow-visible shrink-0 flex items-center">
{children}
<div className="min-w-0 flex-1 flex items-center gap-2">
<h1 className="truncate min-w-0">
<HeaderTitle title={config.title} />
</h1>
{copyableValue && (
<CopyButton
value={copyableValue}
size="iconSm"
variant="ghost"
className="text-fg-tertiary shrink-0"
aria-label="Copy identifier"
/>
)}
</div>

<ClientOnly>
<ThemeSwitcher />
</ClientOnly>

{/* Ghost element - reserves width but not height */}
<div className="h-0 overflow-visible shrink-0 flex items-center">
{children}
</div>
</div>
</div>
)
Expand All @@ -65,7 +78,9 @@ function HeaderTitle({ title }: { title: string | TitleSegment[] }) {
<span className="flex items-center gap-1">
{title.map((segment, index) => (
<Fragment key={index}>
{index > 0 && <span className="text-fg-tertiary select-none shrink-0">/</span>}
{index > 0 && (
<span className="text-fg-tertiary select-none shrink-0">/</span>
)}
{segment.href ? (
<Link
href={segment.href}
Expand Down
21 changes: 21 additions & 0 deletions src/features/dashboard/sandbox/header/controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { COOKIE_KEYS } from '@/configs/cookies'
import { cookies } from 'next/headers'
import KillButton from './kill-button'
import RefreshControl from './refresh'

export default async function SandboxDetailsControls() {
const initialPollingInterval = (await cookies()).get(
COOKIE_KEYS.SANDBOX_INSPECT_POLLING_INTERVAL
)?.value

return (
<div className="flex items-center gap-2 md:pb-2">
<RefreshControl
initialPollingInterval={
initialPollingInterval ? parseInt(initialPollingInterval) : undefined
}
/>
<KillButton />
</div>
)
}
42 changes: 1 addition & 41 deletions src/features/dashboard/sandbox/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,22 @@
import { COOKIE_KEYS } from '@/configs/cookies'
import { PROTECTED_URLS } from '@/configs/urls'
import { SandboxInfo } from '@/types/api.types'
import { ChevronLeftIcon } from 'lucide-react'
import { cookies } from 'next/headers'
import Link from 'next/link'
import { DetailsItem, DetailsRow } from '../../layouts/details-row'
import KillButton from './kill-button'
import Metadata from './metadata'
import RanFor from './ran-for'
import RefreshControl from './refresh'
import RemainingTime from './remaining-time'
import { ResourceUsageClient } from './resource-usage-client'
import StartedAt from './started-at'
import Status from './status'
import TemplateId from './template-id'
import SandboxDetailsTitle from './title'

interface SandboxDetailsHeaderProps {
teamIdOrSlug: string
state: SandboxInfo['state']
}

export default async function SandboxDetailsHeader({
teamIdOrSlug,
state,
}: SandboxDetailsHeaderProps) {
const initialPollingInterval = (await cookies()).get(
COOKIE_KEYS.SANDBOX_INSPECT_POLLING_INTERVAL
)?.value

return (
<header className="bg-bg relative z-30 flex w-full flex-col gap-6 p-3 md:p-6 max-md:pt-0">
<div className="flex flex-col sm:gap-2 md:gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex flex-col gap-1">
<Link
href={PROTECTED_URLS.SANDBOXES_LIST(teamIdOrSlug)}
className="text-fg-tertiary! hover:text-fg! flex items-center gap-1 prose-body-highlight transition-colors"
prefetch
shallow
>
<ChevronLeftIcon className="size-4" />
Sandboxes
</Link>
<SandboxDetailsTitle />
</div>
<div className="flex items-center gap-2 pt-4 sm:pt-0">
<RefreshControl
initialPollingInterval={
initialPollingInterval
? parseInt(initialPollingInterval)
: undefined
}
className="order-2 sm:order-1"
/>
<KillButton className="order-1 sm:order-2" />
</div>
</div>

<header className="bg-bg relative z-30 w-full p-3 md:p-6">
<DetailsRow>
<DetailsItem label="status">
<Status />
Expand Down
9 changes: 5 additions & 4 deletions src/features/dashboard/sandbox/header/kill-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { cn } from '@/lib/utils/ui'
import { killSandboxAction } from '@/server/sandboxes/sandbox-actions'
import { AlertPopover } from '@/ui/alert-popover'
import { Button } from '@/ui/primitives/button'
Expand Down Expand Up @@ -50,12 +51,12 @@ export default function KillButton({ className }: KillButtonProps) {
confirm="Kill Sandbox"
trigger={
<Button
variant="error"
size="sm"
className={className}
variant="ghost"
size="slate"
className={cn('text-accent-error-highlight', className)}
disabled={!isRunning}
>
<TrashIcon className="size-4" />
<TrashIcon className="size-3.5" />
Kill
</Button>
}
Expand Down
24 changes: 0 additions & 24 deletions src/features/dashboard/sandbox/header/title.tsx

This file was deleted.

Loading
Loading