diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 07947d00ef..ae09b197eb 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -71,17 +71,6 @@ const nextConfig = { config.cache.version = config.cache.version + delimiter + codeFilesHash return config }, - async rewrites() { - return { - afterFiles: [ - { - source: '/ingest/:path*', - destination: 'https://app.posthog.com/:path*', - // BEWARE: setting basePath will break the analytics proxy - }, - ], - } - }, async redirects() { return [ { diff --git a/apps/web/package.json b/apps/web/package.json index 22ef210b58..705258de39 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -53,7 +53,6 @@ "mdx-annotations": "^0.1.1", "next": "14.2.25", "next-themes": "^0.2.1", - "posthog-js": "^1.148.0", "react": "18.2.0", "react-dom": "18.2.0", "react-highlight-words": "^0.20.0", diff --git a/apps/web/src/app/(auth)/auth/layout.tsx b/apps/web/src/app/(auth)/auth/layout.tsx deleted file mode 100644 index 38497a564a..0000000000 --- a/apps/web/src/app/(auth)/auth/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Footer } from '@/components/Footer' - -export default async function Layout({ children }) { - return ( -
- {children} -
- ) -} diff --git a/apps/web/src/app/(auth)/auth/reset-password/page.tsx b/apps/web/src/app/(auth)/auth/reset-password/page.tsx deleted file mode 100644 index e8b16739c4..0000000000 --- a/apps/web/src/app/(auth)/auth/reset-password/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import AuthForm from '@/components/AuthForm' -import { Suspense } from 'react' - -export default function Sign() { - return ( - - - - ) -} diff --git a/apps/web/src/app/(auth)/auth/sign-in/page.tsx b/apps/web/src/app/(auth)/auth/sign-in/page.tsx deleted file mode 100644 index 5b27fe7c8a..0000000000 --- a/apps/web/src/app/(auth)/auth/sign-in/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import AuthForm from '@/components/AuthForm' -import { Suspense } from 'react' - -export default function Sign() { - return ( - - - - ) -} diff --git a/apps/web/src/app/(auth)/auth/sign-up/page.tsx b/apps/web/src/app/(auth)/auth/sign-up/page.tsx deleted file mode 100644 index 2030bd754d..0000000000 --- a/apps/web/src/app/(auth)/auth/sign-up/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import AuthForm from '@/components/AuthForm' -import { Suspense } from 'react' - -export default function SignUp() { - return ( - - - - ) -} diff --git a/apps/web/src/app/(auth)/auth/update-password/page.tsx b/apps/web/src/app/(auth)/auth/update-password/page.tsx deleted file mode 100644 index 57f0f3dd84..0000000000 --- a/apps/web/src/app/(auth)/auth/update-password/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import AuthForm from '@/components/AuthForm' -import { Suspense } from 'react' - -export default function UpdatePassword() { - return ( - - - - ) -} diff --git a/apps/web/src/app/(dashboard)/dashboard/layout.tsx b/apps/web/src/app/(dashboard)/dashboard/layout.tsx deleted file mode 100644 index 157bf52f6d..0000000000 --- a/apps/web/src/app/(dashboard)/dashboard/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { FooterMain } from '@/components/Footer' -import { Toaster } from '@/components/ui/toaster' - -export default async function Layout({ children }) { - return ( -
-
- {children} - -
- -
- ) -} diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx deleted file mode 100644 index 7b0f01f502..0000000000 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ /dev/null @@ -1,321 +0,0 @@ -'use client' - -import { Suspense, useEffect, useState } from 'react' -import { useLocalStorage } from 'usehooks-ts' - -import { - ArrowUpRight, - BarChart, - CreditCard, - FileText, - Key, - LucideIcon, - PackageIcon, - PencilRuler, - Settings, - Users, -} from 'lucide-react' - -import { BillingContent } from '@/components/Dashboard/Billing' -import { TeamContent } from '@/components/Dashboard/Team' - -import { E2BUser, Team, useUser } from '@/utils/useUser' -import { KeysContent } from '@/components/Dashboard/Keys' -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' -import { DeveloperContent } from '@/components/Dashboard/Developer' -import { Button } from '@/components/Button' - -function redirectToCurrentURL() { - const url = typeof window !== 'undefined' ? window.location.href : undefined - - if (!url) { - return '' - } - - const encodedURL = encodeURIComponent(url) - return `redirect_to=${encodedURL}` -} - -const menuLabels = [ - 'personal', - 'keys', - 'sandboxes', - 'templates', - 'usage', - 'billing', - 'team', - 'developer', -] as const -type MenuLabel = (typeof menuLabels)[number] - -export default function Page() { - const { user, isLoading, error } = useUser() - const router = useRouter() - - useEffect(() => { - if (isLoading) { - return - } - if (!user) { - router.push(`/auth/sign-in?${redirectToCurrentURL()}`) - } - }, [isLoading, user, router]) - - if (error) { - return
Error: {error.message}
- } - - if (user) { - return ( -
- - - -
- ) - } -} - -const Dashboard = ({ user }) => { - const searchParams = useSearchParams() - - const tab = searchParams!.get('tab') - const teamParam = searchParams!.get('team') - const [teams, setTeams] = useState([]) - const [currentTeam, setCurrentTeam] = useState(null) - - const domainState = useLocalStorage( - 'e2bDomain', - process.env.NEXT_PUBLIC_DOMAIN || '' - ) - - const initialTab = - tab && menuLabels.includes(tab as MenuLabel) - ? (tab as MenuLabel) - : 'personal' - const [selectedItem, setSelectedItem] = useState(initialTab) - - const router = useRouter() - - useEffect(() => { - if (user) { - if (teamParam) { - const team = user.teams.find((team: Team) => team.id === teamParam) - if (team) { - setCurrentTeam(team) - setTeams(user.teams) - } - } else { - const defaultTeam = user.teams.find((team: Team) => team.is_default) - setCurrentTeam(defaultTeam || user.teams[0]) // seems like a sensible default - setTeams(user.teams) - } - } - }, [user, teamParam, setCurrentTeam, setTeams]) - - useEffect(() => { - if (tab !== selectedItem) { - const params = new URLSearchParams(window.location.search) - params.set('tab', selectedItem) - const newUrl = `${window.location.pathname}?${params.toString()}` - router.push(newUrl) - } - }, [selectedItem, tab, router]) - - useEffect(() => { - if (currentTeam && teamParam !== currentTeam?.id) { - const params = new URLSearchParams(window.location.search) - if (currentTeam) { - params.set('team', currentTeam.id) - } else { - params.delete('team') - } - const newUrl = `${window.location.pathname}?${params.toString()}` - router.push(newUrl) - } - }, [currentTeam, teamParam, router]) - - if (currentTeam) { - return ( - <> - -
-
-

- {selectedItem[0].toUpperCase() + selectedItem.slice(1)} -

- {currentTeam.is_blocked && ( - - )} -
- -
- -
- - ) - } -} - -const Sidebar = ({ - selectedItem, - setSelectedItem, - teams, - user, - currentTeam, - setCurrentTeam, - setTeams, - domainState, -}) => ( -
- - -
- {menuLabels.map((label) => ( - setSelectedItem(label)} - /> - ))} -
-
-) - -const iconMap: { [key in MenuLabel]: LucideIcon } = { - personal: Settings, - keys: Key, - usage: BarChart, - billing: CreditCard, - team: Users, - templates: FileText, - sandboxes: PackageIcon, - developer: PencilRuler, -} - -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, - domainState, -}: { - selectedItem: MenuLabel - user: E2BUser - team: Team - teams: Team[] - setTeams: (teams: Team[]) => void - setCurrentTeam: (team: Team) => void - domainState: [string, (value: string) => void] -}) { - switch (selectedItem) { - case 'personal': - return - case 'keys': - return ( - - ) - case 'sandboxes': - return - case 'templates': - return ( - - ) - case 'usage': - return - case 'billing': - return - case 'team': - return ( - - ) - case 'developer': - return - default: - return - } -} - -// TODO send sentry error from this -const ErrorContent = () =>
Error Content
diff --git a/apps/web/src/app/(dashboard)/dashboard/utils.ts b/apps/web/src/app/(dashboard)/dashboard/utils.ts deleted file mode 100644 index c581e6d6ad..0000000000 --- a/apps/web/src/app/(dashboard)/dashboard/utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -function getUrl(domain: string, subdomain: string, path: string) { - let url = domain - const local = domain.startsWith('localhost') || domain.startsWith('127.0.0.') - - if (!domain.startsWith('http')) { - url = `http${local ? '' : 's'}://${domain}` - } - - const parsedUrl = new URL(url) - - if (path) { - const decodedUrl = decodeURIComponent(path) - const [pathname, queryString] = decodedUrl.split('?') - parsedUrl.pathname = pathname - if (queryString) parsedUrl.search = queryString - } - - if (!local) { - parsedUrl.hostname = `${subdomain}.${parsedUrl.hostname}` - } - - return parsedUrl.toString() -} - -export function getAPIUrl(domain: string, path: string) { - return getUrl(domain, 'api', path) -} - -export function getBillingUrl(domain: string, path: string) { - return getUrl(domain, 'billing', path) -} diff --git a/apps/web/src/app/(docs)/docs/api/cli/page.tsx b/apps/web/src/app/(docs)/docs/api/cli/page.tsx deleted file mode 100644 index 3b497e0b32..0000000000 --- a/apps/web/src/app/(docs)/docs/api/cli/page.tsx +++ /dev/null @@ -1,161 +0,0 @@ -'use client' - -import Link from 'next/link' -import { useSearchParams } from 'next/navigation' -import { useApiKey, useUser } from '@/utils/useUser' -import { DialogAnimated } from '@/components/DialogAnimated' -import { CloudIcon, LaptopIcon, Link2Icon } from 'lucide-react' -import { Button } from '@/components/Button' -import { usePostHog } from 'posthog-js/react' -import { Suspense, useEffect } from 'react' - -type UserConfig = { - email: string; - accessToken: string; - defaultTeamApiKey: string; - defaultTeamId: string; -} - -export default function Page() { - const posthog = usePostHog() - const { user, isLoading: userIsLoading } = useUser() - const apiKey = useApiKey() - - useEffect( - function sendAuthorizationStartAnalytics() { - posthog?.capture('opened CLI authorization page') - }, - [posthog] - ) - - return ( -
- {/* It's not easy to override RootLayout without grouping everything into `(root)` dir */} - {/* So I'm hacking a custom layout with full modal overlay */} - {/* https://github.com/vercel/next.js/issues/50591 */} - {}} // intentionally prevent closing - > -
-
-
-

- - - - - - - - - -

-

- Linking CLI with your account -

- - - -
-
-
-
-
- ) -} - -function AuthState({ user, apiKey, posthog, userIsLoading }) { - const searchParams = useSearchParams() - const searchParamsObj = searchParams ? Object.fromEntries(searchParams) : {} - const { next, state } = searchParamsObj - - // TODO: Consider sending back onetime code to be used to get access token - function redirectToCLI() { - if (!next) return - if (!(user?.email && apiKey)) return - - posthog?.capture('started CLI authorization', { email: user.email }) - - const { email, accessToken, defaultTeamId } = user - const newUrl = new URL(next) - const searchParamsObj: UserConfig = { - email, - defaultTeamApiKey: apiKey, - accessToken, - defaultTeamId, - } - newUrl.search = new URLSearchParams(searchParamsObj).toString() - window.location.href = newUrl.toString() - } - - useEffect( - function sendAuthorizationAnalytics() { - if (state === 'success') { - posthog?.capture('successfully authorized CLI') - } else if (state === 'error') { - posthog?.capture('failed to authorize CLI', { - error: searchParamsObj.error, - }) - } - }, - [state, posthog, searchParamsObj.error] - ) - - let content - if (state === 'error') { - content = ( - <> -
Error
-
Something went wrong, please try again
-
{searchParamsObj.error}
- {/* TODO: Nicer, but it should never happen */} - - ) - } else if (state === 'success') { - content = ( - <> -
Successfully linked
-
You can close this page and start using CLI.
- - ) - } else { - const isNextValid = next.startsWith('http://localhost') - if (!isNextValid) { - content = ( - <> -
Error
-
Invalid redirect URL, only localhost is allowed
- - ) - } else if (userIsLoading) { - content = Loading, please wait - } else if (!user) { - content = ( - - - - ) - } else { - content = ( - <> - - - ) - } - } - - return

