Skip to content

Commit

Permalink
Add multiple team handling (#417)
Browse files Browse the repository at this point in the history
# Description
- Allows users to configure team in CLI
- Allows specify team for following commands: `list`, `kill`, `delete`
  • Loading branch information
jakubno authored Aug 5, 2024
2 parents dfe6515 + dab4975 commit 813a88b
Show file tree
Hide file tree
Showing 23 changed files with 526 additions and 217 deletions.
6 changes: 6 additions & 0 deletions .changeset/purple-garlics-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"e2b": patch
"@e2b/cli": patch
---

Add end at and team handling
8 changes: 3 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,9 @@ web_modules/

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

.env.*
!.env.template
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/app/(auth)/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Footer } from '@/components/Footer'

export default async function Layout({ children }) {
return (
<div className="pt-12">
{children}
<Footer />
</div>
)
}
23 changes: 23 additions & 0 deletions apps/web/src/app/(docs)/docs/cli/commands/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ Log out of the CLI. It will remove your access token from `~/.e2b` file.
e2b auth logout
```

## `auth configure`

Configure the default team for the CLI. It will be used for all commands that require a team.

```bash
e2b auth configure
```

## `auth info`

Get info about your current user.
Expand Down Expand Up @@ -103,6 +111,9 @@ If there is no `e2b.toml` config a new template will be created.
<Option type="-d, --dockerfile" name="dockerfile">
Specify the path to Dockerfile. By default E2B tries to find `e2b.Dockerfile` or `Dockerfile` in the root directory.
</Option>
<Option type="-t, --team" name="team">
Specify the team that will be used for the sandbox template. You can find team ID in the team settings in the [E2B dashboard](https://e2b.dev/dashboard?tab=team).
</Option>
<Option type="--cpu-count" name="cpu-count">
Specify the number of CPUs that will be used to run the sandbox. The default value is 2.
</Option>
Expand Down Expand Up @@ -153,6 +164,9 @@ Running `e2b template delete` without specifying a template with the `[template]
<Option type="-y, --yes">
Don't ask for confirmation before deleting the sandbox template.
</Option>
<Option type="-t, --team" name="team">
Specify the team that will be used for the sandbox template. You can find team ID in the team settings in the [E2B dashboard](https://e2b.dev/dashboard?tab=team).
</Option>
</Options>


Expand All @@ -164,6 +178,15 @@ List your sandbox templates.
e2b template list
```

#### **Options**

<Options>
<Option type="-t, --team" name="team">
Specify the team that will be used for the sandbox template. You can find team ID in the team settings in the [E2B dashboard](https://e2b.dev/dashboard?tab=team).
</Option>
</Options>


---

# Sandboxes
Expand Down
84 changes: 51 additions & 33 deletions apps/web/src/components/Dashboard/Team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export const TeamContent = ({ team, user, teams, currentApiKey, setTeams, setCur
}
}, [currentApiKey, user, userAdded])

useEffect(() => {
setTeamName(team.name)
}, [team])

const closeDialog = () => setIsDialogOpen(false)
const openDialog = (id: string) => {
setCurrentMemberId(id)
Expand Down Expand Up @@ -131,7 +135,7 @@ export const TeamContent = ({ team, user, teams, currentApiKey, setTeams, setCur
return (
<div className='flex flex-col justify-center pb-10'>
<h2 className="text-xl font-bold pb-4">Team name</h2>
<div className='flex items-center space-x-2 pb-10'>
<div className='flex items-center space-x-2 pb-4'>
<input
type="text"
className="w-1/2 md:w-1/3 border border-white/10 text-sm focus:outline-none outline-none rounded-md p-2"
Expand All @@ -145,6 +149,19 @@ export const TeamContent = ({ team, user, teams, currentApiKey, setTeams, setCur
<Button variant='outline' onClick={() => changeTeamName()}>Save changes</Button>
</div>

<span
className='flex pb-10 w-fit text-sm text-orange-500 hover:cursor-pointer hover:text-orange-500/30 space-x-2 items-center'
onClick={() => {
navigator.clipboard.writeText(team.id)
toast({
title: 'Team ID copied to clipboard',
})
}}
>
<p>Copy your team ID</p>
<Copy className='h-4 w-4'/>
</span>

<h2 className="text-xl font-bold pb-4">Add new members</h2>
<div className='flex items-center space-x-2 pb-4'>
<input
Expand All @@ -170,48 +187,48 @@ export const TeamContent = ({ team, user, teams, currentApiKey, setTeams, setCur
}}
>
<p>Copy your user ID</p>
<Copy className='h-4 w-4' />
<Copy className='h-4 w-4'/>
</span>

<h2 className="text-xl font-bold pb-4">Team members</h2>
{isLoading ? (<div className="flex items-center w-full pl-4 p-2">
<Spinner size="24px"/>
</div>) : (
<Table>
<TableHeader>
<TableRow className='hover:bg-inherit dark:hover:bg-inherit border-b border-white/5'>
<TableHead>Email</TableHead>
<TableHead></TableHead>
<Table>
<TableHeader>
<TableRow className='hover:bg-inherit dark:hover:bg-inherit border-b border-white/5'>
<TableHead>Email</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{members.length === 0 ? (
<TableRow className='border-b border-white/5'>
<TableCell colSpan={2} className='text-center'>
No members found
</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{members.length === 0 ? (
<TableRow className='border-b border-white/5'>
<TableCell colSpan={2} className='text-center'>
No members found
</TableCell>
</TableRow>
) : (
members.map((user) => (
<TableRow
className='hover:bg-orange-300/10 dark:hover:bg-orange-300/10 border-b border-white/5'
key={user.id}>
<TableCell>{user.email}</TableCell>
<TableCell>
<Button className='text-sm' variant='desctructive' onClick={() => openDialog(user.id)}>
Remove team member
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>)}
) : (
members.map((user) => (
<TableRow
className='hover:bg-orange-300/10 dark:hover:bg-orange-300/10 border-b border-white/5'
key={user.id}>
<TableCell>{user.email}</TableCell>
<TableCell>
<Button className='text-sm' variant='desctructive' onClick={() => openDialog(user.id)}>
Remove team member
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>)}


<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<AlertDialogTrigger asChild>
<Button variant="outline" style={{ display: 'none' }}>Show Dialog</Button>
<Button variant="outline" style={{display: 'none'}}>Show Dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent className="bg-inherit text-white border-black">
<AlertDialogHeader>
Expand All @@ -223,7 +240,8 @@ export const TeamContent = ({ team, user, teams, currentApiKey, setTeams, setCur
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className='border-white/10' onClick={closeDialog}>Cancel</AlertDialogCancel>
<AlertDialogAction className='bg-red-500 text-white hover:bg-red-600' onClick={() => deleteUserFromTeam()}>Continue</AlertDialogAction>
<AlertDialogAction className='bg-red-500 text-white hover:bg-red-600'
onClick={() => deleteUserFromTeam()}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export function Layout({
">
{children}
</main>
<Footer />
</div>
)}
</div>
Expand Down
9 changes: 0 additions & 9 deletions apps/web/src/utils/useUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ type UserContextType = {
teams: Team[];
accessToken: string;
defaultTeamId: string;
pricingTier: {
id: string,
isPromo: boolean,
endsAt: string
}
})
| null;
error: Error | null;
Expand Down Expand Up @@ -128,7 +123,6 @@ export const CustomUserContextProvider = (props) => {
return
}

const pricingTier = defaultTeam.tier
const defaultTeamId = defaultTeam?.id // TODO: Adjust when user can be part of multiple teams

const { data: accessToken, error: accessTokenError } = await supabase
Expand All @@ -145,9 +139,6 @@ export const CustomUserContextProvider = (props) => {
accessToken: accessToken?.access_token,
defaultTeamId,
error: teamsError,
pricingTier: {
id: pricingTier,
},
})
setIsLoading(false)
}
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function ensureAPIKey() {
// If apiKey is not already set (either from env var or from user config), try to get it from config file
if (!apiKey) {
const userConfig = getUserConfig()
apiKey = userConfig?.defaultTeamApiKey
apiKey = userConfig?.teamApiKey || userConfig?.defaultTeamApiKey
}

if (!apiKey) {
Expand All @@ -37,6 +37,15 @@ export function ensureAPIKey() {
}
}

export function ensureUserConfig() {
const userConfig = getUserConfig()
if (!userConfig) {
console.error('No user config found, run `e2b auth login` to log in first.')
process.exit(1)
}
return userConfig
}

export function ensureAccessToken() {
// If accessToken is not already set (either from env var or from user config), try to get it from config file
if (!accessToken) {
Expand Down
57 changes: 57 additions & 0 deletions packages/cli/src/commands/auth/configure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as commander from 'commander'
import * as fs from 'fs'
import * as chalk from 'chalk'
import * as e2b from 'e2b'
import * as path from 'path'

import { USER_CONFIG_PATH } from 'src/user'
import { client, ensureAccessToken, ensureUserConfig } from 'src/api'
import { asFormattedTeam } from '../../utils/format'

const getTeams = e2b.withAccessToken(client.api.path('/teams').method('get').create())

export const configureCommand = new commander.Command('configure')
.description('configure user')
.action(async () => {
const inquirer = await import('inquirer')

console.log('Configuring user...\n')

if (!fs.existsSync(USER_CONFIG_PATH)) {
console.log('No user config found, run `e2b auth login` to log in first.')
return
}

const userConfig = ensureUserConfig()
const accessToken = ensureAccessToken()
const res = await getTeams(accessToken, {})
if (!res.ok) {
const error: e2b.paths['/teams']['get']['responses']['500']['content']['application/json'] = res.data as any

throw new Error(
`Error getting user teams: ${res.statusText}, ${error.message ?? 'no message'
}`,
)
}

const team = (await inquirer.default.prompt([
{
name: 'team',
message: chalk.default.underline('Select team'),
type: 'list',
pageSize: 50,
choices: res.data.map(team => ({
name: asFormattedTeam(team),
value: team,
})),
},
]))['team']

userConfig.teamName = team.name
userConfig.teamId = team.teamID
userConfig.teamApiKey = team.apiKey
fs.mkdirSync(path.dirname(USER_CONFIG_PATH), {recursive: true})
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(userConfig, null, 2))

console.log(`Team ${asFormattedTeam(team)} selected.\n`)
})
2 changes: 2 additions & 0 deletions packages/cli/src/commands/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import * as commander from 'commander'
import { loginCommand } from './login'
import { logoutCommand } from './logout'
import { infoCommand } from './info'
import { configureCommand } from './configure'


export const authCommand = new commander.Command('auth').description('authentication commands')
.addCommand(loginCommand)
.addCommand(logoutCommand)
.addCommand(infoCommand)
.addCommand(configureCommand)
4 changes: 2 additions & 2 deletions packages/cli/src/commands/auth/info.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as commander from 'commander'

import { getUserConfig } from 'src/user'
import { asBold, asFormattedError } from 'src/utils/format'
import { asFormattedConfig, asFormattedError} from 'src/utils/format'

export const infoCommand = new commander.Command('info')
.description('get information about the current user')
Expand All @@ -18,6 +18,6 @@ export const infoCommand = new commander.Command('info')
return
}

console.log(`Logged in as ${asBold(userConfig.email)}`)
console.log(asFormattedConfig(userConfig))
process.exit(0)
})
Loading

0 comments on commit 813a88b

Please sign in to comment.