+
{formattedTimestamp?.datePart ?? '--'}
{' '}
diff --git a/src/features/dashboard/sandboxes/list/table-config.tsx b/src/features/dashboard/sandboxes/list/table-config.tsx
index c317ea3fb..706b85271 100644
--- a/src/features/dashboard/sandboxes/list/table-config.tsx
+++ b/src/features/dashboard/sandboxes/list/table-config.tsx
@@ -13,6 +13,7 @@ import {
MetadataCell,
RamUsageCell,
StartedAtCell,
+ StateCell,
TemplateCell,
} from './table-cells'
@@ -102,6 +103,100 @@ export const templateIdentifierFilter: FilterFn = (
// TABLE CONFIG
export const sandboxListColumns: ColumnDef[] = [
+ {
+ id: 'startedAt',
+ accessorKey: 'startedAt',
+ header: 'Started At',
+ cell: StartedAtCell,
+ size: 150,
+ enableResizing: false,
+ filterFn: startedAtDateRangeFilter,
+ enableColumnFilter: true,
+ enableGlobalFilter: false,
+ sortingFn: (rowA, rowB) => {
+ return rowA.original.startedAt.localeCompare(rowB.original.startedAt)
+ },
+ },
+ {
+ accessorKey: 'sandboxID',
+ header: 'Instance',
+ cell: IdCell,
+ size: 165,
+ minSize: 100,
+ enableResizing: false,
+ enableColumnFilter: false,
+ enableSorting: false,
+ enableGlobalFilter: true,
+ },
+ {
+ accessorKey: 'state',
+ id: 'state',
+ header: 'State',
+ cell: StateCell,
+ size: 90,
+ minSize: 80,
+ enableResizing: false,
+ enableSorting: false,
+ enableColumnFilter: true,
+ filterFn: 'equalsString',
+ enableGlobalFilter: false,
+ },
+ {
+ accessorFn: (row) => row.alias || row.templateID,
+ id: 'template',
+ header: 'TEMPLATE',
+ cell: TemplateCell,
+ size: 250,
+ minSize: 100,
+ maxSize: 350,
+ enableResizing: true,
+ filterFn: templateIdentifierFilter,
+ enableGlobalFilter: false,
+ },
+ {
+ id: 'cpuUsage',
+ header: 'CPU',
+ cell: (props) => ,
+ size: 100,
+ enableResizing: false,
+ enableSorting: false,
+ enableColumnFilter: true,
+ filterFn: resourceEqualsFilter,
+ },
+ {
+ id: 'ramUsage',
+ header: 'Memory',
+ cell: (props) => ,
+ size: 140,
+ enableResizing: false,
+ enableSorting: false,
+ enableColumnFilter: true,
+ filterFn: resourceEqualsFilter,
+ },
+ {
+ id: 'diskUsage',
+ header: 'Disk',
+ cell: (props) => ,
+ size: 100,
+ enableResizing: false,
+ enableSorting: false,
+ enableColumnFilter: false,
+ },
+ {
+ id: 'metadata',
+ accessorFn: (row) => JSON.stringify(row.metadata ?? {}),
+ header: 'Metadata',
+ cell: MetadataCell,
+ filterFn: 'includesStringSensitive',
+ enableGlobalFilter: false,
+ size: 200,
+ minSize: 160,
+ enableResizing: true,
+ enableSorting: false,
+ },
+]
+
+export const legacySandboxListColumns: ColumnDef[] = [
{
accessorKey: 'sandboxID',
header: 'ID',
diff --git a/src/features/dashboard/sandboxes/list/table.tsx b/src/features/dashboard/sandboxes/list/table.tsx
index 3ff356aa7..21c0e6e56 100644
--- a/src/features/dashboard/sandboxes/list/table.tsx
+++ b/src/features/dashboard/sandboxes/list/table.tsx
@@ -1,7 +1,12 @@
'use client'
-import { keepPreviousData, useSuspenseQuery } from '@tanstack/react-query'
import {
+ keepPreviousData,
+ useSuspenseInfiniteQuery,
+ useSuspenseQuery,
+} from '@tanstack/react-query'
+import {
+ type ColumnDef,
type ColumnFiltersState,
type ColumnSizingState,
flexRender,
@@ -13,6 +18,7 @@ import {
import { subHours } from 'date-fns'
import { useEffect, useMemo, useRef } from 'react'
import { useLocalStorage } from 'usehooks-ts'
+import type { Sandboxes } from '@/core/modules/sandboxes/models'
import { useSandboxListTableStore } from '@/features/dashboard/sandboxes/list/stores/table-store'
import { useColumnSizeVars } from '@/lib/hooks/use-column-size-vars'
import { useRouteParams } from '@/lib/hooks/use-route-params'
@@ -31,7 +37,11 @@ import type { SandboxStartedAtFilter } from './stores/table-store'
import { getSandboxListEffectiveSorting } from './stores/table-store'
import { SandboxesTableBody } from './table-body'
import type { SandboxListRow } from './table-config'
-import { sandboxIdFuzzyFilter, sandboxListColumns } from './table-config'
+import {
+ legacySandboxListColumns,
+ sandboxIdFuzzyFilter,
+ sandboxListColumns,
+} from './table-config'
const STARTED_AT_FILTER_HOURS: Record<
Exclude,
@@ -77,13 +87,30 @@ function buildColumnFilters({
return filters
}
-export default function SandboxesTable() {
+const SANDBOXES_PAGE_SIZE = 50
+
+interface SandboxesTableViewProps {
+ sandboxes: Sandboxes
+ columns: ColumnDef[]
+ refetch: () => void
+ isFetching: boolean
+ hasNextPage?: boolean
+ isFetchingNextPage?: boolean
+ fetchNextPage?: () => void
+}
+
+function SandboxesTableView({
+ sandboxes,
+ columns,
+ refetch,
+ isFetching,
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+}: SandboxesTableViewProps) {
'use no memo'
const scrollRef = useRef(null)
- const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/sandboxes'>()
-
- const trpc = useTRPC()
const [columnSizing, setColumnSizing] = useLocalStorage(
'sandboxes:columnSizing:v2',
@@ -94,17 +121,6 @@ export default function SandboxesTable() {
}
)
- const { data, refetch, isFetching } = useSuspenseQuery(
- trpc.sandboxes.getSandboxes.queryOptions(
- { teamSlug },
- {
- refetchOnMount: 'always',
- refetchOnWindowFocus: true,
- placeholderData: keepPreviousData,
- }
- )
- )
-
const {
startedAtFilter,
templateFilters,
@@ -130,8 +146,8 @@ export default function SandboxesTable() {
const activeSorting = getSandboxListEffectiveSorting(sorting)
const table = useReactTable({
- columns: sandboxListColumns,
- data: data.sandboxes,
+ columns,
+ data: sandboxes,
state: {
globalFilter,
sorting: activeSorting,
@@ -154,13 +170,19 @@ export default function SandboxesTable() {
})
const columnSizeVars = useColumnSizeVars(table)
+ const scrollResetKey = useMemo(
+ () => JSON.stringify({ activeSorting, globalFilter, columnFilters }),
+ [activeSorting, globalFilter, columnFilters]
+ )
useEffect(() => {
+ void scrollResetKey
+
if (scrollRef.current) {
scrollRef.current.scrollTop = 0
scrollRef.current.scrollLeft = 0
}
- }, [activeSorting, globalFilter, columnFilters])
+ }, [scrollResetKey])
const tableSorting = table.getState().sorting
@@ -215,9 +237,88 @@ export default function SandboxesTable() {
))}
-
+
)
}
+
+export function NewSandboxesTable() {
+ const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/sandboxes'>()
+ const trpc = useTRPC()
+ const pollingInterval = useSandboxListTableStore(
+ (state) => state.pollingInterval
+ )
+
+ const {
+ data,
+ refetch,
+ isFetching,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useSuspenseInfiniteQuery(
+ trpc.sandboxes.listSandboxesPaginated.infiniteQueryOptions(
+ { teamSlug, limit: SANDBOXES_PAGE_SIZE },
+ {
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
+ initialCursor: undefined,
+ refetchOnMount: 'always',
+ refetchOnWindowFocus: true,
+ refetchInterval: pollingInterval > 0 ? pollingInterval * 1_000 : false,
+ placeholderData: keepPreviousData,
+ }
+ )
+ )
+
+ const sandboxes = useMemo(
+ () => data.pages.flatMap((page) => page.sandboxes),
+ [data]
+ )
+
+ return (
+
+ )
+}
+
+export function LegacySandboxesTable() {
+ const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/sandboxes'>()
+ const trpc = useTRPC()
+
+ const { data, refetch, isFetching } = useSuspenseQuery(
+ trpc.sandboxes.getSandboxes.queryOptions(
+ { teamSlug },
+ {
+ refetchOnMount: 'always',
+ refetchOnWindowFocus: true,
+ placeholderData: keepPreviousData,
+ }
+ )
+ )
+
+ return (
+
+ )
+}
+
+export default LegacySandboxesTable
diff --git a/tests/unit/feature-flags.test.ts b/tests/unit/feature-flags.test.ts
index a99fd139a..8f28651b7 100644
--- a/tests/unit/feature-flags.test.ts
+++ b/tests/unit/feature-flags.test.ts
@@ -66,6 +66,7 @@ describe('createFeatureFlagService', () => {
expect(provider.evaluate).toHaveBeenCalledWith(context, [
FEATURE_FLAGS.agentsEnabled,
FEATURE_FLAGS.isAdmin,
+ FEATURE_FLAGS.newSandboxList,
FEATURE_FLAGS.disableE2BAccessTokenProvisioning,
])
expect(result).toEqual([
@@ -85,6 +86,15 @@ describe('createFeatureFlagService', () => {
defaultValue: false,
value: true,
},
+ {
+ id: 'newSandboxList',
+ key: 'new_sandbox_list',
+ kind: 'boolean',
+ description:
+ 'Enables the new sandbox list with pagination and paused sandbox coverage.',
+ defaultValue: false,
+ value: true,
+ },
{
id: 'disableE2BAccessTokenProvisioning',
key: 'disable_e2b_access_token_provisioning',