{content}

-} diff --git a/apps/web/src/app/(docs)/docs/legacy/api/cli/page.tsx b/apps/web/src/app/(docs)/docs/legacy/api/cli/page.tsx deleted file mode 100644 index 3b497e0b32..0000000000 --- a/apps/web/src/app/(docs)/docs/legacy/api/cli/page.tsx +++ /dev/null @@ -1,161 +0,0 @@ -'use client' - -import Link from 'next/link' -import { useSearchParams } from 'next/navigation' -import { useApiKey, useUser } from '@/utils/useUser' -import { DialogAnimated } from '@/components/DialogAnimated' -import { CloudIcon, LaptopIcon, Link2Icon } from 'lucide-react' -import { Button } from '@/components/Button' -import { usePostHog } from 'posthog-js/react' -import { Suspense, useEffect } from 'react' - -type UserConfig = { - email: string; - accessToken: string; - defaultTeamApiKey: string; - defaultTeamId: string; -} - -export default function Page() { - const posthog = usePostHog() - const { user, isLoading: userIsLoading } = useUser() - const apiKey = useApiKey() - - useEffect( - function sendAuthorizationStartAnalytics() { - posthog?.capture('opened CLI authorization page') - }, - [posthog] - ) - - return ( -
- {/* It's not easy to override RootLayout without grouping everything into `(root)` dir */} - {/* So I'm hacking a custom layout with full modal overlay */} - {/* https://github.com/vercel/next.js/issues/50591 */} - {}} // intentionally prevent closing - > -
-
-
-

- - - - - - - - - -

-

- Linking CLI with your account -

- - - -
-
-
-
-
- ) -} - -function AuthState({ user, apiKey, posthog, userIsLoading }) { - const searchParams = useSearchParams() - const searchParamsObj = searchParams ? Object.fromEntries(searchParams) : {} - const { next, state } = searchParamsObj - - // TODO: Consider sending back onetime code to be used to get access token - function redirectToCLI() { - if (!next) return - if (!(user?.email && apiKey)) return - - posthog?.capture('started CLI authorization', { email: user.email }) - - const { email, accessToken, defaultTeamId } = user - const newUrl = new URL(next) - const searchParamsObj: UserConfig = { - email, - defaultTeamApiKey: apiKey, - accessToken, - defaultTeamId, - } - newUrl.search = new URLSearchParams(searchParamsObj).toString() - window.location.href = newUrl.toString() - } - - useEffect( - function sendAuthorizationAnalytics() { - if (state === 'success') { - posthog?.capture('successfully authorized CLI') - } else if (state === 'error') { - posthog?.capture('failed to authorize CLI', { - error: searchParamsObj.error, - }) - } - }, - [state, posthog, searchParamsObj.error] - ) - - let content - if (state === 'error') { - content = ( - <> -
Error
-
Something went wrong, please try again
-
{searchParamsObj.error}
- {/* TODO: Nicer, but it should never happen */} - - ) - } else if (state === 'success') { - content = ( - <> -
Successfully linked
-
You can close this page and start using CLI.
- - ) - } else { - const isNextValid = next.startsWith('http://localhost') - if (!isNextValid) { - content = ( - <> -
Error
-
Invalid redirect URL, only localhost is allowed
- - ) - } else if (userIsLoading) { - content = Loading, please wait - } else if (!user) { - content = ( - - - - ) - } else { - content = ( - <> - - - ) - } - } - - return

