Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
149c9b5
feat: add sandbox events page and table component
sarimrmalik Apr 20, 2026
7f418d1
feat: enhance SandboxEventsTable with sorting and empty state
sarimrmalik Apr 20, 2026
7c88ab3
feat: enhance timestamp formatting in SandboxEventsTable
sarimrmalik Apr 21, 2026
147f5f7
refactor: simplify EventTypeCell layout in SandboxEventsTable
sarimrmalik Apr 21, 2026
8ea55bf
feat: add lifecycle event filtering and display in SandboxEvents
sarimrmalik Apr 21, 2026
78e9fee
style: update cell alignment and styling in SandboxEventsTable
sarimrmalik Apr 21, 2026
e455d68
refactor: replace DataTable with Table components in SandboxEventsTable
sarimrmalik Apr 21, 2026
2e3ecef
feat: introduce IdBadge component for improved event ID display in Sa…
sarimrmalik Apr 21, 2026
600ed23
style: enhance header cell styling in SandboxEventsTable
sarimrmalik Apr 21, 2026
82e8bd2
refactor: remove listSandboxLifecycleEvents method and related query …
sarimrmalik Apr 23, 2026
9c60048
refactor: simplify SandboxEventTypeBadge and adjust table cell styles
sarimrmalik Apr 23, 2026
e859f5e
Merge remote-tracking branch 'origin/main' into feat/sandbox-details-…
sarimrmalik Apr 23, 2026
e68d611
refactor: enhance SandboxEventTypeBadge and update EventTypeFilter st…
sarimrmalik Apr 23, 2026
a4c92d5
refactor: update event type filter and table styling
sarimrmalik Apr 23, 2026
ae0d5f4
refactor: simplify EventDetailsCell and enhance JSON display
sarimrmalik Apr 23, 2026
5935c98
Run biome format
sarimrmalik Apr 23, 2026
7e9f3c1
refactor: remove 'types' parameter from API specifications and relate…
sarimrmalik Apr 23, 2026
0480f6f
refactor: streamline sandbox lifecycle event handling
sarimrmalik Apr 23, 2026
d1ef298
Rollback pagination changes
sarimrmalik Apr 23, 2026
30781d7
refactor: rename sandbox event data schema for consistency
sarimrmalik Apr 23, 2026
6451a77
Run biome format
sarimrmalik Apr 23, 2026
21236c2
refactor: simplify SandboxEventsTable and remove unused components
sarimrmalik Apr 28, 2026
1615cab
Merge remote-tracking branch 'origin/main' into feat/sandbox-details-…
sarimrmalik Apr 29, 2026
f569522
refactor: update ID badge component to use Button instead of IconButton
sarimrmalik Apr 29, 2026
cc323b8
Remove default exports
sarimrmalik Apr 29, 2026
fec5e28
Merge remote-tracking branch 'origin/main' into feat/sandbox-details-…
sarimrmalik May 4, 2026
5f46012
refactor: enhance event type badge and table components
sarimrmalik May 4, 2026
1df7039
refactor: simplify EventTypeFilter component structure
sarimrmalik May 4, 2026
a1e1a38
Merge branch 'main' into feat/sandbox-details-events-table
ben-fornefeld May 5, 2026
cb125a8
Refactor: centralize IdBadge component usage
sarimrmalik May 5, 2026
4661bbe
feat: enable client-side rendering for event type filter
sarimrmalik May 5, 2026
7040cb9
fix: improve event type parsing in EventTypeFilter component
sarimrmalik May 5, 2026
12cb290
refactor: update column styles in SandboxEventsTable
sarimrmalik May 5, 2026
f7b502c
refactor: replace button with Button component in SandboxEventsTable
sarimrmalik May 5, 2026
891ad0a
refactor: simplify empty state rendering in SandboxEventsTable
sarimrmalik May 5, 2026
a01c7cc
refactor: enhance event type parsing in useSandboxEventFilters
sarimrmalik May 5, 2026
37879bd
refactor: add comment for event ordering in SandboxEventsView
sarimrmalik May 5, 2026
fc342ad
refactor: replace WEBHOOK_EVENTS with SandboxLifecycleEventTypeSchema…
sarimrmalik May 5, 2026
a0d5a1e
refactor: replace log rendering components with virtualized alternatives
sarimrmalik May 7, 2026
a1d4de2
refactor: remove outdated comment in virtualized table UI
sarimrmalik May 7, 2026
c5b8778
Remove unused fragment
sarimrmalik May 7, 2026
9b433da
refactor: enhance sandbox lifecycle event handling and filtering
sarimrmalik May 8, 2026
dd645a7
refactor: enhance SandboxEventsTable layout and data display
sarimrmalik May 8, 2026
d8d3d11
refactor: streamline imports and adjust column widths in SandboxEvent…
sarimrmalik May 8, 2026
50927b1
Merge branch 'main' into feat/sandbox-details-events-table
ben-fornefeld May 9, 2026
87ab898
format
ben-fornefeld May 9, 2026
3bf812b
Merge remote-tracking branch 'origin/main' into feat/sandbox-details-…
sarimrmalik May 11, 2026
9e7e4ee
Merge remote-tracking branch 'origin/main' into feat/sandbox-details-…
sarimrmalik May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions spec/openapi.argus.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,13 @@ paths:
schema:
type: boolean
default: false
- name: types
in: query
required: false
schema:
type: array
items:
type: string
responses:
"200":
description: Successfully returned the sandbox events
Expand Down Expand Up @@ -357,6 +364,13 @@ paths:
schema:
type: boolean
default: false
- name: types
in: query
required: false
schema:
type: array
items:
type: string
responses:
"200":
description: Successfully returned the sandbox events
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SandboxEventsView } from '@/features/dashboard/sandbox/events'

