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 [] + } +}