{content}

-} diff --git a/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/APIKey.tsx b/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/APIKey.tsx deleted file mode 100644 index 68487771f3..0000000000 --- a/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/APIKey.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client' - -import Link from 'next/link' -import clsx from 'clsx' -import { usePostHog } from 'posthog-js/react' - -import { useAccessToken, useApiKey, useUser } from '@/utils/useUser' -import { obfuscateSecret } from '@/utils/obfuscate' -import { Button } from '@/components/Button' -import { CopyButton } from '@/components/CopyButton' -import { Note } from '@/components/mdx' - -export function CopyableSecret({ - secret, - onAfterCopy, - obfuscateStart, - obfuscateEnd, -}: { - secret: string; - onAfterCopy: () => void; - obfuscateStart?: number; - obfuscateEnd?: number; -}) { - return ( -
- - {obfuscateSecret(secret, obfuscateStart, obfuscateEnd)} - - - - -
- ) -} - -function SecretBlock({ name, description, secret, posthog, tip }) { - return ( -
-

- {name} -
- posthog?.capture('copied API key')} - obfuscateStart={12} - obfuscateEnd={5} - /> -
-

- {description} - {tip} -
- ) -} - -function APIKey() { - const { user } = useUser() - const apiKey = useApiKey() - const posthog = usePostHog() - const accessToken = useAccessToken() - - return ( -
- {user ? ( -
- - Use for running the sandboxes. - - } - secret={apiKey} - posthog={posthog} - tip={ - - Set as E2B_API_KEY environment variable to avoid passing it - every time. - - } - /> - - - Used only in the CLI, not needed in the SDK. - - - Access token is for managing sandboxes with CLI - list/kill/connect, and for building custom sandbox templates with CLI. - - - Acess token is not needed when logging in CLI via e2b auth login. - -
- } - secret={accessToken} - posthog={posthog} - tip={ - -
- To authenticate without the browser, you can set E2B_ACCESS_TOKEN as an environment variable. - This can be useful for CI/CD pipelines. -
-
- } - /> -
- ) : ( -
- You can get your API key by signing up. - - - -
- )} -
- ) -} - -export default APIKey diff --git a/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/page.mdx b/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/page.mdx index 4cb0c2f39a..aff12021fa 100644 --- a/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/page.mdx +++ b/apps/web/src/app/(docs)/docs/legacy/getting-started/api-key/page.mdx @@ -1,10 +1,3 @@ -import APIKey from './APIKey' - -# Your API Key - - - - ## Use API key To use the API key, you either: diff --git a/apps/web/src/app/(docs)/docs/legacy/pricing/page.mdx b/apps/web/src/app/(docs)/docs/legacy/pricing/page.mdx index cd25c97566..f48a6169f8 100644 --- a/apps/web/src/app/(docs)/docs/legacy/pricing/page.mdx +++ b/apps/web/src/app/(docs)/docs/legacy/pricing/page.mdx @@ -1,4 +1,3 @@ -import ManageBilling from '@/components/ManageBilling' import SandboxSpec from '@/components/SandboxSpec' # Pricing diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 58c4e5df5c..44fc62b0ad 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,9 +5,7 @@ import Script from 'next/script' import { Providers } from '@/app/providers' import '@/styles/tailwind.css' -import { PostHogAnalytics } from '@/utils/usePostHog' import Canonical from '@/components/Navigation/canonical' -import { Suspense } from 'react' import { Header } from '@/components/Header' import glob from 'fast-glob' import { Section } from '@/components/SectionProvider' @@ -29,6 +27,15 @@ export const metadata: Metadata = { }, } +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + 'chatlio-widget': any + } + } +} + export default async function RootLayout({ children }) { const pages = await glob('**/*.mdx', { cwd: 'src/app/(docs)/docs' }) const allSectionsEntries = (await Promise.all( @@ -71,9 +78,6 @@ export default async function RootLayout({ children }) {
{children} - - - diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index dea12f1b5c..d8aa7685f4 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -2,8 +2,6 @@ import { useEffect } from 'react' import { ThemeProvider, useTheme } from 'next-themes' -import { CustomUserContextProvider } from '@/utils/useUser' -import { PostHogProvider } from '@/utils/usePostHog' function ThemeWatcher() { const { resolvedTheme, setTheme } = useTheme() @@ -37,10 +35,8 @@ export function Providers({ children }) { disableTransitionOnChange forcedTheme="dark" > - - - {children} - + + {children} ) } diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index edbdb99372..9babbd2653 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -1,184 +1,27 @@ import { MetadataRoute } from 'next' -import { XMLParser } from 'fast-xml-parser' import path from 'path' -import { replaceUrls } from '@/utils/replaceUrls' import { getPageForSitemap } from '@/utils/sitemap' -import { - landingPageHostname, - landingPageFramerHostname, - blogFramerHostname, - changelogFramerHostname, -} from '@/app/hostnames' export const dynamic = 'force-static' -type ChangeFrequency = - | 'always' - | 'hourly' - | 'daily' - | 'weekly' - | 'monthly' - | 'yearly' - | 'never' - -type Site = { - sitemapUrl: string - lastModified?: string | Date - changeFrequency?: ChangeFrequency - priority?: number -} - -const sites: Site[] = [ - { - sitemapUrl: `https://${landingPageHostname}/sitemap.xml`, - priority: 1.0, - changeFrequency: 'daily', - }, - { - sitemapUrl: `https://${blogFramerHostname}/sitemap.xml`, - priority: 0.9, - changeFrequency: 'daily', - }, - { - sitemapUrl: `https://${changelogFramerHostname}/sitemap.xml`, - priority: 0.2, - changeFrequency: 'weekly', - }, -] - -type SitemapData = { - loc: string - lastmod?: string | Date - changefreq?: ChangeFrequency - priority?: number -} - -type Sitemap = { - urlset: { - url: SitemapData | SitemapData[] - } -} - -async function getXmlData(url: string): Promise { - const parser = new XMLParser() - - const response = await fetch(url) - - if (!response.ok) { - return { urlset: { url: [] } } - } - - const text = await response.text() - - return parser.parse(text) as Sitemap -} -async function getSitemap(site: Site): Promise { - const data = await getXmlData(site.sitemapUrl) - - if (!data) { - return [] - } - - const normalizeUrl = (inputUrl: string, pathname: string) => { - // First normalize the URL format - let normalizedUrl = inputUrl - .replace(/^www\./, '') // Remove www. prefix - .replace(/https:\/\/https:\/\//, 'https://') // Fix double https:// - .replace(/^https:\/\/www\./, 'https://') // Remove www. after https:// - - // Parse the URL to work with its components - const urlObj = new URL(normalizedUrl) - - // Normalize category URLs to include /blog prefix - if (pathname.startsWith('/category/')) { - urlObj.pathname = `/blog${pathname}` - } - - // Convert back to string for further processing - normalizedUrl = urlObj.toString() - - // Apply replaceUrls after initial normalization - normalizedUrl = replaceUrls(normalizedUrl, urlObj.pathname) - - // Ensure all URLs use e2b.dev domain - // Handle both www. and non-www variants - const hostnames = [ - landingPageFramerHostname, - landingPageHostname, - changelogFramerHostname, - blogFramerHostname, - ] - - for (const hostname of hostnames) { - normalizedUrl = normalizedUrl - .replace(`www.${hostname}`, 'e2b.dev') - .replace(hostname, 'e2b.dev') - } - - // Final cleanup for any remaining double https:// or www. - return normalizedUrl - .replace(/https:\/\/https:\/\//, 'https://') - .replace(/^https:\/\/www\./, 'https://') - } - - if (Array.isArray(data.urlset.url)) { - return data.urlset.url.map((line) => { - const url = new URL(line.loc) - const pathname = url.pathname - - return { - url: normalizeUrl(line.loc, pathname), - priority: line?.priority || site.priority, - changeFrequency: line?.changefreq || site.changeFrequency, - } - }) - } else { - const url = new URL(data.urlset.url.loc) - const pathname = url.pathname - - return [ - { - url: normalizeUrl(data.urlset.url.loc, pathname), - priority: data.urlset.url?.priority || site.priority, - changeFrequency: data.urlset.url?.changefreq || site.changeFrequency, - }, - ] - } -} - export default async function sitemap(): Promise { - let mergedSitemap: MetadataRoute.Sitemap = [] - - const dashboardPath = path.join( - process.cwd(), - 'src', - 'app', - '(dashboard)', - 'dashboard' - ) - const dashboardPages = getPageForSitemap( - dashboardPath, - 'https://e2b.dev/dashboard/', - 0.5 + const docsDirectory = path.join( + process.env.NODE_ENV === 'production' + ? path.join('.', 'src', 'app', '(docs)', 'docs') + : path.join(process.cwd(), 'src', 'app', '(docs)', 'docs') ) - const docsDirectory = path.join(process.cwd(), 'src', 'app', '(docs)', 'docs') const docsPages = getPageForSitemap( docsDirectory, 'https://e2b.dev/docs/', - 0.5 - ).filter((page) => !page.url.startsWith('https://e2b.dev/docs/api/')) - - mergedSitemap = mergedSitemap.concat(dashboardPages, docsPages) - - for (const site of sites) { - const urls = await getSitemap(site) - mergedSitemap = mergedSitemap.concat(...urls) - } + 0.8 + ) + // Filter out legacy docs pages + .filter((entry) => !entry.url.includes('/docs/legacy')) // Deduplicate URLs, keeping the entry with highest priority const urlMap = new Map() - for (const entry of mergedSitemap) { + for (const entry of docsPages) { const existing = urlMap.get(entry.url) if (!existing || (existing.priority || 0) < (entry.priority || 0)) { urlMap.set(entry.url, entry) diff --git a/apps/web/src/components/Auth.tsx b/apps/web/src/components/Auth.tsx deleted file mode 100644 index f4d9d92a30..0000000000 --- a/apps/web/src/components/Auth.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client' - -import Link from 'next/link' -import { LogOutIcon } from 'lucide-react' -import { useRouter } from 'next/navigation' - -import { Button } from '@/components/Button' -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' -import { useUser } from '@/utils/useUser' -import { usePostHog } from 'posthog-js/react' - -export const Auth = function () { - const { user, isLoading, error } = useUser() - const posthog = usePostHog() - const router = useRouter() - const supabase = createClientComponentClient() - - async function signOut() { - await supabase.auth.signOut() - posthog?.reset(true) - router.push('/') - window.location.reload() - } - - function redirectToCurrentURL() { - const url = typeof window !== 'undefined' ? window.location.href : undefined - - if (!url) { - return '' - } - - const encodedURL = encodeURIComponent(url) - return `redirect_to=${encodedURL}` - } - - - if (error) - return ( -
- - Something went wrong - - - - -
- ) - - if (isLoading) - return ( -
-
-
- ) - - return ( - <> - {user ? ( -
-
-
- {user.email} -
- {/* @ts-ignore */} - -
-
- ) : ( -
- - - -
- )} - - ) -} diff --git a/apps/web/src/components/AuthForm.tsx b/apps/web/src/components/AuthForm.tsx deleted file mode 100644 index 09ecb1dfd8..0000000000 --- a/apps/web/src/components/AuthForm.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client' - -import Link from 'next/link' -import { Auth } from '@supabase/auth-ui-react' -import { ThemeSupa, ViewType } from '@supabase/auth-ui-shared' -import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' -import { useSearchParams, useRouter } from 'next/navigation' -import { useEffect } from 'react' -import { Button } from '@/components/Button' -import { useUser } from '@/utils/useUser' - -const supabase = createClientComponentClient() - -export interface Props { - view: ViewType -} - -function AuthForm({ view }: Props) { - const searchParams = useSearchParams() - const redirectTo = searchParams?.get('redirect_to') - const router = useRouter() - const user = useUser() - - useEffect( - function redirect() { - if (user.user && view !== 'update_password') { - router.push(redirectTo || '/dashboard') - } - - if (user.wasUpdated && !redirectTo) { - router.push('/dashboard') - } - }, - [user.user, router, redirectTo, view, user.wasUpdated] - ) - - return ( -
-

- {view === 'sign_in' && 'Sign in to E2B'} - {view === 'sign_up' && 'Create new E2B account'} - {view === 'forgotten_password' && 'Reset password'} - {view === 'update_password' && 'Update password'} -

-
- -
- -
- {(view === 'sign_up' || - view === 'forgotten_password' || - view === 'update_password') && ( -
- Already have an account? - - - -
- )} - - {view === 'sign_in' && ( - - - - )} - - {view === 'sign_in' && ( -
- {"Don't have an account?"} - - - -
- )} -
-
- ) -} - -export default AuthForm diff --git a/apps/web/src/components/Dashboard/AccountSelector.tsx b/apps/web/src/components/Dashboard/AccountSelector.tsx deleted file mode 100644 index 0ed941a4c0..0000000000 --- a/apps/web/src/components/Dashboard/AccountSelector.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { ChevronRight, PlusCircle } from 'lucide-react' -import { toast } from '../ui/use-toast' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '../ui/alert-dialog' -import { useState } from 'react' -import { Button } from '../Button' -import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' - -export const AccountSelector = ({ - teams, - user, - currentTeam, - setCurrentTeam, - setTeams, - domainState, -}) => { - const [domain] = domainState - - const [isDialogOpen, setIsDialogOpen] = useState(false) - const [teamName, setTeamName] = useState('') - const closeDialog = () => setIsDialogOpen(false) - - const createNewTeam = async () => { - const res = await fetch(getBillingUrl(domain, '/teams'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-User-Access-Token': user.accessToken, - }, - body: JSON.stringify({ - name: teamName, - }), - }) - if (!res.ok) { - // TODO: Add sentry event here - console.log(res.status, res.statusText) - toast({ - title: 'An error occurred', - description: 'We were unable to create the team', - }) - return - } - - const team = await res.json() - - toast({ - title: `Team ${team.name} created`, - }) - - setTeams([...teams, team]) - setCurrentTeam(team) - setTeamName('') - } - - return ( - <> - - -
-

Current team

-

{currentTeam.name}

-
- -
- - {teams?.map((team: any) => ( - setCurrentTeam(team)} - > - {team.name} - - ))} - - setIsDialogOpen(true)} - > - - Create Team - - -
- - - - - - - - Give your team a name - setTeamName(e.target.value)} - /> - - - - Cancel - - createNewTeam()} - > - Create - - - - - - ) -} diff --git a/apps/web/src/components/Dashboard/Billing.tsx b/apps/web/src/components/Dashboard/Billing.tsx deleted file mode 100644 index 65e7c6c13a..0000000000 --- a/apps/web/src/components/Dashboard/Billing.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useEffect, useState } from 'react' -import Link from 'next/link' -import { Team, useUser } from '@/utils/useUser' -import { Button } from '../Button' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '../ui/table' -import SwitchToHobbyButton from '@/components/Pricing/SwitchToHobbyButton' -import SwitchToProButton from '@/components/Pricing/SwitchToProButton' -import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' - -function formatCurrency(value: number) { - return value.toLocaleString('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }) -} - -interface Invoice { - cost: number - paid: boolean - url: string - date_created: string -} - -export const BillingContent = ({ - team, - domain, -}: { - team: Team - domain: string -}) => { - const [invoices, setInvoices] = useState([]) - const [credits, setCredits] = useState(null) - - useEffect(() => { - const getInvoices = async function getInvoices() { - setInvoices([]) - const res = await fetch( - getBillingUrl(domain, `/teams/${team.id}/invoices`), - { - headers: { - 'X-Team-API-Key': team.apiKeys[0], - }, - } - ) - if (!res.ok) { - // TODO: add sentry error - console.log(res) - return - } - - const invoices = (await res.json()) as Invoice[] - setInvoices(invoices) - - setCredits(null) - const creditsRes = await fetch( - getBillingUrl(domain, `/teams/${team.id}/usage`), - { - headers: { - 'X-Team-API-Key': team.apiKeys[0], - }, - } - ) - const credits = await creditsRes.json() - setCredits(credits.credits) - } - - getInvoices() - }, [domain, team]) - - return ( -
-
-

Make changes to your billing

- -
- -
-

Credits left

- - Credits are used to bill your team automatically - - {credits === null ? ( - Loading... - ) : ( - - ${formatCurrency(credits ?? 0)} - - )} -
- -
-

Change tier

-
- -
-
-

Hobby tier

- -
-
    -
  • One-time $100 credits
  • -
  • Community support
  • -
  • Up to 1 hour sandbox session length
  • -
  • Up to 20 concurrently running sandboxes
  • -
-
- -
-
-

Pro tier

- -
-
    -
  • One-time $100 credits
  • -
  • Dedicated Slack channel with live Pro support from our team
  • -
  • Prioritized features
  • -
  • - Customize your{' '} - - sandbox compute - -
  • -
  • Up to 24 hours sandbox session length
  • -
  • Up to 100 concurrently running sandboxes
  • -
-
- -
-

Billing history

-
- - - - - Date - Cost - Paid - Invoice url - - - - {invoices && invoices.length > 0 ? ( - invoices.map((item, index) => ( - - - {new Date(item.date_created).toLocaleDateString()} - - ${item.cost.toFixed(2)} - {item.paid ? 'Paid' : 'Unpaid'} - - - View invoice - - - - )) - ) : ( - - - No invoices found - - - )} - -
-
- ) -} - -const ManageBilling = () => { - const { user } = useUser() - const [url, setURL] = useState('') - - useEffect( - function getBillingURL() { - if (!user) return - const u = `${process.env.NEXT_PUBLIC_STRIPE_BILLING_URL}?prefilled_email=${user.teams[0].email}` - setURL(u) - }, - [user] - ) - - if (!user || !url) { - return null - } - - return ( - - ) -} diff --git a/apps/web/src/components/Dashboard/BillingAlerts.tsx b/apps/web/src/components/Dashboard/BillingAlerts.tsx deleted file mode 100644 index 0e7660f05c..0000000000 --- a/apps/web/src/components/Dashboard/BillingAlerts.tsx +++ /dev/null @@ -1,350 +0,0 @@ -'use client' - -import { useState, useEffect, useCallback } from 'react' -import { useToast } from '../ui/use-toast' -import { Button } from '../Button' -import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' -import { Team, useUser } from '@/utils/useUser' -import { Loader2 } from 'lucide-react' - -interface BillingLimit { - limit_amount_gte: number | null - alert_amount_gte: number | null -} - -export const BillingAlerts = ({ - team, - domain, - email, -}: { - team: Team - domain: string - email: string -}) => { - const { toast } = useToast() - const [originalLimits, setOriginalLimits] = useState({ - limit_amount_gte: null, - alert_amount_gte: null, - }) - const [limits, setLimits] = useState({ - limit_amount_gte: null, - alert_amount_gte: null, - }) - const [editMode, setEditMode] = useState({ - limit: false, - alert: false, - }) - const { user } = useUser() - const [isLoading, setIsLoading] = useState({ - limit: { - save: false, - clear: false, - }, - alert: { - save: false, - clear: false, - }, - }) - - const fetchBillingLimits = useCallback(async () => { - if (!user) return - - try { - const res = await fetch( - getBillingUrl(domain, `/teams/${team.id}/billing-limits`), - { - headers: { - 'X-User-Access-Token': user.accessToken, - }, - } - ) - - if (!res.ok) { - toast({ - title: 'Failed to fetch billing alerts', - description: 'Unable to load your billing alert settings', - }) - return - } - - const data = await res.json() - setOriginalLimits(data) - setLimits(data) - } catch (error) { - console.error('Error fetching billing threshold:', error) - toast({ - title: 'Error', - description: 'Failed to load billing alert settings', - }) - } - }, [user, domain, team, toast]) - - const updateBillingLimit = async (type: 'limit' | 'alert') => { - if (!user) return - - setIsLoading((prev) => ({ - ...prev, - [type]: { ...prev[type], save: true }, - })) - - const value = - type === 'limit' ? limits.limit_amount_gte : limits.alert_amount_gte - - try { - const res = await fetch( - getBillingUrl(domain, `/teams/${team.id}/billing-limits`), - { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-User-Access-Token': user.accessToken, - }, - body: JSON.stringify({ - [type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte']: value, - }), - } - ) - - if (!res.ok) { - toast({ - title: 'Failed to update billing alert', - description: 'Unable to save your billing alert setting', - }) - return - } - - setOriginalLimits((prev) => ({ - ...prev, - [type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte']: value, - })) - - toast({ - title: 'Billing alert updated', - description: 'Your billing alert setting has been saved', - }) - } catch (error) { - console.error('Error updating billing threshold:', error) - toast({ - title: 'Error', - description: 'Failed to save billing alert settings', - }) - } finally { - setIsLoading((prev) => ({ - ...prev, - [type]: { ...prev[type], save: false }, - })) - } - } - - const deleteBillingLimit = async (type: 'limit' | 'alert') => { - if (!user) return - - setIsLoading((prev) => ({ - ...prev, - [type]: { ...prev[type], clear: true }, - })) - - try { - const res = await fetch( - getBillingUrl( - domain, - `/teams/${team.id}/billing-limits/${type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte' - }` - ), - { - method: 'DELETE', - headers: { - 'X-User-Access-Token': user.accessToken, - }, - } - ) - - if (!res.ok) { - toast({ - title: 'Failed to clear billing alert', - description: 'Unable to clear your billing alert setting', - }) - return - } - - setOriginalLimits((prev) => ({ - ...prev, - [type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte']: null, - })) - setLimits((prev) => ({ - ...prev, - [type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte']: null, - })) - - toast({ - title: 'Billing alert cleared', - description: 'Your billing alert setting has been cleared', - }) - } catch (error) { - console.error('Error clearing billing threshold:', error) - toast({ - title: 'Error', - description: 'Failed to clear billing alert settings', - }) - } finally { - setIsLoading((prev) => ({ - ...prev, - [type]: { ...prev[type], clear: false }, - })) - } - } - - useEffect(() => { - fetchBillingLimits() - }, [fetchBillingLimits]) - - const handleSubmit = async ( - e: React.FormEvent, - type: 'limit' | 'alert' - ) => { - e.preventDefault() - - const value = - type === 'limit' ? limits.limit_amount_gte : limits.alert_amount_gte - if (value === null) { - return - } - - await updateBillingLimit(type) - setEditMode((prev) => ({ ...prev, [type]: false })) - } - - const renderAmountInput = (type: 'limit' | 'alert') => { - const value = - type === 'limit' ? limits.limit_amount_gte : limits.alert_amount_gte - const originalValue = - type === 'limit' - ? originalLimits.limit_amount_gte - : originalLimits.alert_amount_gte - const isEditing = type === 'limit' ? editMode.limit : editMode.alert - - const buttonClasses = 'h-9 items-center' - - if (originalValue === null || isEditing) { - return ( -
-
- - setLimits({ - ...limits, - [type === 'limit' ? 'limit_amount_gte' : 'alert_amount_gte']: - Number(e.target.value) || null, - }) - } - placeholder={`${type === 'limit' ? 'Limit' : 'Alert'} Amount`} - /> -
- $ -
-
- - {originalValue !== null && ( - - )} -
- ) - } - - return ( -
-
- $ - - {originalValue} - -
- - -
- ) - } - - return ( - <> -
handleSubmit(e, 'limit')} className="space-y-2"> -

