Skip to content

feat: template details page#351

Open
drankou wants to merge 88 commits into
mainfrom
template-dashboard-enhancements-versioning-and-storage-usage-eng-3771
Open

feat: template details page#351
drankou wants to merge 88 commits into
mainfrom
template-dashboard-enhancements-versioning-and-storage-usage-eng-3771

Conversation

@drankou
Copy link
Copy Markdown
Contributor

@drankou drankou commented Jun 2, 2026

Add template detail page with Overview, Builds, and Tags tabs.

Changes:

  • Overview tab: latest build, specs, envd version, visibility, started-sandboxes count.
  • Tags tab: assign, reassign, and rollback against any build, with a searchable build picker and tag history view.
  • Builds tab: server-side search and status filtering per template.

Other UI fixes:

  • Set caret color to accent
  • Fix dialog title styling clashing with heading styles
  • Dialog paddings and close button position
  • Clipped ID cell value in builds list

drankou and others added 30 commits June 2, 2026 11:47
Add a per-template detail page reachable from the templates list at
/templates/[templateId]/{overview,builds,tags}. v1 is read-only across
all three tabs, except the visibility dropdown on the header which
reuses the existing templates.updateTemplate mutation.

Routing
- New route group [templateId]/(detail-tabs) wraps overview, builds,
  and tags so the existing builds/[buildId] build-detail page is
  untouched (sibling, not inside the group).
- [templateId]/page.tsx redirects to /overview.
- Names: name-cell click on the list page navigates to /overview.
  Default (E2B) templates remain non-clickable. Copy moves to a
  hover-revealed icon button in the corner of the cell.

Header
- Reusable usePageTitle Zustand hook (~30 LOC) lets pages override
  the dashboard title bar with fetched data. Global header reads the
  override; falls back to pathname-derived layout config when none.
- DetailsRow with 5 cells: Memory, CPU, Created, Updated, Visibility.
- No-successful-build state renders '--' for cpu/memory.
- Visibility dropdown reuses the existing updateTemplate mutation
  with optimistic updates across both list and detail caches.

Builds tab
- BuildsTable now accepts optional templateId prop; when set, the
  Template column is hidden and a scoped filter hook
  (useTemplateBuildsFilters, statuses-only URL state) is used.
- BuildsHeader gains showSearchInput and scoped props; the all-team
  /templates/builds page is unchanged.

Tags tab
- New templates.getTags tRPC procedure + repository method backed by
  the existing GET /templates/{templateID}/tags infra endpoint.
- Read-only table: tag pill | linked build ID + relative time.
- Sortable on both columns; client-side search by name; info banner
  copy matches Figma; empty state.

tRPC + repository
- New templates.getTemplate procedure filters server-side over the
  existing getTeamTemplates result; throws NOT_FOUND when the
  templateID isn't in the team's list (catches stale links, wrong
  team, and default-template URL guessing).

Telemetry
- PostHog events: 'template detail opened' (mount), 'template detail
  tab switched' (tab change), 'template visibility changed from
  header' (mutation success). 'template detail opened' is also
  emitted on name-cell click from the list with fromTab=list.

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Scoped builds tab now matches the all-team builds toolbar (search +
status filter). New URL param 'q' lives on the scoped filter hook.

The backend query stays scoped by templateID (authoritative); 'q'
applies as a client-side substring filter on build IDs over the
already-loaded pages. This avoids cross-template name-match leakage
that 'buildIdOrTemplate=<q>' could otherwise introduce.

BuildsHeader unifies the all-team and scoped search via a single
'search/setSearch' pair backed by the appropriate hook, with
placeholder swapped to 'Search by build ID' in scoped mode.

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Align toolbar with Figma 5418:195576 / 5630:152026:

