From 149c9b5da5fab1863147e0ec7bbbb9a08cde18d0 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 20 Apr 2026 15:39:44 -0400 Subject: [PATCH 01/41] feat: add sandbox events page and table component - Introduced a new `SandboxEventsPage` component to display lifecycle events for sandboxes. - Created `SandboxEventsTable` to present event data in a structured format with sorting capabilities. - Updated routing in `urls.ts` to include a new endpoint for sandbox events. - Enhanced the dashboard layout to include a new tab for events, improving navigation for users. This implementation provides a comprehensive view of sandbox lifecycle events, enhancing user experience and data accessibility. --- .../sandboxes/[sandboxId]/events/page.tsx | 5 + src/configs/urls.ts | 4 + .../dashboard/sandbox/events/index.ts | 1 + .../dashboard/sandbox/events/table.tsx | 238 ++++++++++++++++++ .../dashboard/sandbox/events/view.tsx | 68 +++++ src/features/dashboard/sandbox/layout.tsx | 15 +- 6 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/events/page.tsx create mode 100644 src/features/dashboard/sandbox/events/index.ts create mode 100644 src/features/dashboard/sandbox/events/table.tsx create mode 100644 src/features/dashboard/sandbox/events/view.tsx diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/events/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/events/page.tsx new file mode 100644 index 000000000..64cce3cbf --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/events/page.tsx @@ -0,0 +1,5 @@ +import { SandboxEventsView } from '@/features/dashboard/sandbox/events' + +export default function SandboxEventsPage() { + return +} diff --git a/src/configs/urls.ts b/src/configs/urls.ts index 768833627..2f93737d1 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -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) => @@ -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 diff --git a/src/features/dashboard/sandbox/events/index.ts b/src/features/dashboard/sandbox/events/index.ts new file mode 100644 index 000000000..828e1cc93 --- /dev/null +++ b/src/features/dashboard/sandbox/events/index.ts @@ -0,0 +1 @@ +export { SandboxEventsView } from './view' diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx new file mode 100644 index 000000000..56edda5f3 --- /dev/null +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -0,0 +1,238 @@ +'use client' + +import { + type CellContext, + type ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + type SortingState, + useReactTable, +} from '@tanstack/react-table' +import { Braces } from 'lucide-react' +import { useMemo, useState } from 'react' +import { z } from 'zod' +import type { SandboxEventModel } from '@/core/modules/sandboxes/models' +import { useColumnSizeVars } from '@/lib/hooks/use-column-size-vars' +import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' +import { + DataTable, + DataTableBody, + DataTableCell, + DataTableHead, + DataTableHeader, + DataTableRow, +} from '@/ui/data-table' +import { JsonPopover } from '@/ui/json-popover' +import { Badge, type BadgeProps } from '@/ui/primitives/badge' + +const sandboxEventDataSchema = z.record(z.string(), z.unknown()) + +const EVENT_TYPE_LABELS: Record = { + 'sandbox.lifecycle.created': 'Created', + 'sandbox.lifecycle.updated': 'Updated', + 'sandbox.lifecycle.paused': 'Paused', + 'sandbox.lifecycle.resumed': 'Resumed', + 'sandbox.lifecycle.killed': 'Killed', +} + +const EVENT_TYPE_VARIANTS: Record< + string, + NonNullable +> = { + 'sandbox.lifecycle.created': 'positive', + 'sandbox.lifecycle.updated': 'main', + 'sandbox.lifecycle.paused': 'warning', + 'sandbox.lifecycle.resumed': 'info', + 'sandbox.lifecycle.killed': 'error', +} + +interface SandboxEventsTableProps { + events: SandboxEventModel[] +} + +export const SandboxEventsTable = ({ events }: SandboxEventsTableProps) => { + const [sorting, setSorting] = useState([ + { id: 'timestamp', desc: true }, + ]) + + const table = useReactTable({ + data: events, + columns: EVENT_COLUMNS, + state: { sorting }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableSorting: true, + enableSortingRemoval: false, + enableMultiSort: false, + onSortingChange: setSorting, + }) + + const columnSizeVars = useColumnSizeVars(table) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + item.id === header.id)?.desc} + > + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + ))} + + ))} + + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + +
+ ) +} + +const TimestampCell = ({ row }: CellContext) => { + const formattedTimestamp = useMemo( + () => + formatLocalLogStyleTimestamp(row.original.timestamp, { + includeYear: true, + }), + [row.original.timestamp] + ) + + return ( +
+ + {formattedTimestamp?.datePart ?? '--'} + {' '} + {formattedTimestamp?.timePart ?? '--'}{' '} + + {formattedTimestamp?.timezonePart ?? ''} + +
+ ) +} + +const EventTypeCell = ({ row }: CellContext) => { + const label = EVENT_TYPE_LABELS[row.original.type] ?? row.original.type + const variant = EVENT_TYPE_VARIANTS[row.original.type] ?? 'default' + + return ( +
+ + {label} + + + {row.original.type} + +
+ ) +} + +const EventDetailsCell = ({ row }: CellContext) => { + const parsedEventData = useMemo( + () => sandboxEventDataSchema.safeParse(row.original.eventData), + [row.original.eventData] + ) + + if (!parsedEventData.success) { + return n/a + } + + const entries = Object.entries(parsedEventData.data) + if (entries.length === 0) { + return n/a + } + + const preview = JSON.stringify(parsedEventData.data) + + return ( + + + + {preview} + + + ) +} + +const EventIdCell = ({ row }: CellContext) => { + return ( +
+ {row.original.id} +
+ ) +} + +const EVENT_COLUMNS: ColumnDef[] = [ + { + accessorKey: 'timestamp', + header: 'Timestamp', + cell: TimestampCell, + size: 250, + minSize: 220, + sortingFn: (rowA, rowB) => + rowA.original.timestamp.localeCompare(rowB.original.timestamp), + sortDescFirst: true, + }, + { + accessorKey: 'type', + header: 'Event', + cell: EventTypeCell, + size: 260, + minSize: 220, + enableSorting: false, + }, + { + id: 'details', + header: 'Details', + cell: EventDetailsCell, + size: 320, + minSize: 220, + maxSize: 420, + enableSorting: false, + }, + { + accessorKey: 'id', + header: 'ID', + cell: EventIdCell, + size: 230, + minSize: 190, + maxSize: 260, + enableSorting: false, + }, +] diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx new file mode 100644 index 000000000..a42c64f51 --- /dev/null +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -0,0 +1,68 @@ +'use client' + +import Link from 'next/link' +import { HELP_URLS } from '@/configs/urls' +import DashboardEmptyFrame from '@/features/dashboard/common/empty-frame' +import LoadingLayout from '@/features/dashboard/loading-layout' +import { useSandboxContext } from '@/features/dashboard/sandbox/context' +import { Badge } from '@/ui/primitives/badge' +import { Button } from '@/ui/primitives/button' +import { ExternalLinkIcon } from '@/ui/primitives/icons' +import { SandboxEventsTable } from './table' + +const LifecycleEventsDocsLink = () => { + return ( + + ) +} + +export const SandboxEventsView = () => { + const { isSandboxInfoLoading, sandboxInfo, sandboxLifecycle } = + useSandboxContext() + + if (isSandboxInfoLoading && !sandboxInfo) { + return + } + + const events = sandboxLifecycle?.events ?? [] + + if (events.length === 0) { + return ( + } + className="flex-1" + /> + ) + } + + return ( +
+
+
+
+

Lifecycle events

+ + {events.length} total + +
+

+ This table mirrors the sandbox lifecycle events API for the current + sandbox and shows the exact event type, timestamp, and attached + event data when available. +

+
+ + +
+ + +
+ ) +} diff --git a/src/features/dashboard/sandbox/layout.tsx b/src/features/dashboard/sandbox/layout.tsx index 89d7523ba..0581b5465 100644 --- a/src/features/dashboard/sandbox/layout.tsx +++ b/src/features/dashboard/sandbox/layout.tsx @@ -5,7 +5,12 @@ import { SANDBOX_INSPECT_MINIMUM_ENVD_VERSION } from '@/configs/versioning' import { useRouteParams } from '@/lib/hooks/use-route-params' import { isVersionCompatible } from '@/lib/utils/version' import { DashboardTab, DashboardTabs } from '@/ui/dashboard-tabs' -import { ListIcon, StorageIcon, TrendIcon } from '@/ui/primitives/icons' +import { + HistoryIcon, + ListIcon, + StorageIcon, + TrendIcon, +} from '@/ui/primitives/icons' import { useSandboxContext } from './context' import SandboxInspectIncompatible from './inspect/incompatible' @@ -63,6 +68,14 @@ export default function SandboxLayout({ > {children} + } + > + {children} + Date: Mon, 20 Apr 2026 15:44:25 -0400 Subject: [PATCH 02/41] feat: enhance SandboxEventsTable with sorting and empty state - Implemented sorting functionality for the events table based on timestamp. - Added an empty state component to display a message when no events are available. - Refactored the table rendering logic to improve readability and maintainability. - Updated the SandboxEventsView to remove the previous empty frame and integrate the new empty state. These changes improve the user experience by providing clear feedback when no events are present and allowing users to sort events effectively. --- .../dashboard/sandbox/events/table.tsx | 123 ++++++++++++------ .../dashboard/sandbox/events/view.tsx | 33 +---- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 56edda5f3..d10703825 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -5,11 +5,9 @@ import { type ColumnDef, flexRender, getCoreRowModel, - getSortedRowModel, type SortingState, useReactTable, } from '@tanstack/react-table' -import { Braces } from 'lucide-react' import { useMemo, useState } from 'react' import { z } from 'zod' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' @@ -25,6 +23,11 @@ import { } from '@/ui/data-table' import { JsonPopover } from '@/ui/json-popover' import { Badge, type BadgeProps } from '@/ui/primitives/badge' +import { + ArrowDownIcon, + HistoryIcon, + MetadataIcon, +} from '@/ui/primitives/icons' const sandboxEventDataSchema = z.record(z.string(), z.unknown()) @@ -55,27 +58,29 @@ export const SandboxEventsTable = ({ events }: SandboxEventsTableProps) => { const [sorting, setSorting] = useState([ { id: 'timestamp', desc: true }, ]) + const isTimestampDescending = sorting[0]?.desc ?? true + const sortedEvents = useMemo( + () => + [...events].sort((eventA, eventB) => + isTimestampDescending + ? eventB.timestamp.localeCompare(eventA.timestamp) + : eventA.timestamp.localeCompare(eventB.timestamp) + ), + [events, isTimestampDescending] + ) const table = useReactTable({ - data: events, + data: sortedEvents, columns: EVENT_COLUMNS, - state: { sorting }, getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableSorting: true, - enableSortingRemoval: false, - enableMultiSort: false, - onSortingChange: setSorting, }) const columnSizeVars = useColumnSizeVars(table) + const rows = table.getRowModel().rows return ( -
- +
+ {table.getHeaderGroups().map((headerGroup) => ( @@ -83,16 +88,42 @@ export const SandboxEventsTable = ({ events }: SandboxEventsTableProps) => { item.id === header.id)?.desc} + sorting={ + header.id === 'timestamp' + ? isTimestampDescending + : undefined + } > - - {header.isPlaceholder - ? null - : flexRender( + {header.isPlaceholder ? null : header.id === 'timestamp' ? ( + + ) : ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + )} ))} @@ -100,25 +131,43 @@ export const SandboxEventsTable = ({ events }: SandboxEventsTableProps) => { - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} + {rows.length > 0 ? ( + rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + )}
) } +const EventsEmptyState = () => { + return ( +
+
+ +

No events found

+
+

+ Lifecycle events for this sandbox will appear here once available. +

+
+ ) +} + const TimestampCell = ({ row }: CellContext) => { const formattedTimestamp = useMemo( () => @@ -182,7 +231,7 @@ const EventDetailsCell = ({ row }: CellContext) => { className: 'w-full justify-start', }} > - + {preview} @@ -201,13 +250,11 @@ const EventIdCell = ({ row }: CellContext) => { const EVENT_COLUMNS: ColumnDef[] = [ { accessorKey: 'timestamp', - header: 'Timestamp', + header: 'TIMESTAMP', cell: TimestampCell, size: 250, minSize: 220, - sortingFn: (rowA, rowB) => - rowA.original.timestamp.localeCompare(rowB.original.timestamp), - sortDescFirst: true, + enableSorting: false, }, { accessorKey: 'type', diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index a42c64f51..11e050086 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -2,7 +2,6 @@ import Link from 'next/link' import { HELP_URLS } from '@/configs/urls' -import DashboardEmptyFrame from '@/features/dashboard/common/empty-frame' import LoadingLayout from '@/features/dashboard/loading-layout' import { useSandboxContext } from '@/features/dashboard/sandbox/context' import { Badge } from '@/ui/primitives/badge' @@ -31,34 +30,12 @@ export const SandboxEventsView = () => { const events = sandboxLifecycle?.events ?? [] - if (events.length === 0) { - return ( - } - className="flex-1" - /> - ) - } - return ( -
-
-
-
-

Lifecycle events

- - {events.length} total - -
-

- This table mirrors the sandbox lifecycle events API for the current - sandbox and shows the exact event type, timestamp, and attached - event data when available. -

-
- +
+
+ + {events.length} total +
From 7c88ab304940fe315e3f086b9c3e6f39b9ba56fe Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 14:10:21 -0400 Subject: [PATCH 03/41] feat: enhance timestamp formatting in SandboxEventsTable - Updated the timestamp formatting in the SandboxEventsTable to include centiseconds for improved precision. - Introduced a new `CopyButtonInline` component to allow users to easily copy the formatted timestamp. - Refactored the `formatLocalLogStyleTimestamp` function to support the new centisecond formatting option. These changes enhance the user experience by providing more detailed timestamp information and improving usability with the copy functionality. --- .../dashboard/sandbox/events/table.tsx | 33 ++++++++++--------- src/lib/utils/formatting.ts | 10 +++++- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index d10703825..ee31a7abd 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -13,6 +13,7 @@ import { z } from 'zod' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' import { useColumnSizeVars } from '@/lib/hooks/use-column-size-vars' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' +import CopyButtonInline from '@/ui/copy-button-inline' import { DataTable, DataTableBody, @@ -23,11 +24,7 @@ import { } from '@/ui/data-table' import { JsonPopover } from '@/ui/json-popover' import { Badge, type BadgeProps } from '@/ui/primitives/badge' -import { - ArrowDownIcon, - HistoryIcon, - MetadataIcon, -} from '@/ui/primitives/icons' +import { ArrowDownIcon, HistoryIcon, MetadataIcon } from '@/ui/primitives/icons' const sandboxEventDataSchema = z.record(z.string(), z.unknown()) @@ -172,21 +169,27 @@ const TimestampCell = ({ row }: CellContext) => { const formattedTimestamp = useMemo( () => formatLocalLogStyleTimestamp(row.original.timestamp, { - includeYear: true, + includeCentiseconds: true, }), [row.original.timestamp] ) + if (!formattedTimestamp) { + return ( +
+ -- +
+ ) + } + return ( -
- - {formattedTimestamp?.datePart ?? '--'} - {' '} - {formattedTimestamp?.timePart ?? '--'}{' '} - - {formattedTimestamp?.timezonePart ?? ''} - -
+ + {formattedTimestamp.datePart}{' '} + {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} + ) } diff --git a/src/lib/utils/formatting.ts b/src/lib/utils/formatting.ts index 20f0b04d3..4a4055912 100644 --- a/src/lib/utils/formatting.ts +++ b/src/lib/utils/formatting.ts @@ -47,20 +47,23 @@ const LOCAL_LOG_STYLE_TIMEZONE_FORMATTER = new Intl.DateTimeFormat(undefined, { /** * Format timestamp parts in local timezone, matching logs table style. - * Example: "Jan 05 14:32:09" + * Example: "Jan 05 14:32:09.93" */ export function formatLocalLogStyleTimestamp( timestamp: number | string | Date, { includeSeconds = true, includeYear = false, + includeCentiseconds = false, }: { includeSeconds?: boolean includeYear?: boolean + includeCentiseconds?: boolean } = {} ): { datePart: string timePart: string + subsecondPart: string | null timezonePart: string iso: string } | null { @@ -86,6 +89,11 @@ export function formatLocalLogStyleTimestamp( ? LOCAL_LOG_STYLE_TIME_FORMATTER : LOCAL_LOG_STYLE_TIME_NO_SECONDS_FORMATTER ).format(date), + subsecondPart: includeCentiseconds + ? Math.floor((date.getMilliseconds() / 10) % 100) + .toString() + .padStart(2, '0') + : null, timezonePart, iso: date.toISOString(), } From 147f5f7f4fc0f6831d0748d3b50b12a8a8065d3a Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 14:11:00 -0400 Subject: [PATCH 04/41] refactor: simplify EventTypeCell layout in SandboxEventsTable - Updated the layout of the EventTypeCell component to enhance visual clarity by removing unnecessary elements. - Adjusted the styling to center the badge and improve overall alignment. These changes streamline the component's presentation, contributing to a cleaner user interface. --- src/features/dashboard/sandbox/events/table.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index ee31a7abd..09abac971 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -198,13 +198,10 @@ const EventTypeCell = ({ row }: CellContext) => { const variant = EVENT_TYPE_VARIANTS[row.original.type] ?? 'default' return ( -
+
{label} - - {row.original.type} -
) } From 8ea55bfa0dc944ac83cc11644b3435d836bf6ba7 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 15:58:43 -0400 Subject: [PATCH 05/41] feat: add lifecycle event filtering and display in SandboxEvents - Introduced a new `types` query parameter in the OpenAPI specification for filtering sandbox lifecycle events by type. - Implemented `listSandboxLifecycleEvents` method in the repository to support fetching events based on specified types. - Created `EventTypeFilter` and `SandboxEventTypeBadge` components for filtering and displaying lifecycle event types in the UI. - Updated `SandboxEventsView` to integrate the new filtering functionality, enhancing user experience by allowing users to filter events by type. These changes improve the usability of the sandbox events feature by providing more control over the displayed data. --- spec/openapi.argus.yaml | 14 ++ .../sandboxes/lifecycle-event-types.ts | 96 ++++++++++++ .../modules/sandboxes/repository.server.ts | 139 ++++++++++++------ src/core/server/api/routers/sandbox.ts | 39 +++++ src/core/shared/contracts/argus-api.types.ts | 2 + .../sandbox/events/event-type-badge.tsx | 30 ++++ .../sandbox/events/event-type-filter.tsx | 72 +++++++++ .../dashboard/sandbox/events/filter-params.ts | 27 ++++ .../dashboard/sandbox/events/table.tsx | 61 ++------ .../events/use-sandbox-event-filters.ts | 68 +++++++++ .../dashboard/sandbox/events/view.tsx | 86 +++++++++-- 11 files changed, 523 insertions(+), 111 deletions(-) create mode 100644 src/core/modules/sandboxes/lifecycle-event-types.ts create mode 100644 src/features/dashboard/sandbox/events/event-type-badge.tsx create mode 100644 src/features/dashboard/sandbox/events/event-type-filter.tsx create mode 100644 src/features/dashboard/sandbox/events/filter-params.ts create mode 100644 src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index 87baf4dca..706c1db22 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -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 @@ -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 diff --git a/src/core/modules/sandboxes/lifecycle-event-types.ts b/src/core/modules/sandboxes/lifecycle-event-types.ts new file mode 100644 index 000000000..0ec7b0baf --- /dev/null +++ b/src/core/modules/sandboxes/lifecycle-event-types.ts @@ -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 +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 } diff --git a/src/core/modules/sandboxes/repository.server.ts b/src/core/modules/sandboxes/repository.server.ts index 2f4ad1aba..3ccfa1cb3 100644 --- a/src/core/modules/sandboxes/repository.server.ts +++ b/src/core/modules/sandboxes/repository.server.ts @@ -3,6 +3,7 @@ import 'server-only' 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, @@ -36,6 +37,13 @@ export interface GetSandboxMetricsOptions { endUnixMs: number } +export interface ListSandboxLifecycleEventsOptions { + offset?: number + limit?: number + orderAsc?: boolean + types?: SandboxLifecycleEventType[] +} + export interface SandboxesRepository { getSandboxLogs( sandboxId: string, @@ -56,6 +64,10 @@ export interface SandboxesRepository { getSandboxLifecycleEvents( sandboxId: string ): Promise> + listSandboxLifecycleEvents( + sandboxId: string, + options?: ListSandboxLifecycleEventsOptions + ): Promise> getSandboxMetrics( sandboxId: string, options: GetSandboxMetricsOptions @@ -89,6 +101,62 @@ export function createSandboxesRepository( 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> => { + const result = await deps.infraClient.GET('/events/sandboxes/{sandboxID}', { + 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( @@ -239,57 +307,16 @@ export function createSandboxesRepository( 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}', @@ -300,10 +327,24 @@ export function createSandboxesRepository( }) 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) + }, async getSandboxMetrics(sandboxId, options) { const startUnixSeconds = Math.floor(options.startUnixMs / 1000) const endUnixSeconds = Math.floor(options.endUnixMs / 1000) diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index 948367c59..0b64848fd 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -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, @@ -29,6 +30,44 @@ const sandboxRepositoryProcedure = protectedTeamProcedure.use( 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, + } + }), + details: sandboxRepositoryProcedure .input( z.object({ diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index b9286818e..8001cdd80 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -53,6 +53,7 @@ export interface paths { offset?: number limit?: number orderAsc?: boolean + types?: string[] } header?: never path: { @@ -98,6 +99,7 @@ export interface paths { offset?: number limit?: number orderAsc?: boolean + types?: string[] } header?: never path?: never diff --git a/src/features/dashboard/sandbox/events/event-type-badge.tsx b/src/features/dashboard/sandbox/events/event-type-badge.tsx new file mode 100644 index 000000000..ac2f755e7 --- /dev/null +++ b/src/features/dashboard/sandbox/events/event-type-badge.tsx @@ -0,0 +1,30 @@ +import { + getSandboxLifecycleEventLabel, + type SandboxLifecycleEventType, + sandboxLifecycleEventTypeSchema, +} from '@/core/modules/sandboxes/lifecycle-event-types' +import { Badge, type BadgeProps } from '@/ui/primitives/badge' + +const SANDBOX_LIFECYCLE_EVENT_VARIANTS: Record< + SandboxLifecycleEventType, + NonNullable +> = { + 'sandbox.lifecycle.created': 'positive', + 'sandbox.lifecycle.updated': 'main', + 'sandbox.lifecycle.paused': 'warning', + 'sandbox.lifecycle.resumed': 'info', + 'sandbox.lifecycle.killed': 'error', +} + +export const SandboxEventTypeBadge = ({ type }: { type: string }) => { + const parsedType = sandboxLifecycleEventTypeSchema.safeParse(type) + const variant = parsedType.success + ? SANDBOX_LIFECYCLE_EVENT_VARIANTS[parsedType.data] + : 'default' + + return ( + + {getSandboxLifecycleEventLabel(type)} + + ) +} diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx new file mode 100644 index 000000000..14701600c --- /dev/null +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -0,0 +1,72 @@ +import { + getSandboxLifecycleEventLabel, + SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES, + type SandboxLifecycleEventType, + sandboxLifecycleEventTypeSchema, +} from '@/core/modules/sandboxes/lifecycle-event-types' +import { cn } from '@/lib/utils' +import { Button } from '@/ui/primitives/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '@/ui/primitives/dropdown-menu' +import { SandboxEventTypeBadge } from './event-type-badge' + +const ALL_EVENT_TYPES_VALUE = 'all' + +interface EventTypeFilterProps { + type: SandboxLifecycleEventType | null + onTypeChange: (type: SandboxLifecycleEventType | null) => void + className?: string +} + +export const EventTypeFilter = ({ + type, + onTypeChange, + className, +}: EventTypeFilterProps) => { + const selectedTypeLabel = type ? getSandboxLifecycleEventLabel(type) : 'All' + + return ( +
+ + + + + + { + if (value === ALL_EVENT_TYPES_VALUE) { + onTypeChange(null) + return + } + + const parsedType = + sandboxLifecycleEventTypeSchema.safeParse(value) + if (!parsedType.success) return + onTypeChange(parsedType.data) + }} + > + + All events + + {SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES.map((type) => ( + + + + ))} + + + +
+ ) +} diff --git a/src/features/dashboard/sandbox/events/filter-params.ts b/src/features/dashboard/sandbox/events/filter-params.ts new file mode 100644 index 000000000..f965af6e3 --- /dev/null +++ b/src/features/dashboard/sandbox/events/filter-params.ts @@ -0,0 +1,27 @@ +import { + createLoader, + parseAsInteger, + parseAsString, + parseAsStringEnum, +} from 'nuqs/server' + +const SANDBOX_EVENTS_PAGE_SIZE = 20 +const SANDBOX_EVENTS_ORDER_VALUES: ['asc', 'desc'] = ['asc', 'desc'] + +type SandboxEventsOrder = (typeof SANDBOX_EVENTS_ORDER_VALUES)[number] + +const sandboxEventsFilterParams = { + type: parseAsString, + offset: parseAsInteger, + order: parseAsStringEnum(SANDBOX_EVENTS_ORDER_VALUES), +} + +const loadSandboxEventsFilters = createLoader(sandboxEventsFilterParams) + +export { + loadSandboxEventsFilters, + SANDBOX_EVENTS_ORDER_VALUES, + SANDBOX_EVENTS_PAGE_SIZE, + sandboxEventsFilterParams, +} +export type { SandboxEventsOrder } diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 09abac971..598a40cd9 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -5,10 +5,9 @@ import { type ColumnDef, flexRender, getCoreRowModel, - type SortingState, useReactTable, } from '@tanstack/react-table' -import { useMemo, useState } from 'react' +import { useMemo } from 'react' import { z } from 'zod' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' import { useColumnSizeVars } from '@/lib/hooks/use-column-size-vars' @@ -23,51 +22,24 @@ import { DataTableRow, } from '@/ui/data-table' import { JsonPopover } from '@/ui/json-popover' -import { Badge, type BadgeProps } from '@/ui/primitives/badge' import { ArrowDownIcon, HistoryIcon, MetadataIcon } from '@/ui/primitives/icons' +import { SandboxEventTypeBadge } from './event-type-badge' const sandboxEventDataSchema = z.record(z.string(), z.unknown()) -const EVENT_TYPE_LABELS: Record = { - 'sandbox.lifecycle.created': 'Created', - 'sandbox.lifecycle.updated': 'Updated', - 'sandbox.lifecycle.paused': 'Paused', - 'sandbox.lifecycle.resumed': 'Resumed', - 'sandbox.lifecycle.killed': 'Killed', -} - -const EVENT_TYPE_VARIANTS: Record< - string, - NonNullable -> = { - 'sandbox.lifecycle.created': 'positive', - 'sandbox.lifecycle.updated': 'main', - 'sandbox.lifecycle.paused': 'warning', - 'sandbox.lifecycle.resumed': 'info', - 'sandbox.lifecycle.killed': 'error', -} - interface SandboxEventsTableProps { events: SandboxEventModel[] + isTimestampDescending: boolean + onToggleTimestampSort: () => void } -export const SandboxEventsTable = ({ events }: SandboxEventsTableProps) => { - const [sorting, setSorting] = useState([ - { id: 'timestamp', desc: true }, - ]) - const isTimestampDescending = sorting[0]?.desc ?? true - const sortedEvents = useMemo( - () => - [...events].sort((eventA, eventB) => - isTimestampDescending - ? eventB.timestamp.localeCompare(eventA.timestamp) - : eventA.timestamp.localeCompare(eventB.timestamp) - ), - [events, isTimestampDescending] - ) - +export const SandboxEventsTable = ({ + events, + isTimestampDescending, + onToggleTimestampSort, +}: SandboxEventsTableProps) => { const table = useReactTable({ - data: sortedEvents, + data: events, columns: EVENT_COLUMNS, getCoreRowModel: getCoreRowModel(), }) @@ -95,11 +67,7 @@ export const SandboxEventsTable = ({ events }: SandboxEventsTableProps) => { + + Page {page} + + + +
- + + setOrder(order === 'desc' ? 'asc' : 'desc') + } + />
) } From 78e9fee2e5f0bd7973ff7d4b563289cf5e8e9e52 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 16:01:27 -0400 Subject: [PATCH 06/41] style: update cell alignment and styling in SandboxEventsTable - Adjusted the styling of `DataTableCell` and `TimestampCell` components to improve visual consistency and alignment. - Changed classes to center items and enhance layout, ensuring a more polished user interface. These updates contribute to a cleaner and more user-friendly presentation of the events table. --- src/features/dashboard/sandbox/events/table.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 598a40cd9..dc37d3920 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -103,7 +103,7 @@ export const SandboxEventsTable = ({ {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -144,7 +144,7 @@ const TimestampCell = ({ row }: CellContext) => { if (!formattedTimestamp) { return ( -
+
--
) @@ -153,7 +153,7 @@ const TimestampCell = ({ row }: CellContext) => { return ( {formattedTimestamp.datePart}{' '} {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} From e455d6823987cac4b1d34173fdfb20018d1dc179 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 16:18:49 -0400 Subject: [PATCH 07/41] refactor: replace DataTable with Table components in SandboxEventsTable - Switched from using `DataTable` to a new `Table` component for rendering the events table, enhancing consistency with the UI library. - Introduced column width constants and improved cell rendering logic for better layout control. - Updated the empty state presentation to align with the new table structure, ensuring a cohesive user experience. These changes streamline the table implementation and improve the overall visual presentation of sandbox events. --- .../dashboard/sandbox/events/table.tsx | 228 +++++++----------- src/ui/data-table.tsx | 6 +- 2 files changed, 90 insertions(+), 144 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index dc37d3920..a7a3780af 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -1,31 +1,36 @@ 'use client' -import { - type CellContext, - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table' import { useMemo } from 'react' import { z } from 'zod' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' -import { useColumnSizeVars } from '@/lib/hooks/use-column-size-vars' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' -import { - DataTable, - DataTableBody, - DataTableCell, - DataTableHead, - DataTableHeader, - DataTableRow, -} from '@/ui/data-table' import { JsonPopover } from '@/ui/json-popover' import { ArrowDownIcon, HistoryIcon, MetadataIcon } from '@/ui/primitives/icons' +import { + Table, + TableBody, + TableCell, + TableEmptyState, + TableHead, + TableHeader, + TableRow, +} from '@/ui/primitives/table' import { SandboxEventTypeBadge } from './event-type-badge' const sandboxEventDataSchema = z.record(z.string(), z.unknown()) +const EVENT_COLUMN_WIDTHS = { + timestamp: 250, + event: 260, + details: 320, + id: 230, +} as const + +const colStyle = (width: number) => ({ + width, + minWidth: width, + maxWidth: width, +}) interface SandboxEventsTableProps { events: SandboxEventModel[] @@ -38,108 +43,86 @@ export const SandboxEventsTable = ({ isTimestampDescending, onToggleTimestampSort, }: SandboxEventsTableProps) => { - const table = useReactTable({ - data: events, - columns: EVENT_COLUMNS, - getCoreRowModel: getCoreRowModel(), - }) - - const columnSizeVars = useColumnSizeVars(table) - const rows = table.getRowModel().rows - return (
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + + + + + + + + + + - ) : ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - )} - - ))} - - ))} - - - - {rows.length > 0 ? ( - rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - + /> + + + Event + Details + ID + + + + + {events.length > 0 ? ( + events.map((event) => ( + + + + + + + + + + + + + + )) ) : ( )} - - + +
) } const EventsEmptyState = () => { return ( -
+
-

No events found

+

No events found

-

- Lifecycle events for this sandbox will appear here once available. -

-
+ ) } -const TimestampCell = ({ row }: CellContext) => { +const TimestampCell = ({ + timestamp, +}: { + timestamp: SandboxEventModel['timestamp'] +}) => { const formattedTimestamp = useMemo( () => - formatLocalLogStyleTimestamp(row.original.timestamp, { + formatLocalLogStyleTimestamp(timestamp, { includeCentiseconds: true, }), - [row.original.timestamp] + [timestamp] ) if (!formattedTimestamp) { @@ -161,18 +144,22 @@ const TimestampCell = ({ row }: CellContext) => { ) } -const EventTypeCell = ({ row }: CellContext) => { +const EventTypeCell = ({ type }: { type: SandboxEventModel['type'] }) => { return (
- +
) } -const EventDetailsCell = ({ row }: CellContext) => { +const EventDetailsCell = ({ + eventData, +}: { + eventData: SandboxEventModel['eventData'] +}) => { const parsedEventData = useMemo( - () => sandboxEventDataSchema.safeParse(row.original.eventData), - [row.original.eventData] + () => sandboxEventDataSchema.safeParse(eventData), + [eventData] ) if (!parsedEventData.success) { @@ -202,47 +189,10 @@ const EventDetailsCell = ({ row }: CellContext) => { ) } -const EventIdCell = ({ row }: CellContext) => { +const EventIdCell = ({ id }: { id: SandboxEventModel['id'] }) => { return (
- {row.original.id} + {id}
) } - -const EVENT_COLUMNS: ColumnDef[] = [ - { - accessorKey: 'timestamp', - header: 'TIMESTAMP', - cell: TimestampCell, - size: 250, - minSize: 220, - enableSorting: false, - }, - { - accessorKey: 'type', - header: 'Event', - cell: EventTypeCell, - size: 260, - minSize: 220, - enableSorting: false, - }, - { - id: 'details', - header: 'Details', - cell: EventDetailsCell, - size: 320, - minSize: 220, - maxSize: 420, - enableSorting: false, - }, - { - accessorKey: 'id', - header: 'ID', - cell: EventIdCell, - size: 230, - minSize: 190, - maxSize: 260, - enableSorting: false, - }, -] diff --git a/src/ui/data-table.tsx b/src/ui/data-table.tsx index 7059a12bd..e1a5d0ff1 100644 --- a/src/ui/data-table.tsx +++ b/src/ui/data-table.tsx @@ -1,9 +1,5 @@ import type { Cell, Header } from '@tanstack/react-table' -import { - ArrowDownWideNarrow, - ArrowUpDown, - ArrowUpNarrowWide, -} from 'lucide-react' +import { ArrowDownWideNarrow, ArrowUpNarrowWide } from 'lucide-react' import * as React from 'react' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' From 2e3ecef5f17a970e35ead76703ba99e74e47ee7e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 16:34:23 -0400 Subject: [PATCH 08/41] feat: introduce IdBadge component for improved event ID display in SandboxEventsTable - Added a new `IdBadge` component to enhance the presentation of event IDs, providing a copy functionality and a more user-friendly display format. - Updated `SandboxEventsTable` to utilize the `IdBadge`, improving the layout and styling of the ID column. - Adjusted column widths and cell styles for better alignment and visual consistency across the table. These changes enhance the user experience by making event IDs more accessible and visually appealing. --- .../dashboard/sandbox/events/table.tsx | 46 +++++++++-------- .../dashboard/settings/keys/id-badge.tsx | 50 +++++++++++++++++++ 2 files changed, 74 insertions(+), 22 deletions(-) create mode 100644 src/features/dashboard/settings/keys/id-badge.tsx diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index a7a3780af..1c4f83556 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react' import { z } from 'zod' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' +import { IdBadge } from '@/features/dashboard/settings/keys/id-badge' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' import { JsonPopover } from '@/ui/json-popover' @@ -20,10 +21,9 @@ import { SandboxEventTypeBadge } from './event-type-badge' const sandboxEventDataSchema = z.record(z.string(), z.unknown()) const EVENT_COLUMN_WIDTHS = { - timestamp: 250, - event: 260, - details: 320, - id: 230, + timestamp: 192, + event: 120, + id: 176, } as const const colStyle = (width: number) => ({ @@ -45,16 +45,16 @@ export const SandboxEventsTable = ({ }: SandboxEventsTableProps) => { return (
- +
- - + + - + - Event - Details - ID + ID + Event + Details @@ -78,18 +78,18 @@ export const SandboxEventsTable = ({ {events.length > 0 ? ( events.map((event) => ( - + - + + + + - + - - - )) ) : ( @@ -136,10 +136,12 @@ const TimestampCell = ({ return ( - {formattedTimestamp.datePart}{' '} - {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} + {formattedTimestamp.datePart} + + {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} + ) } @@ -191,8 +193,8 @@ const EventDetailsCell = ({ const EventIdCell = ({ id }: { id: SandboxEventModel['id'] }) => { return ( -
- {id} +
+
) } diff --git a/src/features/dashboard/settings/keys/id-badge.tsx b/src/features/dashboard/settings/keys/id-badge.tsx new file mode 100644 index 000000000..139d4c63a --- /dev/null +++ b/src/features/dashboard/settings/keys/id-badge.tsx @@ -0,0 +1,50 @@ +'use client' + +import { useClipboard } from '@/lib/hooks/use-clipboard' +import { Badge } from '@/ui/primitives/badge' +import { Button } from '@/ui/primitives/button' +import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' + +/** Builds the visible uppercase ID badge label; e.g. "e2b_c28e178eecf2" -> "E2B_...ECF2". */ +const getIdBadgeLabel = (id: string): string => { + if (id.length <= 8) return id.toUpperCase() + return `${id.slice(0, 4)}...${id.slice(-4)}`.toUpperCase() +} + +interface IdBadgeProps { + id: string +} + +export const IdBadge = ({ id }: IdBadgeProps) => { + const [wasCopied, copy] = useClipboard() + const displayId = getIdBadgeLabel(id) + + const handleCopy = (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + void copy(id) + } + + return ( + + {displayId} + + + ) +} From 600ed233395ac30c63d0f3a644789100a9c515c6 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 21 Apr 2026 16:40:03 -0400 Subject: [PATCH 09/41] style: enhance header cell styling in SandboxEventsTable - Updated the styling of header cells in the SandboxEventsTable to include consistent padding and height adjustments. - Improved visual alignment and consistency across the table headers, contributing to a more polished user interface. These changes enhance the overall presentation of the events table, ensuring a better user experience. --- src/features/dashboard/sandbox/events/table.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 1c4f83556..32628bb6c 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -54,7 +54,10 @@ export const SandboxEventsTable = ({ - + - ID - Event - Details + ID + Event + Details From 82e8bd2ac720fe4c64c2a0c58cc726c87376691f Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 11:53:20 -0400 Subject: [PATCH 10/41] refactor: remove listSandboxLifecycleEvents method and related query logic - Eliminated the `listSandboxLifecycleEvents` method from the `SandboxesRepository` interface and its implementation, streamlining the repository structure. - Updated the `sandboxRouter` to remove the associated query logic for listing sandbox lifecycle events, simplifying the API. - Refactored the event filters in the `useSandboxEventFilters` hook to remove offset handling, enhancing clarity and reducing complexity. - Adjusted the `SandboxEventsView` component to utilize the updated context for lifecycle events, improving data handling and presentation. These changes enhance the overall codebase by reducing redundancy and improving the maintainability of the sandbox events feature. --- .../modules/sandboxes/repository.server.ts | 9 +- src/core/server/api/routers/sandbox.ts | 39 --------- .../dashboard/sandbox/events/filter-params.ts | 4 - .../events/use-sandbox-event-filters.ts | 16 ---- .../dashboard/sandbox/events/view.tsx | 83 +++++-------------- 5 files changed, 21 insertions(+), 130 deletions(-) diff --git a/src/core/modules/sandboxes/repository.server.ts b/src/core/modules/sandboxes/repository.server.ts index 3ccfa1cb3..f84d051fd 100644 --- a/src/core/modules/sandboxes/repository.server.ts +++ b/src/core/modules/sandboxes/repository.server.ts @@ -37,7 +37,7 @@ export interface GetSandboxMetricsOptions { endUnixMs: number } -export interface ListSandboxLifecycleEventsOptions { +interface ListSandboxLifecycleEventsOptions { offset?: number limit?: number orderAsc?: boolean @@ -64,10 +64,6 @@ export interface SandboxesRepository { getSandboxLifecycleEvents( sandboxId: string ): Promise> - listSandboxLifecycleEvents( - sandboxId: string, - options?: ListSandboxLifecycleEventsOptions - ): Promise> getSandboxMetrics( sandboxId: string, options: GetSandboxMetricsOptions @@ -342,9 +338,6 @@ export function createSandboxesRepository( return ok(lifecycleEvents) }, - async listSandboxLifecycleEvents(sandboxId, options = {}) { - return listSandboxLifecycleEventsPage(sandboxId, options) - }, async getSandboxMetrics(sandboxId, options) { const startUnixSeconds = Math.floor(options.startUnixMs / 1000) const endUnixSeconds = Math.floor(options.endUnixMs / 1000) diff --git a/src/core/server/api/routers/sandbox.ts b/src/core/server/api/routers/sandbox.ts index 0b64848fd..948367c59 100644 --- a/src/core/server/api/routers/sandbox.ts +++ b/src/core/server/api/routers/sandbox.ts @@ -1,6 +1,5 @@ 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, @@ -30,44 +29,6 @@ const sandboxRepositoryProcedure = protectedTeamProcedure.use( 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, - } - }), - details: sandboxRepositoryProcedure .input( z.object({ diff --git a/src/features/dashboard/sandbox/events/filter-params.ts b/src/features/dashboard/sandbox/events/filter-params.ts index f965af6e3..69f3134c2 100644 --- a/src/features/dashboard/sandbox/events/filter-params.ts +++ b/src/features/dashboard/sandbox/events/filter-params.ts @@ -1,18 +1,15 @@ import { createLoader, - parseAsInteger, parseAsString, parseAsStringEnum, } from 'nuqs/server' -const SANDBOX_EVENTS_PAGE_SIZE = 20 const SANDBOX_EVENTS_ORDER_VALUES: ['asc', 'desc'] = ['asc', 'desc'] type SandboxEventsOrder = (typeof SANDBOX_EVENTS_ORDER_VALUES)[number] const sandboxEventsFilterParams = { type: parseAsString, - offset: parseAsInteger, order: parseAsStringEnum(SANDBOX_EVENTS_ORDER_VALUES), } @@ -21,7 +18,6 @@ const loadSandboxEventsFilters = createLoader(sandboxEventsFilterParams) export { loadSandboxEventsFilters, SANDBOX_EVENTS_ORDER_VALUES, - SANDBOX_EVENTS_PAGE_SIZE, sandboxEventsFilterParams, } export type { SandboxEventsOrder } diff --git a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts index 0eff5edf4..a71289a15 100644 --- a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts +++ b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts @@ -14,51 +14,35 @@ import { const DEFAULT_SANDBOX_EVENTS_ORDER: SandboxEventsOrder = 'desc' -/** Clamps the offset to a non-negative page start. Example: -20 -> 0. */ -const normalizeOffset = (offset: number | null) => Math.max(offset ?? 0, 0) - const useSandboxEventFilters = () => { const [filters, setFilters] = useQueryStates(sandboxEventsFilterParams, { shallow: true, }) const type = getSandboxLifecycleEventTypeFromUrlValue(filters.type ?? null) - const offset = normalizeOffset(filters.offset) const order = filters.order ?? DEFAULT_SANDBOX_EVENTS_ORDER const setType = useCallback( (type: SandboxLifecycleEventType | null) => { setFilters({ type: type ? getSandboxLifecycleEventUrlValue(type) : null, - offset: null, }) }, [setFilters] ) - const setOffset = useCallback( - (offset: number) => { - const normalizedOffset = normalizeOffset(offset) - setFilters({ offset: normalizedOffset === 0 ? null : normalizedOffset }) - }, - [setFilters] - ) - const setOrder = useCallback( (order: SandboxEventsOrder) => { setFilters({ order: order === DEFAULT_SANDBOX_EVENTS_ORDER ? null : order, - offset: null, }) }, [setFilters] ) return { - offset, order, orderAsc: order === 'asc', - setOffset, setOrder, setType, type, diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index 2d20c968e..2d6f628a4 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -1,19 +1,13 @@ 'use client' -import { useQuery } from '@tanstack/react-query' import Link from 'next/link' +import { useMemo } from 'react' import { HELP_URLS } from '@/configs/urls' import LoadingLayout from '@/features/dashboard/loading-layout' -import { useRouteParams } from '@/lib/hooks/use-route-params' -import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' -import { - ChevronLeftIcon, - ChevronRightIcon, - ExternalLinkIcon, -} from '@/ui/primitives/icons' +import { ExternalLinkIcon } from '@/ui/primitives/icons' +import { useSandboxContext } from '../context' import { EventTypeFilter } from './event-type-filter' -import { SANDBOX_EVENTS_PAGE_SIZE } from './filter-params' import { SandboxEventsTable } from './table' import useSandboxEventFilters from './use-sandbox-event-filters' @@ -29,68 +23,31 @@ const LifecycleEventsDocsLink = () => { } export const SandboxEventsView = () => { - const trpc = useTRPC() - const { teamSlug, sandboxId } = - useRouteParams<'/dashboard/[teamSlug]/sandboxes/[sandboxId]'>() - const { offset, order, orderAsc, setOffset, setOrder, setType, type } = - useSandboxEventFilters() - - const { data, isPending, isFetching } = useQuery( - trpc.sandbox.events.queryOptions( - { - teamSlug, - sandboxId, - type: type ?? undefined, - offset, - limit: SANDBOX_EVENTS_PAGE_SIZE, - orderAsc, - }, - { - refetchOnWindowFocus: false, - } - ) - ) - - if (isPending && !data) { + const { sandboxLifecycle, isSandboxInfoLoading } = useSandboxContext() + const { order, orderAsc, setOrder, setType, type } = useSandboxEventFilters() + + const events = useMemo(() => { + const lifecycleEvents = sandboxLifecycle?.events ?? [] + const filteredEvents = type + ? lifecycleEvents.filter((event) => event.type === type) + : lifecycleEvents + const orderedEvents = orderAsc + ? filteredEvents + : [...filteredEvents].reverse() + + return orderedEvents + }, [orderAsc, sandboxLifecycle?.events, type]) + + if (isSandboxInfoLoading && !sandboxLifecycle) { return } - const events = data?.events ?? [] - const page = Math.floor(offset / SANDBOX_EVENTS_PAGE_SIZE) + 1 - const hasPreviousPage = data?.hasPreviousPage ?? offset > 0 - const hasNextPage = data?.hasNextPage ?? false - return (
-
- - - Page {page} - - - -
+
Date: Thu, 23 Apr 2026 11:58:11 -0400 Subject: [PATCH 11/41] refactor: simplify SandboxEventTypeBadge and adjust table cell styles - Removed unused lifecycle event variant logic from the SandboxEventTypeBadge component, defaulting to a single badge variant for clarity. - Updated the SandboxEventsTable to reduce cell height and padding, enhancing the overall layout and visual consistency. - Streamlined the EventTypeCell and EventIdCell components for improved alignment and presentation. These changes improve the maintainability of the badge component and enhance the user interface of the events table. --- .../sandbox/events/event-type-badge.tsx | 26 +++---------------- .../dashboard/sandbox/events/table.tsx | 14 +++++----- .../dashboard/sandbox/events/view.tsx | 19 +------------- 3 files changed, 11 insertions(+), 48 deletions(-) diff --git a/src/features/dashboard/sandbox/events/event-type-badge.tsx b/src/features/dashboard/sandbox/events/event-type-badge.tsx index ac2f755e7..ddd4a2544 100644 --- a/src/features/dashboard/sandbox/events/event-type-badge.tsx +++ b/src/features/dashboard/sandbox/events/event-type-badge.tsx @@ -1,29 +1,9 @@ -import { - getSandboxLifecycleEventLabel, - type SandboxLifecycleEventType, - sandboxLifecycleEventTypeSchema, -} from '@/core/modules/sandboxes/lifecycle-event-types' -import { Badge, type BadgeProps } from '@/ui/primitives/badge' - -const SANDBOX_LIFECYCLE_EVENT_VARIANTS: Record< - SandboxLifecycleEventType, - NonNullable -> = { - 'sandbox.lifecycle.created': 'positive', - 'sandbox.lifecycle.updated': 'main', - 'sandbox.lifecycle.paused': 'warning', - 'sandbox.lifecycle.resumed': 'info', - 'sandbox.lifecycle.killed': 'error', -} +import { getSandboxLifecycleEventLabel } from '@/core/modules/sandboxes/lifecycle-event-types' +import { Badge } from '@/ui/primitives/badge' export const SandboxEventTypeBadge = ({ type }: { type: string }) => { - const parsedType = sandboxLifecycleEventTypeSchema.safeParse(type) - const variant = parsedType.success - ? SANDBOX_LIFECYCLE_EVENT_VARIANTS[parsedType.data] - : 'default' - return ( - + {getSandboxLifecycleEventLabel(type)} ) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 32628bb6c..8645e5ef6 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -80,17 +80,17 @@ export const SandboxEventsTable = ({ {events.length > 0 ? ( events.map((event) => ( - - + + - + - + - + @@ -151,7 +151,7 @@ const TimestampCell = ({ const EventTypeCell = ({ type }: { type: SandboxEventModel['type'] }) => { return ( -
+
) @@ -196,7 +196,7 @@ const EventDetailsCell = ({ const EventIdCell = ({ id }: { id: SandboxEventModel['id'] }) => { return ( -
+
) diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index 2d6f628a4..71442c6fa 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -1,27 +1,12 @@ 'use client' -import Link from 'next/link' import { useMemo } from 'react' -import { HELP_URLS } from '@/configs/urls' import LoadingLayout from '@/features/dashboard/loading-layout' -import { Button } from '@/ui/primitives/button' -import { ExternalLinkIcon } from '@/ui/primitives/icons' import { useSandboxContext } from '../context' import { EventTypeFilter } from './event-type-filter' import { SandboxEventsTable } from './table' import useSandboxEventFilters from './use-sandbox-event-filters' -const LifecycleEventsDocsLink = () => { - return ( - - ) -} - export const SandboxEventsView = () => { const { sandboxLifecycle, isSandboxInfoLoading } = useSandboxContext() const { order, orderAsc, setOrder, setType, type } = useSandboxEventFilters() @@ -44,10 +29,8 @@ export const SandboxEventsView = () => { return (
-
+
- -
Date: Thu, 23 Apr 2026 12:14:18 -0400 Subject: [PATCH 12/41] refactor: enhance SandboxEventTypeBadge and update EventTypeFilter styling - Expanded the SandboxEventTypeBadge component to include a mapping of lifecycle event types to icons, improving visual representation. - Implemented schema validation for event types to ensure accurate icon rendering. - Updated the EventTypeFilter button to use a secondary variant for better alignment with design standards. These changes enhance the user interface and maintainability of the event components. --- .../sandbox/events/event-type-badge.tsx | 29 ++++++++++++++++++- .../sandbox/events/event-type-filter.tsx | 4 +-- .../dashboard/settings/keys/id-badge.tsx | 10 +++---- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/features/dashboard/sandbox/events/event-type-badge.tsx b/src/features/dashboard/sandbox/events/event-type-badge.tsx index ddd4a2544..dc1c6d7fa 100644 --- a/src/features/dashboard/sandbox/events/event-type-badge.tsx +++ b/src/features/dashboard/sandbox/events/event-type-badge.tsx @@ -1,9 +1,36 @@ -import { getSandboxLifecycleEventLabel } from '@/core/modules/sandboxes/lifecycle-event-types' +import { + getSandboxLifecycleEventLabel, + type SandboxLifecycleEventType, + sandboxLifecycleEventTypeSchema, +} from '@/core/modules/sandboxes/lifecycle-event-types' import { Badge } from '@/ui/primitives/badge' +import { + BlockIcon, + CheckIcon, + DotIcon, + type Icon, + PausedIcon, + RefreshIcon, + RunningIcon, +} from '@/ui/primitives/icons' + +const SANDBOX_EVENT_TYPE_ICON_MAP: Record = { + 'sandbox.lifecycle.created': CheckIcon, + 'sandbox.lifecycle.updated': RefreshIcon, + 'sandbox.lifecycle.paused': PausedIcon, + 'sandbox.lifecycle.resumed': RunningIcon, + 'sandbox.lifecycle.killed': BlockIcon, +} export const SandboxEventTypeBadge = ({ type }: { type: string }) => { + const parsedType = sandboxLifecycleEventTypeSchema.safeParse(type) + const IconComponent = parsedType.success + ? SANDBOX_EVENT_TYPE_ICON_MAP[parsedType.data] + : DotIcon + return ( + {getSandboxLifecycleEventLabel(type)} ) diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx index 14701600c..e470862af 100644 --- a/src/features/dashboard/sandbox/events/event-type-filter.tsx +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -35,8 +35,8 @@ export const EventTypeFilter = ({ diff --git a/src/features/dashboard/settings/keys/id-badge.tsx b/src/features/dashboard/settings/keys/id-badge.tsx index 139d4c63a..dd1dd1652 100644 --- a/src/features/dashboard/settings/keys/id-badge.tsx +++ b/src/features/dashboard/settings/keys/id-badge.tsx @@ -2,7 +2,7 @@ import { useClipboard } from '@/lib/hooks/use-clipboard' import { Badge } from '@/ui/primitives/badge' -import { Button } from '@/ui/primitives/button' +import { IconButton } from '@/ui/primitives/icon-button' import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' /** Builds the visible uppercase ID badge label; e.g. "e2b_c28e178eecf2" -> "E2B_...ECF2". */ @@ -31,11 +31,9 @@ export const IdBadge = ({ id }: IdBadgeProps) => { size="sm" > {displayId} - + ) } From a4c92d52c1e6cc243b80fc822dbb1ff67fb222f6 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 12:28:21 -0400 Subject: [PATCH 13/41] refactor: update event type filter and table styling - Modified the EventTypeFilter button label for improved clarity and consistency with design standards. - Adjusted column widths in the SandboxEventsTable to better fit content and enhance layout. - Updated the CopyButtonInline component to use a button element for better accessibility and styling. These changes enhance the user interface and improve the overall presentation of the events dashboard. --- .../dashboard/sandbox/events/event-type-filter.tsx | 7 ++----- src/features/dashboard/sandbox/events/table.tsx | 14 ++++++++------ src/ui/copy-button-inline.tsx | 13 +++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx index e470862af..0678c1363 100644 --- a/src/features/dashboard/sandbox/events/event-type-filter.tsx +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -34,11 +34,8 @@ export const EventTypeFilter = ({
- diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 8645e5ef6..3f6dba361 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -21,9 +21,11 @@ import { SandboxEventTypeBadge } from './event-type-badge' const sandboxEventDataSchema = z.record(z.string(), z.unknown()) const EVENT_COLUMN_WIDTHS = { - timestamp: 192, - event: 120, - id: 176, + // Match the compact sandbox logs timestamp width. + timestamp: 148 + 16, + // Sized to fit the fixed-width badge content plus cell padding. + id: 92 + 16, + event: 72 + 16, } as const const colStyle = (width: number) => ({ @@ -45,7 +47,7 @@ export const SandboxEventsTable = ({ }: SandboxEventsTableProps) => { return (
-
+
@@ -139,9 +141,9 @@ const TimestampCell = ({ return ( - {formattedTimestamp.datePart} + {formattedTimestamp.datePart}{' '} {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} diff --git a/src/ui/copy-button-inline.tsx b/src/ui/copy-button-inline.tsx index 12e1154a6..cb4036402 100644 --- a/src/ui/copy-button-inline.tsx +++ b/src/ui/copy-button-inline.tsx @@ -1,4 +1,3 @@ -import { useRef, useState } from 'react' import { useClipboard } from '@/lib/hooks/use-clipboard' import { cn } from '@/lib/utils/ui' import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' @@ -20,17 +19,19 @@ export default function CopyButtonInline({ } return ( - - {children} + {children} - + ) } From ae0d5f4089a20c7d1df9947bd7c7d58c787dd526 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 12:37:20 -0400 Subject: [PATCH 14/41] refactor: simplify EventDetailsCell and enhance JSON display - Removed the MetadataIcon from the JsonPopover for a cleaner presentation. - Introduced a new EventDetailsJsonCell component to encapsulate JSON rendering logic, improving code organization and readability. - Updated the JsonPopover to display the JSON data more effectively, enhancing the user interface. These changes improve the maintainability of the event details display and streamline the presentation of JSON data. --- .../dashboard/sandbox/events/table.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 3f6dba361..8ed19e430 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -7,7 +7,7 @@ import { IdBadge } from '@/features/dashboard/settings/keys/id-badge' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' import { JsonPopover } from '@/ui/json-popover' -import { ArrowDownIcon, HistoryIcon, MetadataIcon } from '@/ui/primitives/icons' +import { ArrowDownIcon, HistoryIcon } from '@/ui/primitives/icons' import { Table, TableBody, @@ -178,20 +178,18 @@ const EventDetailsCell = ({ return n/a } - const preview = JSON.stringify(parsedEventData.data) + return +} + +const EventDetailsJsonCell = ({ json }: { json: Record }) => { + const preview = JSON.stringify(json) return ( - - - {preview} - + {preview} ) } From 5935c984db598b79039a7f2ae459403f43df45c3 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 12:39:20 -0400 Subject: [PATCH 15/41] Run biome format --- src/configs/urls.ts | 3 +-- src/core/modules/sandboxes/lifecycle-event-types.ts | 13 ++++++++++++- .../dashboard/sandbox/events/filter-params.ts | 6 +----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/configs/urls.ts b/src/configs/urls.ts index 2f93737d1..fbd4576a8 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -79,8 +79,7 @@ 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', + SANDBOX_LIFECYCLE_EVENTS: 'https://e2b.dev/docs/sandbox/lifecycle-events-api', } export const BASE_URL = process.env.VERCEL_ENV diff --git a/src/core/modules/sandboxes/lifecycle-event-types.ts b/src/core/modules/sandboxes/lifecycle-event-types.ts index 0ec7b0baf..8cc06b38d 100644 --- a/src/core/modules/sandboxes/lifecycle-event-types.ts +++ b/src/core/modules/sandboxes/lifecycle-event-types.ts @@ -93,4 +93,15 @@ const getSandboxLifecycleEventTypeFromUrlValue = (value: string | 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 } +export { + getSandboxLifecycleEventLabel, + getSandboxLifecycleEventTypeFromUrlValue, + getSandboxLifecycleEventUrlValue, + SANDBOX_LIFECYCLE_EVENT_LABELS, + SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES, + SANDBOX_LIFECYCLE_EVENT_URL_VALUES, + sandboxLifecycleEventTypeSchema, + sandboxLifecycleEventUrlValueSchema, + type SandboxLifecycleEventType, + type SandboxLifecycleEventUrlValue, +} diff --git a/src/features/dashboard/sandbox/events/filter-params.ts b/src/features/dashboard/sandbox/events/filter-params.ts index 69f3134c2..40a838508 100644 --- a/src/features/dashboard/sandbox/events/filter-params.ts +++ b/src/features/dashboard/sandbox/events/filter-params.ts @@ -1,8 +1,4 @@ -import { - createLoader, - parseAsString, - parseAsStringEnum, -} from 'nuqs/server' +import { createLoader, parseAsString, parseAsStringEnum } from 'nuqs/server' const SANDBOX_EVENTS_ORDER_VALUES: ['asc', 'desc'] = ['asc', 'desc'] From 7e9f3c1e6a8b730c7384f3ae3bf3e94d3a681453 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 12:51:30 -0400 Subject: [PATCH 16/41] refactor: remove 'types' parameter from API specifications and related code - Eliminated the 'types' query parameter from the OpenAPI specifications for sandbox events, simplifying the API. - Updated the SandboxesRepository and related interfaces to reflect the removal of the 'types' option, enhancing clarity. - Adjusted the sandbox events filter parameters to streamline the codebase. These changes improve the maintainability of the API and related components by reducing complexity. --- spec/openapi.argus.yaml | 14 -------------- src/core/modules/sandboxes/repository.server.ts | 8 ++------ src/core/shared/contracts/argus-api.types.ts | 2 -- .../dashboard/sandbox/events/filter-params.ts | 11 ++--------- 4 files changed, 4 insertions(+), 31 deletions(-) diff --git a/spec/openapi.argus.yaml b/spec/openapi.argus.yaml index 706c1db22..87baf4dca 100644 --- a/spec/openapi.argus.yaml +++ b/spec/openapi.argus.yaml @@ -309,13 +309,6 @@ 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 @@ -364,13 +357,6 @@ 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 diff --git a/src/core/modules/sandboxes/repository.server.ts b/src/core/modules/sandboxes/repository.server.ts index f84d051fd..cd12e40c9 100644 --- a/src/core/modules/sandboxes/repository.server.ts +++ b/src/core/modules/sandboxes/repository.server.ts @@ -3,7 +3,6 @@ import 'server-only' 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, @@ -37,11 +36,10 @@ export interface GetSandboxMetricsOptions { endUnixMs: number } -interface ListSandboxLifecycleEventsOptions { +interface ListSandboxLifecycleEventsPageOptions { offset?: number limit?: number orderAsc?: boolean - types?: SandboxLifecycleEventType[] } export interface SandboxesRepository { @@ -100,7 +98,7 @@ export function createSandboxesRepository( /** Fetches one sandbox lifecycle events page. Example: { offset: 20, limit: 20 } -> the next 20 events. */ const listSandboxLifecycleEventsPage = async ( sandboxId: string, - options: ListSandboxLifecycleEventsOptions = {} + options: ListSandboxLifecycleEventsPageOptions = {} ): Promise> => { const result = await deps.infraClient.GET('/events/sandboxes/{sandboxID}', { params: { @@ -111,7 +109,6 @@ export function createSandboxesRepository( offset: options.offset, limit: options.limit, orderAsc: options.orderAsc, - types: options.types, }, }, headers: { @@ -134,7 +131,6 @@ export function createSandboxesRepository( offset: options.offset, limit: options.limit, order_asc: options.orderAsc, - types: options.types, }, }) diff --git a/src/core/shared/contracts/argus-api.types.ts b/src/core/shared/contracts/argus-api.types.ts index 8001cdd80..b9286818e 100644 --- a/src/core/shared/contracts/argus-api.types.ts +++ b/src/core/shared/contracts/argus-api.types.ts @@ -53,7 +53,6 @@ export interface paths { offset?: number limit?: number orderAsc?: boolean - types?: string[] } header?: never path: { @@ -99,7 +98,6 @@ export interface paths { offset?: number limit?: number orderAsc?: boolean - types?: string[] } header?: never path?: never diff --git a/src/features/dashboard/sandbox/events/filter-params.ts b/src/features/dashboard/sandbox/events/filter-params.ts index 40a838508..ad66ff2ff 100644 --- a/src/features/dashboard/sandbox/events/filter-params.ts +++ b/src/features/dashboard/sandbox/events/filter-params.ts @@ -1,4 +1,4 @@ -import { createLoader, parseAsString, parseAsStringEnum } from 'nuqs/server' +import { parseAsString, parseAsStringEnum } from 'nuqs/server' const SANDBOX_EVENTS_ORDER_VALUES: ['asc', 'desc'] = ['asc', 'desc'] @@ -9,11 +9,4 @@ const sandboxEventsFilterParams = { order: parseAsStringEnum(SANDBOX_EVENTS_ORDER_VALUES), } -const loadSandboxEventsFilters = createLoader(sandboxEventsFilterParams) - -export { - loadSandboxEventsFilters, - SANDBOX_EVENTS_ORDER_VALUES, - sandboxEventsFilterParams, -} -export type { SandboxEventsOrder } +export { sandboxEventsFilterParams, type SandboxEventsOrder } From 0480f6f2725d07c23d636be33e208f5a9a423df5 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 13:25:04 -0400 Subject: [PATCH 17/41] refactor: streamline sandbox lifecycle event handling - Removed unused constants and functions related to sandbox lifecycle events, simplifying the codebase. - Introduced a new event type mapping for icons and labels, enhancing clarity and maintainability. - Updated components to utilize the new mapping and schema validation, improving the user interface and consistency across the dashboard. These changes enhance the overall structure and readability of the event handling logic, making it easier to manage and extend in the future. --- src/configs/urls.ts | 1 - .../sandboxes/lifecycle-event-types.ts | 102 +----------------- .../sandbox/events/event-type-badge.tsx | 41 +++---- .../sandbox/events/event-type-filter.tsx | 16 +-- .../sandbox/events/event-type-map.ts | 22 ++++ .../events/use-sandbox-event-filters.ts | 22 ++-- 6 files changed, 61 insertions(+), 143 deletions(-) create mode 100644 src/features/dashboard/sandbox/events/event-type-map.ts diff --git a/src/configs/urls.ts b/src/configs/urls.ts index fbd4576a8..8e0ef1e7c 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -79,7 +79,6 @@ 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 diff --git a/src/core/modules/sandboxes/lifecycle-event-types.ts b/src/core/modules/sandboxes/lifecycle-event-types.ts index 8cc06b38d..3efec7212 100644 --- a/src/core/modules/sandboxes/lifecycle-event-types.ts +++ b/src/core/modules/sandboxes/lifecycle-event-types.ts @@ -1,107 +1,13 @@ import { z } from 'zod' -const SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES: [ +const SandboxLifecycleEventTypeSchema = z.enum([ '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 -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] +type SandboxLifecycleEventType = z.infer -export { - getSandboxLifecycleEventLabel, - getSandboxLifecycleEventTypeFromUrlValue, - getSandboxLifecycleEventUrlValue, - SANDBOX_LIFECYCLE_EVENT_LABELS, - SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES, - SANDBOX_LIFECYCLE_EVENT_URL_VALUES, - sandboxLifecycleEventTypeSchema, - sandboxLifecycleEventUrlValueSchema, - type SandboxLifecycleEventType, - type SandboxLifecycleEventUrlValue, -} +export { SandboxLifecycleEventTypeSchema, type SandboxLifecycleEventType } diff --git a/src/features/dashboard/sandbox/events/event-type-badge.tsx b/src/features/dashboard/sandbox/events/event-type-badge.tsx index dc1c6d7fa..1044c98b1 100644 --- a/src/features/dashboard/sandbox/events/event-type-badge.tsx +++ b/src/features/dashboard/sandbox/events/event-type-badge.tsx @@ -1,37 +1,24 @@ -import { - getSandboxLifecycleEventLabel, - type SandboxLifecycleEventType, - sandboxLifecycleEventTypeSchema, -} from '@/core/modules/sandboxes/lifecycle-event-types' +import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' import { Badge } from '@/ui/primitives/badge' -import { - BlockIcon, - CheckIcon, - DotIcon, - type Icon, - PausedIcon, - RefreshIcon, - RunningIcon, -} from '@/ui/primitives/icons' - -const SANDBOX_EVENT_TYPE_ICON_MAP: Record = { - 'sandbox.lifecycle.created': CheckIcon, - 'sandbox.lifecycle.updated': RefreshIcon, - 'sandbox.lifecycle.paused': PausedIcon, - 'sandbox.lifecycle.resumed': RunningIcon, - 'sandbox.lifecycle.killed': BlockIcon, -} +import { SANDBOX_EVENT_TYPE_MAP } from './event-type-map' export const SandboxEventTypeBadge = ({ type }: { type: string }) => { - const parsedType = sandboxLifecycleEventTypeSchema.safeParse(type) - const IconComponent = parsedType.success - ? SANDBOX_EVENT_TYPE_ICON_MAP[parsedType.data] - : DotIcon + const parsed = SandboxLifecycleEventTypeSchema.safeParse(type) + + if (!parsed.success) { + return ( + + {type} + + ) + } + + const { icon: IconComponent, label } = SANDBOX_EVENT_TYPE_MAP[parsed.data] return ( - {getSandboxLifecycleEventLabel(type)} + {label} ) } diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx index 0678c1363..7760fe67e 100644 --- a/src/features/dashboard/sandbox/events/event-type-filter.tsx +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -1,8 +1,6 @@ import { - getSandboxLifecycleEventLabel, - SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES, type SandboxLifecycleEventType, - sandboxLifecycleEventTypeSchema, + SandboxLifecycleEventTypeSchema, } from '@/core/modules/sandboxes/lifecycle-event-types' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' @@ -14,6 +12,7 @@ import { DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' import { SandboxEventTypeBadge } from './event-type-badge' +import { SANDBOX_EVENT_TYPE_MAP } from './event-type-map' const ALL_EVENT_TYPES_VALUE = 'all' @@ -28,14 +27,12 @@ export const EventTypeFilter = ({ onTypeChange, className, }: EventTypeFilterProps) => { - const selectedTypeLabel = type ? getSandboxLifecycleEventLabel(type) : 'All' - return (
@@ -47,16 +44,13 @@ export const EventTypeFilter = ({ return } - const parsedType = - sandboxLifecycleEventTypeSchema.safeParse(value) - if (!parsedType.success) return - onTypeChange(parsedType.data) + onTypeChange(SandboxLifecycleEventTypeSchema.parse(value)) }} > All events - {SANDBOX_LIFECYCLE_EVENT_TYPE_VALUES.map((type) => ( + {SandboxLifecycleEventTypeSchema.options.map((type) => ( diff --git a/src/features/dashboard/sandbox/events/event-type-map.ts b/src/features/dashboard/sandbox/events/event-type-map.ts new file mode 100644 index 000000000..fb94cc123 --- /dev/null +++ b/src/features/dashboard/sandbox/events/event-type-map.ts @@ -0,0 +1,22 @@ +import type { SandboxLifecycleEventType } from '@/core/modules/sandboxes/lifecycle-event-types' +import { + BlockIcon, + CheckIcon, + type Icon, + PausedIcon, + RefreshIcon, + RunningIcon, +} from '@/ui/primitives/icons' + +const SANDBOX_EVENT_TYPE_MAP: Record< + SandboxLifecycleEventType, + { icon: Icon; label: string } +> = { + 'sandbox.lifecycle.created': { icon: CheckIcon, label: 'Created' }, + 'sandbox.lifecycle.updated': { icon: RefreshIcon, label: 'Updated' }, + 'sandbox.lifecycle.paused': { icon: PausedIcon, label: 'Paused' }, + 'sandbox.lifecycle.resumed': { icon: RunningIcon, label: 'Resumed' }, + 'sandbox.lifecycle.killed': { icon: BlockIcon, label: 'Killed' }, +} + +export { SANDBOX_EVENT_TYPE_MAP } diff --git a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts index a71289a15..b1c310a1f 100644 --- a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts +++ b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts @@ -3,9 +3,8 @@ import { useQueryStates } from 'nuqs' import { useCallback } from 'react' import { - getSandboxLifecycleEventTypeFromUrlValue, - getSandboxLifecycleEventUrlValue, type SandboxLifecycleEventType, + SandboxLifecycleEventTypeSchema, } from '@/core/modules/sandboxes/lifecycle-event-types' import { type SandboxEventsOrder, @@ -14,19 +13,30 @@ import { const DEFAULT_SANDBOX_EVENTS_ORDER: SandboxEventsOrder = 'desc' +// Strips the "sandbox.lifecycle." prefix for URL display, e.g. "sandbox.lifecycle.created" -> "created" +const toUrlValue = (type: SandboxLifecycleEventType) => + type.split('.').slice(2).join('.') + +// Re-adds the "sandbox.lifecycle." prefix from a URL value, e.g. "created" -> "sandbox.lifecycle.created" +const fromUrlValue = (value: string | null): SandboxLifecycleEventType | null => { + if (!value) return null + const parsed = SandboxLifecycleEventTypeSchema.safeParse( + `sandbox.lifecycle.${value}` + ) + return parsed.success ? parsed.data : null +} + const useSandboxEventFilters = () => { const [filters, setFilters] = useQueryStates(sandboxEventsFilterParams, { shallow: true, }) - const type = getSandboxLifecycleEventTypeFromUrlValue(filters.type ?? null) + const type = fromUrlValue(filters.type) const order = filters.order ?? DEFAULT_SANDBOX_EVENTS_ORDER const setType = useCallback( (type: SandboxLifecycleEventType | null) => { - setFilters({ - type: type ? getSandboxLifecycleEventUrlValue(type) : null, - }) + setFilters({ type: type ? toUrlValue(type) : null }) }, [setFilters] ) From d1ef298191ee04e3f890fdd8bf07935b8640181b Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 13:29:42 -0400 Subject: [PATCH 18/41] Rollback pagination changes --- .../modules/sandboxes/repository.server.ts | 128 +++++++----------- 1 file changed, 49 insertions(+), 79 deletions(-) diff --git a/src/core/modules/sandboxes/repository.server.ts b/src/core/modules/sandboxes/repository.server.ts index cd12e40c9..2f4ad1aba 100644 --- a/src/core/modules/sandboxes/repository.server.ts +++ b/src/core/modules/sandboxes/repository.server.ts @@ -36,12 +36,6 @@ export interface GetSandboxMetricsOptions { endUnixMs: number } -interface ListSandboxLifecycleEventsPageOptions { - offset?: number - limit?: number - orderAsc?: boolean -} - export interface SandboxesRepository { getSandboxLogs( sandboxId: string, @@ -95,60 +89,6 @@ export function createSandboxesRepository( 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: ListSandboxLifecycleEventsPageOptions = {} - ): Promise> => { - const result = await deps.infraClient.GET('/events/sandboxes/{sandboxID}', { - params: { - path: { - sandboxID: sandboxId, - }, - query: { - offset: options.offset, - limit: options.limit, - orderAsc: options.orderAsc, - }, - }, - 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, - }, - }) - - 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( @@ -299,16 +239,57 @@ export function createSandboxesRepository( pageIndex < SANDBOX_EVENTS_MAX_PAGES; pageIndex += 1, offset += SANDBOX_EVENTS_PAGE_SIZE ) { - const result = await listSandboxLifecycleEventsPage(sandboxId, { - offset, - limit: SANDBOX_EVENTS_PAGE_SIZE, - orderAsc: true, - }) + 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) + ) + ) - if (!result.ok) { + if (page.length < SANDBOX_EVENTS_PAGE_SIZE) { + break + } + } catch (error) { l.warn({ - key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_error', - error: result.error, + key: 'repositories:sandboxes:get_sandbox_lifecycle_events:infra_exception', + error, team_id: scope.teamId, context: { path: '/events/sandboxes/{sandboxID}', @@ -319,17 +300,6 @@ export function createSandboxesRepository( }) 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) From 30781d7ea6010647249141b27609f8802bca9d25 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 13:32:00 -0400 Subject: [PATCH 19/41] refactor: rename sandbox event data schema for consistency - Updated the variable name for the sandbox event data schema to follow PascalCase convention, enhancing code readability and consistency. - Adjusted the usage of the renamed schema in the EventDetailsCell component to ensure proper parsing of event data. These changes improve the clarity of the code and maintain a consistent naming convention across the project. --- src/features/dashboard/sandbox/events/table.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 8ed19e430..953052a9d 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -19,11 +19,10 @@ import { } from '@/ui/primitives/table' import { SandboxEventTypeBadge } from './event-type-badge' -const sandboxEventDataSchema = z.record(z.string(), z.unknown()) +const SandboxEventDataSchema = z.record(z.string(), z.unknown()) + const EVENT_COLUMN_WIDTHS = { - // Match the compact sandbox logs timestamp width. timestamp: 148 + 16, - // Sized to fit the fixed-width badge content plus cell padding. id: 92 + 16, event: 72 + 16, } as const @@ -165,7 +164,7 @@ const EventDetailsCell = ({ eventData: SandboxEventModel['eventData'] }) => { const parsedEventData = useMemo( - () => sandboxEventDataSchema.safeParse(eventData), + () => SandboxEventDataSchema.safeParse(eventData), [eventData] ) From 6451a77a653a7d7f33957bd12233e99d0a37b11e Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 23 Apr 2026 13:32:40 -0400 Subject: [PATCH 20/41] Run biome format --- .../dashboard/sandbox/events/use-sandbox-event-filters.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts index b1c310a1f..d8bbdae8b 100644 --- a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts +++ b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts @@ -18,7 +18,9 @@ const toUrlValue = (type: SandboxLifecycleEventType) => type.split('.').slice(2).join('.') // Re-adds the "sandbox.lifecycle." prefix from a URL value, e.g. "created" -> "sandbox.lifecycle.created" -const fromUrlValue = (value: string | null): SandboxLifecycleEventType | null => { +const fromUrlValue = ( + value: string | null +): SandboxLifecycleEventType | null => { if (!value) return null const parsed = SandboxLifecycleEventTypeSchema.safeParse( `sandbox.lifecycle.${value}` From 21236c220387e9be6b7c1f0659626be7f1a03024 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 28 Apr 2026 13:07:11 -0400 Subject: [PATCH 21/41] refactor: simplify SandboxEventsTable and remove unused components - Adjusted column widths in the SandboxEventsTable for improved layout and readability. - Removed the EventDetailsCell and related JSON rendering logic, streamlining the event display. - Updated the table structure to enhance clarity and maintainability. These changes improve the overall presentation and organization of the events dashboard. --- .../dashboard/sandbox/events/table.tsx | 53 ++----------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 953052a9d..38acf02a2 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -1,12 +1,10 @@ 'use client' import { useMemo } from 'react' -import { z } from 'zod' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' import { IdBadge } from '@/features/dashboard/settings/keys/id-badge' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' -import { JsonPopover } from '@/ui/json-popover' import { ArrowDownIcon, HistoryIcon } from '@/ui/primitives/icons' import { Table, @@ -19,12 +17,9 @@ import { } from '@/ui/primitives/table' import { SandboxEventTypeBadge } from './event-type-badge' -const SandboxEventDataSchema = z.record(z.string(), z.unknown()) - const EVENT_COLUMN_WIDTHS = { timestamp: 148 + 16, id: 92 + 16, - event: 72 + 16, } as const const colStyle = (width: number) => ({ @@ -46,11 +41,10 @@ export const SandboxEventsTable = ({ }: SandboxEventsTableProps) => { return (
-
+
- @@ -73,8 +67,7 @@ export const SandboxEventsTable = ({ ID - Event - Details + Event @@ -88,11 +81,8 @@ export const SandboxEventsTable = ({ - - - - + )) @@ -107,7 +97,7 @@ export const SandboxEventsTable = ({ const EventsEmptyState = () => { return ( - +

No events found

@@ -158,41 +148,6 @@ const EventTypeCell = ({ type }: { type: SandboxEventModel['type'] }) => { ) } -const EventDetailsCell = ({ - eventData, -}: { - eventData: SandboxEventModel['eventData'] -}) => { - const parsedEventData = useMemo( - () => SandboxEventDataSchema.safeParse(eventData), - [eventData] - ) - - if (!parsedEventData.success) { - return n/a - } - - const entries = Object.entries(parsedEventData.data) - if (entries.length === 0) { - return n/a - } - - return -} - -const EventDetailsJsonCell = ({ json }: { json: Record }) => { - const preview = JSON.stringify(json) - - return ( - - {preview} - - ) -} - const EventIdCell = ({ id }: { id: SandboxEventModel['id'] }) => { return (
From f5695229dc37d934486df176d1251e127594e487 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 29 Apr 2026 15:58:05 -0400 Subject: [PATCH 22/41] refactor: update ID badge component to use Button instead of IconButton - Replaced IconButton with Button for the copy action in the ID badge component. - Adjusted button properties for improved styling and accessibility. - Enhanced ID display with wider tracking for better readability. --- .../dashboard/settings/keys/id-badge.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/features/dashboard/settings/keys/id-badge.tsx b/src/features/dashboard/settings/keys/id-badge.tsx index dd1dd1652..caae419c3 100644 --- a/src/features/dashboard/settings/keys/id-badge.tsx +++ b/src/features/dashboard/settings/keys/id-badge.tsx @@ -2,7 +2,7 @@ import { useClipboard } from '@/lib/hooks/use-clipboard' import { Badge } from '@/ui/primitives/badge' -import { IconButton } from '@/ui/primitives/icon-button' +import { Button } from '@/ui/primitives/button' import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' /** Builds the visible uppercase ID badge label; e.g. "e2b_c28e178eecf2" -> "E2B_...ECF2". */ @@ -26,23 +26,18 @@ export const IdBadge = ({ id }: IdBadgeProps) => { } return ( - - {displayId} - + {displayId} + ) } From cc323b80d3484c90d3d8171354110836ac5ffc3c Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 29 Apr 2026 16:01:15 -0400 Subject: [PATCH 23/41] Remove default exports --- .../dashboard/sandbox/events/use-sandbox-event-filters.ts | 4 +--- src/features/dashboard/sandbox/events/view.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts index d8bbdae8b..987b14504 100644 --- a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts +++ b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts @@ -28,7 +28,7 @@ const fromUrlValue = ( return parsed.success ? parsed.data : null } -const useSandboxEventFilters = () => { +export const useSandboxEventFilters = () => { const [filters, setFilters] = useQueryStates(sandboxEventsFilterParams, { shallow: true, }) @@ -60,5 +60,3 @@ const useSandboxEventFilters = () => { type, } } - -export default useSandboxEventFilters diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index 71442c6fa..2a7f5f93d 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -5,7 +5,7 @@ import LoadingLayout from '@/features/dashboard/loading-layout' import { useSandboxContext } from '../context' import { EventTypeFilter } from './event-type-filter' import { SandboxEventsTable } from './table' -import useSandboxEventFilters from './use-sandbox-event-filters' +import { useSandboxEventFilters } from './use-sandbox-event-filters' export const SandboxEventsView = () => { const { sandboxLifecycle, isSandboxInfoLoading } = useSandboxContext() From 5f46012eb7bf9d46a0dea2eb70a269aa3a6ff352 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 4 May 2026 13:04:26 -0400 Subject: [PATCH 24/41] refactor: enhance event type badge and table components - Updated the `SandboxEventTypeBadge` component to include `align-middle` class for better vertical alignment. - Refactored the `SandboxEventsTable` to streamline the rendering of event rows, replacing deprecated components with updated ones. - Improved timestamp formatting in the table by integrating a new inline copy button for better user experience. - Removed unused components and optimized the layout for cleaner presentation. This refactor aims to improve the overall usability and maintainability of the dashboard's event display features. --- .../sandbox/events/event-type-badge.tsx | 4 +- .../dashboard/sandbox/events/table.tsx | 158 +++++++----------- .../dashboard/sandbox/events/view.tsx | 7 +- .../dashboard/settings/keys/id-badge.tsx | 2 +- 4 files changed, 69 insertions(+), 102 deletions(-) diff --git a/src/features/dashboard/sandbox/events/event-type-badge.tsx b/src/features/dashboard/sandbox/events/event-type-badge.tsx index 1044c98b1..7f41f8b68 100644 --- a/src/features/dashboard/sandbox/events/event-type-badge.tsx +++ b/src/features/dashboard/sandbox/events/event-type-badge.tsx @@ -7,7 +7,7 @@ export const SandboxEventTypeBadge = ({ type }: { type: string }) => { if (!parsed.success) { return ( - + {type} ) @@ -16,7 +16,7 @@ export const SandboxEventTypeBadge = ({ type }: { type: string }) => { const { icon: IconComponent, label } = SANDBOX_EVENT_TYPE_MAP[parsed.data] return ( - + {label} diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 38acf02a2..b32df12c5 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -1,6 +1,5 @@ 'use client' -import { useMemo } from 'react' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' import { IdBadge } from '@/features/dashboard/settings/keys/id-badge' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' @@ -40,58 +39,79 @@ export const SandboxEventsTable = ({ onToggleTimestampSort, }: SandboxEventsTableProps) => { return ( -
-
- - - - - - - - + + + + + + + + + - - ID - Event - - + TIMESTAMP + + + + ID + Event + + - - {events.length > 0 ? ( - events.map((event) => ( + + {events.length > 0 ? ( + events.map((event) => { + const formattedTimestamp = formatLocalLogStyleTimestamp( + event.timestamp, + { + includeCentiseconds: true, + } + ) + + return ( - + {formattedTimestamp ? ( + + + {formattedTimestamp.datePart} + {' '} + + {formattedTimestamp.timePart}. + {formattedTimestamp.subsecondPart} + + + ) : ( +
+ -- +
+ )}
- - + + - +
- )) - ) : ( - - )} -
-
-
+ ) + }) + ) : ( + + )} + + ) } @@ -105,53 +125,3 @@ const EventsEmptyState = () => { ) } - -const TimestampCell = ({ - timestamp, -}: { - timestamp: SandboxEventModel['timestamp'] -}) => { - const formattedTimestamp = useMemo( - () => - formatLocalLogStyleTimestamp(timestamp, { - includeCentiseconds: true, - }), - [timestamp] - ) - - if (!formattedTimestamp) { - return ( -
- -- -
- ) - } - - return ( - - {formattedTimestamp.datePart}{' '} - - {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} - - - ) -} - -const EventTypeCell = ({ type }: { type: SandboxEventModel['type'] }) => { - return ( -
- -
- ) -} - -const EventIdCell = ({ id }: { id: SandboxEventModel['id'] }) => { - return ( -
- -
- ) -} diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index 2a7f5f93d..1b74bb449 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -28,11 +28,8 @@ export const SandboxEventsView = () => { } return ( -
-
- -
- +
+ { } return ( - + {displayId} - - - { - if (value === ALL_EVENT_TYPES_VALUE) { - onTypeChange(null) - return - } + + + + + + { + if (value === ALL_EVENT_TYPES_VALUE) { + onTypeChange(null) + return + } - onTypeChange(SandboxLifecycleEventTypeSchema.parse(value)) - }} - > - - All events + onTypeChange(SandboxLifecycleEventTypeSchema.parse(value)) + }} + > + + All events + + {SandboxLifecycleEventTypeSchema.options.map((type) => ( + + - {SandboxLifecycleEventTypeSchema.options.map((type) => ( - - - - ))} - - - -
+ ))} + + + ) } From cb125a84b77b125352b5ffc8ba3027ef0ca0c6d4 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 16:58:37 -0400 Subject: [PATCH 26/41] Refactor: centralize IdBadge component usage - Moved the IdBadge component to a shared location for better reusability across the dashboard. - Updated references in the API keys table row and events table to utilize the new shared IdBadge component. - Simplified the API keys utility functions by removing the now redundant getApiKeyIdBadgeLabel function. This refactor aims to enhance code maintainability and promote component reuse within the dashboard. --- .../dashboard/sandbox/events/table.tsx | 2 +- .../settings/keys/api-keys-table-row.tsx | 39 +++++-------------- .../dashboard/settings/keys/api-keys-utils.ts | 18 +++------ .../{settings/keys => shared}/id-badge.tsx | 17 ++++++-- src/features/dashboard/shared/index.ts | 1 + 5 files changed, 29 insertions(+), 48 deletions(-) rename src/features/dashboard/{settings/keys => shared}/id-badge.tsx (78%) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index b32df12c5..8e07aa83b 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -1,7 +1,7 @@ 'use client' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' -import { IdBadge } from '@/features/dashboard/settings/keys/id-badge' +import { IdBadge } from '@/features/dashboard/shared' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' import { ArrowDownIcon, HistoryIcon } from '@/ui/primitives/icons' diff --git a/src/features/dashboard/settings/keys/api-keys-table-row.tsx b/src/features/dashboard/settings/keys/api-keys-table-row.tsx index 4c82af330..9806fa8c8 100644 --- a/src/features/dashboard/settings/keys/api-keys-table-row.tsx +++ b/src/features/dashboard/settings/keys/api-keys-table-row.tsx @@ -1,25 +1,22 @@ 'use client' import { usePostHog } from 'posthog-js/react' -import type { MouseEvent } from 'react' import { CLI_GENERATED_KEY_NAME } from '@/configs/api' import type { TeamAPIKey } from '@/core/modules/keys/models' -import { UserAvatar } from '@/features/dashboard/shared' -import { useClipboard } from '@/lib/hooks/use-clipboard' +import { IdBadge, UserAvatar } from '@/features/dashboard/shared' import { defaultSuccessToast, useToast } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { formatDate, formatUTCTimestamp } from '@/lib/utils/formatting' import { E2BLogo } from '@/ui/brand' -import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' -import { CheckIcon, CopyIcon, KeyIcon, TrashIcon } from '@/ui/primitives/icons' +import { KeyIcon, TrashIcon } from '@/ui/primitives/icons' import { TableCell, TableRow } from '@/ui/primitives/table' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/ui/primitives/tooltip' -import { getApiKeyIdBadgeLabel, getLastUsedLabel } from './api-keys-utils' +import { getLastUsedLabel } from './api-keys-utils' const tableCellClassName = 'py-3 text-left [tr:first-child>&]:pt-1.5' @@ -31,7 +28,6 @@ interface ApiKeysTableRowProps { export const ApiKeysTableRow = ({ apiKey, onDelete }: ApiKeysTableRowProps) => { const posthog = usePostHog() const { toast } = useToast() - const [wasCopied, copy] = useClipboard() const addedDate = apiKey.createdAt ? (formatDate(new Date(apiKey.createdAt), 'MMM d, yyyy') ?? '—') @@ -40,14 +36,9 @@ export const ApiKeysTableRow = ({ apiKey, onDelete }: ApiKeysTableRowProps) => { const lastUsedAt = apiKey.lastUsed const lastUsedLabel = getLastUsedLabel(apiKey) const isCliKey = apiKey.name === CLI_GENERATED_KEY_NAME - const displayId = getApiKeyIdBadgeLabel(apiKey.id) const createdByEmail = apiKey.createdBy?.email?.trim() || 'Unknown user' - const handleCopy = async (event: MouseEvent) => { - event.preventDefault() - event.stopPropagation() - - await copy(apiKey.id) + const handleIdCopied = () => { posthog.capture('copied API key id') toast(defaultSuccessToast('ID copied to clipboard')) } @@ -68,23 +59,11 @@ export const ApiKeysTableRow = ({ apiKey, onDelete }: ApiKeysTableRowProps) => {
- - {displayId} - - + {lastUsedAt ? ( diff --git a/src/features/dashboard/settings/keys/api-keys-utils.ts b/src/features/dashboard/settings/keys/api-keys-utils.ts index fa02b864c..d5f188590 100644 --- a/src/features/dashboard/settings/keys/api-keys-utils.ts +++ b/src/features/dashboard/settings/keys/api-keys-utils.ts @@ -3,32 +3,22 @@ import type { TeamAPIKey } from '@/core/modules/keys/models' import { formatRelativeAgo } from '@/lib/utils/formatting' /** Builds a short masked id string for search and display; e.g. input mask fields → `"e2b_…a1b2"` */ -export const getMaskedIdSearchString = (apiKey: TeamAPIKey): string => { +const getMaskedIdSearchString = (apiKey: TeamAPIKey): string => { const { prefix, maskedValuePrefix, maskedValueSuffix } = apiKey.mask return `${prefix}${maskedValuePrefix}...${maskedValueSuffix}`.toLowerCase() } -/** Builds the visible uppercase ID badge label; e.g. `"e2b_c28e178eecf2"` -> `"E2B_...ECF2"` */ -export const getApiKeyIdBadgeLabel = (id: string): string => { - if (id.length <= 8) return id.toUpperCase() - return `${id.slice(0, 4)}...${id.slice(-4)}`.toUpperCase() -} - /** Returns true when the key name or masked id contains the trimmed query (case-insensitive). */ -export const matchesApiKeySearch = ( - apiKey: TeamAPIKey, - query: string -): boolean => { +const matchesApiKeySearch = (apiKey: TeamAPIKey, query: string): boolean => { const q = query.trim().toLowerCase() if (!q) return true if (apiKey.name.toLowerCase().includes(q)) return true if (apiKey.id.toLowerCase().includes(q)) return true - if (getApiKeyIdBadgeLabel(apiKey.id).toLowerCase().includes(q)) return true return getMaskedIdSearchString(apiKey).includes(q) } /** Human line for last-used cell, matching legacy semantics for pre-collection keys. */ -export const getLastUsedLabel = (apiKey: TeamAPIKey): string => { +const getLastUsedLabel = (apiKey: TeamAPIKey): string => { if (apiKey.lastUsed) return formatRelativeAgo(new Date(apiKey.lastUsed)) const createdBefore = @@ -38,3 +28,5 @@ export const getLastUsedLabel = (apiKey: TeamAPIKey): string => { if (createdBefore) return 'N/A' return 'Never' } + +export { getLastUsedLabel, getMaskedIdSearchString, matchesApiKeySearch } diff --git a/src/features/dashboard/settings/keys/id-badge.tsx b/src/features/dashboard/shared/id-badge.tsx similarity index 78% rename from src/features/dashboard/settings/keys/id-badge.tsx rename to src/features/dashboard/shared/id-badge.tsx index b7a7b8d2d..56fff2934 100644 --- a/src/features/dashboard/settings/keys/id-badge.tsx +++ b/src/features/dashboard/shared/id-badge.tsx @@ -1,5 +1,6 @@ 'use client' +import type { MouseEvent } from 'react' import { useClipboard } from '@/lib/hooks/use-clipboard' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' @@ -13,16 +14,24 @@ const getIdBadgeLabel = (id: string): string => { interface IdBadgeProps { id: string + copyAriaLabel?: string + onCopied?: () => void } -export const IdBadge = ({ id }: IdBadgeProps) => { +export const IdBadge = ({ + id, + copyAriaLabel = 'Copy full ID', + onCopied, +}: IdBadgeProps) => { const [wasCopied, copy] = useClipboard() const displayId = getIdBadgeLabel(id) - const handleCopy = (event: React.MouseEvent) => { + const handleCopy = async (event: MouseEvent) => { event.preventDefault() event.stopPropagation() - void copy(id) + + await copy(id) + onCopied?.() } return ( @@ -33,7 +42,7 @@ export const IdBadge = ({ id }: IdBadgeProps) => { variant="quaternary" size="none" className="text-fg-tertiary hover:text-fg h-3.5 w-3.5 shrink-0 active:translate-y-0 [&_svg]:size-3.5" - aria-label="Copy full ID" + aria-label={copyAriaLabel} onClick={handleCopy} > {wasCopied ? : } diff --git a/src/features/dashboard/shared/index.ts b/src/features/dashboard/shared/index.ts index acbd7102a..9722c392c 100644 --- a/src/features/dashboard/shared/index.ts +++ b/src/features/dashboard/shared/index.ts @@ -1 +1,2 @@ +export { IdBadge } from './id-badge' export { UserAvatar } from './user-avatar' From 4661bbe0c65ef60a3927821c90c65c9ffc2e1851 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:00:09 -0400 Subject: [PATCH 27/41] feat: enable client-side rendering for event type filter --- src/features/dashboard/sandbox/events/event-type-filter.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx index eda26400d..b4a77fcf8 100644 --- a/src/features/dashboard/sandbox/events/event-type-filter.tsx +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -1,3 +1,5 @@ +'use client' + import { type SandboxLifecycleEventType, SandboxLifecycleEventTypeSchema, From 7040cb99f041f1b1f8cce66d2d682c7ec3bec56a Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:02:40 -0400 Subject: [PATCH 28/41] fix: improve event type parsing in EventTypeFilter component - Updated the event type change handler to use safe parsing with SandboxLifecycleEventTypeSchema. - Added error handling to set the event type to null if parsing fails, ensuring robustness in user input handling. This change enhances the reliability of the EventTypeFilter component by preventing invalid event types from being processed. --- .../dashboard/sandbox/events/event-type-filter.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx index b4a77fcf8..17e722f9f 100644 --- a/src/features/dashboard/sandbox/events/event-type-filter.tsx +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -42,7 +42,13 @@ export const EventTypeFilter = ({ return } - onTypeChange(SandboxLifecycleEventTypeSchema.parse(value)) + const parsed = SandboxLifecycleEventTypeSchema.safeParse(value) + if (!parsed.success) { + onTypeChange(null) + return + } + + onTypeChange(parsed.data) }} > From 12cb29029602eafb5cca02dc3b7096139e7a0a0b Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:04:48 -0400 Subject: [PATCH 29/41] refactor: update column styles in SandboxEventsTable - Removed hardcoded column width constants and replaced them with Tailwind CSS classes for improved styling flexibility. - This change enhances the responsiveness and maintainability of the SandboxEventsTable component. --- src/features/dashboard/sandbox/events/table.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 8e07aa83b..69ec5509c 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -16,17 +16,6 @@ import { } from '@/ui/primitives/table' import { SandboxEventTypeBadge } from './event-type-badge' -const EVENT_COLUMN_WIDTHS = { - timestamp: 148 + 16, - id: 92 + 16, -} as const - -const colStyle = (width: number) => ({ - width, - minWidth: width, - maxWidth: width, -}) - interface SandboxEventsTableProps { events: SandboxEventModel[] isTimestampDescending: boolean @@ -41,8 +30,8 @@ export const SandboxEventsTable = ({ return ( - - + + From f7b502c37c34b465c55c5377cbdde2a7253a56b3 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:19:20 -0400 Subject: [PATCH 30/41] refactor: replace button with Button component in SandboxEventsTable - Updated the timestamp sorting button to use the new Button component for consistency in styling and improved accessibility. - Adjusted class names to align with the new component's design specifications, enhancing the overall appearance of the table header. This change aims to standardize UI components across the dashboard for better maintainability. --- src/features/dashboard/sandbox/events/table.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 69ec5509c..61091c215 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -4,6 +4,7 @@ import type { SandboxEventModel } from '@/core/modules/sandboxes/models' import { IdBadge } from '@/features/dashboard/shared' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' +import { Button } from '@/ui/primitives/button' import { ArrowDownIcon, HistoryIcon } from '@/ui/primitives/icons' import { Table, @@ -37,18 +38,20 @@ export const SandboxEventsTable = ({ - + ID Event From 891ad0af8960478c799318da05f33b0adc039444 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:22:12 -0400 Subject: [PATCH 31/41] refactor: simplify empty state rendering in SandboxEventsTable - Replaced the custom EventsEmptyState component with a direct implementation of TableEmptyState for improved clarity and reduced complexity. - This change enhances the maintainability of the SandboxEventsTable by streamlining the rendering logic for the empty state. --- src/features/dashboard/sandbox/events/table.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 61091c215..776350e8d 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -100,20 +100,12 @@ export const SandboxEventsTable = ({ ) }) ) : ( - + + + No events found + )}
) } - -const EventsEmptyState = () => { - return ( - -
- -

No events found

-
-
- ) -} From a01c7cc284c13a6458d02e27b8f5381396477874 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:28:24 -0400 Subject: [PATCH 32/41] refactor: enhance event type parsing in useSandboxEventFilters - Introduced useMemo for optimized parsing of event types, ensuring efficient updates based on filter changes. - Added useEffect to reset filters when parsing fails, improving error handling and robustness. - This change enhances the maintainability and reliability of the useSandboxEventFilters hook. --- .../events/use-sandbox-event-filters.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts index 987b14504..bd726e5f3 100644 --- a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts +++ b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts @@ -1,7 +1,7 @@ 'use client' import { useQueryStates } from 'nuqs' -import { useCallback } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { type SandboxLifecycleEventType, SandboxLifecycleEventTypeSchema, @@ -17,25 +17,25 @@ const DEFAULT_SANDBOX_EVENTS_ORDER: SandboxEventsOrder = 'desc' const toUrlValue = (type: SandboxLifecycleEventType) => type.split('.').slice(2).join('.') -// Re-adds the "sandbox.lifecycle." prefix from a URL value, e.g. "created" -> "sandbox.lifecycle.created" -const fromUrlValue = ( - value: string | null -): SandboxLifecycleEventType | null => { - if (!value) return null - const parsed = SandboxLifecycleEventTypeSchema.safeParse( - `sandbox.lifecycle.${value}` - ) - return parsed.success ? parsed.data : null -} - export const useSandboxEventFilters = () => { const [filters, setFilters] = useQueryStates(sandboxEventsFilterParams, { shallow: true, }) - const type = fromUrlValue(filters.type) + const parsedType = useMemo(() => { + if (!filters.type) return null + + return SandboxLifecycleEventTypeSchema.safeParse( + `sandbox.lifecycle.${filters.type}` + ) + }, [filters.type]) + const type = parsedType?.success ? parsedType.data : null const order = filters.order ?? DEFAULT_SANDBOX_EVENTS_ORDER + useEffect(() => { + if (parsedType && !parsedType.success) setFilters({ type: null }) + }, [parsedType, setFilters]) + const setType = useCallback( (type: SandboxLifecycleEventType | null) => { setFilters({ type: type ? toUrlValue(type) : null }) From 37879bdb4c61b39129ab8de0a733b799bcdd45f3 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:32:24 -0400 Subject: [PATCH 33/41] refactor: add comment for event ordering in SandboxEventsView - Added a comment to clarify that sandbox lifecycle events are derived in ascending timestamp order. - This change improves code readability and understanding of the event ordering logic. --- src/features/dashboard/sandbox/events/view.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index 1b74bb449..5efba6be1 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -16,6 +16,7 @@ export const SandboxEventsView = () => { const filteredEvents = type ? lifecycleEvents.filter((event) => event.type === type) : lifecycleEvents + // Sandbox lifecycle events are derived in ascending timestamp order. const orderedEvents = orderAsc ? filteredEvents : [...filteredEvents].reverse() From fc342ad2196d5d3ce26ba9542a7d76efa71c5000 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 5 May 2026 17:35:11 -0400 Subject: [PATCH 34/41] refactor: replace WEBHOOK_EVENTS with SandboxLifecycleEventTypeSchema options - Updated the WebhookAddEditDialog and its steps to utilize SandboxLifecycleEventTypeSchema for event handling instead of the deprecated WEBHOOK_EVENTS constant. - This change enhances maintainability by centralizing event definitions and ensuring consistency across the webhook configuration components. --- .../settings/webhooks/add-edit-dialog-steps.tsx | 4 ++-- .../dashboard/settings/webhooks/add-edit-dialog.tsx | 10 ++++++---- src/features/dashboard/settings/webhooks/constants.ts | 10 ---------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx index a4620b702..0244951bb 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog-steps.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from 'react' import type { UseFormReturn } from 'react-hook-form' import ShikiHighlighter from 'react-shiki' import { useShikiTheme } from '@/configs/shiki' +import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types' import type { UpsertWebhookSchemaType } from '@/core/server/functions/webhooks/schema' import { Button } from '@/ui/primitives/button' import { Checkbox } from '@/ui/primitives/checkbox' @@ -22,7 +23,6 @@ import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' import { Separator } from '@/ui/primitives/separator' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' import { - WEBHOOK_EVENTS, WEBHOOK_EXAMPLE_PAYLOAD, WEBHOOK_SIGNATURE_VALIDATION_DOCS_URL, } from './constants' @@ -176,7 +176,7 @@ export function WebhookAddEditDialogSteps({ {/* Individual event checkboxes */} - {WEBHOOK_EVENTS.map((event) => ( + {SandboxLifecycleEventTypeSchema.options.map((event) => (
selectedEvents.includes(event)) + selectedEvents.length === SandboxLifecycleEventTypeSchema.options.length && + SandboxLifecycleEventTypeSchema.options.every((event) => + selectedEvents.includes(event) + ) const { errors } = form.formState @@ -143,7 +145,7 @@ export default function WebhookAddEditDialog({ if (allEventsSelected) { form.setValue('events', []) } else { - form.setValue('events', [...WEBHOOK_EVENTS]) + form.setValue('events', [...SandboxLifecycleEventTypeSchema.options]) } } diff --git a/src/features/dashboard/settings/webhooks/constants.ts b/src/features/dashboard/settings/webhooks/constants.ts index 07a172cd8..f61731556 100644 --- a/src/features/dashboard/settings/webhooks/constants.ts +++ b/src/features/dashboard/settings/webhooks/constants.ts @@ -1,13 +1,3 @@ -export const WEBHOOK_EVENTS = [ - 'sandbox.lifecycle.created', - 'sandbox.lifecycle.paused', - 'sandbox.lifecycle.resumed', - 'sandbox.lifecycle.updated', - 'sandbox.lifecycle.killed', -] as const - -export type WebhookEvent = (typeof WEBHOOK_EVENTS)[number] - export const WEBHOOK_EXAMPLE_PAYLOAD = `{ "version": "v1", "id": "", From a0d5a1ed77d06dd7e4f1b0874a822d1f2205d0d5 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 13:00:33 -0400 Subject: [PATCH 35/41] refactor: replace log rendering components with virtualized alternatives - Updated the Logs and SandboxLogs components to utilize VirtualizedTableLoaderBody and VirtualizedTableRow for improved performance and rendering efficiency. - Removed deprecated LogsLoaderBody and LogVirtualRow components, streamlining the codebase and enhancing maintainability. - This change aims to optimize the log viewing experience by leveraging virtualization techniques. --- src/features/dashboard/build/logs.tsx | 22 +- .../dashboard/common/log-viewer-ui.tsx | 57 ----- .../dashboard/common/virtualized-table-ui.tsx | 54 +++++ .../dashboard/sandbox/events/table.tsx | 198 +++++++++++++----- .../dashboard/sandbox/events/view.tsx | 32 +-- src/features/dashboard/sandbox/logs/logs.tsx | 22 +- src/ui/primitives/table.tsx | 4 +- 7 files changed, 241 insertions(+), 148 deletions(-) create mode 100644 src/features/dashboard/common/virtualized-table-ui.tsx diff --git a/src/features/dashboard/build/logs.tsx b/src/features/dashboard/build/logs.tsx index 3d177a474..4176e881e 100644 --- a/src/features/dashboard/build/logs.tsx +++ b/src/features/dashboard/build/logs.tsx @@ -18,10 +18,12 @@ import { LogLevelFilter } from '@/features/dashboard/common/log-level-filter' import { LogStatusCell, LogsEmptyBody, - LogsLoaderBody, LogsTableHeader, - LogVirtualRow, } from '@/features/dashboard/common/log-viewer-ui' +import { + VirtualizedTableLoaderBody, + VirtualizedTableRow, +} from '@/features/dashboard/common/virtualized-table-ui' import { cn } from '@/lib/utils' import { Loader } from '@/ui/primitives/loader' import { Table, TableBody, TableCell } from '@/ui/primitives/table' @@ -70,7 +72,7 @@ export default function Logs({ levelWidth={COLUMN_WIDTHS_PX.level} timestampSortDirection="asc" /> - +
@@ -172,7 +174,7 @@ function LogsContent({ timestampSortDirection="asc" /> - {showLoader && } + {showLoader && } {showEmpty && ( )} @@ -514,7 +516,7 @@ function LogRow({ const millisAfterStart = log.timestampUnix - startedAt return ( - - + ) } @@ -567,7 +569,7 @@ function StatusRow({ isFetchingNextPage, }: StatusRowProps) { return ( - - + ) } @@ -603,7 +605,7 @@ function LiveStatusRow({ isBuilding, }: LiveStatusRowProps) { return ( - - + ) } diff --git a/src/features/dashboard/common/log-viewer-ui.tsx b/src/features/dashboard/common/log-viewer-ui.tsx index c62a62209..e40dd5d2d 100644 --- a/src/features/dashboard/common/log-viewer-ui.tsx +++ b/src/features/dashboard/common/log-viewer-ui.tsx @@ -1,8 +1,6 @@ -import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual' import type { CSSProperties, ReactNode } from 'react' import { cn } from '@/lib/utils' import { ArrowDownIcon, ListIcon } from '@/ui/primitives/icons' -import { Loader } from '@/ui/primitives/loader' import { TableBody, TableCell, @@ -57,20 +55,6 @@ export function LogsTableHeader({ ) } -export function LogsLoaderBody() { - return ( - - - -
- -
-
-
-
- ) -} - interface LogsEmptyBodyProps { description?: ReactNode } @@ -95,47 +79,6 @@ export function LogsEmptyBody({ description }: LogsEmptyBodyProps) { ) } -export function getLogVirtualRowStyle( - virtualRow: VirtualItem, - height: number -): CSSProperties { - return { - display: 'flex', - position: 'absolute', - left: 0, - transform: `translateY(${virtualRow.start}px)`, - minWidth: '100%', - height, - } -} - -interface LogVirtualRowProps { - virtualRow: VirtualItem - virtualizer: Virtualizer - height: number - className?: string - children: ReactNode -} - -export function LogVirtualRow({ - virtualRow, - virtualizer, - height, - className, - children, -}: LogVirtualRowProps) { - return ( - virtualizer.measureElement(node)} - className={className} - style={getLogVirtualRowStyle(virtualRow, height)} - > - {children} - - ) -} - const STATUS_ROW_CELL_STYLE: CSSProperties = { display: 'flex', alignItems: 'center', diff --git a/src/features/dashboard/common/virtualized-table-ui.tsx b/src/features/dashboard/common/virtualized-table-ui.tsx new file mode 100644 index 000000000..64f81296b --- /dev/null +++ b/src/features/dashboard/common/virtualized-table-ui.tsx @@ -0,0 +1,54 @@ +import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual' +import type { CSSProperties, ReactNode } from 'react' +import { Loader } from '@/ui/primitives/loader' +import { TableBody, TableCell, TableRow } from '@/ui/primitives/table' + +// Style for an absolutely-positioned virtualized row; e.g. translateY(120px) at 26px height +export const getVirtualizedRowStyle = ( + virtualRow: VirtualItem, + height: number +): CSSProperties => ({ + display: 'flex', + position: 'absolute', + left: 0, + transform: `translateY(${virtualRow.start}px)`, + minWidth: '100%', + height, +}) + +interface VirtualizedTableRowProps { + virtualRow: VirtualItem + virtualizer: Virtualizer + height: number + className?: string + children: ReactNode +} + +export const VirtualizedTableRow = ({ + virtualRow, + virtualizer, + height, + className, + children, +}: VirtualizedTableRowProps) => ( + virtualizer.measureElement(node)} + className={className} + style={getVirtualizedRowStyle(virtualRow, height)} + > + {children} + +) + +export const VirtualizedTableLoaderBody = () => ( + + + +
+ +
+
+
+
+) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 776350e8d..a6551ff26 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -1,6 +1,16 @@ 'use client' +import { + useVirtualizer, + type VirtualItem, + type Virtualizer, +} from '@tanstack/react-virtual' +import { useMemo } from 'react' import type { SandboxEventModel } from '@/core/modules/sandboxes/models' +import { + VirtualizedTableLoaderBody, + VirtualizedTableRow, +} from '@/features/dashboard/common/virtualized-table-ui' import { IdBadge } from '@/features/dashboard/shared' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' @@ -17,27 +27,34 @@ import { } from '@/ui/primitives/table' import { SandboxEventTypeBadge } from './event-type-badge' +const ROW_HEIGHT_PX = 32 +const VIRTUAL_OVERSCAN = 16 + interface SandboxEventsTableProps { events: SandboxEventModel[] + isLoading: boolean + scrollContainer: HTMLDivElement | null isTimestampDescending: boolean onToggleTimestampSort: () => void } export const SandboxEventsTable = ({ events, + isLoading, + scrollContainer, isTimestampDescending, onToggleTimestampSort, }: SandboxEventsTableProps) => { + 'use no memo' + return ( - - - - - - - - - +
+ + + - ID - Event + + ID + + Event - - {events.length > 0 ? ( - events.map((event) => { - const formattedTimestamp = formatLocalLogStyleTimestamp( - event.timestamp, - { - includeCentiseconds: true, - } - ) - - return ( - - - {formattedTimestamp ? ( - - - {formattedTimestamp.datePart} - {' '} - - {formattedTimestamp.timePart}. - {formattedTimestamp.subsecondPart} - - - ) : ( -
- -- -
- )} -
- - - - - - -
- ) - }) - ) : ( + {isLoading ? ( + + ) : events.length > 0 ? ( + + ) : ( + No events found - )} - +
+ )}
) } + +interface VirtualizedEventsBodyProps { + events: SandboxEventModel[] + scrollContainer: HTMLDivElement | null +} + +const VirtualizedEventsBody = ({ + events, + scrollContainer, +}: VirtualizedEventsBodyProps) => { + 'use no memo' + + const initialRect = useMemo(() => { + if (!scrollContainer) return undefined + + return { + height: scrollContainer.clientHeight, + width: scrollContainer.clientWidth, + } + }, [scrollContainer]) + + const virtualizer = useVirtualizer({ + count: events.length, + estimateSize: () => ROW_HEIGHT_PX, + getScrollElement: () => scrollContainer, + initialRect, + overscan: VIRTUAL_OVERSCAN, + paddingStart: 8, + }) + + return ( + + {virtualizer.getVirtualItems().map((virtualRow) => { + const event = events[virtualRow.index] + if (!event) return null + + return ( + + ) + })} + + ) +} + +interface SandboxEventRowProps { + event: SandboxEventModel + virtualRow: VirtualItem + virtualizer: Virtualizer +} + +const SandboxEventRow = ({ + event, + virtualRow, + virtualizer, +}: SandboxEventRowProps) => { + const formattedTimestamp = formatLocalLogStyleTimestamp(event.timestamp, { + includeCentiseconds: true, + }) + + return ( + + + {formattedTimestamp ? ( + + + {formattedTimestamp.datePart} + {' '} + + {formattedTimestamp.timePart}.{formattedTimestamp.subsecondPart} + + + ) : ( +
+ -- +
+ )} +
+ + + + + + +
+ ) +} diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index 5efba6be1..a14c90282 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -1,15 +1,19 @@ 'use client' -import { useMemo } from 'react' -import LoadingLayout from '@/features/dashboard/loading-layout' +import { useMemo, useState } from 'react' import { useSandboxContext } from '../context' import { EventTypeFilter } from './event-type-filter' import { SandboxEventsTable } from './table' import { useSandboxEventFilters } from './use-sandbox-event-filters' export const SandboxEventsView = () => { + 'use no memo' + const { sandboxLifecycle, isSandboxInfoLoading } = useSandboxContext() const { order, orderAsc, setOrder, setType, type } = useSandboxEventFilters() + const [scrollContainer, setScrollContainer] = useState( + null + ) const events = useMemo(() => { const lifecycleEvents = sandboxLifecycle?.events ?? [] @@ -24,20 +28,20 @@ export const SandboxEventsView = () => { return orderedEvents }, [orderAsc, sandboxLifecycle?.events, type]) - if (isSandboxInfoLoading && !sandboxLifecycle) { - return - } - return ( -
+
- - setOrder(order === 'desc' ? 'asc' : 'desc') - } - /> +
+ + setOrder(order === 'desc' ? 'asc' : 'desc') + } + /> +
) } diff --git a/src/features/dashboard/sandbox/logs/logs.tsx b/src/features/dashboard/sandbox/logs/logs.tsx index d9e276a8e..1ecdecc1a 100644 --- a/src/features/dashboard/sandbox/logs/logs.tsx +++ b/src/features/dashboard/sandbox/logs/logs.tsx @@ -22,10 +22,12 @@ import { LogLevelFilter } from '@/features/dashboard/common/log-level-filter' import { LogStatusCell, LogsEmptyBody, - LogsLoaderBody, LogsTableHeader, - LogVirtualRow, } from '@/features/dashboard/common/log-viewer-ui' +import { + VirtualizedTableLoaderBody, + VirtualizedTableRow, +} from '@/features/dashboard/common/virtualized-table-ui' import { cn } from '@/lib/utils' import { DebouncedInput } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader' @@ -81,7 +83,7 @@ export default function SandboxLogs({ teamSlug, sandboxId }: LogsProps) { levelWidth={COLUMN_WIDTHS_PX.level} timestampSortDirection="asc" /> - +
@@ -204,7 +206,7 @@ function LogsContent({ timestampSortDirection="asc" /> - {showLoader && } + {showLoader && } {showEmpty && ( - + ) } @@ -682,7 +684,7 @@ function StatusRow({ isFetchingNextPage, }: StatusRowProps) { return ( - - + ) } @@ -718,7 +720,7 @@ function LiveStatusRow({ isRunning, }: LiveStatusRowProps) { return ( - - + ) } diff --git a/src/ui/primitives/table.tsx b/src/ui/primitives/table.tsx index 69f1e499c..6febf4e29 100644 --- a/src/ui/primitives/table.tsx +++ b/src/ui/primitives/table.tsx @@ -151,8 +151,8 @@ const TableEmptyState = ({ children, className, }: TableEmptyStateProps) => ( - - + +
Date: Thu, 7 May 2026 13:06:41 -0400 Subject: [PATCH 36/41] refactor: remove outdated comment in virtualized table UI --- src/features/dashboard/common/virtualized-table-ui.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/dashboard/common/virtualized-table-ui.tsx b/src/features/dashboard/common/virtualized-table-ui.tsx index 64f81296b..2fc24d918 100644 --- a/src/features/dashboard/common/virtualized-table-ui.tsx +++ b/src/features/dashboard/common/virtualized-table-ui.tsx @@ -3,7 +3,6 @@ import type { CSSProperties, ReactNode } from 'react' import { Loader } from '@/ui/primitives/loader' import { TableBody, TableCell, TableRow } from '@/ui/primitives/table' -// Style for an absolutely-positioned virtualized row; e.g. translateY(120px) at 26px height export const getVirtualizedRowStyle = ( virtualRow: VirtualItem, height: number From c5b877819a49ed19e544af2800cbc36f7349352d Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Thu, 7 May 2026 13:07:25 -0400 Subject: [PATCH 37/41] Remove unused fragment --- .../settings/webhooks/add-edit-dialog.tsx | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index 1706380b0..eadd33660 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -234,39 +234,34 @@ export default function WebhookAddEditDialog({ Confirm + ) : currentStep === 1 ? ( + ) : ( - /* Add mode: show next/back navigation */ <> - {currentStep === 1 ? ( - - ) : ( - <> - - - - )} + + )} From 9b433da0e82c2e466e5787ce3529829389a8d7cd Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Fri, 8 May 2026 16:24:35 -0400 Subject: [PATCH 38/41] refactor: enhance sandbox lifecycle event handling and filtering - Introduced a prefix for sandbox lifecycle event types to improve consistency and maintainability. - Updated the EventTypeFilter component to support multi-selection of event types using checkboxes. - Refactored the useSandboxEventFilters hook to handle an array of selected event types, improving flexibility in filtering. - Enhanced URL parsing for event types to align with the new schema, ensuring robust handling of lifecycle events. This change aims to streamline event type management and improve the user experience in filtering sandbox events. --- .../sandboxes/lifecycle-event-types.ts | 18 +++-- .../sandbox/events/event-type-filter.tsx | 79 +++++++++++-------- .../dashboard/sandbox/events/filter-params.ts | 25 +++++- .../events/use-sandbox-event-filters.ts | 34 +++----- .../dashboard/sandbox/events/view.tsx | 14 ++-- 5 files changed, 101 insertions(+), 69 deletions(-) diff --git a/src/core/modules/sandboxes/lifecycle-event-types.ts b/src/core/modules/sandboxes/lifecycle-event-types.ts index 3efec7212..220284bce 100644 --- a/src/core/modules/sandboxes/lifecycle-event-types.ts +++ b/src/core/modules/sandboxes/lifecycle-event-types.ts @@ -1,13 +1,19 @@ import { z } from 'zod' +const SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX = 'sandbox.lifecycle.' + const SandboxLifecycleEventTypeSchema = z.enum([ - 'sandbox.lifecycle.created', - 'sandbox.lifecycle.updated', - 'sandbox.lifecycle.paused', - 'sandbox.lifecycle.resumed', - 'sandbox.lifecycle.killed', + `${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}created`, + `${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}updated`, + `${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}paused`, + `${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}resumed`, + `${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}killed`, ]) type SandboxLifecycleEventType = z.infer -export { SandboxLifecycleEventTypeSchema, type SandboxLifecycleEventType } +export { + SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX, + SandboxLifecycleEventTypeSchema, + type SandboxLifecycleEventType, +} diff --git a/src/features/dashboard/sandbox/events/event-type-filter.tsx b/src/features/dashboard/sandbox/events/event-type-filter.tsx index 17e722f9f..6d19d4df4 100644 --- a/src/features/dashboard/sandbox/events/event-type-filter.tsx +++ b/src/features/dashboard/sandbox/events/event-type-filter.tsx @@ -7,59 +7,72 @@ import { import { Button } from '@/ui/primitives/button' import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' import { SandboxEventTypeBadge } from './event-type-badge' import { SANDBOX_EVENT_TYPE_MAP } from './event-type-map' -const ALL_EVENT_TYPES_VALUE = 'all' +const getTriggerLabel = (selected: SandboxLifecycleEventType[]) => { + if (selected.length === SandboxLifecycleEventTypeSchema.options.length) + return 'All' + if (selected.length === 0) return 'None' + const [first] = selected + if (selected.length === 1 && first) return SANDBOX_EVENT_TYPE_MAP[first].label + return `${selected.length}/${SandboxLifecycleEventTypeSchema.options.length}` +} interface EventTypeFilterProps { - type: SandboxLifecycleEventType | null - onTypeChange: (type: SandboxLifecycleEventType | null) => void + types: SandboxLifecycleEventType[] + onTypesChange: (types: SandboxLifecycleEventType[]) => void } export const EventTypeFilter = ({ - type, - onTypeChange, + types, + onTypesChange, }: EventTypeFilterProps) => { + const isAllSelected = + types.length === SandboxLifecycleEventTypeSchema.options.length + + const toggleType = (type: SandboxLifecycleEventType) => { + const next = types.includes(type) + ? types.filter((t) => t !== type) + : [...types, type] + onTypesChange(next) + } + + const toggleAll = (checked: boolean) => { + onTypesChange(checked ? [...SandboxLifecycleEventTypeSchema.options] : []) + } + return ( - { - if (value === ALL_EVENT_TYPES_VALUE) { - onTypeChange(null) - return - } - - const parsed = SandboxLifecycleEventTypeSchema.safeParse(value) - if (!parsed.success) { - onTypeChange(null) - return - } - - onTypeChange(parsed.data) - }} + e.preventDefault()} > - - All events - - {SandboxLifecycleEventTypeSchema.options.map((type) => ( - - - - ))} - + All events + + + {SandboxLifecycleEventTypeSchema.options.map((type) => ( + toggleType(type)} + onSelect={(e) => e.preventDefault()} + > + + + ))} ) diff --git a/src/features/dashboard/sandbox/events/filter-params.ts b/src/features/dashboard/sandbox/events/filter-params.ts index ad66ff2ff..511d4ff12 100644 --- a/src/features/dashboard/sandbox/events/filter-params.ts +++ b/src/features/dashboard/sandbox/events/filter-params.ts @@ -1,11 +1,32 @@ -import { parseAsString, parseAsStringEnum } from 'nuqs/server' +import { + createParser, + parseAsArrayOf, + parseAsStringEnum, +} from 'nuqs/server' +import { + SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX, + type SandboxLifecycleEventType, + SandboxLifecycleEventTypeSchema, +} from '@/core/modules/sandboxes/lifecycle-event-types' const SANDBOX_EVENTS_ORDER_VALUES: ['asc', 'desc'] = ['asc', 'desc'] type SandboxEventsOrder = (typeof SANDBOX_EVENTS_ORDER_VALUES)[number] +// Maps URL value to lifecycle event type, e.g. "created" -> "sandbox.lifecycle.created" +const eventTypeParser = createParser({ + parse: (value) => { + const result = SandboxLifecycleEventTypeSchema.safeParse( + `${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}${value}` + ) + return result.success ? result.data : null + }, + serialize: (value: SandboxLifecycleEventType) => + value.slice(SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX.length), +}) + const sandboxEventsFilterParams = { - type: parseAsString, + types: parseAsArrayOf(eventTypeParser), order: parseAsStringEnum(SANDBOX_EVENTS_ORDER_VALUES), } diff --git a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts index bd726e5f3..c405d57f2 100644 --- a/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts +++ b/src/features/dashboard/sandbox/events/use-sandbox-event-filters.ts @@ -1,7 +1,7 @@ 'use client' import { useQueryStates } from 'nuqs' -import { useCallback, useEffect, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { type SandboxLifecycleEventType, SandboxLifecycleEventTypeSchema, @@ -13,32 +13,22 @@ import { const DEFAULT_SANDBOX_EVENTS_ORDER: SandboxEventsOrder = 'desc' -// Strips the "sandbox.lifecycle." prefix for URL display, e.g. "sandbox.lifecycle.created" -> "created" -const toUrlValue = (type: SandboxLifecycleEventType) => - type.split('.').slice(2).join('.') - export const useSandboxEventFilters = () => { const [filters, setFilters] = useQueryStates(sandboxEventsFilterParams, { shallow: true, }) - const parsedType = useMemo(() => { - if (!filters.type) return null - - return SandboxLifecycleEventTypeSchema.safeParse( - `sandbox.lifecycle.${filters.type}` - ) - }, [filters.type]) - const type = parsedType?.success ? parsedType.data : null + const types = useMemo( + () => filters.types ?? [...SandboxLifecycleEventTypeSchema.options], + [filters.types] + ) const order = filters.order ?? DEFAULT_SANDBOX_EVENTS_ORDER - useEffect(() => { - if (parsedType && !parsedType.success) setFilters({ type: null }) - }, [parsedType, setFilters]) - - const setType = useCallback( - (type: SandboxLifecycleEventType | null) => { - setFilters({ type: type ? toUrlValue(type) : null }) + const setTypes = useCallback( + (next: SandboxLifecycleEventType[]) => { + const isAll = + next.length === SandboxLifecycleEventTypeSchema.options.length + setFilters({ types: isAll ? null : next }) }, [setFilters] ) @@ -56,7 +46,7 @@ export const useSandboxEventFilters = () => { order, orderAsc: order === 'asc', setOrder, - setType, - type, + setTypes, + types, } } diff --git a/src/features/dashboard/sandbox/events/view.tsx b/src/features/dashboard/sandbox/events/view.tsx index a14c90282..6b587122c 100644 --- a/src/features/dashboard/sandbox/events/view.tsx +++ b/src/features/dashboard/sandbox/events/view.tsx @@ -10,27 +10,29 @@ export const SandboxEventsView = () => { 'use no memo' const { sandboxLifecycle, isSandboxInfoLoading } = useSandboxContext() - const { order, orderAsc, setOrder, setType, type } = useSandboxEventFilters() + const { order, orderAsc, setOrder, setTypes, types } = + useSandboxEventFilters() const [scrollContainer, setScrollContainer] = useState( null ) const events = useMemo(() => { const lifecycleEvents = sandboxLifecycle?.events ?? [] - const filteredEvents = type - ? lifecycleEvents.filter((event) => event.type === type) - : lifecycleEvents + const typesSet = new Set(types) + const filteredEvents = lifecycleEvents.filter((event) => + typesSet.has(event.type) + ) // Sandbox lifecycle events are derived in ascending timestamp order. const orderedEvents = orderAsc ? filteredEvents : [...filteredEvents].reverse() return orderedEvents - }, [orderAsc, sandboxLifecycle?.events, type]) + }, [orderAsc, sandboxLifecycle?.events, types]) return (
- +
Date: Fri, 8 May 2026 16:48:10 -0400 Subject: [PATCH 39/41] refactor: enhance SandboxEventsTable layout and data display - Increased the minimum width of the table for better visibility and usability. - Adjusted column widths for ID, Event, and Data to improve layout consistency. - Added a new JsonPopover component to display event data, enhancing user interaction with event details. - Updated empty state handling to accommodate the new column structure. These changes aim to improve the overall user experience and maintainability of the SandboxEventsTable. --- .../dashboard/sandbox/events/table.tsx | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index a6551ff26..4282c07c0 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -14,6 +14,7 @@ import { import { IdBadge } from '@/features/dashboard/shared' import { formatLocalLogStyleTimestamp } from '@/lib/utils/formatting' import CopyButtonInline from '@/ui/copy-button-inline' +import { JsonPopover } from '@/ui/json-popover' import { Button } from '@/ui/primitives/button' import { ArrowDownIcon, HistoryIcon } from '@/ui/primitives/icons' import { @@ -48,7 +49,7 @@ export const SandboxEventsTable = ({ 'use no memo' return ( - +
- + ID - Event + + Event + + + Data + @@ -87,7 +93,7 @@ export const SandboxEventsTable = ({ /> ) : ( - + No events found @@ -162,6 +168,10 @@ const SandboxEventRow = ({ const formattedTimestamp = formatLocalLogStyleTimestamp(event.timestamp, { includeCentiseconds: true, }) + const eventDataValue = useMemo( + () => JSON.stringify(event.eventData ?? {}), + [event.eventData] + ) return ( - + {formattedTimestamp ? ( )} - + - + + + {!event.eventData || eventDataValue.trim() === '{}' ? ( + + n/a + + ) : ( + + {eventDataValue} + + )} + ) } From d8d3d111ef3c990e47c078742f7b1dbbb198a7fe Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Fri, 8 May 2026 17:01:06 -0400 Subject: [PATCH 40/41] refactor: streamline imports and adjust column widths in SandboxEventsTable - Consolidated import statements for cleaner code. - Adjusted column widths for improved layout consistency in the SandboxEventsTable. - Enhanced text alignment in the JsonPopover component for better readability. These changes aim to improve code maintainability and user experience in the event table. --- .../dashboard/sandbox/events/filter-params.ts | 6 +----- .../dashboard/sandbox/events/table.tsx | 18 +++++++++--------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/features/dashboard/sandbox/events/filter-params.ts b/src/features/dashboard/sandbox/events/filter-params.ts index 511d4ff12..5a825130b 100644 --- a/src/features/dashboard/sandbox/events/filter-params.ts +++ b/src/features/dashboard/sandbox/events/filter-params.ts @@ -1,8 +1,4 @@ -import { - createParser, - parseAsArrayOf, - parseAsStringEnum, -} from 'nuqs/server' +import { createParser, parseAsArrayOf, parseAsStringEnum } from 'nuqs/server' import { SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX, type SandboxLifecycleEventType, diff --git a/src/features/dashboard/sandbox/events/table.tsx b/src/features/dashboard/sandbox/events/table.tsx index 4282c07c0..1d8008465 100644 --- a/src/features/dashboard/sandbox/events/table.tsx +++ b/src/features/dashboard/sandbox/events/table.tsx @@ -53,7 +53,7 @@ export const SandboxEventsTable = ({ - + ID Event - - Data - + Data @@ -179,7 +177,7 @@ const SandboxEventRow = ({ virtualizer={virtualizer} height={ROW_HEIGHT_PX} > - + {formattedTimestamp ? ( )} - + @@ -211,10 +209,12 @@ const SandboxEventRow = ({ ) : ( - {eventDataValue} + + {eventDataValue} + )} From 87ab898d9822b4ec888b50ceceb01c50494978e2 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Sat, 9 May 2026 11:36:37 -0700 Subject: [PATCH 41/41] format --- src/features/dashboard/sandbox/layout.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/layout.tsx b/src/features/dashboard/sandbox/layout.tsx index 694d4305b..a2f2cc814 100644 --- a/src/features/dashboard/sandbox/layout.tsx +++ b/src/features/dashboard/sandbox/layout.tsx @@ -4,7 +4,12 @@ import { notFound } from 'next/navigation' import { PROTECTED_URLS } from '@/configs/urls' import { useRouteParams } from '@/lib/hooks/use-route-params' import { DashboardTabsList } from '@/ui/dashboard-tabs' -import { HistoryIcon, ListIcon, StorageIcon, TrendIcon } from '@/ui/primitives/icons' +import { + HistoryIcon, + ListIcon, + StorageIcon, + TrendIcon, +} from '@/ui/primitives/icons' import { useSandboxContext } from './context' interface SandboxLayoutProps {