Skip to content
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
7 changes: 4 additions & 3 deletions src/app/dashboard/[teamSlug]/byoc/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AUTH_URLS } from '@/configs/urls'
import { featureFlags } from '@/core/modules/feature-flags/feature-flags.server'
import { getAuthContext } from '@/core/server/auth'
import { getTeamIdFromSlug } from '@/core/server/functions/team/get-team-id-from-slug'
import { ByocSetup } from '@/features/dashboard/byoc/byoc-setup'

export const metadata: Metadata = {
title: 'BYOC - E2B',
Expand Down Expand Up @@ -31,7 +32,7 @@ export default async function ByocPage({ params }: ByocPageProps) {
notFound()
}

const byocEnabled = await featureFlags.isEnabled('byocEnabled', {
const byocSetup = await featureFlags.getPayload('byocSetup', {
user: {
id: authContext.user.id,
email: authContext.user.email ?? undefined,
Expand All @@ -42,9 +43,9 @@ export default async function ByocPage({ params }: ByocPageProps) {
},
})

if (!byocEnabled) {
if (!byocSetup.enabled) {
notFound()
}

return null
return <ByocSetup config={byocSetup} />
}
8 changes: 5 additions & 3 deletions src/configs/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ type SidebarNavArgs = {
teamSlug?: string
}

export type SidebarFeatureFlagId = BooleanFeatureFlagId | 'byocSetup'

export type SidebarNavItem = {
label: string
href: (args: SidebarNavArgs) => string
icon: Icon
group?: string
activeMatch?: string
featureFlag?: BooleanFeatureFlagId
featureFlag?: SidebarFeatureFlagId
}

export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [
Expand Down Expand Up @@ -69,7 +71,7 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [
href: (args) => PROTECTED_URLS.BYOC(args.teamSlug!),
icon: CloudIcon,
activeMatch: `/dashboard/*/byoc`,
featureFlag: 'byocEnabled',
featureFlag: 'byocSetup',
},
...(INCLUDE_ARGUS
? [
Expand Down Expand Up @@ -148,7 +150,7 @@ export const SIDEBAR_ALL_LINKS = [...SIDEBAR_MAIN_LINKS, ...SIDEBAR_EXTRA_LINKS]

export function filterSidebarLinks(
links: SidebarNavItem[],
isEnabled: (flagId: BooleanFeatureFlagId) => boolean
isEnabled: (flagId: SidebarFeatureFlagId) => boolean
) {
return links.filter(
(link) => !link.featureFlag || isEnabled(link.featureFlag)
Expand Down
7 changes: 0 additions & 7 deletions src/core/modules/feature-flags/boolean-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ export const BOOLEAN_FEATURE_FLAGS = {
description: 'Enables the dashboard agents launcher.',
exposure: 'both',
},
byocEnabled: {
kind: 'boolean',
key: 'byoc_enabled',
defaultValue: false,
description: 'Enables the dashboard BYOC page.',
exposure: 'both',
},
connectionsEnabled: {
kind: 'boolean',
key: 'connections_enabled',
Expand Down
40 changes: 40 additions & 0 deletions src/core/modules/feature-flags/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,48 @@ export const developmentConnectionSchema = z.object({

export type DevelopmentConnection = z.infer<typeof developmentConnectionSchema>

const gcpRegionSchema = z
.string()
.trim()
.regex(/^[a-z][a-z0-9-]{2,62}$/)

const byocSetupTemplateSchema = z.string().trim().min(1).max(50_000)

export const byocSetupSchema = z.object({
enabled: z.literal(true),
principal: z
.string()
.trim()
.regex(
/^serviceAccount:[a-z][a-z0-9-]{4,28}[a-z0-9]@[a-z][a-z0-9-]{4,28}[a-z0-9]\.iam\.gserviceaccount\.com$/
),
regions: z
.array(gcpRegionSchema)
.min(1)
.max(20)
.refine((regions) => new Set(regions).size === regions.length),
templates: z.object({
gcloud: byocSetupTemplateSchema,
terraform: byocSetupTemplateSchema,
}),
})

export type ByocSetupConfig = z.infer<typeof byocSetupSchema>
export type ByocSetupValue = ByocSetupConfig | { enabled: false }

export const FEATURE_FLAGS = {
...BOOLEAN_FEATURE_FLAGS,
byocSetup: {
kind: 'payload',
key: 'byoc_setup',
defaultValue: { enabled: false } as ByocSetupValue,
schema: z.discriminatedUnion('enabled', [
z.object({ enabled: z.literal(false) }),
byocSetupSchema,
]),
description: 'Configures team-targeted BYOC onboarding.',
exposure: 'both',
},
developmentConnections: {
kind: 'payload',
key: 'dev_connections',
Expand Down
16 changes: 12 additions & 4 deletions src/core/modules/feature-flags/feature-flags.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { EvaluatedFeatureFlag } from '@/core/modules/feature-flags/types'
type FeatureFlagsContextValue = {
flags: EvaluatedFeatureFlag[]
isEnabled(flagId: BooleanFeatureFlagId): boolean
hasPayload(flagId: 'byocSetup'): boolean
}

const FeatureFlagsContext = createContext<FeatureFlagsContextValue | undefined>(
Expand Down Expand Up @@ -47,10 +48,17 @@ export function FeatureFlagsProvider({
[flagsById]
)

const value = useMemo(
() => ({ flags: initialFlags, isEnabled }),
[initialFlags, isEnabled]
)
const hasPayload = (flagId: 'byocSetup') => {
const value = flagsById.get(flagId)?.value
return (
typeof value === 'object' &&
value !== null &&
'enabled' in value &&
value.enabled === true
)
}

const value = { flags: initialFlags, hasPayload, isEnabled }

return (
<FeatureFlagsContext.Provider value={value}>
Expand Down
9 changes: 4 additions & 5 deletions src/core/modules/feature-flags/feature-flags.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,12 @@ export function createFeatureFlagService(
},

async getPayload(flagId, context) {
const flag = FEATURE_FLAGS[flagId]
const flag = FEATURE_FLAGS[flagId] as PayloadFeatureFlagDefinition<
PayloadFeatureFlagValue<typeof flagId>
>
const snapshot = await provider.evaluate(context, [flag])

return parsePayload(
flag,
snapshot.getPayload(flag.key)
) as PayloadFeatureFlagValue<typeof flagId>
return parsePayload(flag, snapshot.getPayload(flag.key))
},

async evaluateAll(context) {
Expand Down
174 changes: 174 additions & 0 deletions src/features/dashboard/byoc/byoc-setup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
'use client'

import { useState } from 'react'
import type { ByocSetupConfig } from '@/core/modules/feature-flags/definitions'
import { Page } from '@/features/dashboard/layouts/page'
import { CodeBlock } from '@/ui/code-block'
import { CloudIcon, InfoIcon, TerminalIcon } from '@/ui/primitives/icons'
import { Input } from '@/ui/primitives/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/ui/primitives/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs'
import { isValidGcpProjectId, renderByocSetupTemplate } from './terraform'

const PROJECT_PLACEHOLDER = 'YOUR_GCP_PROJECT_ID'

export function ByocSetup({ config }: { config: ByocSetupConfig }) {
const [projectId, setProjectId] = useState('')
const [region, setRegion] = useState(() => config.regions[0] ?? '')
const trimmedProjectId = projectId.trim()
const projectIdValid = isValidGcpProjectId(trimmedProjectId)
const projectIdInvalid = trimmedProjectId.length > 0 && !projectIdValid
const templateValues = {
principal: config.principal,
projectId: projectIdValid ? trimmedProjectId : PROJECT_PLACEHOLDER,
region,
}
const templates = {
gcloud: renderByocSetupTemplate({
...templateValues,
template: config.templates.gcloud,
}),
terraform: renderByocSetupTemplate({
...templateValues,
template: config.templates.terraform,
}),
}

return (
<Page className="max-w-[1100px]">
<div className="flex flex-col gap-6">
<header className="flex max-w-2xl flex-col gap-1">
<div className="flex items-center gap-2">
<CloudIcon className="text-accent-main-highlight size-5" />
<h1 className="prose-title text-fg">Set up BYOC</h1>
</div>
<p className="prose-body text-fg-tertiary">
Create the project-local identity E2B will use to deploy and operate
your region. E2B receives impersonation access, never a service
account key.
</p>
</header>

<div className="border-stroke grid min-w-0 border-y lg:grid-cols-[minmax(260px,0.55fr)_minmax(0,1.45fr)]">
<section className="border-stroke flex min-w-0 flex-col gap-5 px-3 py-5 md:px-5 lg:border-r">
<div>
<p className="prose-label text-fg-tertiary">Configuration</p>
<h2 className="prose-headline-small text-fg mt-1">
Choose the destination
</h2>
</div>

<label className="flex flex-col gap-2" htmlFor="byoc-region">
<span className="prose-body-highlight text-fg">Region</span>
<Select onValueChange={setRegion} value={region}>
<SelectTrigger id="byoc-region">
<SelectValue />
</SelectTrigger>
<SelectContent>
{config.regions.map((availableRegion) => (
<SelectItem key={availableRegion} value={availableRegion}>
{availableRegion}
</SelectItem>
))}
</SelectContent>
</Select>
</label>

<label className="flex flex-col gap-2" htmlFor="byoc-project-id">
<span className="prose-body-highlight text-fg">
Google Cloud project ID
</span>
<Input
aria-describedby="byoc-project-id-help"
aria-invalid={projectIdInvalid}
autoComplete="off"
className="font-mono"
id="byoc-project-id"
onChange={(event) => setProjectId(event.currentTarget.value)}
placeholder="your-gcp-project-id"
spellCheck={false}
value={projectId}
/>
<p
className={
projectIdInvalid
? 'prose-caption text-accent-error-highlight'
: 'prose-caption text-fg-tertiary'
}
id="byoc-project-id-help"
>
{projectIdInvalid
? 'Enter a valid Google Cloud project ID.'
: 'The Terraform preview updates as you type.'}
</p>
</label>

<div className="border-stroke bg-bg-1 flex min-w-0 gap-3 border p-3">
<InfoIcon className="text-fg-tertiary mt-0.5 size-4 shrink-0" />
<div className="min-w-0">
<p className="prose-caption text-fg-tertiary">
E2B access principal
</p>
<p className="prose-caption text-fg mt-1 break-all font-mono">
{config.principal}
</p>
</div>
</div>
</section>

<section className="bg-bg-1 flex min-w-0 flex-col gap-4 px-3 py-5 md:px-5">
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between sm:gap-4">
<div>
<p className="prose-label text-fg-tertiary">Generated file</p>
<h2 className="prose-headline-small text-fg mt-1">
Access setup
</h2>
</div>
<p className="prose-caption text-fg-tertiary">
Choose Terraform or gcloud
</p>
</div>
<Tabs className="min-w-0 gap-3" defaultValue="terraform">
<TabsList className="h-9 gap-5 border-b-0 bg-transparent p-0 max-md:px-0">
<TabsTrigger layoutkey="byoc-setup-code-tabs" value="terraform">
Terraform
</TabsTrigger>
<TabsTrigger layoutkey="byoc-setup-code-tabs" value="gcloud">
gcloud
</TabsTrigger>
</TabsList>
<TabsContent className="mt-0 min-w-0" value="terraform">
<CodeBlock
className="min-w-0 max-w-full overflow-hidden"
icon={<TerminalIcon />}
lang="hcl"
title="main.tf"
viewportProps={{ className: 'max-h-[560px] max-w-full' }}
>
{templates.terraform}
</CodeBlock>
</TabsContent>
<TabsContent className="mt-0 min-w-0" value="gcloud">
<CodeBlock
className="min-w-0 max-w-full overflow-hidden"
icon={<TerminalIcon />}
lang="bash"
title="Run in Cloud Shell"
viewportProps={{ className: 'max-h-[560px] max-w-full' }}
>
{templates.gcloud}
</CodeBlock>
</TabsContent>
</Tabs>
</section>
</div>
</div>
</Page>
)
}
22 changes: 22 additions & 0 deletions src/features/dashboard/byoc/terraform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const GCP_PROJECT_ID_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/

export function isValidGcpProjectId(projectId: string) {
return GCP_PROJECT_ID_PATTERN.test(projectId)
}

export function renderByocSetupTemplate({
principal,
projectId,
region,
template,
}: {
principal: string
projectId: string
region: string
template: string
}) {
return template
.replaceAll('{{PROJECT_ID}}', projectId)
.replaceAll('{{REGION}}', region)
.replaceAll('{{E2B_PRINCIPAL}}', principal)
}
13 changes: 9 additions & 4 deletions src/features/dashboard/sidebar/use-visible-links.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use client'

import { useMemo } from 'react'
import { filterSidebarLinks, type SidebarNavItem } from '@/configs/sidebar'
import {
filterSidebarLinks,
type SidebarFeatureFlagId,
type SidebarNavItem,
} from '@/configs/sidebar'
import { useFeatureFlags } from '@/core/modules/feature-flags/feature-flags.client'

export function useVisibleSidebarLinks(links: SidebarNavItem[]) {
const { isEnabled } = useFeatureFlags()
const { hasPayload, isEnabled } = useFeatureFlags()

return useMemo(() => filterSidebarLinks(links, isEnabled), [isEnabled, links])
return filterSidebarLinks(links, (flagId: SidebarFeatureFlagId) =>
flagId === 'byocSetup' ? hasPayload(flagId) : isEnabled(flagId)
)
}
Loading
Loading