Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions spec/openapi.dashboard-api.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/core/modules/sandboxes/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ interface SandboxDetailsBaseModel {
cpuCount: number
memoryMB: number
diskSizeMB: number
// True when the sandbox ended more than the retention window ago, so its
// monitoring, events, and logs data is no longer available.
retentionExpired: boolean
lifecycle?: SandboxLifecycleModel
}

Expand Down Expand Up @@ -169,6 +172,8 @@ export function mapInfraSandboxDetailsToModel(
diskSizeMB: sandbox.diskSizeMB,
metadata: sandbox.metadata,
state: sandbox.state,
// Infra only serves live sandboxes, whose data is never past retention.
retentionExpired: false,
}
}

Expand All @@ -189,5 +194,6 @@ export function mapApiSandboxRecordToModel(
memoryMB: sandbox.memoryMB,
diskSizeMB: sandbox.diskSizeMB,
state: 'killed',
retentionExpired: sandbox.retentionExpired,
}
}
23 changes: 22 additions & 1 deletion src/core/server/api/routers/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ import { SandboxIdSchema } from '@/core/shared/schemas/api'
import { SANDBOX_MONITORING_METRICS_RETENTION_MS } from '@/features/dashboard/sandbox/monitoring/utils/constants'
import { TERMINAL_SANDBOX_TIMEOUT_MS } from '@/features/dashboard/terminal/constants'

function isPastRetention(timestamp: string | null): boolean {
if (!timestamp) {
return false
}
const timestampMs = new Date(timestamp).getTime()
if (Number.isNaN(timestampMs)) {
return false
}
return Date.now() - timestampMs > SANDBOX_MONITORING_METRICS_RETENTION_MS
}

