Skip to content

Commit 165c2f0

Browse files
committed
refactor: sandbox details header + sidebar is active state determination
1 parent 8ae77da commit 165c2f0

8 files changed

Lines changed: 197 additions & 46 deletions

File tree

src/app/dashboard/[teamIdOrSlug]/sandboxes/[sandboxId]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SandboxProvider } from '@/features/dashboard/sandbox/context'
2-
import SandboxDetailsHeader from '@/features/dashboard/sandbox/header'
2+
import SandboxDetailsHeader from '@/features/dashboard/sandbox/header/header'
33
import SandboxDetailsTabs from '@/features/dashboard/sandbox/tabs'
44
import { resolveTeamIdInServerComponent } from '@/lib/utils/server'
55
import { getSandboxDetails } from '@/server/sandboxes/get-sandbox-details'

src/features/dashboard/sandbox/header.tsx

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { SandboxInfo } from '@/types/api'
2+
3+
interface CreatedAtProps {
4+
startedAt: SandboxInfo['startedAt']
5+
}
6+
7+
export default function CreatedAt({ startedAt }: CreatedAtProps) {
8+
return <p>{new Date(startedAt).toLocaleString()}</p>
9+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { PROTECTED_URLS } from '@/configs/urls'
2+
import { SandboxInfo } from '@/types/api'
3+
import { Label } from '@/ui/primitives/label'
4+
import { ChevronLeftIcon, Dot } from 'lucide-react'
5+
import Link from 'next/link'
6+
import RanFor from './ran-for'
7+
import Status from './status'
8+
import Resource from './resource'
9+
import CreatedAt from './created_at'
10+
11+
interface SandboxDetailsHeaderProps {
12+
teamIdOrSlug: string
13+
sandboxInfo: SandboxInfo
14+
}
15+
16+
export default function SandboxDetailsHeader({
17+
teamIdOrSlug,
18+
sandboxInfo,
19+
}: SandboxDetailsHeaderProps) {
20+
const headerItems = {
21+
state: {
22+
label: 'status',
23+
value: <Status state={sandboxInfo.state} />,
24+
},
25+
templateID: {
26+
label: 'template id',
27+
value: sandboxInfo.templateID?.toString(),
28+
},
29+
startedAt: {
30+
label: 'created at',
31+
value: <CreatedAt startedAt={sandboxInfo.startedAt} />,
32+
},
33+
endAt: {
34+
label: sandboxInfo.state === 'running' ? 'running since' : 'ran for',
35+
value: (
36+
<RanFor
37+
state={sandboxInfo.state}
38+
startedAt={sandboxInfo.startedAt}
39+
endAt={sandboxInfo.endAt}
40+
/>
41+
),
42+
},
43+
memoryMB: {
44+
label: 'mem',
45+
value: <Resource type="mem" value={sandboxInfo.memoryMB?.toString()} />,
46+
},
47+
cpuCount: {
48+
label: 'cpu',
49+
value: <Resource type="cpu" value={sandboxInfo.cpuCount?.toString()} />,
50+
},
51+
}
52+
53+
return (
54+
<header className="flex w-full flex-col gap-16 p-8">
55+
<div className="flex flex-col gap-1">
56+
<Link
57+
href={PROTECTED_URLS.SANDBOXES(teamIdOrSlug)}
58+
className="text-fg-300 hover:text-fg flex items-center gap-1.5 transition-colors"
59+
prefetch
60+
shallow
61+
>
62+
<ChevronLeftIcon className="size-5" />
63+
Sandboxes
64+
</Link>
65+
<h1 className="text-fg-500 text-2xl font-bold">
66+
<span className="text-fg">{sandboxInfo.sandboxID}</span>'S DETAILS
67+
</h1>
68+
</div>
69+
<div className="flex flex-wrap items-center gap-7">
70+
{Object.entries(headerItems).map(([key, { label, value }]) => (
71+
<HeaderItem key={key} label={label} value={value} />
72+
))}
73+
</div>
74+
</header>
75+
)
76+
}
77+
78+
interface HeaderItemProps {
79+
label: string
80+
value: string | React.ReactNode
81+
}
82+
83+
function HeaderItem({ label, value }: HeaderItemProps) {
84+
return (
85+
<div className="flex flex-col gap-2">
86+
<Label className="text-fg-500 text-xs uppercase">{label}</Label>
87+
{typeof value === 'string' ? <p className="text-sm">{value}</p> : value}
88+
</div>
89+
)
90+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client'
2+
3+
import { SandboxInfo } from '@/types/api'
4+
import { useState, useEffect } from 'react'
5+
6+
interface RanForProps {
7+
state?: SandboxInfo['state']
8+
startedAt: SandboxInfo['startedAt']
9+
endAt?: SandboxInfo['endAt']
10+
}
11+
12+
export default function RanFor({ state, startedAt, endAt }: RanForProps) {
13+
const [ranFor, setRanFor] = useState<string>('-')
14+
15+
useEffect(() => {
16+
function calcRanFor() {
17+
if (!startedAt) return '-'
18+
19+
const start = new Date(startedAt)
20+
const end =
21+
state === 'running' ? new Date() : endAt ? new Date(endAt) : new Date()
22+
const diffMs = end.getTime() - start.getTime()
23+
if (diffMs < 0) return '-'
24+
25+
const hours = Math.floor(diffMs / (1000 * 60 * 60))
26+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
27+
return `${hours} hours ${minutes} minutes`
28+
}
29+
30+
setRanFor((prev) => {
31+
const next = calcRanFor()
32+
return prev !== next ? next : prev
33+
})
34+
35+
const interval = setInterval(() => {
36+
setRanFor((prev) => {
37+
const next = calcRanFor()
38+
return prev !== next ? next : prev
39+
})
40+
}, 5000)
41+
42+
return () => clearInterval(interval)
43+
}, [state, startedAt, endAt])
44+
45+
return <p>{ranFor}</p>
46+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
interface ResourceProps {
2+
type: 'mem' | 'cpu'
3+
value: string
4+
}
5+
6+
export default function Resource({ type, value }: ResourceProps) {
7+
const label = type === 'mem' ? 'MB' : 'Core'
8+
9+
return (
10+
<p>
11+
{value} {label}
12+
</p>
13+
)
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { SandboxInfo } from '@/types/api'
2+
import { Badge } from '@/ui/primitives/badge'
3+
4+
interface StatusProps {
5+
state: SandboxInfo['state']
6+
}
7+
8+
export default function Status({ state }: StatusProps) {
9+
return (
10+
<Badge
11+
variant={state === 'running' ? 'success' : 'error'}
12+
className="gap-1 uppercase"
13+
>
14+
<span className="line-height-0 h-0 w-2.5 -translate-y-0.25 align-middle text-xl leading-0">
15+
16+
</span>
17+
{state}
18+
</Badge>
19+
)
20+
}

src/features/dashboard/sidebar/content.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,23 @@ export default function DashboardSidebarContent() {
4444
)
4545

4646
const isActive = (href: string) => {
47-
return href === pathname
47+
if (!pathname) return false
48+
49+
if (pathname === href) return true
50+
51+
// split into segments for prefix comparison
52+
const hrefSegments = href.split('/').filter(Boolean)
53+
const pathSegments = pathname.split('/').filter(Boolean)
54+
55+
if (pathSegments.length < hrefSegments.length) return false
56+
57+
for (let i = 0; i < hrefSegments.length; i++) {
58+
if (hrefSegments[i] !== pathSegments[i]) {
59+
return false
60+
}
61+
}
62+
63+
return true
4864
}
4965

5066
return (

0 commit comments

Comments
 (0)