Add referrer manifests preference to the artifact version list
This change is behind the feature flag :artifact_registry_ui, which is default-off.
What does this MR do and why?
This is Step 21 of the monolith/S14 artifact version list slice, and the last frontend step in it. It adds a Preferences section to the version list's view-options popover, shown only for container (Docker/OCI) artifacts, with one switch: Referrer manifests. It starts on.
A container artifact's manifest list mixes two kinds of row: subjects (top-level images and multi-arch platform children) and referrers (signatures, SBOMs, attestations), which the Artifact Registry contract discriminates on subject_digest. The manifests endpoint's include_referrers parameter defaults to false. This view deliberately overrides that default: the Figma popover and the prototype both show the switch on, and closed beta shows the full inventory, so the first load sends true. The schema argument keeps the endpoint's own false.
This MR is stacked on !252093 (merged) and targets zcuddy-s14-step19-sortable-columns, not master. It is rebased onto that MR's current revision, which dropped the Version sort. applyQueryParams, the cursor-drop rule, and the sort entry in keyArgs only exist on that branch. Off master this would duplicate about forty lines Step 19 owns and hard-conflict with it in version_list.vue and cache_config.js. Retarget to master once !252093 (merged) merges.
Changes
- The preference lives in the route query, not local storage. The key is
include_referrers, and an absent key reads as on, so only the departure from the default (?include_referrers=false) ever appears in the URL. This makes a filtered list shareable and lets it survive a reload. It deliberately skipsLocalStorageSync, which the Columns section uses: columns are presentation, this is a query argument that changes the result set, the same distinction Step 19 made for the sort. There is also no stored value that could make a return visit sendfalse, so "sends true on first load" holds unconditionally. - Toggling resets to the first page at no cost in new code. It reuses Step 19's
applyQueryParams, which writes the route query and drops both cursors; one call is the whole reset. Step 19's description anticipated this, and the reset is necessary because referrer-included and referrer-excluded are distinct server-side scans, so a cursor cut against one names nothing in the other. - The referrer argument is declared twice in the query document, on the
@clientimagefield and on the nestedmanifestsconnection, for the same reason the paging and sort arguments already are: Apollo collects no variables from inside a removed@clientfield, so one declared only on the connection would survive into the server-bound document and be rejected. The existing guard inqueries_spec.jscatches a regression here. - The mock resolver excludes referrers when the argument is omitted, matching the endpoint's own default rather than being more permissive than the schema it stands in for;
mock_resolvers.jsalready documents that a schema mismatch must not be able to hide behind a permissive mock. The filter runs before the ordering and the paging, so the cursors andpageInfoare cut over the set the caller asked for. The default lives in the resolver rather than the typedef, because Apollo's local resolvers build arguments from the document rather than the schema and would never apply one written there. - The manifests connection's cache policy now keys on the referrer argument as well as the sort (
keyArgs: ['sort', 'includeReferrers']), so a referrer-excluded page cannot overwrite the entry a referrer-included one wrote. The two versions policies are unchanged, since that connection takes no referrer argument. !251846 (merged) anticipated this addition. - The variables are spread conditionally, so only the manifests read carries
includeReferrers. The page shares onevariables()for both documents and swaps which one it issues; the versions document never declares the variable. - Coercion happens once, in the view.
$route.query.include_referrers !== 'false'yields a real boolean for any input, so a hand-edited or stale?include_referrers=maybefalls back to the default instead of putting a value on the wire that the endpoint would reject with a400. - No new live-region string. A toggle flips the loading state, so the region already goes "Loading versions." then "Version list for <name> updated.". !251846 (merged) established that the loading pass is what separates one announcement from the next, and that the clear-and-restore the plan anticipated was unnecessary.
- Comments kept to the ones that stop a reader breaking something. An earlier revision of this MR carried 44 added comment lines; this one carries four. What survives is the typedef's note that a
= falsedefault written there would never apply (Apollo's local resolvers read arguments off the document, not the schema), the one-liner on the conditional variables spread, and a pre-existing comment relocated in a spec. The rest was design rationale and now lives in this description and the commit message.
Screenshots or screen recordings
docker-images/catalog, an image carrying 24 manifests of which 19 are subjects. Driven against the local GDK with the AR client stub.
| Preference on (default) | Preference off |
|---|---|
![]() |
![]() |
On the left, Columns then a rule then Preferences, the switch on, 20 rows of 24 with the pager showing a second page. On the right, the switch off, 19 rows, no referrer rows, the Type column narrowed to Index and Image, ?include_referrers=false in the URL, and no pager because the filtered set is one page.
How to set up and validate locally
Feature.enable(:artifact_registry_ui), and install the local AR client stub.- Open the
docker-imagesrepository and click into thecatalogartifact. It carries 24 manifests, 19 of them subjects, which is what makes step 5 visible. - Open the view-options popover. Confirm it renders
Columns, then a rule, thenPreferenceswith Referrer manifests on, and that the URL carries no query key. - Confirm the table opens with referrer rows present, i.e. Type cells reading "Signature for ...", "SLSA attestation for ...", and similar.
- Switch the preference off. Confirm five rows disappear, the Type column narrows to
IndexandImage,?include_referrers=falseappears in the URL, the panel stays open, and the pager disappears, because 19 rows is a single page where 24 was two. - Page to page two with the preference on, then switch it off. Confirm the cursor leaves the URL and the list returns to the first page of the filtered set.
- Switch it back on and confirm the key is removed from the URL rather than set to
true. - Reload
?include_referrers=falsefrom scratch and confirm it renders off, so a shared link reproduces. - Open a Maven or npm artifact. Confirm the popover shows one section and no rule, with no
Preferencesheading and no switch, and that the same query key in the URL has no effect.
Agent Notes
- The Preferences section ships with one switch, not two. The prototype also shows an Untagged manifests switch above this one, which the spec defers to Phase 2 because the contract carries no tagged/untagged filter. Two switches in the prototype capture against one here is a scope decision, not a gap.
- The empty-state sentence is not in this MR. The spec has the container empty state name this preference when reached with it off. That component only exists on !250733 (merged), and both the spec and the plan say whichever of the two lands second carries the sentence into place; this one lands first. Worth a reviewer's attention: !250733 (merged) currently has an unresolved thread asking whether the empty state is needed at all, so if it is dropped, the sentence has nowhere to go and that part of criterion 18 needs rethinking rather than just re-assigning.
- The switch is a no-op on a remote container repository, and this is a finding rather than a fix.
api/openapi/v1.yamlsayssubject_digestis "Alwaysnullon a remote repository, whose cache does not store it, which is whyinclude_referrerschanges nothing there." Hiding the switch would mean selectingkindon the entity query and gating the section, which is past what the spec asks for. Raising it for the follow-up bucket instead. - The
nested-interactiveaxe trade-off is inherited, not introduced. !251881 (merged) documented thatGlDisclosureDropdownItemrenders its slot inside a button whileGlTogglerenders arole="switch"button. This switch follows the same shape as the column switches rather than adding a second idiom. Verified in the browser that it does expose an accessible name andaria-checkedstate. - Four existing assertions were updated rather than left to break, and each is worth a glance.
view_options_spec.jsasserted the full ordered list of every toggle in the panel, so its finder is now scoped to the Columns section.version_list_spec.jsasserted the whole connection-arguments object withtoStrictEqualagainst expectations shared by Maven and Docker, so the table now carries the per-format extra arguments. The three manifests cases inmock_resolvers_spec.jswanted the whole ladder and now ask for it explicitly. - The referrer preference is tested container-only on purpose. An earlier revision ran the toggle block under both format families and the Maven case failed, correctly: pushing the key changes no variable on a versions read, so no loading pass fires and nothing is re-announced. The popover never emits the event for a package family, so testing it there tested something unreachable.
- 1575 Jest tests pass on both Vue 2 and Vue 3. Two failures in
repository_detail_spec.jsare pre-existing; confirmed by reverting this branch's changes and re-running against the base. Nothing here touches that page. ⚠️ Everyversion_listStorybook story renders the not-found state on this branch, including ones this MR does not touch. Confirmed pre-existing by reverting all of this MR's application code and reloading a fresh iframe:DefaultandDockerImageboth render "Page not found". So the newReferrersExcludedstory is added for consistency withSortedByVersionbut is not currently exercising anything, and the automated axe run over this popover is not doing what it looks like it is doing. Not diagnosed and not fixed here, since the cause predates this step, but it also means !252093 (merged)'s story-based axe coverage claim needs a second look.
This MR was authored with the assistance of an AI coding agent and reviewed by @zcuddy before submission.

