diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx
index 6adf744e5..cefe1d8de 100644
--- a/apps/web/src/app/(dashboard)/dashboard/page.tsx
+++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx
@@ -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'
@@ -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
@@ -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
Error: {error.message}
}
@@ -62,7 +82,10 @@ const Dashboard = ({ user }) => {
const [teams, setTeams] = useState([])
const [currentTeam, setCurrentTeam] = useState(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(initialTab)
const router = useRouter()
@@ -83,7 +106,6 @@ const Dashboard = ({ user }) => {
}
}, [user, teamParam, setCurrentTeam, setTeams])
-
useEffect(() => {
if (tab !== selectedItem) {
const params = new URLSearchParams(window.location.search)
@@ -109,22 +131,51 @@ const Dashboard = ({ user }) => {
if (currentTeam) {
return (
<>
-
+
-
{selectedItem[0].toUpperCase() + selectedItem.slice(1)}
-
-
+
+ {selectedItem[0].toUpperCase() + selectedItem.slice(1)}
+
+
+
>
)
}
}
-
-const Sidebar = ({ selectedItem, setSelectedItem, teams, user, currentTeam, setCurrentTeam, setTeams }) => (
+const Sidebar = ({
+ selectedItem,
+ setSelectedItem,
+ teams,
+ user,
+ currentTeam,
+ setCurrentTeam,
+ setTeams,
+}) => (
-
-
+
{menuLabels.map((label) => (
@@ -137,7 +188,6 @@ const Sidebar = ({ selectedItem, setSelectedItem, teams, user, currentTeam, setC
/>
))}
-
)
@@ -147,27 +197,53 @@ 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
+}) => (
-
+
{label[0].toUpperCase() + label.slice(1)}
)
-
-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) {
@@ -175,12 +251,24 @@ function MainContent({ selectedItem, user, team, teams, setTeams, setCurrentTeam
return
case 'keys':
return
+ case 'sandboxes':
+ return
+ case 'templates':
+ return
case 'usage':
return
case 'billing':
return
case 'team':
- return
+ return (
+
+ )
default:
return
}
diff --git a/apps/web/src/components/Dashboard/Sandboxes.tsx b/apps/web/src/components/Dashboard/Sandboxes.tsx
index 9e4620c91..4ba410f9a 100644
--- a/apps/web/src/components/Dashboard/Sandboxes.tsx
+++ b/apps/web/src/components/Dashboard/Sandboxes.tsx
@@ -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
+ sandboxID: string
startedAt: string
- clientID: string
+ templateID: string
}
-export const SandboxesContent = ({ team } : { team: Team }) => {
+export function SandboxesContent({ team }: { team: Team }) {
const [runningSandboxes, setRunningSandboxes] = useState([])
useEffect(() => {
- const interval = setInterval(() => {
+ function f() {
const apiKey = team.apiKeys[0]
if (apiKey) {
fetchSandboxes(apiKey).then((newSandboxes) => {
@@ -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 (
-
-
Total Running Sandboxes: {runningSandboxes.length}
-
-
- ID
- Template
- CPU Count
- Memory (MB)
- Started At
- Client ID
+
+ Sandbox ID
+ Template ID
+ Alias
+ Started at
+ End at
+ vCPUs
+ RAM MiB
- {runningSandboxes.map((sandbox) => (
-
- {sandbox.sandboxID}
- {sandbox.templateID}
- {sandbox.cpuCount}
- {sandbox.memoryMB}
- {new Date(sandbox.startedAt).toLocaleString('en-UK', {timeZoneName: 'short'})}
- {sandbox.clientID}
+ {runningSandboxes.length === 0 ? (
+
+
+ No running sandboxes
+
- ))}
+ ) : (
+ runningSandboxes.map((sandbox) => (
+
+ {sandbox.sandboxID}
+ {sandbox.templateID}
+ {sandbox.alias}
+
+ {new Date(sandbox.startedAt).toLocaleString('en-UK', {
+ timeZoneName: 'short',
+ })}
+
+
+ {new Date(sandbox.endAt).toLocaleString('en-UK', {
+ timeZoneName: 'short',
+ })}
+
+ {sandbox.cpuCount}
+ {sandbox.memoryMB}
+
+ ))
+ )}
-
)
}
-const fetchSandboxes = async (apiKey: string): Promise => {
-
+async function fetchSandboxes(apiKey: string): Promise {
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 []
}
-}
-
-
-
+}
diff --git a/apps/web/src/components/Dashboard/Templates.tsx b/apps/web/src/components/Dashboard/Templates.tsx
new file mode 100644
index 000000000..45b54d795
--- /dev/null
+++ b/apps/web/src/components/Dashboard/Templates.tsx
@@ -0,0 +1,98 @@
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+
+import { useState } from 'react'
+import { useEffect } from 'react'
+import { E2BUser } from '@/utils/useUser'
+
+interface Template {
+ aliases: string[]
+ buildID: string
+ cpuCount: number
+ memoryMB: number
+ public: boolean
+ templateID: string
+}
+
+export function TemplatesContent({ user }: { user: E2BUser }) {
+ const [templates, setTemplates] = useState([])
+
+ useEffect(() => {
+ function f() {
+ const apiKey = user.accessToken
+ if (apiKey) {
+ fetchTemplates(apiKey).then((newTemplates) => {
+ if (newTemplates) {
+ setTemplates(newTemplates)
+ }
+ })
+ }
+ }
+
+ const interval = setInterval(() => {
+ f()
+ }, 5000)
+
+ f()
+ // Cleanup interval on component unmount
+ return () => clearInterval(interval)
+ }, [user])
+
+ return (
+
+
+
+
+ Template ID
+ Template Name
+ vCPUs
+ RAM MiB
+
+
+
+ {templates.length === 0 ? (
+
+
+ No templates
+
+
+ ) : (
+ templates.map((template) => (
+
+ {template.templateID}
+ {template.aliases[0]}
+ {template.cpuCount}
+ {template.memoryMB}
+
+ ))
+ )}
+
+
+
+ )
+}
+
+async function fetchTemplates(apiKey: string): Promise {
+ const res = await fetch('https://api.e2b.dev/templates', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ })
+ try {
+ const data: Template[] = await res.json()
+ return data
+ } catch (e) {
+ // TODO: add sentry event here
+ return []
+ }
+}