diff --git a/src/features/dashboard/sandbox/header/controls.tsx b/src/features/dashboard/sandbox/header/controls.tsx index 3f7471487..24126692a 100644 --- a/src/features/dashboard/sandbox/header/controls.tsx +++ b/src/features/dashboard/sandbox/header/controls.tsx @@ -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 (
+ {isRunning && } + {isPaused && }
) diff --git a/src/features/dashboard/sandbox/header/kill-button.tsx b/src/features/dashboard/sandbox/header/kill-button.tsx index f177191bf..61befaf01 100644 --- a/src/features/dashboard/sandbox/header/kill-button.tsx +++ b/src/features/dashboard/sandbox/header/kill-button.tsx @@ -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' @@ -45,7 +45,7 @@ export default function KillButton({ className }: KillButtonProps) { } return ( - setOpen(false)} /> ) } diff --git a/src/features/dashboard/sandbox/header/pause-button.tsx b/src/features/dashboard/sandbox/header/pause-button.tsx new file mode 100644 index 000000000..dea46d6df --- /dev/null +++ b/src/features/dashboard/sandbox/header/pause-button.tsx @@ -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 ( + + ) +} diff --git a/src/features/dashboard/sandbox/header/resume-button.tsx b/src/features/dashboard/sandbox/header/resume-button.tsx new file mode 100644 index 000000000..5910b8421 --- /dev/null +++ b/src/features/dashboard/sandbox/header/resume-button.tsx @@ -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 ( + + ) +} diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons.tsx index 126e7797a..281a715d5 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons.tsx @@ -1416,6 +1416,23 @@ export const PausedIcon = ({ className, ...props }: IconProps) => ( ) +export const PlayIcon = ({ className, ...props }: IconProps) => ( + + + +) + export const DotIcon = ({ className, ...props }: IconProps) => (