Enable Budget Limit

-

- If your team exceeds this threshold in a given month, - subsequent API requests will be blocked. -

-

- You will automatically receive email notifications when your usage - reaches 50%, 80%, 90%, and 100% of this - limit. -

-

- Caution: Enabling a Budget Limit may cause interruptions to your - service. Once your Budget Limit is reached, your team will not be able - to create new sandboxes in the given month unless the limit - is increased. -

-
{renderAmountInput('limit')}
-
- -
handleSubmit(e, 'alert')} className="space-y-2"> -

Set a Budget Alert

-

- If your team exceeds this threshold in a given month, you'll - receive an alert notification to {email}. - This will not result in any interruptions to your service. -

-
{renderAmountInput('alert')}
-
- - ) -} diff --git a/apps/web/src/components/Dashboard/Chart.tsx b/apps/web/src/components/Dashboard/Chart.tsx deleted file mode 100644 index ae54b05469..0000000000 --- a/apps/web/src/components/Dashboard/Chart.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { ResponsiveLine } from '@nivo/line' - -const generateTicks = (minValue: number, maxValue: number, numTicks: number): number[] => { - const range = maxValue - minValue - const step = range / (numTicks - 1) - - // Determine magnitude of the step to round it to a sensible value - const magnitude = Math.pow(10, Math.floor(Math.log10(step))) - const roundedStep = Math.ceil(step / magnitude) * magnitude - - // Round minValue down to the nearest roundedStep - const start = Math.floor(minValue / roundedStep) * roundedStep - - const ticks: number[] = [] - for (let i = 0; i < numTicks; i++) { - ticks.push(start + i * roundedStep) - } - - if (ticks[ticks.length - 1] < maxValue) { - ticks.push(Math.ceil(maxValue / roundedStep) * roundedStep) - } - - return ticks.map((tick) => parseFloat(tick.toFixed(2))) -} - -// If you wonder how to style this chart, I highly recommend this interactive tool: https://nivo.rocks/line/ -export default function LineChart({ series, ...props }) { - - const allValues = series.flatMap((s: any) => s.data.map((d: any) => d.y)) - const maxValue = Math.max(...allValues) // Add a 10% buffer to the maximum value - - const ticks = generateTicks(0, maxValue, 6) - - return ( -
- -
- ) -} - -const CustomTooltip = ({ point }) => ( -
- {point.data.y.toFixed(2)} -
-) diff --git a/apps/web/src/components/Dashboard/Developer.tsx b/apps/web/src/components/Dashboard/Developer.tsx deleted file mode 100644 index 6898c944da..0000000000 --- a/apps/web/src/components/Dashboard/Developer.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client' - -import { Button } from '../Button' -import { useState } from 'react' - -const DEFAULT_DOMAIN = 'e2b.dev' -const DOMAIN = process.env.NEXT_PUBLIC_DOMAIN || DEFAULT_DOMAIN - -export const DeveloperContent = ({ - domainState, -}: { - domainState: [string, (value: string) => void] -}) => { - const [domain, setDomain] = domainState - const [url, setUrl] = useState(domain) - - function isUrl(url: string) { - try { - new URL(url) - return true - } catch (e) { - return false - } - } - - function changeUrl(url: string) { - setUrl(url) - - // Add protocol if missing - let urlWithProtocol: string = url - if (!url.startsWith('http://') && !url.startsWith('https://')) { - urlWithProtocol = `https://${url}` - } - - if (isUrl(urlWithProtocol)) { - const domain = new URL(urlWithProtocol).host - let hostParts = domain.split('.') - if (hostParts.length > 2 && hostParts[0] === 'api') { - hostParts = hostParts.slice(1) - } - setDomain(hostParts.join('.')) - } - } - - return ( -
-
-

