Add the Docker and OCI manifest detail page
What does this MR do and why?
The container branch of the version list dead-ends. A repository of either container format renders its manifests, and a reader who wants to know what one of them is — which platforms it indexes, what it is tagged, what attests to it — has nowhere to go, because Artifact Registry served no single-manifest read.
That cleared on 2026-09-04: ops/artifact-registry!2317 declared manifest detail by digest along with the ten fields the page renders. The implementation is ops/artifact-registry#1150, ten Go step MRs away, so this page is built against the local Apollo layer the rest of the slice was built against and connects when those land.
This is Step 1 of monolith/S06 Phase 2 — which the S06 spec owns outright: "Phase 2 is not a separate slice: this slice owns the version-detail surface, so it owns every format that surface covers."
What lands, and what does not
Lands: the route, the whole page-document local layer, the four page states, and the header. The page has no entry point yet — the manifests table rows are still inert — so it is reachable by URL alone until the row link lands. Tabs, sidebar, tag badges, Overview, annotations, Manifest tab and Referrers each arrive in their own step.
Three prototype elements are designed out rather than deferred, because no contract will serve them: the sidebar Source block (container manifests carry no created_by, project_id or git_commit_sha — Version has all three and #1150 puts image-level source out of scope), Downloads (Go-service S18; last_downloaded_at is a timestamp, not a count), and the Docker Labels panel (nothing stored at all).
Four things worth a reviewer's attention
The route carries the full canonical sha256:<hex>. That is how the contract addresses a manifest and how the design keys the page, and shortDigest truncates to twelve characters with no way back, so the shortened form could not address anything. vue-router leaves the colon unencoded in the path — legal per RFC 3986 — and index_spec.js pins the round-trip in a form that holds whether or not a router encodes it, on both Vue versions.
The segment is a literal manifests/ ahead of the bare :versionId. vue-router resolves statically and matches in declaration order, so the bare parameter would otherwise claim the segment and render the package page for a container repository. A repository is never both formats, so the two pages are never both reachable — but the router does not know that.
The local layer carries the whole declared resource, not the subset this step selects. The list element and the detail are one resource in the contract, and the seed data is the expensive half: the ladder modelled no tags, no index children, no platform triple, no annotations and no parent digests. Seeding once leaves the sidebar, the tag badges and the annotations panel as view changes. Three of those fields read across rows, so they are layered on after both ladder passes — an index names its children's digests, each child names the index back, and a referrer count is the referrer rows already built. The platform an index advertises for a child is the triple that child carries, so the Platforms block and the child's own header cannot disagree; mock_artifacts_spec.js pins that.
A sibling component, not a format branch on version_detail.vue. That file is ~340 lines and Maven/npm all the way down — two package-shaped documents, a format-keyed tab map, a statusMessage reading "version %{version} of %{name}". Branching would roughly double every computed and push it past 700 lines. The state machine, live region and breadcrumb-publishing shape are copied; nothing else was reusable.
queries_spec.js now polices this document too — the digest addresses the @client field's own arguments, so it leaves with that field and the server never sees it declared-but-unused.
Screenshots or screen recordings
Driven against the GDK at /o/default/-/artifact_registry/acme/repositories/<repo>/<imageId>/manifests/<digest>. The page renders below the header only from Step 2 on — the empty main column is what this step ships.
Docker, multi-arch index — short digest as the heading, Index · 3 platforms off childrenCount, the media type, and the whole digest with a copy action.
Docker, single-platform image — the same page for a child of that index. The kind line reads Image with no platform count, and the digest line is labelled Image digest rather than Index digest.
OCI index — the OCI media type, and the letter-avatar fallback in place of a format logo. REPOSITORY_FORMAT_LOGOS has no OCI entry: its logo waits on Linux Foundation trademark approval (gitlab-com/legal-and-compliance#3607), so Docker and OCI do not share an icon here even though they share the container path segment and tool list.
The breadcrumb resolves to Default / Repositories / docker-images / service-client / 167b1b0d167b and the document title to the short digest, both off the new manifestName slot. The live region announces Manifest 167b1b0d167b of service-client.
How to set up and validate locally
Feature.enable(:artifact_registry_ui), sign in asroot.- The page is not reachable by clicking yet. Run Storybook (
yarn storybook:start, port 6006) and openee/artifact_registry/repositories/versions/detail/manifest_detail— the stories cover the Docker index, the OCI index, a single-platform child, a referrer, a remote repository, and every failure state. yarn jest ee/spec/frontend/packages_and_registries/artifact_registry/and the same withVUE_VERSION=3— 1745 pass on both.
version_list_spec.js and repository_detail_spec.js fail to run locally on missing generated fixtures. Verified pre-existing on a clean master — they contribute 0 tests either way, and CI generates the fixtures.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.


