Skip to content

Commit

Permalink
added sandbox and templates overview to dashboard (#468)
Browse files Browse the repository at this point in the history
  • Loading branch information
mishushakov authored Nov 7, 2024
2 parents f26ec10 + e5f29e6 commit 592deaf
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 65 deletions.
142 changes: 115 additions & 27 deletions apps/web/src/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
'use client'

import { Suspense, useEffect, useState } from 'react'
import { BarChart, CreditCard, Key, LucideIcon, Settings, Users } from 'lucide-react'
import {
BarChart,
CreditCard,
FileText,
Key,
LucideIcon,
PackageIcon,
Settings,
Users,
} from 'lucide-react'

import { BillingContent } from '@/components/Dashboard/Billing'
import { TeamContent } from '@/components/Dashboard/Team'
Expand All @@ -12,6 +21,8 @@ import { UsageContent } from '@/components/Dashboard/Usage'
import { AccountSelector } from '@/components/Dashboard/AccountSelector'
import { useRouter, useSearchParams } from 'next/navigation'
import { PersonalContent } from '@/components/Dashboard/Personal'
import { TemplatesContent } from '@/components/Dashboard/Templates'
import { SandboxesContent } from '@/components/Dashboard/Sandboxes'

function redirectToCurrentURL() {
const url = typeof window !== 'undefined' ? window.location.href : undefined
Expand All @@ -24,21 +35,30 @@ function redirectToCurrentURL() {
return `redirect_to=${encodedURL}`
}

const menuLabels = ['personal', 'keys', 'usage', 'billing', 'team',] as const
type MenuLabel = typeof menuLabels[number]
const menuLabels = [
'personal',
'keys',
'sandboxes',
'templates',
'usage',
'billing',
'team',
] as const
type MenuLabel = (typeof menuLabels)[number]

export default function Page() {
const { user, isLoading, error } = useUser()
const router = useRouter()

useEffect(() => {
if (isLoading) { return }
if (isLoading) {
return
}
if (!user) {
router.push(`/auth/sign-in?${redirectToCurrentURL()}`)
}
}, [isLoading, user, router])


if (error) {
return <div>Error: {error.message}</div>
}
Expand All @@ -62,7 +82,10 @@ const Dashboard = ({ user }) => {
const [teams, setTeams] = useState<Team[]>([])
const [currentTeam, setCurrentTeam] = useState<Team | null>(null)

const initialTab = tab && menuLabels.includes(tab as MenuLabel) ? (tab as MenuLabel) : 'personal'
const initialTab =
tab && menuLabels.includes(tab as MenuLabel)
? (tab as MenuLabel)
: 'personal'
const [selectedItem, setSelectedItem] = useState<MenuLabel>(initialTab)

const router = useRouter()
Expand All @@ -83,7 +106,6 @@ const Dashboard = ({ user }) => {
}
}, [user, teamParam, setCurrentTeam, setTeams])


useEffect(() => {
if (tab !== selectedItem) {
const params = new URLSearchParams(window.location.search)
Expand All @@ -109,22 +131,51 @@ const Dashboard = ({ user }) => {
if (currentTeam) {
return (
<>
<Sidebar selectedItem={selectedItem} setSelectedItem={setSelectedItem} teams={teams} user={user} currentTeam={currentTeam} setCurrentTeam={setCurrentTeam} setTeams={setTeams} />
<Sidebar
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
teams={teams}
user={user}
currentTeam={currentTeam}
setCurrentTeam={setCurrentTeam}
setTeams={setTeams}
/>
<div className="flex-1 md:pl-10">
<h2 className='text-2xl mb-2 font-bold'>{selectedItem[0].toUpperCase() + selectedItem.slice(1)}</h2>
<div className='border border-white/5 w-full h-[1px] mb-10' />
<MainContent selectedItem={selectedItem} user={user} team={currentTeam} teams={teams} setTeams={setTeams} setCurrentTeam={setCurrentTeam} />
<h2 className="text-2xl mb-2 font-bold">
{selectedItem[0].toUpperCase() + selectedItem.slice(1)}
</h2>
<div className="border border-white/5 w-full h-[1px] mb-10" />
<MainContent
selectedItem={selectedItem}
user={user}
team={currentTeam}
teams={teams}
setTeams={setTeams}
setCurrentTeam={setCurrentTeam}
/>
</div>
</>
)
}
}


const Sidebar = ({ selectedItem, setSelectedItem, teams, user, currentTeam, setCurrentTeam, setTeams }) => (
const Sidebar = ({
selectedItem,
setSelectedItem,
teams,
user,
currentTeam,
setCurrentTeam,
setTeams,
}) => (
<div className="md:h-full md:w-48 space-y-2 pb-10 md:pb-0">

<AccountSelector teams={teams} user={user} currentTeam={currentTeam} setCurrentTeam={setCurrentTeam} setTeams={setTeams} />
<AccountSelector
teams={teams}
user={user}
currentTeam={currentTeam}
setCurrentTeam={setCurrentTeam}
setTeams={setTeams}
/>

<div className="flex flex-row justify-center space-x-4 md:space-x-0 md:space-y-2 md:flex-col">
{menuLabels.map((label) => (
Expand All @@ -137,7 +188,6 @@ const Sidebar = ({ selectedItem, setSelectedItem, teams, user, currentTeam, setC
/>
))}
</div>

</div>
)

Expand All @@ -147,40 +197,78 @@ const iconMap: { [key in MenuLabel]: LucideIcon } = {
usage: BarChart,
billing: CreditCard,
team: Users,
templates: FileText,
sandboxes: PackageIcon,
}

const MenuItem = ({ icon: Icon, label, selected, onClick }: { icon: LucideIcon; label: MenuLabel; selected: boolean; onClick: () => void }) => (
const MenuItem = ({
icon: Icon,
label,
selected,
onClick,
}: {
icon: LucideIcon
label: MenuLabel
selected: boolean
onClick: () => void
}) => (
<div
className={`flex w-fit md:w-full hover:bg-[#995100] hover:cursor-pointer rounded-lg items-center p-2 space-x-2 ${selected ? 'bg-[#995100]' : ''}`}
className={`flex w-fit md:w-full hover:bg-[#995100] hover:cursor-pointer rounded-lg items-center p-2 space-x-2 ${
selected ? 'bg-[#995100]' : ''
}`}
onClick={onClick}
>
<Icon width={20} height={20} />
<p className={`${!label || !window.matchMedia('(min-width: 768)').matches ? 'sr-only sm:not-sr-only' : ''}`}>
<p
className={`${
!label || !window.matchMedia('(min-width: 768)').matches
? 'sr-only sm:not-sr-only'
: ''
}`}
>
{label[0].toUpperCase() + label.slice(1)}
</p>
</div>
)


function MainContent({ selectedItem, user, team, teams, setTeams, setCurrentTeam }: {
selectedItem: MenuLabel,
user: E2BUser,
team: Team,
teams: Team[],
setTeams: (teams: Team[]) => void,
function MainContent({
selectedItem,
user,
team,
teams,
setTeams,
setCurrentTeam,
}: {
selectedItem: MenuLabel
user: E2BUser
team: Team
teams: Team[]
setTeams: (teams: Team[]) => void
setCurrentTeam: (team: Team) => void
}) {
switch (selectedItem) {
case 'personal':
return <PersonalContent user={user} />
case 'keys':
return <KeysContent currentTeam={team} />
case 'sandboxes':
return <SandboxesContent team={team} />
case 'templates':
return <TemplatesContent user={user} />
case 'usage':
return <UsageContent team={team} />
case 'billing':
return <BillingContent team={team} />
case 'team':
return <TeamContent team={team} user={user} teams={teams} setTeams={setTeams} setCurrentTeam={setCurrentTeam} />
return (
<TeamContent
team={team}
user={user}
teams={teams}
setTeams={setTeams}
setCurrentTeam={setCurrentTeam}
/>
)
default:
return <ErrorContent />
}
Expand Down
99 changes: 61 additions & 38 deletions apps/web/src/components/Dashboard/Sandboxes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,25 @@ import {

import { useState } from 'react'
import { useEffect } from 'react'
import { Team} from '@/utils/useUser'
import { Team } from '@/utils/useUser'

interface Sandbox {
sandboxID: string
templateID: string
alias: string
clientID: string
cpuCount: number
endAt: string
memoryMB: number
metadata: Record<string, any>
sandboxID: string
startedAt: string
clientID: string
templateID: string
}

export const SandboxesContent = ({ team } : { team: Team }) => {
export function SandboxesContent({ team }: { team: Team }) {
const [runningSandboxes, setRunningSandboxes] = useState<Sandbox[]>([])

useEffect(() => {
const interval = setInterval(() => {
function f() {
const apiKey = team.apiKeys[0]
if (apiKey) {
fetchSandboxes(apiKey).then((newSandboxes) => {
Expand All @@ -33,65 +36,85 @@ export const SandboxesContent = ({ team } : { team: Team }) => {
}
})
}
}, 2000)
}

const interval = setInterval(() => {
f()
}, 5000)

f()
// Cleanup interval on component unmount
return () => clearInterval(interval)
}, [team])

return (
<div className="flex flex-col justify-center">

<h2 className="text-xl font-bold pb-4">Total Running Sandboxes: {runningSandboxes.length}</h2>

<Table>
<TableHeader>
<TableRow className='hover:bg-orange-500/10 dark:hover:bg-orange-500/10 border-b border-white/5 '>
<TableHead>ID</TableHead>
<TableHead>Template</TableHead>
<TableHead>CPU Count</TableHead>
<TableHead>Memory (MB)</TableHead>
<TableHead>Started At</TableHead>
<TableHead>Client ID</TableHead>
<TableRow className="hover:bg-orange-500/10 dark:hover:bg-orange-500/10 border-b border-white/5 ">
<TableHead>Sandbox ID</TableHead>
<TableHead>Template ID</TableHead>
<TableHead>Alias</TableHead>
<TableHead>Started at</TableHead>
<TableHead>End at</TableHead>
<TableHead>vCPUs</TableHead>
<TableHead>RAM MiB</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{runningSandboxes.map((sandbox) => (
<TableRow
className='hover:bg-orange-300/10 dark:hover:bg-orange-300/10 border-b border-white/5'
key={sandbox.sandboxID}>
<TableCell>{sandbox.sandboxID}</TableCell>
<TableCell>{sandbox.templateID}</TableCell>
<TableCell>{sandbox.cpuCount}</TableCell>
<TableCell>{sandbox.memoryMB}</TableCell>
<TableCell>{new Date(sandbox.startedAt).toLocaleString('en-UK', {timeZoneName: 'short'})}</TableCell>
<TableCell>{sandbox.clientID}</TableCell>
{runningSandboxes.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center">
No running sandboxes
</TableCell>
</TableRow>
))}
) : (
runningSandboxes.map((sandbox) => (
<TableRow
className="hover:bg-orange-300/10 dark:hover:bg-orange-300/10 border-b border-white/5"
key={sandbox.sandboxID}
>
<TableCell>{sandbox.sandboxID}</TableCell>
<TableCell>{sandbox.templateID}</TableCell>
<TableCell>{sandbox.alias}</TableCell>
<TableCell>
{new Date(sandbox.startedAt).toLocaleString('en-UK', {
timeZoneName: 'short',
})}
</TableCell>
<TableCell>
{new Date(sandbox.endAt).toLocaleString('en-UK', {
timeZoneName: 'short',
})}
</TableCell>
<TableCell>{sandbox.cpuCount}</TableCell>
<TableCell>{sandbox.memoryMB}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>

</div>
)
}

const fetchSandboxes = async (apiKey: string): Promise<Sandbox[]> => {

async function fetchSandboxes(apiKey: string): Promise<Sandbox[]> {
const res = await fetch('https://api.e2b.dev/sandboxes', {
method: 'GET',
headers: {
'X-API-KEY': apiKey,
}
},
})
try {
const data: Sandbox[] = await res.json()
data.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime())
return data

// Latest sandboxes first
return data.sort(
(a, b) =>
new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
)
} catch (e) {
// TODO: add sentry event here
return []
}
}



}
Loading

0 comments on commit 592deaf

Please sign in to comment.