Dashboard settings

- - API URL - - Set API URL so the dashboard can connect to your E2B Cluster and - correctly display running sandboxes and templates. - - - {/* Env var has to be set if the domain is not equal to the default */} - {domain !== DEFAULT_DOMAIN && ( -
-

- Setting custom API URL in the E2B SDK & CLI -

-
- In your environment variables, set the E2B_DOMAIN{' '} - variable to your custom domain: -
-
-                E2B_DOMAIN=
-                {domain ?? 'Invalid URL'}
-              
-
-
- )} - -
- changeUrl(e.target.value)} - /> - -
-
-
- ) -} diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx deleted file mode 100644 index e5beec49b2..0000000000 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ /dev/null @@ -1,430 +0,0 @@ -import { Button } from '../Button' -import { Button as AlertDialogAction } from '../ui/button' -import { - AlertDialog, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog' -import { Edit, MoreVertical, Trash } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' -import { useToast } from '../ui/use-toast' -import { E2BUser, Team } from '@/utils/useUser' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '../ui/table' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '../ui/dropdown-menu' -import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' - -type TeamApiKey = { - id: string - value: string | null - maskedValue: string - name: string - createdBy: { - email: string - id: string - } | null - createdAt: string - lastUsed: string | null - updatedAt: string | null -} - -export const KeysContent = ({ - currentTeam, - user, - domain, -}: { - currentTeam: Team - user: E2BUser - domain: string -}) => { - const { toast } = useToast() - const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false) - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) - const [isKeyPreviewDialogOpen, setIsKeyPreviewDialogOpen] = useState(false) - const [currentKey, setCurrentKey] = useState(null) - const [newApiKeyInput, setNewApiKeyInput] = useState('') - const [apiKeys, setApiKeys] = useState([]) - - useEffect(() => { - async function fetchApiKeys() { - const res = await fetch( - getBillingUrl(domain, `/teams/${currentTeam.id}/api-keys`), - { - headers: { - 'X-USER-ACCESS-TOKEN': user.accessToken, - }, - } - ) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to fetch the team API keys', - }) - console.log(res.statusText) - return - } - - const keys = await res.json() - setApiKeys(keys) - } - - fetchApiKeys() - }, [domain, currentTeam, user.accessToken]) - - async function deleteApiKey() { - if (apiKeys.length === 1) { - toast({ - title: 'Cannot delete the last API key', - description: 'You must have at least one API key', - }) - return - } - - const res = await fetch( - getBillingUrl( - domain, - `/teams/${currentTeam.id}/api-keys/${currentKey?.id}` - ), - { - method: 'DELETE', - headers: { - 'X-USER-ACCESS-TOKEN': user.accessToken, - }, - } - ) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to delete the API key', - }) - } - - setApiKeys(apiKeys.filter((apiKey) => apiKey.id !== currentKey?.id)) - setCurrentKey(null) - setIsDeleteDialogOpen(false) - } - - async function createApiKey() { - const res = await fetch( - getBillingUrl(domain, `/teams/${currentTeam.id}/api-keys`), - { - method: 'POST', - headers: { - 'X-USER-ACCESS-TOKEN': user.accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: newApiKeyInput, - }), - } - ) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to create the API key', - }) - - return - } - - const newKey = await res.json() - - setApiKeys([...apiKeys, newKey]) - setNewApiKeyInput('') - setCurrentKey(newKey) - setIsKeyPreviewDialogOpen(true) - } - - async function updateApiKey() { - const res = await fetch( - getBillingUrl( - domain, - `/teams/${currentTeam.id}/api-keys/${currentKey?.id}` - ), - { - method: 'PATCH', - headers: { - 'X-USER-ACCESS-TOKEN': user.accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: newApiKeyInput, - }), - } - ) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to update the API key', - }) - } - - toast({ - title: 'API key updated', - }) - - setApiKeys( - apiKeys.map((apiKey) => - apiKey.id === currentKey?.id - ? { - ...apiKey, - name: newApiKeyInput, - } - : apiKey - ) - ) - - setNewApiKeyInput('') - setCurrentKey(null) - setIsKeyDialogOpen(false) - } - - const sortedApiKeys = useMemo(() => { - return apiKeys.sort((a, b) => { - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - }) - }, [apiKeys]) - - function copyApiKey() { - navigator.clipboard.writeText( - currentKey?.value || currentKey?.maskedValue || '' - ) - - toast({ - title: 'Copied API key to clipboard', - }) - } - - return ( -
-
- -
- - - - - Name - Key - Created by - Created at - - - - - {sortedApiKeys.length === 0 ? ( - - - Click on "Add API Key" button above to create your - first API key. - - - ) : ( - sortedApiKeys.map((apiKey, index) => ( - - {apiKey.name} - - {apiKey.maskedValue} - - {apiKey.createdBy?.email} - - {new Date(apiKey.createdAt).toLocaleString()} - - - - - - - - { - setNewApiKeyInput(apiKey.name) - setCurrentKey(apiKey) - setIsKeyDialogOpen(true) - }} - > - - Edit - - { - setCurrentKey(apiKey) - setIsDeleteDialogOpen(true) - }} - > - - Delete - - - - - - )) - )} - -
- - - - - - You are about to {currentKey ? 'edit' : 'create'} an API key - - -
{ - e.preventDefault() - currentKey ? updateApiKey() : createApiKey() - setIsKeyDialogOpen(false) - }} - > - { - setNewApiKeyInput(e.target.value) - }} - autoFocus - required - /> - - - { - setIsKeyDialogOpen(false) - setCurrentKey(null) - setNewApiKeyInput('') - }} - > - Cancel - - - {currentKey ? 'Update' : 'Create'} - - -
-
-
- - - - - Your API key - - You will only see the API key once. Make sure to copy it now. - - -
- - - - { - setIsKeyPreviewDialogOpen(false) - setCurrentKey(null) - }} - > - Cancel - - { - copyApiKey() - setIsKeyPreviewDialogOpen(false) - }} - > - Copy - - -
-
-
- - - - - - - - - You are about to delete an API key - - - This action cannot be undone. This will permanently delete the API - key with immediate effect. - - - - { - setIsDeleteDialogOpen(false) - setCurrentKey(null) - }} - > - Cancel - - - Delete - - - - -
- ) -} diff --git a/apps/web/src/components/Dashboard/Personal.tsx b/apps/web/src/components/Dashboard/Personal.tsx deleted file mode 100644 index f66d01b6d2..0000000000 --- a/apps/web/src/components/Dashboard/Personal.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client' - -import { useToast } from '../ui/use-toast' -import { Button } from '../Button' -import Link from 'next/link' -import { useState } from 'react' -import { Copy } from 'lucide-react' -import { E2BUser } from '@/utils/useUser' -import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' - -export const PersonalContent = ({ - user, - domain, -}: { - user: E2BUser - domain: string -}) => { - const { toast } = useToast() - const [hovered, setHovered] = useState(false) - const [email, setEmail] = useState(user.email) - - const maskAccessToken = (key: string) => { - const firstSeven = key.slice(0, 7) - const lastFour = key.slice(-4) - const stars = '*'.repeat(key.length - 11) // use fixed-width character - return `${firstSeven}${stars}${lastFour}` - } - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - toast({ - title: 'Access token copied to clipboard', - }) - } - - const updateUserEmail = async () => { - const res = await fetch(getBillingUrl(domain, '/users'), { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-User-Access-Token': `Bearer ${user.accessToken}`, - }, - body: JSON.stringify({ - email, - }), - }) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to update the email', - }) - console.log(res.status, res.statusText) - // TODO: Add sentry event here - return - } - - toast({ - title: 'Email updated', - }) - } - - return ( -
-
-

Personal settings

- - Email -
- setEmail(e.target.value)} - /> - -
- - User ID -
- -
- { - navigator.clipboard.writeText(user.id) - toast({ - title: 'User ID copied to clipboard', - }) - }} - > -