export default function SandboxEventsPage() {
return <SandboxEventsView />
}
4 changes: 4 additions & 0 deletions src/configs/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const PROTECTED_URLS = {
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/monitoring`,
SANDBOX_MONITORING: (teamSlug: string, sandboxId: string) =>
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/monitoring`,
SANDBOX_EVENTS: (teamSlug: string, sandboxId: string) =>
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/events`,
SANDBOX_LOGS: (teamSlug: string, sandboxId: string) =>
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/logs`,
SANDBOX_FILESYSTEM: (teamSlug: string, sandboxId: string) =>
Expand Down Expand Up @@ -77,6 +79,8 @@ export const HELP_URLS = {
BUILD_TEMPLATE:
'https://e2b.dev/docs/sandbox-template#4-build-your-sandbox-template',
START_COMMAND: 'https://e2b.dev/docs/sandbox-template/start-cmd',
SANDBOX_LIFECYCLE_EVENTS:
'https://e2b.dev/docs/sandbox/lifecycle-events-api',
}

export const BASE_URL = process.env.VERCEL_ENV
Expand Down
96 changes: 96 additions & 0 deletions src/core/modules/sandboxes/lifecycle-event-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { z } from 'zod'

const SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES: [
'sandbox.lifecycle.created',
'sandbox.lifecycle.updated',
'sandbox.lifecycle.paused',
'sandbox.lifecycle.resumed',
'sandbox.lifecycle.killed',
] = [
'sandbox.lifecycle.created',
'sandbox.lifecycle.updated',
'sandbox.lifecycle.paused',
'sandbox.lifecycle.resumed',
'sandbox.lifecycle.killed',
]

const SANDBOX_LIFECYCLE_EVENT_URL_VALUES: [
'created',
'updated',
'paused',
'resumed',
'killed',
] = ['created', 'updated', 'paused', 'resumed', 'killed']

const sandboxLifecycleEventTypeSchema = z.enum(
SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES
)
const sandboxLifecycleEventUrlValueSchema = z.enum(
SANDBOX_LIFECYCLE_EVENT_URL_VALUES
)

type SandboxLifecycleEventType = z.infer<typeof sandboxLifecycleEventTypeSchema>
type SandboxLifecycleEventUrlValue = z.infer<
typeof sandboxLifecycleEventUrlValueSchema
>

const SANDBOX_LIFECYCLE_EVENT_LABELS: Record<
SandboxLifecycleEventType,
string
> = {
'sandbox.lifecycle.created': 'Created',
'sandbox.lifecycle.updated': 'Updated',
'sandbox.lifecycle.paused': 'Paused',
'sandbox.lifecycle.resumed': 'Resumed',
'sandbox.lifecycle.killed': 'Killed',
}

const SANDBOX_LIFECYCLE_EVENT_TYPE_TO_URL_VALUE: Record<
SandboxLifecycleEventType,
SandboxLifecycleEventUrlValue
> = {
'sandbox.lifecycle.created': 'created',
'sandbox.lifecycle.updated': 'updated',
'sandbox.lifecycle.paused': 'paused',
'sandbox.lifecycle.resumed': 'resumed',
'sandbox.lifecycle.killed': 'killed',
}

const SANDBOX_LIFECYCLE_EVENT_URL_VALUE_TO_TYPE: Record<
SandboxLifecycleEventUrlValue,
SandboxLifecycleEventType
> = {
created: 'sandbox.lifecycle.created',
updated: 'sandbox.lifecycle.updated',
paused: 'sandbox.lifecycle.paused',
resumed: 'sandbox.lifecycle.resumed',
killed: 'sandbox.lifecycle.killed',
}

/** Returns a lifecycle event label. Example: "sandbox.lifecycle.created" -> "Created". */
const getSandboxLifecycleEventLabel = (type: string) => {
const parsedType = sandboxLifecycleEventTypeSchema.safeParse(type)
if (!parsedType.success) return type
return SANDBOX_LIFECYCLE_EVENT_LABELS[parsedType.data]
}

const getSandboxLifecycleEventTypeFromUrlValue = (value: string | null) => {
if (!value) return null

const parsedUrlValue = sandboxLifecycleEventUrlValueSchema.safeParse(value)
if (parsedUrlValue.success) {
return SANDBOX_LIFECYCLE_EVENT_URL_VALUE_TO_TYPE[parsedUrlValue.data]
}

const parsedType = sandboxLifecycleEventTypeSchema.safeParse(value)
if (parsedType.success) {
return parsedType.data
}

return null
}

const getSandboxLifecycleEventUrlValue = (type: SandboxLifecycleEventType) =>
SANDBOX_LIFECYCLE_EVENT_TYPE_TO_URL_VALUE[type]

export { getSandboxLifecycleEventLabel, getSandboxLifecycleEventTypeFromUrlValue, getSandboxLifecycleEventUrlValue, SANDBOX_LIFECYCLE_EVENT_LABELS, SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES, SANDBOX_LIFECYCLE_EVENT_URL_VALUES, sandboxLifecycleEventTypeSchema, sandboxLifecycleEventUrlValueSchema, type SandboxLifecycleEventType, type SandboxLifecycleEventUrlValue }
139 changes: 90 additions & 49 deletions src/core/modules/sandboxes/repository.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
import type { components as DashboardComponents } from '@/contracts/dashboard-api'
import type { components as InfraComponents } from '@/contracts/infra-api'
import type { SandboxLifecycleEventType } from '@/core/modules/sandboxes/lifecycle-event-types'
import type {
SandboxEventModel,
Sandboxes,
Expand Down Expand Up @@ -36,6 +37,13 @@
endUnixMs: number
}

export interface ListSandboxLifecycleEventsOptions {
offset?: number
limit?: number
orderAsc?: boolean
types?: SandboxLifecycleEventType[]
}

export interface SandboxesRepository {
getSandboxLogs(
sandboxId: string,
Expand All @@ -56,6 +64,10 @@
getSandboxLifecycleEvents(
sandboxId: string
): Promise<RepoResult<SandboxEventModel[]>>
listSandboxLifecycleEvents(
sandboxId: string,
options?: ListSandboxLifecycleEventsOptions
): Promise<RepoResult<SandboxEventModel[]>>
getSandboxMetrics(
sandboxId: string,
options: GetSandboxMetricsOptions
Expand Down Expand Up @@ -89,6 +101,62 @@
authHeaders: SUPABASE_AUTH_HEADERS,
}
): SandboxesRepository {
/** Fetches one sandbox lifecycle events page. Example: { offset: 20, limit: 20 } -> the next 20 events. */
const listSandboxLifecycleEventsPage = async (
sandboxId: string,
options: ListSandboxLifecycleEventsOptions = {}
): Promise<RepoResult<SandboxEventModel[]>> => {
const result = await deps.infraClient.GET('/events/sandboxes/{sandboxID}', {
Comment thread
sarimrmalik marked this conversation as resolved.
Outdated
params: {
path: {
sandboxID: sandboxId,
},
query: {
offset: options.offset,
limit: options.limit,
orderAsc: options.orderAsc,
types: options.types,
},
},
headers: {
...deps.authHeaders(scope.accessToken, scope.teamId),
},
cache: 'no-store',
})

if (!result.response.ok || result.error) {
const status = result.response.status

l.error({
key: 'repositories:sandboxes:list_sandbox_lifecycle_events:infra_error',
error: result.error,
team_id: scope.teamId,
context: {
status,
path: '/events/sandboxes/{sandboxID}',
sandbox_id: sandboxId,
offset: options.offset,
limit: options.limit,
order_asc: options.orderAsc,
types: options.types,
},
})

return err(
repoErrorFromHttp(
status,
status === 404
? SANDBOX_NOT_FOUND_MESSAGE
: (result.error?.message ??
'Failed to fetch sandbox lifecycle events'),
result.error
)
)
}

return ok(result.data ?? [])
}

return {
async getSandboxLogs(sandboxId, options = {}) {
const result = await deps.infraClient.GET(
Expand Down Expand Up @@ -239,57 +307,16 @@
pageIndex < SANDBOX_EVENTS_MAX_PAGES;
pageIndex += 1, offset += SANDBOX_EVENTS_PAGE_SIZE
) {
try {
const result = await deps.infraClient.GET(
'/events/sandboxes/{sandboxID}',
{
params: {
path: {
sandboxID: sandboxId,
},
query: {
offset,
limit: SANDBOX_EVENTS_PAGE_SIZE,
orderAsc: true,
},
},
headers: {
...deps.authHeaders(scope.accessToken, scope.teamId),
},
cache: 'no-store',
}
)

if (!result.response.ok || result.error) {
l.warn({
key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_error',
error: result.error,
team_id: scope.teamId,
context: {
status: result.response.status,
path: '/events/sandboxes/{sandboxID}',
sandbox_id: sandboxId,
offset,
limit: SANDBOX_EVENTS_PAGE_SIZE,
},
})
break
}

const page = result.data ?? []
lifecycleEvents.push(
...page.filter((event) =>
event.type.startsWith(SANDBOX_LIFECYCLE_EVENT_PREFIX)
)
)
const result = await listSandboxLifecycleEventsPage(sandboxId, {
offset,
limit: SANDBOX_EVENTS_PAGE_SIZE,
orderAsc: true,
})

if (page.length < SANDBOX_EVENTS_PAGE_SIZE) {
break
}
} catch (error) {
if (!result.ok) {
l.warn({
key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_exception',
error,
key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_error',
error: result.error,
team_id: scope.teamId,
context: {
path: '/events/sandboxes/{sandboxID}',
Expand All @@ -300,10 +327,24 @@
})
break
}

const page = result.data
lifecycleEvents.push(
...page.filter((event) =>
event.type.startsWith(SANDBOX_LIFECYCLE_EVENT_PREFIX)
)
)

if (page.length < SANDBOX_EVENTS_PAGE_SIZE) {
break
}
}

return ok(lifecycleEvents)
},
async listSandboxLifecycleEvents(sandboxId, options = {}) {
return listSandboxLifecycleEventsPage(sandboxId, options)

Check failure on line 346 in src/core/modules/sandboxes/repository.server.ts

View check run for this annotation

Claude / Claude Code Review

listSandboxLifecycleEvents returns all event types, not just lifecycle events

The new `listSandboxLifecycleEvents` method passes `options` directly to the API with no lifecycle prefix filter, so selecting "All" in the event type filter fetches every sandbox event type from the backend—including non-lifecycle events like `sandbox.process.oom`. Fix by defaulting `types` to `SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES` when `options.types` is undefined, or by filtering results client-side with `event.type.startsWith(SANDBOX_LIFECYCLE_EVENT_PREFIX)`.
Comment thread
sarimrmalik marked this conversation as resolved.
Outdated
},
async getSandboxMetrics(sandboxId, options) {
const startUnixSeconds = Math.floor(options.startUnixMs / 1000)
const endUnixSeconds = Math.floor(options.endUnixMs / 1000)
Expand Down
39 changes: 39 additions & 0 deletions src/core/server/api/routers/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { millisecondsInDay } from 'date-fns/constants'
import { z } from 'zod'
import { SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES } from '@/core/modules/sandboxes/lifecycle-event-types'
import {
deriveSandboxLifecycleFromEvents,
mapApiSandboxRecordToModel,
Expand Down Expand Up @@ -29,6 +30,44 @@
export const sandboxRouter = createTRPCRouter({
// QUERIES

events: sandboxRepositoryProcedure
.input(
z.object({
sandboxId: SandboxIdSchema,
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).max(100).optional(),
orderAsc: z.boolean().optional(),
type: z.enum(SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES).optional(),
})
)
.query(async ({ ctx, input }) => {
const { sandboxId, type } = input
const offset = input.offset ?? 0
const limit = input.limit ?? 20
const orderAsc = input.orderAsc ?? false

const eventsResult = await ctx.sandboxesRepository.listSandboxLifecycleEvents(
sandboxId,
{
offset,
limit,
orderAsc,
types: type ? [type] : undefined,
}
)
if (!eventsResult.ok) {
throwTRPCErrorFromRepoError(eventsResult.error)
}

return {
events: eventsResult.data,
hasNextPage: eventsResult.data.length === limit,
hasPreviousPage: offset > 0,
limit,
offset,
}

Check failure on line 68 in src/core/server/api/routers/sandbox.ts

View check run for this annotation

Claude / Claude Code Review

hasNextPage false positive on exact page boundary

The `hasNextPage` flag in the `sandbox.events` tRPC query is determined by `eventsResult.data.length === limit`, which produces a false positive when the final page contains exactly `limit` (default 20) events. In that case the Next button remains enabled, and clicking it fetches an empty page that shows "No events found" with no indication the user has reached the end of the list.
Comment thread
sarimrmalik marked this conversation as resolved.
Outdated
}),

details: sandboxRepositoryProcedure
.input(
z.object({
Expand Down
Loading
Loading