Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use billing subdomain to simplify dashboard for custom domains #514

Merged
merged 5 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 20 additions & 33 deletions apps/web/src/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,9 @@ const Dashboard = ({ user }) => {
const [teams, setTeams] = useState<Team[]>([])
const [currentTeam, setCurrentTeam] = useState<Team | null>(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 =
Expand Down Expand Up @@ -166,8 +162,7 @@ const Dashboard = ({ user }) => {
teams={teams}
setTeams={setTeams}
setCurrentTeam={setCurrentTeam}
apiUrlState={apiUrlState}
billingUrlState={billingUrlState}
domainState={domainState}
/>
</div>
</>
Expand Down Expand Up @@ -230,16 +225,18 @@ const MenuItem = ({
onClick: () => void
}) => (
<div
className={`flex w-fit md:w-full hover:bg-[#995100] hover:cursor-pointer rounded-lg items-center p-2 space-x-2 ${selected ? 'bg-[#995100]' : ''
}`}
className={`flex w-fit md:w-full hover:bg-[#995100] hover:cursor-pointer rounded-lg items-center p-2 space-x-2 ${
selected ? 'bg-[#995100]' : ''
}`}
onClick={onClick}
>
<Icon width={20} height={20} />
<p
className={`${!label || !window.matchMedia('(min-width: 768)').matches
className={`${
!label || !window.matchMedia('(min-width: 768)').matches
? 'sr-only sm:not-sr-only'
: ''
}`}
}`}
>
{label[0].toUpperCase() + label.slice(1)}
</p>
Expand All @@ -253,43 +250,37 @@ function MainContent({
teams,
setTeams,
setCurrentTeam,
apiUrlState,
billingUrlState,
domainState,
}: {
selectedItem: MenuLabel
user: E2BUser
team: Team
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 <PersonalContent user={user} billingUrl={billingUrlState[0]} />
return <PersonalContent user={user} domain={domainState[0]} />
case 'keys':
return (
<KeysContent
currentTeam={team}
user={user}
billingUrl={billingUrlState[0]}
/>
<KeysContent currentTeam={team} user={user} domain={domainState[0]} />
)
case 'sandboxes':
return <SandboxesContent team={team} apiUrl={apiUrlState[0]} />
return <SandboxesContent team={team} domain={domainState[0]} />
case 'templates':
return (
<TemplatesContent
user={user}
teamId={team.id}
apiUrl={apiUrlState[0]}
domain={domainState[0]}
/>
)
case 'usage':
return <UsageContent team={team} billingUrl={billingUrlState[0]} />
return <UsageContent team={team} domain={domainState[0]} />
case 'billing':
return <BillingContent team={team} billingUrl={billingUrlState[0]} />
return <BillingContent team={team} domain={domainState[0]} />
case 'team':
return (
<TeamContent
Expand All @@ -298,15 +289,11 @@ function MainContent({
teams={teams}
setTeams={setTeams}
setCurrentTeam={setCurrentTeam}
billingUrl={billingUrlState[0]}
domain={domainState[0]}
/>
)
case 'developer':
return (
<DeveloperContent
apiUrlState={apiUrlState}
/>
)
return <DeveloperContent domainState={domainState} />
default:
return <ErrorContent />
}
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/app/(dashboard)/dashboard/utils.ts
Original file line number Diff line number Diff line change
@@ -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)
}
35 changes: 21 additions & 14 deletions apps/web/src/components/Dashboard/Billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand All @@ -29,22 +30,25 @@ interface Invoice {

export const BillingContent = ({
team,
billingUrl,
domain,
}: {
team: Team
billingUrl: string
domain: string
}) => {
const [invoices, setInvoices] = useState<Invoice[]>([])
const [credits, setCredits] = useState<number | null>(null)

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)
Expand All @@ -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 (
<div className="flex flex-col w-full">
Expand Down Expand Up @@ -108,7 +115,7 @@ export const BillingContent = ({
<div className="flex flex-col items-start justify-center pb-10">
<div className="flex items-center space-x-4">
<h2>Pro tier</h2>
<SwitchToProButton billingApiURL={billingUrl} team={team} />
<SwitchToProButton domain={domain} team={team} />
</div>
<ul className="flex flex-col list-disc list-inside text-neutral-400">
<li>One-time $100 credits</li>
Expand Down
58 changes: 34 additions & 24 deletions apps/web/src/components/Dashboard/Developer.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(domain)

function isUrl(url: string) {
try {
Expand All @@ -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 (
Expand All @@ -35,10 +49,12 @@ export const DeveloperContent = ({

<span className="font-bold text-neutral-300 pb-2">API URL</span>
<span className="text-sm text-neutral-300 pb-4">
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.
</span>

{apiUrl !== defaultAPIUrl && (
{/* Env var has to be set if the domain is not equal to the default */}
{domain !== DEFAULT_DOMAIN && (
<div
className="bg-white/5 border-l-4 border-orange-500/20 text-gray-300 p-4 mb-4 lg:w-1/2 w-full"
role="alert"
Expand All @@ -47,15 +63,12 @@ export const DeveloperContent = ({
Setting custom API URL in the E2B SDK & CLI
</h4>
<div className="text-sm text-gray-400">
In your environment variables, set the <code>E2B_DOMAIN</code> variable to your
custom domain:
In your environment variables, set the <code>E2B_DOMAIN</code>{' '}
variable to your custom domain:
<br />
<pre className="mt-2 w-full bg-neutral-700 px-2 py-1 rounded-md text-brand-300">
E2B_DOMAIN={
isUrl(apiUrl)
? `"${removeApiSubdomain(new URL(apiUrl))}"`
: 'Invalid URL'
}
E2B_DOMAIN=
{domain ?? 'Invalid URL'}
</pre>
</div>
</div>
Expand All @@ -65,13 +78,10 @@ export const DeveloperContent = ({
<input
type="text"
className="w-1/2 border border-white/10 text-sm focus:outline-none outline-none rounded-md p-2"
value={apiUrl}
onChange={(e) => setApiUrl(e.target.value)}
value={url}
onChange={(e) => changeUrl(e.target.value)}
/>
<Button
variant="desctructive"
onClick={() => setApiUrl(defaultAPIUrl)}
>
<Button variant="desctructive" onClick={() => changeUrl(DOMAIN)}>
Reset to default
</Button>
</div>
Expand Down
Loading
Loading