Skip to content
12 changes: 12 additions & 0 deletions src/features/dashboard/sandbox/header/controls.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
'use client'

import { useSandboxContext } from '../context'
import KillButton from './kill-button'
import PauseButton from './pause-button'
import ResumeButton from './resume-button'

export default function SandboxDetailsControls() {
const { sandboxInfo } = useSandboxContext()

const isPaused = sandboxInfo?.state === 'paused'
const isRunning = sandboxInfo?.state === 'running'

return (
<div className="flex items-center gap-2 md:pb-2">
{isRunning && <PauseButton />}
{isPaused && <ResumeButton />}
<KillButton />
</div>
)
Expand Down
5 changes: 2 additions & 3 deletions src/features/dashboard/sandbox/header/kill-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useAction } from 'next-safe-action/hooks'
import { useState } from 'react'
import { toast } from 'sonner'
import { killSandboxAction } from '@/core/server/actions/sandbox-actions'
import { AlertPopover } from '@/ui/alert-popover'
import { AlertDialog } from '@/ui/alert-dialog'
import { Button } from '@/ui/primitives/button'
import { TrashIcon } from '@/ui/primitives/icons'
import { useDashboard } from '../../context'
Expand Down Expand Up @@ -45,7 +45,7 @@ export default function KillButton({ className }: KillButtonProps) {
}

return (
<AlertPopover
<AlertDialog
open={open}
onOpenChange={setOpen}
title="Kill Sandbox"
Expand All @@ -62,7 +62,6 @@ export default function KillButton({ className }: KillButtonProps) {
loading: isExecuting ? 'Killing...' : undefined,
}}
onConfirm={handleKill}
onCancel={() => setOpen(false)}
/>
)
}
67 changes: 67 additions & 0 deletions src/features/dashboard/sandbox/header/pause-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import Sandbox from 'e2b'
import { useCallback, useState } from 'react'
import { toast } from 'sonner'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import { supabase } from '@/core/shared/clients/supabase/client'
import { cn } from '@/lib/utils/ui'
import { Button } from '@/ui/primitives/button'
import { PausedIcon } from '@/ui/primitives/icons'
import { useDashboard } from '../../context'
import { useSandboxContext } from '../context'

interface PauseButtonProps {
className?: string
}

export default function PauseButton({ className }: PauseButtonProps) {
const { sandboxInfo, refetchSandboxInfo } = useSandboxContext()
const { team } = useDashboard()
const [isExecuting, setIsExecuting] = useState(false)
const canPause = sandboxInfo?.state === 'running'

const handlePause = useCallback(async () => {
if (!canPause || !sandboxInfo?.sandboxID) return

setIsExecuting(true)
try {
const { data } = await supabase.auth.getSession()
if (!data?.session) {
toast.error('Session expired. Please sign in again.')
return
}

await Sandbox.pause(sandboxInfo.sandboxID, {
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
headers: {
...SUPABASE_AUTH_HEADERS(data.session.access_token, team.id),
},
})

toast.success('Sandbox paused successfully')
refetchSandboxInfo()
} catch (err) {
toast.error(
err instanceof Error
? err.message
: 'Failed to pause sandbox. Please try again.'
)
} finally {
setIsExecuting(false)
}
}, [canPause, sandboxInfo?.sandboxID, team.id, refetchSandboxInfo])

return (
<Button
variant="secondary"
className={cn(className)}
disabled={!canPause || isExecuting}
onClick={handlePause}
loading={isExecuting ? 'Pausing...' : undefined}
>
<PausedIcon className="size-3.5" />
Pause
</Button>
)
}
67 changes: 67 additions & 0 deletions src/features/dashboard/sandbox/header/resume-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import Sandbox from 'e2b'
import { useCallback, useState } from 'react'
import { toast } from 'sonner'
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import { supabase } from '@/core/shared/clients/supabase/client'
import { cn } from '@/lib/utils/ui'
import { Button } from '@/ui/primitives/button'
import { PlayIcon } from '@/ui/primitives/icons'
import { useDashboard } from '../../context'
import { useSandboxContext } from '../context'

interface ResumeButtonProps {
className?: string
}

export default function ResumeButton({ className }: ResumeButtonProps) {
const { sandboxInfo, refetchSandboxInfo } = useSandboxContext()
const { team } = useDashboard()
const [isExecuting, setIsExecuting] = useState(false)
const canResume = sandboxInfo?.state === 'paused'

const handleResume = useCallback(async () => {
if (!canResume || !sandboxInfo?.sandboxID) return

setIsExecuting(true)
try {
const { data } = await supabase.auth.getSession()
if (!data?.session) {
toast.error('Session expired. Please sign in again.')
return
}

await Sandbox.connect(sandboxInfo.sandboxID, {
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
headers: {
...SUPABASE_AUTH_HEADERS(data.session.access_token, team.id),
},
})

toast.success('Sandbox resumed successfully')
refetchSandboxInfo()
} catch (err) {
toast.error(
err instanceof Error
? err.message
: 'Failed to resume sandbox. Please try again.'
)
} finally {
setIsExecuting(false)
}
}, [canResume, sandboxInfo?.sandboxID, team.id, refetchSandboxInfo])

return (
<Button
variant="secondary"
className={cn('text-accent-positive-highlight', className)}
disabled={!canResume || isExecuting}
onClick={handleResume}
loading={isExecuting ? 'Resuming...' : undefined}
>
<PlayIcon className="size-3.5" />
Resume
</Button>
)
}
17 changes: 17 additions & 0 deletions src/ui/primitives/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,23 @@ export const PausedIcon = ({ className, ...props }: IconProps) => (
</svg>
)

export const PlayIcon = ({ className, ...props }: IconProps) => (
<svg
className={cn(DEFAULT_CLASS_NAMES, className)}
fill="none"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3 1.5L10 6L3 10.5V1.5Z"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinejoin="round"
/>
</svg>
)

export const DotIcon = ({ className, ...props }: IconProps) => (
<svg
className={cn(DEFAULT_CLASS_NAMES, className)}
Expand Down
Loading