Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
0e9f6f6
feat(templates): v1 read-only template detail page
drankou May 25, 2026
0154cf3
feat(templates): add build ID search to detail builds tab
drankou May 25, 2026
dc1c58c
fix(tags): drop callout box, inline info + count row
drankou May 25, 2026
4a04423
feat(templates): link 'Read more' in tags info banner to e2b docs
drankou May 25, 2026
a77ce4d
fix(tags): match templates list sort header styling
drankou May 25, 2026
413a38d
fix(tags): underline build IDs by default in Assigned to column
drankou May 25, 2026
4578e04
refactor(builds): split BuildsHeader into explicit variants
drankou May 25, 2026
3fda967
chore(templates): trim restating comments across the branch
drankou May 25, 2026
accc6c9
refactor(builds): lift filter resolution to page consumers
drankou May 25, 2026
460fedb
update delete dialog
drankou May 28, 2026
10da554
disable context menu for default tag
drankou May 28, 2026
16caa33
no need for promote/rollback in context menu for mobile
drankou May 28, 2026
2472ddd
clean up imports
drankou May 28, 2026
61262cc
add assign new tag flow
drankou May 28, 2026
aabee69
scrollable table
drankou May 29, 2026
67c491d
promote->reassign
drankou May 29, 2026
b50c3c9
make actions visible on section hover
drankou May 29, 2026
2fc503a
tag history page
drankou May 29, 2026
da9760f
chore(ui): polish dialog, heading, caret primitives
drankou May 29, 2026
81bc69e
add rollback dialog, update history and assign
drankou May 29, 2026
eb26f9b
reassign dialog
drankou May 29, 2026
450a39b
reuse rollback dialog on history page
drankou May 29, 2026
796d782
truncate tag badge
drankou May 29, 2026
3c3fbf1
handle invalid format
drankou May 29, 2026
eaedfae
add envd version to overview
drankou May 29, 2026
2b8ccbd
invalidate queries on rollback
drankou May 29, 2026
c0b5083
Update builds table for template
drankou Jun 1, 2026
b24e5d6
Update invalid format tooltip
drankou Jun 1, 2026
2411560
Fix tag history scroll
drankou Jun 1, 2026
21062a8
Middle truncate tag badge
drankou Jun 1, 2026
d1075eb
Reuse dialog footer and polish spacing
drankou Jun 1, 2026
0aca2a4
show currently assigned for same build row in history
drankou Jun 1, 2026
157c0e0
Improve empty state
drankou Jun 1, 2026
e3ba893
Handle same build in picker search
drankou Jun 1, 2026
5196197
Read more hover
drankou Jun 1, 2026
0260ff4
Copy template naem
drankou Jun 1, 2026
ad638fa
Navigate to template detail page from builds and sandboxes
drankou Jun 1, 2026
fe2b68a
Cleanup verbose comments
drankou Jun 1, 2026
edbb60a
Improve template header fetching
drankou Jun 1, 2026
6152467
Reset tags table store
drankou Jun 1, 2026
c968562
Fix active tab from path matching
drankou Jun 1, 2026
cbf837b
Clean up
drankou Jun 1, 2026
a167643
Extract and reuse common components for dialog
drankou Jun 1, 2026
ff30cd8
refactor: template display name helper
drankou Jun 1, 2026
462322a
defensive tag assignment
drankou Jun 1, 2026
61a4a33
memoize
drankou Jun 1, 2026
9edce30
use dashboard-api get template handler
drankou Jun 1, 2026
0de71e7
Server-side builds search per template
drankou Jun 2, 2026
61bfb69
Template overview page with default build info
drankou Jun 2, 2026
9aa7b10
Clean up comments
drankou Jun 2, 2026
2b75df5
Paginated tags groups loading
drankou Jun 2, 2026
10c0640
fix max width for dialgos
drankou Jun 2, 2026
09f2e84
Fix assign dialog tag field state
drankou Jun 2, 2026
f7eea33
Polish ui
drankou Jun 2, 2026
7337c8f
style: apply biome formatting
drankou Jun 2, 2026
6ea77c4
chore(tags): extract shared constants
drankou Jun 2, 2026
5f1fe42
chore(templates): drop duplicate getTemplate prefetch
drankou Jun 2, 2026
55e0c00
refactor(tags): decouple table store from analytics
drankou Jun 2, 2026
29b4f5c
feat(tags): add useTagAssignmentMutation hook
drankou Jun 2, 2026
cc3ac69
refactor(tags): migrate rollback dialog to useTagAssignmentMutation
drankou Jun 2, 2026
88460f4
refactor(tags): migrate reassign dialog to useTagAssignmentMutation
drankou Jun 2, 2026
3a29952
refactor(tags): migrate assign dialog to useTagAssignmentMutation and…
drankou Jun 2, 2026
b71f77d
refactor(time): extract useNow hook
drankou Jun 2, 2026
898b78a
fix caret color
drankou Jun 2, 2026
53bf652
add started sandboxes count
drankou Jun 2, 2026
d01c07c
Update tab icon
drankou Jun 2, 2026
c81b2fc
overview fixes
drankou Jun 2, 2026
d2d5b04
cleanup
drankou Jun 2, 2026
5c3f79c
fix virtualizer
drankou Jun 2, 2026
422cd2b
cleanup
drankou Jun 2, 2026
478a8fe
Prefetch improvements
drankou Jun 2, 2026
cf382e8
clean up
drankou Jun 2, 2026
d5f7f06
fix tailwind class
drankou Jun 2, 2026
d04c559
simplify default template overview data fetch
drankou Jun 2, 2026
bb1b506
fix reassign dialog retry
drankou Jun 2, 2026
3e2cd7e
Remove getDefaultTemplate handler
drankou Jun 2, 2026
1f1467f
Handle TemplateDetail model
drankou Jun 2, 2026
1120524
style: apply biome formatting
drankou Jun 2, 2026
f67cc9b
fix type
drankou Jun 2, 2026
c027b40
fix type
drankou Jun 2, 2026
119ecb2
Fix inline button truncation
drankou Jun 4, 2026
7e3ee38
correct id column truncation
drankou Jun 4, 2026
1c3063d
Set min width for delete dialog button
drankou Jun 4, 2026
9426e2f
Fix build id column position
drankou Jun 4, 2026
6cff7b2
style: apply biome formatting
drankou Jun 4, 2026
c9409d4
update template overview page
drankou Jun 4, 2026
dc104ee
add success animation in dialog
drankou Jun 4, 2026
d4ffd0d
Polish build picker row
drankou Jun 4, 2026
53f60fe
improve vertical paddign
drankou Jun 5, 2026
e3135e0
virtualize tag groups list
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
644 changes: 644 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 Down Expand Up @@ -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]!
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