- Info text is plain gray body, no icon, no border, no background.
- Info + tag count sit on a single horizontal row with space-between
  (mirrors the design's 'tags identify\u2026' / '8 tags in total' bar).
- 'Read more.' is rendered as an underlined inert span until a docs
  URL is wired in.
- Build column header is renamed 'Assigned to' to match the design
  and the actual semantic (timestamp = when the tag was assigned).

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Replaces the inert placeholder with a live link to
https://e2b.dev/docs/template/tags, with a trailing external-link icon
for consistency with other off-site references in the dashboard.

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Mirror the DataTableHead visual treatment used on /templates/list:

- font-mono uppercase prose-label
- text-fg-tertiary by default \u2014 dims the column name
- prose-label-highlight + text-fg on the active column
- SortAscIcon / SortDescIcon (was a generic three-line SortIcon)
- icon hidden until hover when inactive; on hover it previews the
  direction the first click will sort to (defaultDir prop per column:
  asc for Name, desc for Assigned to)
- active column shows the current direction icon always

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Persistent underline (decoration-fg-tertiary) makes the build IDs read
as links at rest, matching the Figma. Hover transitions the
decoration to the brighter foreground color.

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
BuildsHeader carried two booleans (scoped, showSearchInput) and called
both filter hooks every render to keep hook order stable. It also
mirrored URL state into local useState via two useEffects to keep the
inputs snappy while the URL writes debounced.

Replace with the same pattern used by features/dashboard/sandbox/events:
- BuildsStatusFilter: pure presentational, statuses + onStatusesChange,
  no hooks beyond UI primitives. Mirrors EventTypeFilter.
- AllBuildsHeader: uses useFilters, composes the filter + search.
- TemplateBuildsHeader: uses useTemplateBuildsFilters (statuses + q),
  composes the same filter + search with a scoped placeholder.

The bespoke local-state + mirroring useEffect goes away. Search inputs
switch to the DebouncedInput primitive (ui/primitives/input.tsx), which
owns the mirror+debounce pattern once instead of duplicating it per
consumer. Filter setters drop the useDebounceCallback wrapper \u2014 status
clicks aren't bursty events and debouncing was only there to mask the
URL-write lag the local mirror was compensating for.

Both consumers updated:
- /templates/builds                            \u2192 <AllBuildsHeader />
- /templates/[templateId]/builds               \u2192 <TemplateBuildsHeader />

Rule alignment (vercel-composition-patterns,
vercel-react-best-practices):
- architecture-avoid-boolean-props: zero booleans on either header
- patterns-explicit-variants: each consumer is named and self-contained
- rerender-derived-state-no-effect: no state mirroring in the headers
- state-decouple-implementation: BuildsStatusFilter doesn't know where
  its state comes from

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
Remove JSDoc blocks and inline comments that restate the code or
narrate section boundaries. Keep only the ~10 single-line notes that
communicate non-obvious context naming cannot express:

- use-page-title cleanup-equality (subtle ownership check)
- layout.ts pathname-fallback role + glob ordering + flush-tabs note
- repository.server.ts: 'no per-template GET on infra' explainer
- title-binder.tsx: 'drives title + 404 entry point for all tabs'
- list/table-cells: 'defaults have no detail page'
- status-filter: 'can't deselect the last status' business rule
- use-filters / use-template-builds-filters: 'debounce in DebouncedInput'
- table.tsx: 'hook order stability' + 'q is client-side narrow'

Also rename BuildsTable's 'colSpan' to 'visibleColumnCount' so the
isTemplateScoped ? 5 : 6 calculation reads itself without needing a
3-line comment.

Net change: -131 +20 = -111 lines of comment surface area on the
branch (~60% reduction).

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
BuildsTable previously called both useFilters and useTemplateBuildsFilters
every render to keep React hook order stable, and derived
isTemplateScoped from the presence of an optional templateId prop \u2014
which then gated six conditional branches (statuses selection,
buildIdOrTemplate selection, client-side q narrowing, colgroup,
TableHead, per-row TableCell).

Replace the templateId presence-based mode toggle with explicit,
pre-resolved props:

- filters: { statuses, buildIdOrTemplate } \u2014 passed straight to the
  backend query
- postFilter?: (build) => boolean \u2014 optional client-side narrowing
- showTemplateColumn?: boolean \u2014 explicit structural visibility toggle

Filter hooks move up to the two page consumers, each calling exactly
one (the right one for its route). The detail builds page builds its
own postFilter from the scoped 'q' URL param; the templateID flows
into filters.buildIdOrTemplate directly. Both pages become 'use
client' (they were trivial wrappers with no server-only logic).

Rule alignment (vercel-composition-patterns):
- architecture-avoid-boolean-props: the one remaining boolean
  (showTemplateColumn) is a purely structural visibility flag, not the
  exponential-state-space behavioural booleans the rule targets
- state-decouple-implementation: BuildsTable no longer knows which URL
  hook drives its filters \u2014 consumer chooses, table works with any
  source

The variant identity now lives at the page level: TemplateBuildsPage
vs TemplateDetailBuildsPage are the explicit variants. Each calls one
hook, builds its own props, hands a fully-resolved configuration to a
single shared component.

Co-Authored-By: Craft Agent <agents-noreply@craft.do>
@drankou drankou requested a review from ben-fornefeld as a code owner June 2, 2026 15:24
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 478a8fe671

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/modules/templates/repository.server.ts Outdated
Comment thread src/features/dashboard/templates/tags/reassign-dialog.tsx
Comment thread src/features/dashboard/templates/detail/visibility-dropdown.tsx
Comment thread src/configs/urls.ts Outdated
Comment on lines 14 to 19
| 'names'
| 'createdAt'
| 'updatedAt'
| 'createdBy'
| 'lastSpawnedAt'
| 'spawnCount'
| 'buildCount'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing nit cleanup: createDefaultTemplatesRepository.getDefaultTemplatesCached in src/core/modules/templates/repository.server.ts still assigns createdBy: null on each mapped DefaultTemplate, even though this PR removed createdBy from the Template Pick (models.ts) and from every mock entry (mock-data.ts). It's dead — no compile error, no consumer reads it — but the assignment should be dropped to finish the cleanup the PR started.

Extended reasoning...

What the issue is. This PR removes createdBy from the Pick<> in src/core/modules/templates/models.ts (lines 14–19 in the diff) and removes createdBy: … / createdBy: null entries from every mock template in src/configs/mock-data.ts. However, the matching mapping in createDefaultTemplatesRepository.getDefaultTemplatesCached (around src/core/modules/templates/repository.server.ts:585) was not touched and still does createdBy: null inside its .map(...) callback when building templates: DefaultTemplate[].\n\nWhy this is not a build break. The original report hypothesized a TypeScript excess-property error, and a verifier confirmed via bunx tsc --noEmit that no such error fires. The literal flows through generic inference into the callback's return type U, the resulting U[] is then structurally assignable to DefaultTemplate[] (extra properties are fine for subtyping at the array level, and excess-property freshness doesn't apply once the literal has been inferred through a generic). Vercel's "Ready" deployment status is consistent with that. So the original "build break" framing is incorrect — I'm filing this corrected.\n\nWhat the actual impact is. Each returned DefaultTemplate object carries a createdBy: null property that the type system no longer surfaces and no consumer reads. The OpenAPI DefaultTemplate response shape doesn't include it, and the dashboard renders templates from the typed shape, so there is zero behavioral effect at runtime. This is dead/stale code, not a bug that affects users — which is why I'm marking it pre-existing/nit rather than normal.\n\nStep-by-step proof.\n1. Template in models.ts (post-PR) is Pick<…, 'templateID' | 'buildID' | … | 'updatedAt' | 'lastSpawnedAt' | …> — no createdBy.\n2. DefaultTemplate = Template & { isDefault: true; defaultDescription?: string } — also no createdBy.\n3. In repository.server.ts, the mapped object literal still includes createdBy: null, between updatedAt: t.createdAt, and lastSpawnedAt: t.createdAt,.\n4. TS narrows the callback return to the literal's shape, then unifies into U[] and finally checks U[] assignability against DefaultTemplate[] — that check accepts extra properties (no fresh-literal excess-property check applies post-inference), so the file compiles cleanly.\n5. At runtime, every entry of templates carries an untyped createdBy: null property. Nothing in the dashboard reads it, so it just sits there.\n\nHow to fix it. Drop the createdBy: null, line in the .map callback alongside the matching removals in models.ts and mock-data.ts. That completes the cleanup with no behavior change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant