Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
dfc0600
feat(templates): v1 read-only template detail page
drankou May 25, 2026
5a445e1
feat(templates): add build ID search to detail builds tab
drankou May 25, 2026
6a211e6
fix(tags): drop callout box, inline info + count row
drankou May 25, 2026
6320f9e
feat(templates): link 'Read more' in tags info banner to e2b docs
drankou May 25, 2026
6383d66
fix(tags): match templates list sort header styling
drankou May 25, 2026
c07157b
fix(tags): underline build IDs by default in Assigned to column
drankou May 25, 2026
6cf11d8
refactor(builds): split BuildsHeader into explicit variants
drankou May 25, 2026
5f6dcf8
chore(templates): trim restating comments across the branch
drankou May 25, 2026
f470f17
refactor(builds): lift filter resolution to page consumers
drankou May 25, 2026
cf4e10c
update delete dialog
drankou May 28, 2026
cdb10af
disable context menu for default tag
drankou May 28, 2026
442a7e2
no need for promote/rollback in context menu for mobile
drankou May 28, 2026
e6c6c37
clean up imports
drankou May 28, 2026
40a8782
add assign new tag flow
drankou May 28, 2026
005f4a7
scrollable table
drankou May 29, 2026
d6c3a69
promote->reassign
drankou May 29, 2026
6b0be68
make actions visible on section hover
drankou May 29, 2026
03911e8
tag history page
drankou May 29, 2026
30651da
chore(ui): polish dialog, heading, caret primitives
drankou May 29, 2026
ae002c1
add rollback dialog, update history and assign
drankou May 29, 2026
3af332a
reassign dialog
drankou May 29, 2026
1f69e52
reuse rollback dialog on history page
drankou May 29, 2026
6f87212
truncate tag badge
drankou May 29, 2026
66dbae6
handle invalid format
drankou May 29, 2026
35425e0
add envd version to overview
drankou May 29, 2026
7e3ee5c
invalidate queries on rollback
drankou May 29, 2026
c53b3be
Update builds table for template
drankou Jun 1, 2026
e05c621
Update invalid format tooltip
drankou Jun 1, 2026
8b2e7a8
Fix tag history scroll
drankou Jun 1, 2026
fdc686a
Middle truncate tag badge
drankou Jun 1, 2026
e4bf9d3
Reuse dialog footer and polish spacing
drankou Jun 1, 2026
7ec1e9f
show currently assigned for same build row in history
drankou Jun 1, 2026
d3968bc
Improve empty state
drankou Jun 1, 2026
3ac3d2a
Handle same build in picker search
drankou Jun 1, 2026
2d9cc61
Read more hover
drankou Jun 1, 2026
5862738
Copy template naem
drankou Jun 1, 2026
d4e2d55
Navigate to template detail page from builds and sandboxes
drankou Jun 1, 2026
1d07ac9
Cleanup verbose comments
drankou Jun 1, 2026
fa4b7d6
Improve template header fetching
drankou Jun 1, 2026
fc3c875
Reset tags table store
drankou Jun 1, 2026
fb97e69
Fix active tab from path matching
drankou Jun 1, 2026
50ff7b1
Clean up
drankou Jun 1, 2026
a08a91d
Extract and reuse common components for dialog
drankou Jun 1, 2026
6f15f06
refactor: template display name helper
drankou Jun 1, 2026
1b5338f
defensive tag assignment
drankou Jun 1, 2026
ebcd2db
memoize
drankou Jun 1, 2026
31380df
use dashboard-api get template handler
drankou Jun 1, 2026
b50888b
Server-side builds search per template
drankou Jun 2, 2026
aab9d14
Template overview page with default build info
drankou Jun 2, 2026
734751b
Clean up comments
drankou Jun 2, 2026
9b9d6e1
Paginated tags groups loading
drankou Jun 2, 2026
b9ca3a8
fix max width for dialgos
drankou Jun 2, 2026
28358d1
Fix assign dialog tag field state
drankou Jun 2, 2026
cd3a836
Polish ui
drankou Jun 2, 2026
4545474
style: apply biome formatting
drankou Jun 2, 2026
7e7a67a
chore(tags): extract shared constants
drankou Jun 2, 2026
89ca3cc
chore(templates): drop duplicate getTemplate prefetch
drankou Jun 2, 2026
01f8fec
refactor(tags): decouple table store from analytics
drankou Jun 2, 2026
49dfce4
feat(tags): add useTagAssignmentMutation hook
drankou Jun 2, 2026
9642329
refactor(tags): migrate rollback dialog to useTagAssignmentMutation
drankou Jun 2, 2026
31abae0
refactor(tags): migrate reassign dialog to useTagAssignmentMutation
drankou Jun 2, 2026
7cc8ce1
refactor(tags): migrate assign dialog to useTagAssignmentMutation and…
drankou Jun 2, 2026
fa382c3
refactor(time): extract useNow hook
drankou Jun 2, 2026
42a778b
fix caret color
drankou Jun 2, 2026
7a3c7b3
add started sandboxes count
drankou Jun 2, 2026
9519c7f
Update tab icon
drankou Jun 2, 2026
6b69711
overview fixes
drankou Jun 2, 2026
9c341ce
cleanup
drankou Jun 2, 2026
eea6f62
fix virtualizer
drankou Jun 2, 2026
df0a7c1
cleanup
drankou Jun 2, 2026
9da12e1
Prefetch improvements
drankou Jun 2, 2026
57d4da1
clean up
drankou Jun 2, 2026
436e851
fix tailwind class
drankou Jun 2, 2026
f60a447
simplify default template overview data fetch
drankou Jun 2, 2026
113ffd3
fix reassign dialog retry
drankou Jun 2, 2026
d439c5d
Remove getDefaultTemplate handler
drankou Jun 2, 2026
260d95c
Handle TemplateDetail model
drankou Jun 2, 2026
d00310f
fix type
drankou Jun 2, 2026
5936f27
fix type
drankou Jun 2, 2026
29b1424
Fix inline button truncation
drankou Jun 4, 2026
eb17ca3
correct id column truncation
drankou Jun 4, 2026
38693e4
Set min width for delete dialog button
drankou Jun 4, 2026
91c7e2d
Fix build id column position
drankou Jun 4, 2026
c292b61
style: apply biome formatting
drankou Jun 4, 2026
b94bb85
update template overview page
drankou Jun 4, 2026
dfe2120
add success animation in dialog
drankou Jun 4, 2026
0c05287
Polish build picker row
drankou Jun 4, 2026
4e61667
improve vertical paddign
drankou Jun 5, 2026
404f1c3
virtualize tag groups list
drankou Jun 5, 2026
1b6b744
Collapsible animation
drankou Jun 5, 2026
3dd5f0b
Polish tag's history
drankou Jun 5, 2026
9e8153d
Include tag into page title for history page
drankou Jun 5, 2026
e51fcfa
Handle longer tags in dialog title
drankou Jun 5, 2026
8c2ca01
lift tag row dialogs into provider
drankou Jun 5, 2026
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
427 changes: 427 additions & 0 deletions spec/openapi.dashboard-api.yaml

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions src/app/dashboard/[teamSlug]/templates/(tabs)/builds/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import BuildsHeader from '@/features/dashboard/templates/builds/header'
'use client'