const sandboxRepositoryProcedure = protectedTeamProcedure.use(
withTeamAuthedRequestRepository(
createSandboxesRepository,
Expand Down Expand Up @@ -67,11 +78,21 @@ export const sandboxRouter = createTRPCRouter({
? (mappedDetails.stoppedAt ?? mappedDetails.endAt)
: null

const pausedAt = derivedLifecycle.pausedAt ?? fallbackPausedAt

// A paused sandbox keeps no fresh data, so once it has been paused longer
// than the retention window its monitoring/events/logs are gone too. The
// killed case is already flagged by the backend (mappedDetails).
const retentionExpired =
mappedDetails.retentionExpired ||
(mappedDetails.state === 'paused' && isPastRetention(pausedAt))

return {
...mappedDetails,
retentionExpired,
lifecycle: {
createdAt: derivedLifecycle.createdAt ?? mappedDetails.startedAt,
pausedAt: derivedLifecycle.pausedAt ?? fallbackPausedAt,
pausedAt,
endedAt: derivedLifecycle.endedAt ?? fallbackEndedAt,
events: derivedLifecycle.events,
},
Expand Down
2 changes: 2 additions & 0 deletions src/core/shared/contracts/dashboard-api.types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LOG_RETENTION_MS } from '@/configs/logs'
import { ExpiryIcon } from '@/ui/primitives/icons'

const RETENTION_DAYS = LOG_RETENTION_MS / 24 / 60 / 60 / 1000

export function DataRetentionExpired() {
return (
<div className="flex h-full min-h-[35vh] w-full flex-col items-center justify-center gap-2 p-6 text-center">
<div className="flex items-center gap-2">
<ExpiryIcon className="size-5" />
<p className="prose-body-highlight">Data retention is over</p>
</div>
<p className="text-fg-tertiary text-sm">
This sandbox ended more than {RETENTION_DAYS} days ago, so its
monitoring, events, and logs data is no longer available.
</p>
</div>
)
}
8 changes: 7 additions & 1 deletion src/features/dashboard/sandbox/events/view.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useMemo, useState } from 'react'
import { DataRetentionExpired } from '@/features/dashboard/sandbox/common/data-retention-expired'
import { useSandboxContext } from '../context'
import { EventTypeFilter } from './event-type-filter'
import { SandboxEventsTable } from './table'
Expand All @@ -9,7 +10,8 @@ import { useSandboxEventFilters } from './use-sandbox-event-filters'
export const SandboxEventsView = () => {
'use no memo'

const { sandboxLifecycle, isSandboxInfoLoading } = useSandboxContext()
const { sandboxInfo, sandboxLifecycle, isSandboxInfoLoading } =
useSandboxContext()
const { order, orderAsc, setOrder, setTypes, types } =
useSandboxEventFilters()
const [scrollContainer, setScrollContainer] = useState<HTMLDivElement | null>(
Expand All @@ -30,6 +32,10 @@ export const SandboxEventsView = () => {
return orderedEvents
}, [orderAsc, sandboxLifecycle?.events, types])

if (sandboxInfo?.retentionExpired) {
return <DataRetentionExpired />
}

return (
<div className="flex min-h-0 flex-1 flex-col gap-3 md:gap-6 overflow-hidden p-3 md:p-6">
<EventTypeFilter types={types} onTypesChange={setTypes} />
Expand Down
40 changes: 9 additions & 31 deletions src/features/dashboard/sandbox/logs/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
useRef,
useState,
} from 'react'
import { LOG_RETENTION_MS } from '@/configs/logs'
import type { SandboxLogModel } from '@/core/modules/sandboxes/models'
import {
LOG_LEVEL_LEFT_BORDER_CLASS,
Expand All @@ -28,6 +27,7 @@ import {
VirtualizedTableLoaderBody,
VirtualizedTableRow,
} from '@/features/dashboard/common/virtualized-table-ui'
import { DataRetentionExpired } from '@/features/dashboard/sandbox/common/data-retention-expired'
import { cn } from '@/lib/utils'
import { DebouncedInput } from '@/ui/primitives/input'
import { Loader } from '@/ui/primitives/loader'
Expand All @@ -44,29 +44,22 @@ const ROW_HEIGHT_PX = 26
const LIVE_STATUS_ROW_HEIGHT_PX = ROW_HEIGHT_PX + 16
const VIRTUAL_OVERSCAN = 16
const SCROLL_LOAD_THRESHOLD_PX = 200
const LOG_RETENTION_DAYS = LOG_RETENTION_MS / 24 / 60 / 60 / 1000

interface LogsProps {
teamSlug: string
sandboxId: string
}

function checkIfSandboxStillHasLogs(startedAtIso: string) {
const startedAtUnix = new Date(startedAtIso).getTime()

if (Number.isNaN(startedAtUnix)) {
return true
}

return Date.now() - startedAtUnix < LOG_RETENTION_MS
}

export default function SandboxLogs({ teamSlug, sandboxId }: LogsProps) {
'use no memo'

const { sandboxInfo, sandboxLifecycle, isRunning } = useSandboxContext()
const { sandboxInfo, isRunning } = useSandboxContext()
const { level, setLevel, search, setSearch } = useLogFilters()

if (sandboxInfo?.retentionExpired) {
return <DataRetentionExpired />
}

if (!sandboxInfo) {
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden relative gap-3 md:gap-6">
Expand All @@ -90,16 +83,11 @@ export default function SandboxLogs({ teamSlug, sandboxId }: LogsProps) {
)
}

const hasRetainedLogs = checkIfSandboxStillHasLogs(
sandboxLifecycle?.createdAt ?? sandboxInfo.startedAt
)

return (
<LogsContent
teamSlug={teamSlug}
sandboxId={sandboxId}
isRunning={isRunning}
hasRetainedLogs={hasRetainedLogs}
level={level}
search={search}
setLevel={setLevel}
Expand All @@ -112,7 +100,6 @@ interface LogsContentProps {
teamSlug: string
sandboxId: string
isRunning: boolean
hasRetainedLogs: boolean
level: SandboxLogLevelFilter | null
search: string
setLevel: (level: SandboxLogLevelFilter | null) => void
Expand All @@ -123,7 +110,6 @@ function LogsContent({
teamSlug,
sandboxId,
isRunning,
hasRetainedLogs,
level,
search,
setLevel,
Expand Down Expand Up @@ -207,12 +193,7 @@ function LogsContent({
/>

{showLoader && <VirtualizedTableLoaderBody />}
{showEmpty && (
<EmptyBody
hasRetainedLogs={hasRetainedLogs}
errorMessage={initialLoadError}
/>
)}
{showEmpty && <EmptyBody errorMessage={initialLoadError} />}
{hasLogs && scrollContainerElement && (
<VirtualizedLogsBody
logs={renderedLogs}
Expand Down Expand Up @@ -266,16 +247,13 @@ function useFilterRefetchTracking(
}

interface EmptyBodyProps {
hasRetainedLogs: boolean
errorMessage: string | null
}

function EmptyBody({ hasRetainedLogs, errorMessage }: EmptyBodyProps) {
function EmptyBody({ errorMessage }: EmptyBodyProps) {
const description = errorMessage
? errorMessage
: !hasRetainedLogs
? `This sandbox has exceeded the ${LOG_RETENTION_DAYS} day retention limit.`
: 'Sandbox logs will appear here once available.'
: 'Sandbox logs will appear here once available.'

return <LogsEmptyBody description={description} />
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import LoadingLayout from '@/features/dashboard/loading-layout'
import { DataRetentionExpired } from '@/features/dashboard/sandbox/common/data-retention-expired'
import { useSandboxContext } from '@/features/dashboard/sandbox/context'
import SandboxMetricsCharts from './monitoring-charts'

Expand All @@ -17,6 +18,10 @@ export default function SandboxMonitoringView({
return <LoadingLayout />
}

if (sandboxInfo?.retentionExpired) {
return <DataRetentionExpired />
}

return (
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<SandboxMetricsCharts sandboxId={sandboxId} />
Expand Down
Loading