Skip to content

Commit 5f688d3

Browse files
Feat: Sandbox details events table (#300)
Co-authored-by: Ben Fornefeld <50748440+ben-fornefeld@users.noreply.github.com> Co-authored-by: ben-fornefeld <ben.fornefeld@gmail.com>
1 parent d2c1df0 commit 5f688d3

26 files changed

Lines changed: 713 additions & 177 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { SandboxEventsView } from '@/features/dashboard/sandbox/events'
2+
3+
export default function SandboxEventsPage() {
4+
return <SandboxEventsView />
5+
}

src/configs/urls.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const PROTECTED_URLS = {
3131
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/monitoring`,
3232
SANDBOX_MONITORING: (teamSlug: string, sandboxId: string) =>
3333
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/monitoring`,
34+
SANDBOX_EVENTS: (teamSlug: string, sandboxId: string) =>
35+
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/events`,
3436
SANDBOX_LOGS: (teamSlug: string, sandboxId: string) =>
3537
`/dashboard/${teamSlug}/sandboxes/${sandboxId}/logs`,
3638
SANDBOX_FILESYSTEM: (teamSlug: string, sandboxId: string) =>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { z } from 'zod'
2+
3+
const SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX = 'sandbox.lifecycle.'
4+
5+
const SandboxLifecycleEventTypeSchema = z.enum([
6+
`${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}created`,
7+
`${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}updated`,
8+
`${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}paused`,
9+
`${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}resumed`,
10+
`${SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX}killed`,
11+
])
12+
13+
type SandboxLifecycleEventType = z.infer<typeof SandboxLifecycleEventTypeSchema>
14+
15+
export {
16+
SANDBOX_LIFECYCLE_EVENT_TYPE_PREFIX,
17+
SandboxLifecycleEventTypeSchema,
18+
type SandboxLifecycleEventType,
19+
}

src/features/dashboard/build/logs.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import { LogLevelFilter } from '@/features/dashboard/common/log-level-filter'
1818
import {
1919
LogStatusCell,
2020
LogsEmptyBody,
21-
LogsLoaderBody,
2221
LogsTableHeader,
23-
LogVirtualRow,
2422
} from '@/features/dashboard/common/log-viewer-ui'
23+
import {
24+
VirtualizedTableLoaderBody,
25+
VirtualizedTableRow,
26+
} from '@/features/dashboard/common/virtualized-table-ui'
2527
import { cn } from '@/lib/utils'
2628
import { Loader } from '@/ui/primitives/loader'
2729
import { Table, TableBody, TableCell } from '@/ui/primitives/table'
@@ -70,7 +72,7 @@ export default function Logs({
7072
levelWidth={COLUMN_WIDTHS_PX.level}
7173
timestampSortDirection="asc"
7274
/>
73-
<LogsLoaderBody />
75+
<VirtualizedTableLoaderBody />
7476
</Table>
7577
</div>
7678
</div>
@@ -172,7 +174,7 @@ function LogsContent({
172174
timestampSortDirection="asc"
173175
/>
174176

175-
{showLoader && <LogsLoaderBody />}
177+
{showLoader && <VirtualizedTableLoaderBody />}
176178
{showEmpty && (
177179
<EmptyBody hasRetainedLogs={buildDetails.hasRetainedLogs} />
178180
)}
@@ -514,7 +516,7 @@ function LogRow({
514516
const millisAfterStart = log.timestampUnix - startedAt
515517

516518
return (
517-
<LogVirtualRow
519+
<VirtualizedTableRow
518520
virtualRow={virtualRow}
519521
virtualizer={virtualizer}
520522
height={ROW_HEIGHT_PX}
@@ -551,7 +553,7 @@ function LogRow({
551553
>
552554
<Message message={log.message} />
553555
</TableCell>
554-
</LogVirtualRow>
556+
</VirtualizedTableRow>
555557
)
556558
}
557559

@@ -567,7 +569,7 @@ function StatusRow({
567569
isFetchingNextPage,
568570
}: StatusRowProps) {
569571
return (
570-
<LogVirtualRow
572+
<VirtualizedTableRow
571573
virtualRow={virtualRow}
572574
virtualizer={virtualizer}
573575
height={ROW_HEIGHT_PX}
@@ -587,7 +589,7 @@ function StatusRow({
587589
)}
588590
</span>
589591
</LogStatusCell>
590-
</LogVirtualRow>
592+
</VirtualizedTableRow>
591593
)
592594
}
593595

@@ -603,7 +605,7 @@ function LiveStatusRow({
603605
isBuilding,
604606
}: LiveStatusRowProps) {
605607
return (
606-
<LogVirtualRow
608+
<VirtualizedTableRow
607609
virtualRow={virtualRow}
608610
virtualizer={virtualizer}
609611
height={LIVE_STATUS_ROW_HEIGHT_PX}
@@ -628,6 +630,6 @@ function LiveStatusRow({
628630
</span>
629631
</span>
630632
</LogStatusCell>
631-
</LogVirtualRow>
633+
</VirtualizedTableRow>
632634
)
633635
}

src/features/dashboard/common/log-viewer-ui.tsx

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
21
import type { CSSProperties, ReactNode } from 'react'
32
import { cn } from '@/lib/utils'
43
import { ArrowDownIcon, ListIcon } from '@/ui/primitives/icons'
5-
import { Loader } from '@/ui/primitives/loader'
64
import {
75
TableBody,
86
TableCell,
@@ -57,20 +55,6 @@ export function LogsTableHeader({
5755
)
5856
}
5957

60-
export function LogsLoaderBody() {
61-
return (
62-
<TableBody style={{ display: 'grid' }}>
63-
<TableRow style={{ display: 'flex', minWidth: '100%', marginTop: 8 }}>
64-
<TableCell className="flex-1">
65-
<div className="h-[35svh] w-full flex justify-center items-center">
66-
<Loader variant="slash" size="lg" />
67-
</div>
68-
</TableCell>
69-
</TableRow>
70-
</TableBody>
71-
)
72-
}
73-
7458
interface LogsEmptyBodyProps {
7559
description?: ReactNode
7660
}
@@ -95,47 +79,6 @@ export function LogsEmptyBody({ description }: LogsEmptyBodyProps) {
9579
)
9680
}
9781

98-
export function getLogVirtualRowStyle(
99-
virtualRow: VirtualItem,
100-
height: number
101-
): CSSProperties {
102-
return {
103-
display: 'flex',
104-
position: 'absolute',
105-
left: 0,
106-
transform: `translateY(${virtualRow.start}px)`,
107-
minWidth: '100%',
108-
height,
109-
}
110-
}
111-
112-
interface LogVirtualRowProps {
113-
virtualRow: VirtualItem
114-
virtualizer: Virtualizer<HTMLDivElement, Element>
115-
height: number
116-
className?: string
117-
children: ReactNode
118-
}
119-
120-
export function LogVirtualRow({
121-
virtualRow,
122-
virtualizer,
123-
height,
124-
className,
125-
children,
126-
}: LogVirtualRowProps) {
127-
return (
128-
<TableRow
129-
data-index={virtualRow.index}
130-
ref={(node) => virtualizer.measureElement(node)}
131-
className={className}
132-
style={getLogVirtualRowStyle(virtualRow, height)}
133-
>
134-
{children}
135-
</TableRow>
136-
)
137-
}
138-
13982
const STATUS_ROW_CELL_STYLE: CSSProperties = {
14083
display: 'flex',
14184
alignItems: 'center',
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
2+
import type { CSSProperties, ReactNode } from 'react'
3+
import { Loader } from '@/ui/primitives/loader'
4+
import { TableBody, TableCell, TableRow } from '@/ui/primitives/table'
5+
6+
export const getVirtualizedRowStyle = (
7+
virtualRow: VirtualItem,
8+
height: number
9+
): CSSProperties => ({
10+
display: 'flex',
11+
position: 'absolute',
12+
left: 0,
13+
transform: `translateY(${virtualRow.start}px)`,
14+
minWidth: '100%',
15+
height,
16+
})
17+
18+
interface VirtualizedTableRowProps {
19+
virtualRow: VirtualItem
20+
virtualizer: Virtualizer<HTMLDivElement, Element>
21+
height: number
22+
className?: string
23+
children: ReactNode
24+
}
25+
26+
export const VirtualizedTableRow = ({
27+
virtualRow,
28+
virtualizer,
29+
height,
30+
className,
31+
children,
32+
}: VirtualizedTableRowProps) => (
33+
<TableRow
34+
data-index={virtualRow.index}
35+
ref={(node) => virtualizer.measureElement(node)}
36+
className={className}
37+
style={getVirtualizedRowStyle(virtualRow, height)}
38+
>
39+
{children}
40+
</TableRow>
41+
)
42+
43+
export const VirtualizedTableLoaderBody = () => (
44+
<TableBody className="grid">
45+
<TableRow className="flex min-w-full mt-2">
46+
<TableCell className="flex-1">
47+
<div className="h-[35svh] w-full flex justify-center items-center">
48+
<Loader variant="slash" size="lg" />
49+
</div>
50+
</TableCell>
51+
</TableRow>
52+
</TableBody>
53+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { SandboxLifecycleEventTypeSchema } from '@/core/modules/sandboxes/lifecycle-event-types'
2+
import { Badge } from '@/ui/primitives/badge'
3+
import { SANDBOX_EVENT_TYPE_MAP } from './event-type-map'
4+
5+
export const SandboxEventTypeBadge = ({ type }: { type: string }) => {
6+
const parsed = SandboxLifecycleEventTypeSchema.safeParse(type)
7+
8+
if (!parsed.success) {
9+
return (
10+
<Badge variant="default" size="sm" className="align-middle uppercase">
11+
{type}
12+
</Badge>
13+
)
14+
}
15+
16+
const { icon: IconComponent, label } = SANDBOX_EVENT_TYPE_MAP[parsed.data]
17+
18+
return (
19+
<Badge variant="default" size="sm" className="align-middle uppercase">
20+
<IconComponent />
21+
{label}
22+
</Badge>
23+
)
24+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client'
2+
3+
import {
4+
type SandboxLifecycleEventType,
5+
SandboxLifecycleEventTypeSchema,
6+
} from '@/core/modules/sandboxes/lifecycle-event-types'
7+
import { Button } from '@/ui/primitives/button'
8+
import {
9+
DropdownMenu,
10+
DropdownMenuCheckboxItem,
11+
DropdownMenuContent,
12+
DropdownMenuSeparator,
13+
DropdownMenuTrigger,
14+
} from '@/ui/primitives/dropdown-menu'
15+
import { SandboxEventTypeBadge } from './event-type-badge'
16+
import { SANDBOX_EVENT_TYPE_MAP } from './event-type-map'
17+
18+
const getTriggerLabel = (selected: SandboxLifecycleEventType[]) => {
19+
if (selected.length === SandboxLifecycleEventTypeSchema.options.length)
20+
return 'All'
21+
if (selected.length === 0) return 'None'
22+
const [first] = selected
23+
if (selected.length === 1 && first) return SANDBOX_EVENT_TYPE_MAP[first].label
24+
return `${selected.length}/${SandboxLifecycleEventTypeSchema.options.length}`
25+
}
26+
27+
interface EventTypeFilterProps {
28+
types: SandboxLifecycleEventType[]
29+
onTypesChange: (types: SandboxLifecycleEventType[]) => void
30+
}
31+
32+
export const EventTypeFilter = ({
33+
types,
34+
onTypesChange,
35+
}: EventTypeFilterProps) => {
36+
const isAllSelected =
37+
types.length === SandboxLifecycleEventTypeSchema.options.length
38+
39+
const toggleType = (type: SandboxLifecycleEventType) => {
40+
const next = types.includes(type)
41+
? types.filter((t) => t !== type)
42+
: [...types, type]
43+
onTypesChange(next)
44+
}
45+
46+
const toggleAll = (checked: boolean) => {
47+
onTypesChange(checked ? [...SandboxLifecycleEventTypeSchema.options] : [])
48+
}
49+
50+
return (
51+
<DropdownMenu>
52+
<DropdownMenuTrigger asChild>
53+
<Button variant="secondary" className="font-sans w-min normal-case">
54+
Events · {getTriggerLabel(types)}
55+
</Button>
56+
</DropdownMenuTrigger>
57+
<DropdownMenuContent align="start">
58+
<DropdownMenuCheckboxItem
59+
checked={isAllSelected}
60+
onCheckedChange={toggleAll}
61+
onSelect={(e) => e.preventDefault()}
62+
>
63+
All events
64+
</DropdownMenuCheckboxItem>
65+
<DropdownMenuSeparator />
66+
{SandboxLifecycleEventTypeSchema.options.map((type) => (
67+
<DropdownMenuCheckboxItem
68+
key={type}
69+
checked={types.includes(type)}
70+
onCheckedChange={() => toggleType(type)}
71+
onSelect={(e) => e.preventDefault()}
72+
>
73+
<SandboxEventTypeBadge type={type} />
74+
</DropdownMenuCheckboxItem>
75+
))}
76+
</DropdownMenuContent>
77+
</DropdownMenu>
78+
)
79+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { SandboxLifecycleEventType } from '@/core/modules/sandboxes/lifecycle-event-types'
2+
import {
3+
BlockIcon,
4+
CheckIcon,
5+
type Icon,
6+
PausedIcon,
7+
RefreshIcon,
8+
RunningIcon,
9+
} from '@/ui/primitives/icons'
10+
11+
const SANDBOX_EVENT_TYPE_MAP: Record<
12+
SandboxLifecycleEventType,
13+
{ icon: Icon; label: string }
14+
> = {
15+
'sandbox.lifecycle.created': { icon: CheckIcon, label: 'Created' },
16+
'sandbox.lifecycle.updated': { icon: RefreshIcon, label: 'Updated' },
17+
'sandbox.lifecycle.paused': { icon: PausedIcon, label: 'Paused' },
18+
'sandbox.lifecycle.resumed': { icon: RunningIcon, label: 'Resumed' },
19+
'sandbox.lifecycle.killed': { icon: BlockIcon, label: 'Killed' },
20+
}
21+
22+
export { SANDBOX_EVENT_TYPE_MAP }

0 commit comments

Comments
 (0)