import { AllBuildsHeader } from '@/features/dashboard/templates/builds/all-builds-header'
import BuildsTable from '@/features/dashboard/templates/builds/table'
import useFilters from '@/features/dashboard/templates/builds/use-filters'

export default function TemplateBuildsPage() {
const { statuses, buildIdOrTemplate } = useFilters()

return (
<div className="h-full min-h-0 flex-1 p-3 md:p-6 flex flex-col gap-3">
<BuildsHeader />
<BuildsTable />
<AllBuildsHeader />
<BuildsTable filters={{ statuses, buildIdOrTemplate }} />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import { use, useCallback } from 'react'
import type { ListedBuildModel } from '@/core/modules/builds/models'
import BuildsTable from '@/features/dashboard/templates/builds/table'
import { TemplateBuildsHeader } from '@/features/dashboard/templates/builds/template-builds-header'
import useTemplateBuildsFilters from '@/features/dashboard/templates/builds/use-template-builds-filters'
import { isValidUuid } from '@/features/dashboard/templates/tags/helpers'

export default function TemplateDetailBuildsPage({
params,
}: PageProps<'/dashboard/[teamSlug]/templates/[templateId]'>) {
const { templateId } = use(params)
const { statuses, q } = useTemplateBuildsFilters()

const trimmed = q?.trim() ?? ''
const isSearching = trimmed.length > 0
const isValidSearch = !isSearching || isValidUuid(trimmed)

const postFilter = useCallback(
(build: ListedBuildModel) => build.templateId === templateId,
[templateId]
)

return (
<div className="h-full min-h-0 flex-1 p-3 md:p-6 flex flex-col gap-3">
<TemplateBuildsHeader />
<BuildsTable
filters={{
statuses,
buildIdOrTemplate:
isSearching && isValidSearch ? trimmed : templateId,
}}
postFilter={isSearching && isValidSearch ? postFilter : undefined}
disabled={isSearching && !isValidSearch}
showTemplateColumn={false}
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Suspense } from 'react'
import TemplateDetailTabs from '@/features/dashboard/templates/detail/tabs'
import TemplateTitleBinder from '@/features/dashboard/templates/detail/title-binder'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'

export default async function TemplateDetailLayout({
children,
params,
}: LayoutProps<'/dashboard/[teamSlug]/templates/[templateId]'>) {
const { teamSlug, templateId } = await params

prefetch(trpc.templates.getTemplate.queryOptions({ teamSlug, templateId }))

return (
<HydrateClient>
<div className="pt-2 flex-1 md:pt-3 min-h-0 h-full flex flex-col">
<Suspense fallback={null}>
<TemplateTitleBinder teamSlug={teamSlug} templateId={templateId} />
</Suspense>
<TemplateDetailTabs teamSlug={teamSlug} templateId={templateId} />
{children}
</div>
</HydrateClient>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import LoadingLayout from '@/features/dashboard/loading-layout'

export default function TemplateDetailLoading() {
return <LoadingLayout />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Suspense } from 'react'
import TemplateOverview from '@/features/dashboard/templates/detail/overview'
import { TemplateOverviewSkeleton } from '@/features/dashboard/templates/detail/overview/skeleton'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'

export default async function TemplateOverviewPage({
params,
}: PageProps<'/dashboard/[teamSlug]/templates/[templateId]'>) {
const { teamSlug, templateId } = await params

prefetch(trpc.templates.getTemplate.queryOptions({ teamSlug, templateId }))

return (
<HydrateClient>
<div className="p-6 md:p-10 flex flex-col gap-6 w-full max-w-[600px] mx-auto">
<Suspense fallback={<TemplateOverviewSkeleton />}>
<TemplateOverview teamSlug={teamSlug} templateId={templateId} />
</Suspense>
</div>
</HydrateClient>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Suspense } from 'react'
import LoadingLayout from '@/features/dashboard/loading-layout'
import { TAG_HISTORY_PAGE_LIMIT } from '@/features/dashboard/templates/tags/constants'
import TagHistoryView from '@/features/dashboard/templates/tags/history/tag-history-view'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'

export default async function TemplateTagHistoryPage({
params,
}: PageProps<'/dashboard/[teamSlug]/templates/[templateId]/tags/[tag]'>) {
const { teamSlug, templateId, tag } = await params
const decodedTag = decodeURIComponent(tag)

prefetch(
trpc.templates.getTagAssignments.infiniteQueryOptions({
teamSlug,
templateId,
tag: decodedTag,
limit: TAG_HISTORY_PAGE_LIMIT,
})
)

return (
<HydrateClient>
<div className="h-full min-h-0 flex-1 py-6 px-8 md:px-11 flex flex-col gap-3 max-w-[924px] mx-auto w-full">
<Suspense fallback={<LoadingLayout />}>
<TagHistoryView
teamSlug={teamSlug}
templateId={templateId}
tag={decodedTag}
/>
</Suspense>
</div>
</HydrateClient>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Suspense } from 'react'
import LoadingLayout from '@/features/dashboard/loading-layout'
import { TAGS_PAGE_LIMIT } from '@/features/dashboard/templates/tags/constants'
import TagsTable from '@/features/dashboard/templates/tags/table'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'

export default async function TemplateTagsPage({
params,
}: PageProps<'/dashboard/[teamSlug]/templates/[templateId]'>) {
const { teamSlug, templateId } = await params

prefetch(
trpc.templates.getTagGroups.infiniteQueryOptions({
teamSlug,
templateId,
limit: TAGS_PAGE_LIMIT,
search: undefined,
sort: undefined,
})
)
prefetch(trpc.templates.getTagCount.queryOptions({ teamSlug, templateId }))

return (
<HydrateClient>
<div className="h-full min-h-0 flex-1 pt-6 pb-2 md:pt-10 md:pb-4 px-8 md:px-11 flex flex-col gap-3 max-w-[924px] mx-auto w-full">
<Suspense fallback={<LoadingLayout />}>
<TagsTable teamSlug={teamSlug} templateId={templateId} />
</Suspense>
</div>
</HydrateClient>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client'

import { TRPCClientError } from '@trpc/client'
import { notFound } from 'next/navigation'
import { DashboardRouteError } from '@/features/dashboard/shared/route-error'

export default function TemplateDetailsError({
Expand All @@ -9,5 +11,9 @@ export default function TemplateDetailsError({
error: Error & { digest?: string }
reset: () => void
}) {
if (error instanceof TRPCClientError && error.data?.code === 'NOT_FOUND') {
notFound()
}

return <DashboardRouteError error={error} reset={reset} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

import { Button } from '@/ui/primitives/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
} from '@/ui/primitives/card'
import { ArrowLeftIcon } from '@/ui/primitives/icons'

export default function TemplateNotFound() {
return (
<div className="flex min-h-[60vh] items-center justify-center">
<Card className="w-full max-w-md border border-stroke bg-bg-1/40 backdrop-blur-lg">
<CardHeader className="text-center">
<span className="prose-value-big">404</span>
<CardDescription>Template not found</CardDescription>
</CardHeader>
<CardContent className="text-center text-fg-secondary">
<p>We couldn’t find this template in your team.</p>
</CardContent>
<CardFooter>
<Button
variant="secondary"
onClick={() => window.history.back()}
className="w-full"
>
<ArrowLeftIcon />
Go Back
</Button>
</CardFooter>
</Card>
</div>
)
}
10 changes: 10 additions & 0 deletions src/app/dashboard/[teamSlug]/templates/[templateId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { redirect } from 'next/navigation'
import { PROTECTED_URLS } from '@/configs/urls'

export default async function TemplateDetailPage({
params,
}: PageProps<'/dashboard/[teamSlug]/templates/[templateId]'>) {
const { teamSlug, templateId } = await params

redirect(PROTECTED_URLS.TEMPLATE_OVERVIEW(teamSlug, templateId))
}
32 changes: 32 additions & 0 deletions src/configs/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
}),
'/dashboard/*/sandboxes/*/*': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 41 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const sandboxId = parts[4]!

Check warning on line 42 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.

return {
title: [
Expand Down Expand Up @@ -70,8 +70,8 @@
}),
'/dashboard/*/templates/*/builds/*': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 73 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const buildId = parts.pop()!

Check warning on line 74 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const buildIdSliced = `${buildId.slice(0, 6)}...${buildId.slice(-6)}`

return {
Expand All @@ -89,6 +89,15 @@
},
}
},
'/dashboard/*/templates/*/overview': (pathname) =>
templateDetailLayoutConfig(pathname),
'/dashboard/*/templates/*/tags': (pathname) =>
templateDetailLayoutConfig(pathname),
'/dashboard/*/templates/*/tags/*': (pathname) =>
templateDetailLayoutConfig(pathname),
// Keep this more specific glob ahead of /templates/*/builds/* (build detail).
'/dashboard/*/templates/*/builds': (pathname) =>
templateDetailLayoutConfig(pathname),

// integrations
'/dashboard/*/webhooks': () => ({
Expand Down Expand Up @@ -128,7 +137,7 @@
}),
'/dashboard/*/billing/plan': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 140 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.

return {
title: [
Expand All @@ -142,7 +151,7 @@
},
'/dashboard/*/billing/plan/select': (pathname) => {
const parts = pathname.split('/')
const teamSlug = parts[2]!

Check warning on line 154 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.

return {
title: [
Expand All @@ -163,6 +172,29 @@
}),
}

// Pathname fallback for detail tabs; usePageTitle replaces with the friendly template name once data loads.
function templateDetailLayoutConfig(pathname: string): DashboardLayoutConfig {
const parts = pathname.split('/')
const teamSlug = parts[2]!
const templateId = parts[4]!

Check warning on line 179 in src/configs/layout.ts

View workflow job for this annotation

GitHub Actions / Lint

lint/style/noNonNullAssertion

Forbidden non-null assertion.
const templateIdSliced =
templateId.length > 14
? `${templateId.slice(0, 6)}...${templateId.slice(-6)}`
: templateId

return {
title: [
{
label: 'Templates',
href: PROTECTED_URLS.TEMPLATES_LIST(teamSlug),
},
{ label: templateIdSliced },
],
type: 'custom',
copyValue: templateId,
}
}

/**
* Returns the layout config for a given dashboard pathname.
* @param pathname - The current route pathname
Expand Down
Loading
Loading