diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx index 872b149c5..4224bf658 100644 --- a/apps/web/src/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx @@ -87,13 +87,9 @@ const Dashboard = ({ user }) => { const [teams, setTeams] = useState([]) const [currentTeam, setCurrentTeam] = useState(null) - const apiUrlState = useLocalStorage( - 'apiUrl', - process.env.NEXT_PUBLIC_API_URL || '' - ) - const billingUrlState = useLocalStorage( - 'billingUrl', - process.env.NEXT_PUBLIC_BILLING_API_URL || '' + const domainState = useLocalStorage( + 'e2bDomain', + process.env.NEXT_PUBLIC_DOMAIN || '' ) const initialTab = @@ -166,8 +162,7 @@ const Dashboard = ({ user }) => { teams={teams} setTeams={setTeams} setCurrentTeam={setCurrentTeam} - apiUrlState={apiUrlState} - billingUrlState={billingUrlState} + domainState={domainState} /> @@ -230,16 +225,18 @@ const MenuItem = ({ onClick: () => void }) => (

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

@@ -253,8 +250,7 @@ function MainContent({ teams, setTeams, setCurrentTeam, - apiUrlState, - billingUrlState, + domainState, }: { selectedItem: MenuLabel user: E2BUser @@ -262,34 +258,29 @@ function MainContent({ teams: Team[] setTeams: (teams: Team[]) => void setCurrentTeam: (team: Team) => void - apiUrlState: [string, (value: string) => void] - billingUrlState: [string, (value: string) => void] + domainState: [string, (value: string) => void] }) { switch (selectedItem) { case 'personal': - return + return case 'keys': return ( - + ) case 'sandboxes': - return + return case 'templates': return ( ) case 'usage': - return + return case 'billing': - return + return case 'team': return ( ) case 'developer': - return ( - - ) + return default: return } diff --git a/apps/web/src/app/(dashboard)/dashboard/utils.ts b/apps/web/src/app/(dashboard)/dashboard/utils.ts new file mode 100644 index 000000000..c581e6d6a --- /dev/null +++ b/apps/web/src/app/(dashboard)/dashboard/utils.ts @@ -0,0 +1,31 @@ +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/components/Dashboard/Billing.tsx b/apps/web/src/components/Dashboard/Billing.tsx index 8c6a01d84..65e7c6c13 100644 --- a/apps/web/src/components/Dashboard/Billing.tsx +++ b/apps/web/src/components/Dashboard/Billing.tsx @@ -12,6 +12,7 @@ import { } 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', { @@ -29,10 +30,10 @@ interface Invoice { export const BillingContent = ({ team, - billingUrl, + domain, }: { team: Team - billingUrl: string + domain: string }) => { const [invoices, setInvoices] = useState([]) const [credits, setCredits] = useState(null) @@ -40,11 +41,14 @@ export const BillingContent = ({ useEffect(() => { const getInvoices = async function getInvoices() { setInvoices([]) - const res = await fetch(`${billingUrl}/teams/${team.id}/invoices`, { - headers: { - 'X-Team-API-Key': team.apiKeys[0], - }, - }) + 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) @@ -55,17 +59,20 @@ export const BillingContent = ({ setInvoices(invoices) setCredits(null) - const creditsRes = await fetch(`${billingUrl}/teams/${team.id}/usage`, { - headers: { - 'X-Team-API-Key': team.apiKeys[0], - }, - }) + 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() - }, [team]) + }, [domain, team]) return (
@@ -108,7 +115,7 @@ export const BillingContent = ({

Pro tier

- +
  • One-time $100 credits
  • diff --git a/apps/web/src/components/Dashboard/Developer.tsx b/apps/web/src/components/Dashboard/Developer.tsx index f2c3eda74..6898c944d 100644 --- a/apps/web/src/components/Dashboard/Developer.tsx +++ b/apps/web/src/components/Dashboard/Developer.tsx @@ -1,15 +1,18 @@ 'use client' import { Button } from '../Button' +import { useState } from 'react' -const defaultAPIUrl = process.env.NEXT_PUBLIC_API_URL || '' +const DEFAULT_DOMAIN = 'e2b.dev' +const DOMAIN = process.env.NEXT_PUBLIC_DOMAIN || DEFAULT_DOMAIN export const DeveloperContent = ({ - apiUrlState, + domainState, }: { - apiUrlState: [string, (value: string) => void] + domainState: [string, (value: string) => void] }) => { - const [apiUrl, setApiUrl] = apiUrlState + const [domain, setDomain] = domainState + const [url, setUrl] = useState(domain) function isUrl(url: string) { try { @@ -20,12 +23,23 @@ export const DeveloperContent = ({ } } - function removeApiSubdomain(url: URL): string { - const hostParts = url.host.split('.') - if (hostParts.length > 2 && hostParts[0] === 'api') { - return hostParts.slice(1).join('.') + 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 url.host } return ( @@ -35,10 +49,12 @@ export const DeveloperContent = ({ API URL - Set API URL so the dashboard can connect to your E2B Cluster and correctly display running sandboxes and templates. + Set API URL so the dashboard can connect to your E2B Cluster and + correctly display running sandboxes and templates. - {apiUrl !== defaultAPIUrl && ( + {/* Env var has to be set if the domain is not equal to the default */} + {domain !== DEFAULT_DOMAIN && (
    - In your environment variables, set the E2B_DOMAIN variable to your - custom domain: + In your environment variables, set the E2B_DOMAIN{' '} + variable to your custom domain:
    -                E2B_DOMAIN={
    -                  isUrl(apiUrl)
    -                    ? `"${removeApiSubdomain(new URL(apiUrl))}"`
    -                    : 'Invalid URL'
    -                }
    +                E2B_DOMAIN=
    +                {domain ?? 'Invalid URL'}
                   
    @@ -65,13 +78,10 @@ export const DeveloperContent = ({ setApiUrl(e.target.value)} + value={url} + onChange={(e) => changeUrl(e.target.value)} /> -
diff --git a/apps/web/src/components/Dashboard/Keys.tsx b/apps/web/src/components/Dashboard/Keys.tsx index 8790d7cc7..e5beec49b 100644 --- a/apps/web/src/components/Dashboard/Keys.tsx +++ b/apps/web/src/components/Dashboard/Keys.tsx @@ -28,6 +28,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu' +import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' type TeamApiKey = { id: string @@ -46,11 +47,11 @@ type TeamApiKey = { export const KeysContent = ({ currentTeam, user, - billingUrl, + domain, }: { currentTeam: Team user: E2BUser - billingUrl: string + domain: string }) => { const { toast } = useToast() const [isKeyDialogOpen, setIsKeyDialogOpen] = useState(false) @@ -63,7 +64,7 @@ export const KeysContent = ({ useEffect(() => { async function fetchApiKeys() { const res = await fetch( - `${billingUrl}/teams/${currentTeam.id}/api-keys`, + getBillingUrl(domain, `/teams/${currentTeam.id}/api-keys`), { headers: { 'X-USER-ACCESS-TOKEN': user.accessToken, @@ -85,7 +86,7 @@ export const KeysContent = ({ } fetchApiKeys() - }, [currentTeam]) + }, [domain, currentTeam, user.accessToken]) async function deleteApiKey() { if (apiKeys.length === 1) { @@ -97,7 +98,10 @@ export const KeysContent = ({ } const res = await fetch( - `${billingUrl}/teams/${currentTeam.id}/api-keys/${currentKey?.id}`, + getBillingUrl( + domain, + `/teams/${currentTeam.id}/api-keys/${currentKey?.id}` + ), { method: 'DELETE', headers: { @@ -119,16 +123,19 @@ export const KeysContent = ({ } async function createApiKey() { - const res = await fetch(`${billingUrl}/teams/${currentTeam.id}/api-keys`, { - method: 'POST', - headers: { - 'X-USER-ACCESS-TOKEN': user.accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: newApiKeyInput, - }), - }) + 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({ @@ -149,7 +156,10 @@ export const KeysContent = ({ async function updateApiKey() { const res = await fetch( - `${billingUrl}/teams/${currentTeam.id}/api-keys/${currentKey?.id}`, + getBillingUrl( + domain, + `/teams/${currentTeam.id}/api-keys/${currentKey?.id}` + ), { method: 'PATCH', headers: { diff --git a/apps/web/src/components/Dashboard/Personal.tsx b/apps/web/src/components/Dashboard/Personal.tsx index 419163685..f66d01b6d 100644 --- a/apps/web/src/components/Dashboard/Personal.tsx +++ b/apps/web/src/components/Dashboard/Personal.tsx @@ -6,13 +6,14 @@ 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, - billingUrl, + domain, }: { user: E2BUser - billingUrl: string + domain: string }) => { const { toast } = useToast() const [hovered, setHovered] = useState(false) @@ -33,7 +34,7 @@ export const PersonalContent = ({ } const updateUserEmail = async () => { - const res = await fetch(`${billingUrl}/users`, { + const res = await fetch(getBillingUrl(domain, '/users'), { method: 'PATCH', headers: { 'Content-Type': 'application/json', diff --git a/apps/web/src/components/Dashboard/Sandboxes.tsx b/apps/web/src/components/Dashboard/Sandboxes.tsx index 0a5e7b90e..dc6165bb7 100644 --- a/apps/web/src/components/Dashboard/Sandboxes.tsx +++ b/apps/web/src/components/Dashboard/Sandboxes.tsx @@ -10,6 +10,7 @@ import { import { useState } from 'react' import { useEffect } from 'react' import { Team } from '@/utils/useUser' +import { getAPIUrl } from '@/app/(dashboard)/dashboard/utils' interface Sandbox { alias: string @@ -25,10 +26,10 @@ interface Sandbox { export function SandboxesContent({ team, - apiUrl, + domain, }: { team: Team - apiUrl: string + domain: string }) { const [runningSandboxes, setRunningSandboxes] = useState([]) @@ -36,7 +37,7 @@ export function SandboxesContent({ function f() { const apiKey = team.apiKeys[0] if (apiKey) { - fetchSandboxes(apiUrl, apiKey).then((newSandboxes) => { + fetchSandboxes(domain, apiKey).then((newSandboxes) => { if (newSandboxes) { setRunningSandboxes(newSandboxes) } @@ -101,10 +102,10 @@ export function SandboxesContent({ } async function fetchSandboxes( - apiUrl: string, + domain: string, apiKey: string ): Promise { - const res = await fetch(`${apiUrl}/sandboxes`, { + const res = await fetch(getAPIUrl(domain, '/sandboxes'), { method: 'GET', headers: { 'X-API-KEY': apiKey, diff --git a/apps/web/src/components/Dashboard/Team.tsx b/apps/web/src/components/Dashboard/Team.tsx index ff580dbed..02aa56a59 100644 --- a/apps/web/src/components/Dashboard/Team.tsx +++ b/apps/web/src/components/Dashboard/Team.tsx @@ -24,6 +24,7 @@ import { AlertDialogTitle, } from '../ui/alert-dialog' import Spinner from '@/components/Spinner' +import { getBillingUrl } from '@/app/(dashboard)/dashboard/utils' interface TeamMember { id: string @@ -45,14 +46,14 @@ export const TeamContent = ({ teams, setTeams, setCurrentTeam, - billingUrl, + domain, }: { team: Team user: E2BUser teams: Team[] setTeams: (teams: Team[]) => void setCurrentTeam: (team: Team) => void - billingUrl: string + domain: string }) => { const [isDialogOpen, setIsDialogOpen] = useState(false) const [currentMemberId, setCurrentMemberId] = useState(null) @@ -64,12 +65,15 @@ export const TeamContent = ({ useEffect(() => { const getTeamMembers = async () => { - const res = await fetch(`${billingUrl}/teams/${team.id}/users`, { - headers: { - 'X-User-Access-Token': user.accessToken, - 'X-Team-API-Key': team.apiKeys[0], - }, - }) + 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({ @@ -88,7 +92,7 @@ export const TeamContent = ({ } getTeamMembers() - }, [user, userAdded, team]) + }, [user, userAdded, team, domain]) useEffect(() => { setTeamName(team.name) @@ -101,7 +105,7 @@ export const TeamContent = ({ } const deleteUserFromTeam = async () => { - const res = await fetch(`${billingUrl}/teams/${team.id}/users`, { + const res = await fetch(getBillingUrl(domain, `/teams/${team.id}/users`), { method: 'DELETE', headers: { 'X-User-Access-Token': user.accessToken, @@ -124,7 +128,7 @@ export const TeamContent = ({ } const changeTeamName = async () => { - const res = await fetch(`${billingUrl}/teams/${team.id}`, { + const res = await fetch(getBillingUrl(domain, `/teams/${team.id}`), { headers: { 'X-Team-API-Key': team.apiKeys[0], 'Content-Type': 'application/json', @@ -161,7 +165,7 @@ export const TeamContent = ({ return } - const res = await fetch(`${billingUrl}/teams/${team.id}/users`, { + const res = await fetch(getBillingUrl(domain, `/teams/${team.id}/users`), { headers: { 'X-User-Access-Token': user.accessToken, 'Content-Type': 'application/json', diff --git a/apps/web/src/components/Dashboard/Templates.tsx b/apps/web/src/components/Dashboard/Templates.tsx index 939df885c..85c0a0337 100644 --- a/apps/web/src/components/Dashboard/Templates.tsx +++ b/apps/web/src/components/Dashboard/Templates.tsx @@ -28,6 +28,7 @@ import { AlertDialogAction, } from '../ui/alert-dialog' import { toast } from '../ui/use-toast' +import { getAPIUrl } from '@/app/(dashboard)/dashboard/utils' interface Template { aliases: string[] @@ -47,11 +48,11 @@ interface Template { export function TemplatesContent({ user, teamId, - apiUrl, + domain, }: { user: E2BUser teamId: string - apiUrl: string + domain: string }) { const [templates, setTemplates] = useState([]) const [currentTemplate, setCurrentTemplate] = useState