feat(frontend): add the repository Commits tab [IFC-3150]
PR 6 of 11 for IFC-3101 · ticket IFC-3150 · spec set dev/specs/ifc-3101-repo-commit-visibility/, block 3.5 (T049–T058).
Base is the epic branch pog-repo-commit-visibility-ifc-3101: it holds the generated types from PR 2 (#10566). Frontend only, no backend file touched.
What this adds
A Commits tab on the repository detail page (CoreGenericRepository and its kinds), following the branch selected in the header like every other tab.
- Newest-first rows with short hash, summary, author and date, paged through
DataTableinsideInfiniteScroll. No total anywhere (FR-024). - Per-row state comes only from the response's
state: Remote head, Imported, Pending import, Not on current history as text badges; a row that is both head and imported carries both (FR-005, FR-028). - Header: refresh button inline with
Tracking <git_ref>and theChecked/Updatedfreshness line (FR-007), the pending count underBEHIND, and an amber notice forREWRITTENandORPHANED. - Row menu, as the ticket specifies: Copy commit hash (confirmed with a toast, or an error toast when copying fails) and View on GitHub, shown only when the repository
locationis a GitHub remote. The link is rebuilt from the validated owner and repo, so credentials inlocationnever reach it. - Not-yet-available state (
UNAVAILABLE): retried every 10 s for up to 30 attempts (about five minutes) while the reason isNOT_CLONEDorTIMEOUT, then the tab asks the user to refresh.NOT_IMPLEMENTEDis not retried and says the log is not available in this version. All of these stay distinct from a general error (FR-014).NOT_TRACKEDandNO_REMOTEget their own empty states. - Tab pill styled like the Tasks tab: the pending-import count when
BEHIND,0whenIN_SYNC, nothing otherwise. Never a total; the number carries a visually hidden "pending import" suffix.
Architecture
Follows dev/knowledge/frontend/entities-structure.md:
- api/:
get-repository-commits-from-api.ts(requestspending_counton the first page only,@include(if: $isFirstPage)) andrepository-commits.mappers.ts, the only place the wire shape changes (DateTimenarrowed from gql.tada'sunknown, edges flattened). - domain/: the model uses the generated enums and types directly. The use-case throws a typed
RepositoryGitUnavailableError(reason, message, and the log for the header) forUNAVAILABLE; every other condition is data. Rules are pure and tested (should-retry-git-unavailable,get-pending-import-count,get-commit-web-url,get-repository-location). - ui/queries/: one infinite query serves both the list and the tab pill (
selecton the first page), so there is one cache entry and one worker read. TanStack's own error handling keeps loaded pages when a refresh fails and retries a failed next page;retrywith a fixedretryDelaycovers warm-up without the loading-state flicker an interval refetch would cause.refetchOnWindowFocusis off and the stale time is a minute, because every loaded page is a worker round trip. - ui/:
RepositoryCommitsTabis the link tab andRepositoryCommitsManagerthe content, likeRepositoryObjectsTab/RepositoryObjectsManager; display decisions live inrepository-commits.view.tswith unit tests, components only render.
Shared changes:
DataTablegains an optionalgetRowId, so rows that are not Infrahub nodes need no fabricatedid/__typename; selection stays node-only at the type level. Existing callers are unchanged.copyTextToClipboardinshared/utils/clipboard.tsreports whether the copy worked (used byuseCopyToClipboardand the row menu);CopyToClipboardButtongains a visually hiddenrole="status"announcement (CopiedAnnouncement);NoDataFoundgains an optionaltitle.
Where the implementation deviates from the task text (recorded on the tasks)
- T054:
Paginationcannot hide its total, so the tab usesDataTable+InfiniteScroll(the ticket text already said so). - T053: retries with a fixed delay on the unavailable error, capped at 30, instead of
placeholderDataor an interval. - T051/T057:
NOT_IMPLEMENTEDis not retried because it is permanent for a deployment;NOT_CLONED,TIMEOUTand a missing reason are. - T058: fragment is
changelog/+ifc-3101-repository-commits-tab.added.md(towncrier orphan prefix); it describes what the tab shows once Infrahub can read commit history, and that until then the tab says the log is not available.
T094 (design canvas)
Built on the answers already encoded in T054: no commit-count badge, check time on the freshness line, a distinct notice for a rewritten ref. The fourth item, where "check remote now" sits (FR-015), stays with T072 in PR 9. T094 remains unchecked for that reason.
Docs
User-facing documentation is deliberately not in this PR: T088 (Phase 6, PR 11) owns it, and the backend still answers UNAVAILABLE / NOT_IMPLEMENTED for every repository until PR 5 lands, so the live tab shows the not-available state today.
Known cost
The tab pill reads the first page of the commit log (20 commits) on every repository page, one worker request, and retries it during a warm-up like the tab does.
Review findings declined, with reasons
- Refresh spins and is disabled while a warm-up retries: a retry is a fetch in progress, and the cap limits it to about five minutes.
- Memoizing
onLoadMore: the React Compiler memoizes it; manualuseCallbackis against the repo rule. - Amber notice uses the same colour literals as
branch-working-notice.tsx: no warning token exists yet. - gql.tada types
DateTimeasunknown(noscalarsconfig on the client): narrowed in the mapper; configuringscalars: { DateTime: string }app-wide is a follow-up because it shifts the betterer baseline.
Out of scope
Search, filter and sort (the contract takes only limit and offset; order is fixed newest-first), Check remote now (T072, PR 9), web links for non-GitHub hosts, and the e2e test T059/T060 (PR 7).
Verification
pnpm test(full browser suite): 229 files, 1806 tests passed on7ea148ec01.biome ci .(workspace root),pnpm knip,betterer ci: clean, baseline unchanged (176).- cubic local review: clean on
7ea148ec01before it was pushed. - Tried end to end against a local stack with a stand-in git-state reader (not committed), since the real worker read lands in PR 5.
🤖 Generated with Claude Code