Skip to content
Merged
25 changes: 12 additions & 13 deletions src/features/dashboard/sandbox/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,18 @@ export function SandboxProvider({
isLoading: isSandboxInfoLoading,
isValidating: isSandboxInfoValidating,
} = useSWR<SandboxInfo | void>(
!serverSandboxInfo?.sandboxID
!lastFallbackData?.sandboxID
? null
: [`/api/sandbox/details`, serverSandboxInfo?.sandboxID],
: [`/api/sandbox/details`, lastFallbackData?.sandboxID],
async ([url]) => {
if (!serverSandboxInfo?.sandboxID) return
if (!lastFallbackData?.sandboxID) return

const origin = document.location.origin

const requestUrl = new URL(url, origin)

requestUrl.searchParams.set('teamId', teamId)
requestUrl.searchParams.set('sandboxId', serverSandboxInfo.sandboxID)
requestUrl.searchParams.set('sandboxId', lastFallbackData.sandboxID)

const response = await fetch(requestUrl.toString(), {
method: 'GET',
Expand Down Expand Up @@ -102,21 +102,18 @@ export function SandboxProvider({
)

const { data: metricsData } = useSWR(
!serverSandboxInfo?.sandboxID
!lastFallbackData?.sandboxID
? null
: [
`/api/teams/${teamId}/sandboxes/metrics`,
serverSandboxInfo?.sandboxID,
],
: [`/api/teams/${teamId}/sandboxes/metrics`, lastFallbackData?.sandboxID],
async ([url]) => {
if (!serverSandboxInfo?.sandboxID || !isRunning) return null
if (!lastFallbackData?.sandboxID || !isRunningState) return null

const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ sandboxIds: [serverSandboxInfo.sandboxID] }),
body: JSON.stringify({ sandboxIds: [lastFallbackData.sandboxID] }),
cache: 'no-store',
})

Expand All @@ -128,15 +125,17 @@ export function SandboxProvider({

const data = (await response.json()) as MetricsResponse

return data.metrics[serverSandboxInfo.sandboxID]
return data.metrics[lastFallbackData.sandboxID]
},
{
errorRetryInterval: 1000,
errorRetryCount: 3,
revalidateIfStale: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: SANDBOXES_DETAILS_METRICS_POLLING_MS,
refreshInterval: isRunningState
? SANDBOXES_DETAILS_METRICS_POLLING_MS
: 0,
refreshWhenHidden: false,
refreshWhenOffline: false,
}
Expand Down
20 changes: 12 additions & 8 deletions src/features/dashboard/sandbox/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SandboxInfo } from '@/types/api'
import { ChevronLeftIcon } from 'lucide-react'
import { cookies } from 'next/headers'
import Link from 'next/link'
import KillButton from './kill-button'
import Metadata from './metadata'
import RanFor from './ran-for'
import RefreshControl from './refresh'
Expand Down Expand Up @@ -81,14 +82,17 @@ export default async function SandboxDetailsHeader({
</Link>
<SandboxDetailsTitle />
</div>
<RefreshControl
initialPollingInterval={
initialPollingInterval
? parseInt(initialPollingInterval)
: undefined
}
className="pt-4 sm:pt-0"
/>
<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>

<div className="flex flex-wrap items-center gap-5 md:gap-7">
Expand Down
70 changes: 70 additions & 0 deletions src/features/dashboard/sandbox/header/kill-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client'

import { useSelectedTeam } from '@/lib/hooks/use-teams'
import { killSandboxAction } from '@/server/sandboxes/sandbox-actions'
import { AlertPopover } from '@/ui/alert-popover'
import { Button } from '@/ui/primitives/button'
import { TrashIcon } from '@/ui/primitives/icons'
import { useAction } from 'next-safe-action/hooks'
import { useState } from 'react'
import { toast } from 'sonner'
import { useSandboxContext } from '../context'

interface KillButtonProps {
className?: string
}

export default function KillButton({ className }: KillButtonProps) {
const [open, setOpen] = useState(false)
const { sandboxInfo, refetchSandboxInfo, isRunning } = useSandboxContext()
const selectedTeam = useSelectedTeam()

const { execute, isExecuting } = useAction(killSandboxAction, {
onSuccess: async () => {
toast.success('Sandbox killed successfully')
setOpen(false)
refetchSandboxInfo()
},
onError: ({ error }) => {
toast.error(
error.serverError || 'Failed to kill sandbox. Please try again.'
)
},
})

const handleKill = () => {
if (!sandboxInfo?.sandboxID || !isRunning || !selectedTeam?.id) return

execute({
teamId: selectedTeam.id,
sandboxId: sandboxInfo.sandboxID,
})
}

return (
<AlertPopover
open={open}
onOpenChange={setOpen}
title="Kill Sandbox"
description="Are you sure you want to kill this sandbox? The sandbox state will be lost and cannot be recovered."
confirm="Kill Sandbox"
trigger={
<Button
variant="error"
size="sm"
className={className}
disabled={!isRunning}
>
<TrashIcon className="size-4" />
Kill
</Button>
}
confirmProps={{
disabled: isExecuting,
loading: isExecuting,
}}
onConfirm={handleKill}
onCancel={() => setOpen(false)}
/>
)
}
5 changes: 3 additions & 2 deletions src/features/dashboard/sandbox/header/refresh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export default function RefreshControl({
initialPollingInterval ?? pollingIntervals[2]!.value
)

const { refetchSandboxInfo, isSandboxInfoLoading } = useSandboxContext()
const { refetchSandboxInfo, isSandboxInfoLoading, isRunning } =
useSandboxContext()

const handleIntervalChange = useCallback(
async (interval: PollingInterval) => {
Expand All @@ -54,7 +55,7 @@ export default function RefreshControl({
return (
<PollingButton
intervals={pollingIntervals}
pollingInterval={pollingInterval}
pollingInterval={isRunning ? pollingInterval : 0}
onIntervalChange={handleIntervalChange}
isPolling={isSandboxInfoLoading}
onRefresh={refetchSandboxInfo}
Expand Down
120 changes: 119 additions & 1 deletion src/features/dashboard/sandboxes/list/table-cells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,32 @@ import { PROTECTED_URLS } from '@/configs/urls'
import ResourceUsage from '@/features/dashboard/common/resource-usage'
import { useServerContext } from '@/features/dashboard/server-context'
import { useTemplateTableStore } from '@/features/dashboard/templates/stores/table-store'
import { useSelectedTeam } from '@/lib/hooks/use-teams'
import {
defaultErrorToast,
defaultSuccessToast,
useToast,
} from '@/lib/hooks/use-toast'
import { parseUTCDateComponents } from '@/lib/utils/formatting'
import { killSandboxAction } from '@/server/sandboxes/sandbox-actions'
import { Template } from '@/types/api'
import { JsonPopover } from '@/ui/json-popover'
import { Button } from '@/ui/primitives/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/ui/primitives/dropdown-menu'
import { Loader } from '@/ui/primitives/loader'
import { CellContext } from '@tanstack/react-table'
import { ArrowUpRight } from 'lucide-react'
import { ArrowUpRight, MoreVertical, Trash2 } from 'lucide-react'
import { useAction } from 'next-safe-action/hooks'
import { useRouter } from 'next/navigation'
import React, { useMemo } from 'react'
import { useSandboxMetricsStore } from './stores/metrics-store'
Expand All @@ -21,6 +41,104 @@ declare module '@tanstack/react-table' {
}
}

export function ActionsCell({ row }: CellContext<SandboxWithMetrics, unknown>) {
const sandbox = row.original
const selectedTeam = useSelectedTeam()
const router = useRouter()
const { toast } = useToast()

const { execute: executeKillSandbox, isExecuting: isKilling } = useAction(
killSandboxAction,
{
onSuccess: () => {
toast(
defaultSuccessToast(`Sandbox ${sandbox.sandboxID} has been killed.`)
)
router.refresh()
},
onError: ({ error }) => {
toast(
defaultErrorToast(
error.serverError || 'Failed to kill sandbox. Please try again.'
)
)
},
}
)

const handleKill = () => {
if (!selectedTeam?.id) return

executeKillSandbox({
teamId: selectedTeam.id,
sandboxId: sandbox.sandboxID,
})
}

return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger
asChild
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
>
<Button
variant="ghost"
size="icon"
className="text-fg-tertiary size-5"
disabled={isKilling || sandbox.state !== 'running'}
>
{isKilling ? (
<Loader className="size-4" />
) : (
<MoreVertical className="size-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuLabel>Danger Zone</DropdownMenuLabel>
<DropdownMenuSub>
<DropdownMenuSubTrigger variant="error">
<Trash2 className="!size-3" />
Kill
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<div className="space-y-3 p-3 max-w-xs">
<div className="space-y-1">
<h4>Confirm Kill</h4>
<p className="prose-body text-fg-tertiary">
Are you sure you want to kill this sandbox? This action
cannot be undone.
</p>
</div>
<div className="flex items-center gap-2 justify-end">
<Button
variant="error"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleKill()
}}
disabled={isKilling}
loading={isKilling}
>
Kill Sandbox
</Button>
</div>
</div>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}

type CpuUsageProps = { sandboxId: string; totalCpu?: number }
export const CpuUsageCellView = React.memo(function CpuUsageCellView({
sandboxId,
Expand Down
9 changes: 9 additions & 0 deletions src/features/dashboard/sandboxes/list/table-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ export const resourceRangeFilter: FilterFn<SandboxWithMetrics> = (
export const fallbackData: SandboxWithMetrics[] = []

export const COLUMNS: ColumnDef<SandboxWithMetrics>[] = [
// TODO: add actions column back in as soon as performance is stabilized
// {
// id: 'actions',
// enableSorting: false,
// enableGlobalFilter: false,
// enableResizing: false,
// size: 35,
// cell: ActionsCell,
// },
{
accessorKey: 'sandboxID',
header: 'ID',
Expand Down
1 change: 1 addition & 0 deletions src/features/dashboard/sandboxes/list/table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const TableRow = memo(function TableRow({ row }: TableRowProps) {
row.original.sandboxID
)}
prefetch={false}
passHref
>
<DataTableRow
key={row.id}
Expand Down
Loading
Loading