Add keyset pagination to the artifact version list
What does this MR do and why?
This change is behind the feature flag :artifact_registry_ui, which is default-off.
The artifact version list (Maven/npm versions, Docker/OCI manifests) rendered only the first 20 rows a connection returned and had no pager, so an artifact with more versions or manifests than one page had rows that were permanently unreachable. This adds keyset pagination to both tables.
There is no design for this, and that was checked rather than assumed: the design proposal's
prototype was driven in a browser and its version-list frame renders six rows with no pager
element at all. The merged S14 spec reaches the same conclusion — "the design frames show no
pager; keyset previous and next is the only pagination the contract supports." So this step
follows the monolith convention already merged for repository detail instead of a design: the
same GlKeysetPagination + getPageParams pairing, with the cursor held in the route query,
that repositories/detail/repository_detail.vue and repositories/list/repositories_list.vue
already use. The pager sits centred below the table, matching repository detail, and exposes
no total and no numbered pages, because the Artifact Registry contract carries no total.
Changes
- The paging arguments are declared twice, deliberately. They repeat on the
package/imagefield as well as on the nestedversions/manifestsconnection, which looks redundant and is not. Apollo'sremoveDirectivesFromDocumentcollects variables from a removed@clientfield's own arguments, then returns without descending into its selection set. Because@clientsits onpackage(id:)/image(id:)and the connection is nested inside it, arguments declared only onversionswould survive into the document sent to the server, and the endpoint rejects the whole query withVariable $first is declared by <operation> but not used— which surfaces in the UI as a permanent "service is unavailable" alert. This was confirmed empirically for this branch rather than taken on trust: the document was first written the natural way, with the arguments on the connection alone, and the existing guard inee/spec/frontend/packages_and_registries/artifact_registry/graphql/queries_spec.jsfailed, reportingfirst,last,before, andafterleft declared on the server-bound document. That guard already covers every Artifact Registry document, so it protects this without a new test.graphql/typedefs.graphqldeclares the arguments onpackageandimageso the local schema accepts them; the resolvers ignore them, and they go away with the directive at the connect step. - The new cache policies replace a page rather than appending to it.
graphql/cache_config.jsgains two connection policies, reusing the existingARTIFACT_CONNECTION_POLICYshape ({ keyArgs: [], merge: (_, incoming) => incoming }):versionson both package types, andmanifestson the image type. A version and a manifest each carry anid, so the elements normalize with nokeyFieldsentry, but the connections still need a policy — the parent artifact is a single cache entity, so without a replacing merge a second page would extend the first under the same entry rather than replace it. Steps 19 (sort) and 21 (referrer preference) add their own arguments to these policies'keyArgswhen they land. - Re-announcement needs no extra machinery. The live region re-announces on a page change
with nothing added: a live region stays silent on a message identical to the one it already
holds, and the loading pass a cursor change causes is what separates one page's
announcement from the next. This is the same mechanism
repository_detail.vuealready relies on, so the clear-and-restore the plan anticipated was not needed. - One mock-seed change, so the manifest pager is reachable by hand.
graphql/mock_artifacts.jsraisesMAX_SUBJECT_MANIFESTSfrom 12 to 34. Manifest ladders topped out near 17 rows once referrers were added, under the page size of 20, so no container artifact could reach a second page and the manifest pager was drivable only from a spec, never in a browser. With the raise, several container artifacts carry 21 to 37 manifests. The version ladders already ran to 40 and needed no change. - Dropping the active cursor is not in this step. The rule that clears the cursor when the rest of the query changes belongs to Step 19, which introduces the sort. Today the route query holds nothing but the cursor, so there is nothing to drop it against. One spec here asserts the pager leaves query parameters it does not own alone, which is what Steps 19 and 21 will rely on.
- Tests.
version_list_spec.jsgains a pager block that runs over both connection families — Maven for versions, Docker for manifests — covering the pager driven from the returnedpageInfo, the pager hidden for a single-page connection, the cursor written to the route query, the opposite cursor cleared, unrelated query parameters left alone, the page arguments the route query produces for the first, next, and previous page, the forward walk, the walk back, a return to a cursor already visited, and the live-region re-announcement.cache_config_spec.jsgains a matching pair for the two new policies. 1260 Jest tests pass on both Vue 2 and Vue 3. Two failures inrepository_detail_spec.jsare pre-existing on unmodifiedmaster— confirmed by running that spec onorigin/master— and are unrelated to this branch.
Verified in a browser against the local GDK for both families. On an npm artifact with 36
versions, page one renders 20 rows with Previous disabled; Next writes after=<cursor> to the
URL and renders the remaining 16 rows with Next then disabled; Previous clears after, writes
before=<cursor>, and returns the first 20. Sixteen rows rather than 36 on page two is what
shows the cache policy replacing rather than appending. The same walk holds for a Docker
artifact with 36 manifests, and a deep-linked ?after=<cursor> URL reloaded from scratch
renders the second page, so a page is shareable and survives a reload.
How to set up and validate locally
- Enable the flag:
Feature.enable(:artifact_registry_ui). - Visit
/o/<organization>/-/artifact_registry/acme/repositories. - Open an npm repository and pick an artifact with more than 20 versions — the detail table's Versions column shows the count, so choose a row above 20.
- Confirm the version table renders 20 rows with Previous disabled.
- Click Next; confirm the remaining rows render, the URL gains
after=<cursor>, and Next is now disabled. - Click Previous; confirm the first page returns,
afteris gone, andbefore=<cursor>is written in its place. - Reload a deep-linked
?after=<cursor>URL and confirm it renders the second page directly. - Repeat the walk on a container repository. There is no count column for images, so open a few artifacts until the pager appears.
Screenshots or screen recordings
An npm artifact with 36 versions. Page one: 20 rows, Previous disabled.
Page two after Next, reached at ?after=<cursor>: the remaining 16 rows, Previous now live and
Next disabled. Sixteen rows rather than 36 is the cache policy replacing the page rather than
appending to it.
The container side, on a Docker artifact with 36 manifests — the pager sits centred below the table, as it does on repository detail.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.


