Add keyset pagination to the artifact registry artifact table

What this does

The repository detail page rendered whatever the artifact connection returned in one ungrouped run. This pages it: keyset Previous / Next over the opaque cursors, with the cursor held in the route query so a page is shareable, survives a reload, and answers the browser's back button.

Step 13 of the repository detail plan, closing acceptance criterion 8 (paging part) and criterion 15 (pagination part). Behind feature flag :artifact_registry_ui, still dark.

This is the last frontend step of monolith/S05. Everything left in the plan is the backend track and the connect step.

A Maven repository in the GDK, page one Page two
detail-page-one detail-page-two

payment-core seeds 25 packages, so page one holds 20 and page two the remaining 5, with Next disabled at the end. A single-page repository renders no pager at all — GlKeysetPagination hides itself when neither page exists, which covers the first read, a short repository, and the empty state without a guard in the template:

detail-single-page

The paging arguments are declared twice, and deleting the redundant-looking copy breaks the page

artifactRegistryRepository(...) @client repeats the four paging arguments that packages(...) and images(...) already take, and its resolver ignores them. This is the one thing in the diff worth your attention.

Apollo removes the @client subtree before the request, but it only drops the variable definitions the removed field used in its own arguments — removeDirectivesFromDocument collects from node.arguments and then returns null, never descending into the selection set. A cursor variable used only by a nested client field therefore survives into a document that no longer references it, and the endpoint rejects the whole query:

Variable $first is declared by getArtifactRegistryRepositoryPackages but not used

which surfaced as a permanent "The Artifact Registry service is unavailable." alert over the artifact table, with nothing in the browser console. Repeating the arguments one level up is what gets them pruned.

Every fully-local document in the codebase puts its arguments on the top-level @client field for this reason — k8s_dashboard_pods, the pipeline editor's client queries, and the sibling repositories list. This slice cannot, because the connection is nested under the repository.

graphql/queries_spec.js pins it: it strips each Artifact Registry document the way Apollo does and fails on any variable left declared but unreferenced. It covers all five documents, so the next one to grow a nested argument fails in CI rather than in a browser. The duplication goes when the @client directive does, in Step 14.

Deviation from the prototype

The prototype is the SSoT, and its detail page has no pager at all — the artifact arrays are hard-coded literals and it reads no page parameter, so there is nothing to match. The spec's "keyset previous/next over the opaque cursors, the same pager convention as monolith/S04" governs instead, and the prototype's repositories list supplies the visual language: two buttons, no numbered pages, no page-size selector, no result count. That is what GlKeysetPagination renders.

Where the pager lives, and why not in the section

In repository_detail.vue, below <artifacts-section> in the main column. Step 11 (!249049 (merged)) deliberately made that section presentational — props down, nothing emits — and routing pager events back through it would undo that. The page already owns the connection, so it already owns the page info behind the pager.

Two consequences a reviewer should look at

  • The cache holds every cursor of one repository under a single entry that the incoming page replaces, so paging does not accumulate a copy of each page visited. That entry is why the read policy is CACHE_AND_NETWORK: cache-first would answer a page change with the page it already held. Only cache_config_spec.js can see this — the page renders the same either way — so that is where it is pinned, and I mutation-checked it.

  • The live region empties on the route change and refills when the page arrives, rather than doing both in one tick. A live region does not announce a message identical to the one it holds, and every page here announces the same sentence, so without the gap only the first page would ever be announced. Verified in a real browser with real timing: "Artifact list updated." → "" → "Artifact list updated.".

Modelled on !249266 (merged)

@rchanila's sibling pager on the repositories list landed the conventions this follows, and the two share mock_data.js. I took his opaque cursor constants (FIRST_PAGE_END_CURSOR, SECOND_PAGE_START_CURSOR) and their values verbatim, his mockPageInfo(overrides) helper, the cursor-driven resolver, the pageForward/pageBack helpers, the paged-arguments table test, and his approach of handing the mock cache the view's own type policies. We had independently introduced the same two constant names with different values — that is now reconciled here, so whichever lands second has one file to merge rather than two conventions.

One difference worth flagging back: his cache assertion lives in the component spec. I mutation-checked the equivalent here and the component renders identically with or without the policy, because the resolver answers each cursor and the network result wins — so the same gap may apply to the list's policy.

The mock paginate helper now takes a cursor key extractor, because artifacts key on id (a Maven package has no name) where repositories key on name. That is the shared-file rebase point between the two MRs.

Testing

  • 649 Jest tests across 32 suites, including the new queries_spec.js and cache_config_spec.js
  • Driven in the GDK against the mock resolvers: paged forward and back on Maven (25) and Docker (24), confirmed the route query, a reload on page two, and no pager on npm (3)

Merge request reports

Loading
Loading