Skip to content

Commit

Permalink
Template management in dashboard (#483)
Browse files Browse the repository at this point in the history
- Adds more info to templates section
- Publish/unpublish templates
- Delete templates
- Requires latest infra backend to test
  • Loading branch information
jakubno authored Dec 9, 2024
2 parents 407f330 + 6252eab commit 784f2ad
Show file tree
Hide file tree
Showing 4 changed files with 642 additions and 11 deletions.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.1.4",
"@sentry/nextjs": "^8.18.0",
"@sindresorhus/slugify": "^2.1.1",
"@supabase/auth-helpers-nextjs": "^0.7.4",
Expand Down
236 changes: 225 additions & 11 deletions apps/web/src/components/Dashboard/Templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ import {
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'

interface Template {
aliases: string[]
Expand All @@ -18,6 +36,12 @@ interface Template {
memoryMB: number
public: boolean
templateID: string
createdAt: string
updatedAt: string
createdBy: {
email: string
id: string
} | null
}

export function TemplatesContent({
Expand All @@ -30,9 +54,14 @@ export function TemplatesContent({
apiUrl: string
}) {
const [templates, setTemplates] = useState<Template[]>([])
const [currentTemplate, setCurrentTemplate] = useState<Template | null>(null)
const [isPublishTemplateDialogOpen, setIsPublishTemplateDialogOpen] =
useState(false)
const [isDeleteTemplateDialogOpen, setIsDeleteTemplateDialogOpen] =
useState(false)

useEffect(() => {
function f() {
async function f() {
const accessToken = user.accessToken
if (accessToken) {
fetchTemplates(apiUrl, accessToken, teamId).then((newTemplates) => {
Expand All @@ -43,30 +72,93 @@ export function TemplatesContent({
}
}

const interval = setInterval(() => {
f()
}, 5000)

f()
// Cleanup interval on component unmount
return () => clearInterval(interval)
}, [user, teamId])

async function deleteTemplate() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/templates/${currentTemplate?.templateID}`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${user.accessToken}`,
},
}
)

if (!res.ok) {
return toast({
title: 'An error occurred',
description: 'We were unable to delete the template',
})
}

setTemplates(
templates.filter(
(template) => template.templateID !== currentTemplate?.templateID
)
)
setCurrentTemplate(null)
setIsDeleteTemplateDialogOpen(false)
}

async function publishTemplate() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/templates/${currentTemplate?.templateID}`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${user.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
public: !currentTemplate?.public,
}),
}
)

if (!res.ok) {
return toast({
title: 'An error occurred',
description: 'We were unable to update the template',
})
}

toast({
title: 'Template updated',
description: 'The template has been updated successfully',
})

setTemplates(
templates.map((template) =>
template.templateID === currentTemplate?.templateID
? { ...template, public: !template.public }
: template
)
)
setCurrentTemplate(null)
setIsPublishTemplateDialogOpen(false)
}

return (
<div className="flex flex-col justify-center">
<Table>
<TableHeader>
<TableRow className="hover:bg-orange-500/10 dark:hover:bg-orange-500/10 border-b border-white/5 ">
<TableRow className="hover:bg-orange-500/10 dark:hover:bg-orange-500/10 border-b border-white/5">
<TableHead>Visibility</TableHead>
<TableHead>Template ID</TableHead>
<TableHead>Template Name</TableHead>
<TableHead>vCPUs</TableHead>
<TableHead>RAM MiB</TableHead>
<TableHead>Created by</TableHead>
<TableHead>Created at</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{templates.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center">
<TableCell colSpan={8} className="text-center">
No templates
</TableCell>
</TableRow>
Expand All @@ -76,15 +168,134 @@ export function TemplatesContent({
className="hover:bg-orange-300/10 dark:hover:bg-orange-300/10 border-b border-white/5"
key={template.templateID}
>
<TableCell>
<div className="flex items-center gap-2">
{template.public ? (
<LockOpen className="w-4 h-4" />
) : (
<Lock className="w-4 h-4" />
)}
<span className="text-xs">
{template.public ? 'Visible' : 'Private'}
</span>
</div>
</TableCell>
<TableCell>{template.templateID}</TableCell>
<TableCell>{template.aliases[0]}</TableCell>
<TableCell className="font-mono">
{template.aliases.join(', ')}
</TableCell>
<TableCell>{template.cpuCount}</TableCell>
<TableCell>{template.memoryMB}</TableCell>
<TableCell>{template.createdBy?.email}</TableCell>
<TableCell>
{new Date(template.createdAt).toLocaleString()}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger>
<MoreVertical className="w-4 h-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => {
setCurrentTemplate(template)
setIsPublishTemplateDialogOpen(true)
}}
>
{template.public ? (
<Lock className="w-4 h-4 mr-2" />
) : (
<LockOpen className="w-4 h-4 mr-2" />
)}
{template.public ? 'Make private' : 'Make public'}
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-500"
onClick={() => {
setCurrentTemplate(template)
setIsDeleteTemplateDialogOpen(true)
}}
>
<Trash className="w-4 h-4 mr-2" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>

<AlertDialog
open={isPublishTemplateDialogOpen}
onOpenChange={setIsPublishTemplateDialogOpen}
>
<AlertDialogContent className="bg-inherit text-white border-black">
<AlertDialogHeader>
<AlertDialogTitle>
You are about to{' '}
{currentTemplate?.public ? 'unpublish' : 'publish'} a template
</AlertDialogTitle>
<AlertDialogDescription className="text-white/90">
This will make the template{' '}
{currentTemplate?.public
? 'private to your team'
: 'public to everyone outside your team'}{' '}
with immediate effect.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
className="border-white/10"
onClick={() => {
setIsPublishTemplateDialogOpen(false)
setCurrentTemplate(null)
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={publishTemplate}>
{currentTemplate?.public ? 'Unpublish' : 'Publish'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<AlertDialog
open={isDeleteTemplateDialogOpen}
onOpenChange={setIsDeleteTemplateDialogOpen}
>
<AlertDialogContent className="bg-inherit text-white border-black">
<AlertDialogHeader>
<AlertDialogTitle>
You are about to delete a template
</AlertDialogTitle>
<AlertDialogDescription className="text-white/90">
This action cannot be undone. This will permanently delete the
template with immediate effect.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
className="border-white/10"
onClick={() => {
setIsDeleteTemplateDialogOpen(false)
setCurrentTemplate(null)
}}
>
Cancel
</AlertDialogCancel>
<AlertDialogAction
className="bg-red-500 text-white hover:bg-red-600"
onClick={deleteTemplate}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
Expand All @@ -102,7 +313,10 @@ async function fetchTemplates(
})
try {
const data: Template[] = await res.json()
return data
const orderedData = data.sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
})
return orderedData
} catch (e) {
// TODO: add sentry event here
return []
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'

import { cn } from '@/lib/utils'

const TooltipProvider = TooltipPrimitive.Provider

const Tooltip = TooltipPrimitive.Root

const TooltipTrigger = TooltipPrimitive.Trigger

const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
Loading

0 comments on commit 784f2ad

Please sign in to comment.