Copy your user ID

- -
- - Access token - - This is your personal access token. It is used in CLI (e.g. for - building new templates). - -
-
setHovered(true)} - onMouseLeave={() => setHovered(false)} - onClick={() => copyToClipboard(user.accessToken)} - > - {hovered ? user.accessToken : maskAccessToken(user.accessToken!)} -
-
- - {user.app_metadata.provider === 'email' && ( - <> -

Reset password

- - Resetting will send an email with a link to reset the password.{' '} -
-
-
- - - -
- - )} -
-
- ) -} diff --git a/apps/web/src/components/Dashboard/Sandboxes.tsx b/apps/web/src/components/Dashboard/Sandboxes.tsx deleted file mode 100644 index dc6165bb7b..0000000000 --- a/apps/web/src/components/Dashboard/Sandboxes.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' - -import { useState } from 'react' -import { useEffect } from 'react' -import { Team } from '@/utils/useUser' -import { getAPIUrl } from '@/app/(dashboard)/dashboard/utils' - -interface Sandbox { - alias: string - clientID: string - cpuCount: number - endAt: string - memoryMB: number - metadata: Record - sandboxID: string - startedAt: string - templateID: string -} - -export function SandboxesContent({ - team, - domain, -}: { - team: Team - domain: string -}) { - const [runningSandboxes, setRunningSandboxes] = useState([]) - - useEffect(() => { - function f() { - const apiKey = team.apiKeys[0] - if (apiKey) { - fetchSandboxes(domain, apiKey).then((newSandboxes) => { - if (newSandboxes) { - setRunningSandboxes(newSandboxes) - } - }) - } - } - - const interval = setInterval(() => { - f() - }, 5000) - - f() - // Cleanup interval on component unmount - return () => clearInterval(interval) - }, [team]) - - return ( -
- - - - Sandbox ID - Template ID - Alias - Started at - End at - vCPUs - RAM MiB - - - - {runningSandboxes.length === 0 ? ( - - - No running sandboxes - - - ) : ( - runningSandboxes.map((sandbox) => ( - - {sandbox.sandboxID} - {sandbox.templateID} - {sandbox.alias} - - {new Date(sandbox.startedAt).toLocaleString()} - - - {new Date(sandbox.endAt).toLocaleString()} - - {sandbox.cpuCount} - {sandbox.memoryMB} - - )) - )} - -
-
- ) -} - -async function fetchSandboxes( - domain: string, - apiKey: string -): Promise { - const res = await fetch(getAPIUrl(domain, '/sandboxes'), { - method: 'GET', - headers: { - 'X-API-KEY': apiKey, - }, - }) - try { - const data: Sandbox[] = await res.json() - - // 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/Team.tsx b/apps/web/src/components/Dashboard/Team.tsx deleted file mode 100644 index 02aa56a590..0000000000 --- a/apps/web/src/components/Dashboard/Team.tsx +++ /dev/null @@ -1,332 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { Button } from '../Button' -import { E2BUser, Team } from '@/utils/useUser' -import { toast } from '../ui/use-toast' -import { Copy } from 'lucide-react' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '../ui/table' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '../ui/alert-dialog' -import Spinner from '@/components/Spinner' -import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' - -interface TeamMember { - id: string - email: string - added_by: { - id: string - email: string - } | null - added_at: string -} - -const emailRegex = new RegExp( - '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$' -) - -export const TeamContent = ({ - team, - user, - teams, - setTeams, - setCurrentTeam, - domain, -}: { - team: Team - user: E2BUser - teams: Team[] - setTeams: (teams: Team[]) => void - setCurrentTeam: (team: Team) => void - domain: string -}) => { - const [isDialogOpen, setIsDialogOpen] = useState(false) - const [currentMemberId, setCurrentMemberId] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [members, setMembers] = useState([]) - const [teamName, setTeamName] = useState(team.name) - const [userToAdd, setUserToAdd] = useState('') - const [userAdded, setUserAdded] = useState(false) - - useEffect(() => { - const getTeamMembers = async () => { - const res = await fetch( - getBillingUrl(domain, `/teams/${team.id}/users`), - { - headers: { - 'X-User-Access-Token': user.accessToken, - 'X-Team-API-Key': team.apiKeys[0], - }, - } - ) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to fetch the team members', - }) - console.log(res.statusText) - // TODO: Add sentry event here - return - } - const members = (await res.json()).filter( - (member: TeamMember) => member.id !== user.id - ) - setMembers(members) - setIsLoading(false) - } - - getTeamMembers() - }, [user, userAdded, team, domain]) - - useEffect(() => { - setTeamName(team.name) - }, [team]) - - const closeDialog = () => setIsDialogOpen(false) - const openDialog = (id: string) => { - setCurrentMemberId(id) - setIsDialogOpen(true) - } - - const deleteUserFromTeam = async () => { - const res = await fetch(getBillingUrl(domain, `/teams/${team.id}/users`), { - method: 'DELETE', - headers: { - 'X-User-Access-Token': user.accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user_id: currentMemberId }), - }) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to delete the user from the team', - }) - console.log(res.statusText) - // TODO: Add sentry event here - return - } - setMembers(members.filter((member) => member.id !== currentMemberId)) - closeDialog() - } - - const changeTeamName = async () => { - const res = await fetch(getBillingUrl(domain, `/teams/${team.id}`), { - headers: { - 'X-Team-API-Key': team.apiKeys[0], - 'Content-Type': 'application/json', - }, - method: 'PATCH', - body: JSON.stringify({ name: teamName }), - }) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: 'We were unable to change the team name', - }) - console.log(res.statusText) - return - } - - toast({ - title: 'Team name changed', - }) - setTeamName(teamName) - setTeams( - teams.map((t) => (t.id === team.id ? { ...t, name: teamName } : t)) - ) - setCurrentTeam({ ...team, name: teamName }) - } - - const addUserToTeam = async () => { - if (!emailRegex.test(userToAdd)) { - toast({ - title: 'Invalid email', - description: 'The email must be a valid email address', - }) - return - } - - const res = await fetch(getBillingUrl(domain, `/teams/${team.id}/users`), { - headers: { - 'X-User-Access-Token': user.accessToken, - 'Content-Type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ user_email: userToAdd.trim() }), - }) - - if (!res.ok) { - toast({ - title: 'An error occurred', - description: - 'We were unable to add the user to the team. Make sure the user is registered with the email address provided.', - }) - console.log(res.statusText) - return - } - - setUserToAdd('') - setUserAdded(!userAdded) - toast({ - title: 'User added to team', - }) - } - - return ( -
-

Team name

-
- { - e.preventDefault() - setTeamName(e.target.value) - }} - /> - -
-

Team ID

-
- -
- - { - navigator.clipboard.writeText(team.id) - toast({ - title: 'Team ID copied to clipboard', - }) - }} - > -

Copy your team ID

- -
- -

Add members to your team

- -
- { - e.preventDefault() - setUserToAdd(e.target.value) - }} - /> - -
- -

Team members

- {isLoading ? ( -
- -
- ) : ( - - - - Email - Added by - Added at - - - - - {members.length === 0 ? ( - - - No members found - - - ) : ( - members.map((user) => ( - - {user.email} - {user.added_by?.email} - - {user.added_at - ? new Date(user.added_at).toLocaleString() - : ''} - - - - - - )) - )} - -
- )} - - - - - - You are about to remove a member from the team - - - This action cannot be undone. This will permanently remove the - member from the team. - - - - - Cancel - - deleteUserFromTeam()} - > - Remove - - - - -
- ) -} diff --git a/apps/web/src/components/Dashboard/Templates.tsx b/apps/web/src/components/Dashboard/Templates.tsx deleted file mode 100644 index bd1319d839..0000000000 --- a/apps/web/src/components/Dashboard/Templates.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' - -import { useState } from 'react' -import { useEffect } from 'react' -import { E2BUser } from '@/utils/useUser' -import { Lock, LockOpen, MoreVertical, Trash } from 'lucide-react' -import { - DropdownMenu, - DropdownMenuItem, - DropdownMenuContent, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' -import { - AlertDialog, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogAction, -} from '../ui/alert-dialog' -import { toast } from '../ui/use-toast' -import { getAPIUrl } from '@/app/(dashboard)/dashboard/utils' - -interface Template { - aliases: string[] - buildID: string - cpuCount: number - memoryMB: number - public: boolean - templateID: string - createdAt: string - updatedAt: string - createdBy: { - email: string - id: string - } | null -} - -export function TemplatesContent({ - user, - teamId, - domain, -}: { - user: E2BUser - teamId: string - domain: string -}) { - const [templates, setTemplates] = useState([]) - const [currentTemplate, setCurrentTemplate] = useState