monolith/S05 virtual Step 4: Virtual main region and the upstream table

What this does

Makes the Artifact Registry repository detail page serve a virtual repository. A virtual repository's main content is its ordered upstream list rather than an artifact table, so the view gains a third smart query for that list, skips the artifact connection on that kind, and renders a new upstream table with Order, Repository, and Type columns plus the Order help affordance.

Step 3 (!255496 (merged)) has merged, so this targets master and reads the real upstreamRepositories field, a deliberate departure from the plan's local Apollo stand-in for this step. See Difference from the plan.

Step 4 of the monolith/S05 virtual repository detail plan, dark behind artifact_registry_ui. Plan: gitlab-org/ops/artifact-registry!2534 (merged) ("Step 4: Virtual main region and the upstream table").

Closes #628665 (closed)

Changes

  • constants.js: REPOSITORY_KIND_VIRTUAL, the association/summary typenames, UPSTREAM_REPOSITORIES_TABLE_FIELDS, and UPSTREAM_REPOSITORIES_ORDER_HELP.
  • graphql/queries/get_repository_upstream_repositories.query.graphql (new): selects id position upstreamRepository { id name kind }, no @client, so the whole document goes to the server; format is deliberately omitted.
  • graphql/cache_config.js: the association keys on its own id, not the nested upstream's, since two virtual repositories can hold the same upstream at different positions. The summary is unnormalized (keyFields: false) since nothing reads it alone; the upstreamRepositories replace-merge silences Apollo's cache-loss warning on list shrink.
  • repositories/detail/repository_detail.vue: skips the upstream query unless the repository is virtual, and now also skips the artifact connection on that kind. Loading, emptiness, and not-found derive from whichever read the kind selects; the sidebar and Setup button read the same signal. No pager on the virtual branch.
  • repositories/detail/upstream_repositories_table.vue (new): three columns, no sort, filter, reorder, pager, row link, or actions column, with a keyboard-operable HelpPopover on the Order header. An empty list gets a one-line placeholder rather than bare headers; see Deliberately not in this step.
  • repositories/detail/repository_detail.stories.js: six virtual stories for the test-storybook axe run.
  • Tests and fixtures: four spec files, plus fixture updates in ee/spec/frontend/packages_and_registries/artifact_registry/mock_data.js.

Notes for the reviewer:

  • The artifacts live region does not render on a virtual repository: the artifact query never runs there and this step has no upstream announcement copy, so suppressing it beats inventing wording.
  • The story's handlers write organization.artifactRegistryRepository, which two pre-existing reads keyed on the wrong typename; this step's read named the real type first, risking a story crash on an absent createdAt. Fixture-only; the shared fixture is left as-is for now, since three merged specs count refetches against its current cache identity.

Deliberately not in this step

The empty upstream list gets a placeholder, not an empty state: one line of subtle text inside the table body, the same shape artifacts_table.vue uses for a remote repository. The illustrated empty state and the Setup-button rule that goes with it are Step 6 (#628667 (closed)), so that step replaces a line rather than filling a void. A placeholder is worth having in the meantime because an empty upstream list is not a corner case: AR's docs/specs/S30-maven-virtual.md calls it the likeliest closed-beta misconfiguration and the one an operator most needs to tell apart from a package no upstream holds.

"Virtual sidebar stats" is #628666 (closed) and the per-row kebab is #628668 (closed). The Size column, the visibility icon, the upstream URL, the reachability check mark, and the filter bar are spec deferrals; format renders nowhere in the table.

How to set up and validate locally

Frontend only, and the stories mock the detail read, so this needs no GDK and no Artifact Registry. Run from the repository root. A root yarn <script> fails here on pre-existing check-dependencies drift, so the steps below call the binaries directly.

Step 1 - the new and changed frontend specs

node_modules/.bin/jest --config jest.config.js \
  ee/spec/frontend/packages_and_registries/artifact_registry/repositories/detail/upstream_repositories_table_spec.js \
  ee/spec/frontend/packages_and_registries/artifact_registry/repositories/detail/repository_detail_spec.js \
  ee/spec/frontend/packages_and_registries/artifact_registry/graphql
Output (✅ 9 suites, 332 tests)
Test Suites: 9 passed, 9 total
Tests:       332 passed, 332 total

Step 2 - the whole Artifact Registry frontend suite, as a regression check

node_modules/.bin/jest --config jest.config.js \
  ee/spec/frontend/packages_and_registries/artifact_registry
Output (⚠️ 59 of 67 suites, 2082 of 2082 tests, eight pre-existing load failures)
Test Suites: 8 failed, 59 passed, 67 total
Tests:       2082 passed, 2082 total

Zero test failures. The eight suites that do not run are all under repositories/versions/**, and each fails to load rather than failing an assertion, because three generated fixtures are absent from the working tree: get_artifact.query.graphql.json, get_version.query.graphql.json, and get_version_files.query.graphql.json. Master has added more version-detail specs that read these fixtures since this branch diverged, which is why the failure count grew from one to eight. This branch defines none of those specs or fixtures, and no such fixture exists anywhere in the tree.

Step 3 - ESLint and Prettier on every JS and Vue file the branch touches

git diff --name-only --diff-filter=d origin/master...HEAD \
  | grep -E '\.(js|vue)$' > /tmp/f.txt
tr '\n' '\0' < /tmp/f.txt | xargs -0 node_modules/.bin/eslint
tr '\n' '\0' < /tmp/f.txt | xargs -0 node_modules/.bin/prettier --check
Output (✅ 13 files, eslint exit 0, no findings)
[V] Using Vue.js 2 (compiler 2)
Checking formatting...
All matched files use Prettier code style!

Step 4 - the i18n extractor is idempotent, so the committed pot is complete

tooling/bin/gettext_extractor locale/gitlab.pot
git diff --stat locale/gitlab.pot
Output (✅ pot unchanged after re-running the extractor)

The three msgids the branch adds:

+msgid "ArtifactRegistry|Order"
+msgid "ArtifactRegistry|Repositories are checked in this order when resolving an artifact. To reorder, edit this repository."
+msgid "ArtifactRegistry|There are no upstream repositories yet"

"Repository" and "Type", the table's other two column labels, reuse existing entries.

Step 5 - the query document goes to the server whole

node_modules/.bin/jest --config jest.config.js \
  ee/spec/frontend/packages_and_registries/artifact_registry/graphql/queries_spec.js \
  -t "getArtifactRegistryRepositoryUpstreamRepositories" --verbose
Output (✅)
getArtifactRegistryRepositoryUpstreamRepositories
  ✓ sends the whole document to the server, upstream list included
  ✓ selects the association fields the table and the cache need
  ✓ selects the upstream summary without its format
  ✓ declares the identity variables alone, since the list is unpaginated

Stripping the client sets leaves the document unchanged, which is the assertion that no local stand-in is left behind.

Evidence

The recordings below are the latest virtual-repository detail captures against the local GDK: real client, resolver, GraphQL, Apollo, Vue, and CSS, with only Artifact Registry's HTTP boundary stubbed, because AR cannot serve a virtual repository (listUpstreamRepositories is unserved for all four formats and virtual create still answers 422 for Maven and npm; see gitlab-org/ops/artifact-registry#314 (closed)).

Re-verified live on 2026-09-16 against this branch's code (the Step 5 tip 0421da2ed82248, which includes this MR's 1e80cf5355bf66): every scenario below still renders as recorded, and the empty upstream list now shows this branch's placeholder, captured as a still since it is the one state this MR changes.

What was tested (no regressions)

Every scenario was re-run live on this branch, plus the repository listing and the hosted-and-remote-only case. All pass. The only intended change is the empty state.

Scenario Result
Listing, hosted and remote only (no virtual repo) Both rows render, columns sortable, no error
Hosted detail Package/Actions table, 3 Maven packages, row links, no upstream table
Remote detail Package/Last downloaded/Actions, 2 cached packages, connection section, no upstream table
Virtual, populated Upstream table, 4 rows, positions 1-4, Remote/Hosted/Hosted/Remote, Order help
Virtual, empty upstream list Placeholder "There are no upstream repositories yet" (the change this MR makes)
Virtual, upstream list resolves null Not-found state, no heading or sidebar
Virtual, upstream read fails Service-unavailable alert in the main region, heading and sidebar kept

Frontend specs cover the same states: 2079 Artifact Registry Jest tests pass, 0 failures. The eight suites that do not load are a pre-existing worktree fixture gap under repositories/versions/**, unrelated to this branch.

The virtual page

Four upstreams at positions 1-4, Remote/Hosted/Hosted/Remote badges, ending with the Order help popover opened from the keyboard.

The empty upstream list

gdk-virtual-empty

Kept as a still because this is the one state the MR changes: the placeholder "There are no upstream repositories yet" now stands in place of the bare headers the earlier build showed. The illustrated empty state is Step 6 (#628667 (closed)).

Regression: a hosted repository is unchanged

Artifact table with three Maven packages and row links intact; no upstream table or query.

Regression: a remote repository is unchanged

Two cached packages, Last downloaded column, Connection status Healthy; no upstream table.

Regression: hosted and remote only, no virtual repository

gdk-listing-hosted-remote-only

An organization with only hosted and remote repositories, no virtual one: the list renders both and each opens, so nothing on this page requires a virtual repository to exist.

The upstream list resolves null

Not-found state alone, no heading or sidebar.

The upstream read fails

Service-unavailable alert in the main region, heading and sidebar still rendered.

Order help affordance, from the keyboard

Reached by Tab, opened with Enter. The popover text and the trigger's accessible name are both exactly:

Repositories are checked in this order when resolving an artifact. To reorder, edit this repository.

HelpPopover is given no title, so the accessible name is the copy itself.

Measured on the rendered page

Measured at 990af9bdb236f9, when the widths were inline thStyle percentages. Six of the eight properties matched the Figma design within rounding and still hold. The two marked below no longer do: the widths moved to gl-w-* utilities (see Design fidelity), which the style guide asks for but which cannot express 68.9% or 21.2% without a Tailwind arbitrary value, so each drifts about a point. Say the word if you would rather hold the exact design numbers and take the inline style.

Property Design Measured
Order column width 75 of 756, so 9.9% gl-w-1/10, 10.0%
Repository column width 521 of 756, so 68.9% ⚠️ gl-w-7/10, 70.0%
Type column width 160 of 756, so 21.2% ⚠️ gl-w-1/5, 20.0%
Order badge centring centred offset from cell centre 0px
Stat row gap 8px, icon plus one text node 8px, 2 children
Stat text colour #3a383f default rgb(58, 56, 63)
Attribution colour and weight subtle, 400 rgb(98, 97, 104), 400
Order label to icon gap 4px 4px

Design fidelity

Four fixes, found by reading bound variables out of the file rather than eyeballing:

  • The Order help affordance used the help component's question-o default icon; the design names information-o.
  • The Order value was left-aligned text; the design centres it as a badge and hides the plain-text variant.
  • The Order column took 25.2% of the table; the design gives it 9.9%. The three widths are gl-w-1/10 / gl-w-7/10 / gl-w-1/5 rather than inline thStyle percentages, per the style guide's preference for utility classes, and the Order header picked up the centring its cells already had. thStyle appears 21 times across the codebase against many thClass, and no sibling table here uses it.
  • The sidebar stat row and attribution rows used the wrong gap, colour, and weight; corrected to the design's icon-plus-text layout at 8px, default colour and weight throughout. Also fixes repository_sidebar.vue and attributed_timestamp.vue on every repository kind.

One difference is deliberate: the design's stat icons resolve to the default text colour where this ships variant="subtle", matching this codebase's established pattern.

Difference from the plan

The plan specifies a local Apollo stand-in for this step (an @client field, typedefs.graphql, mock_resolvers.js, a fixture module), removed later in its Step 8. This MR ships the server-bound read instead, so Step 8 loses that removal work.

Both of the plan's reasons for @client, parallelism and drivability ahead of Step 3, have expired now that Step 3 has merged and Storybook plus the specs cover all six virtual states.

What tipped the decision is the closed beta: artifact_registry_ui is rolling out to design partners, so the mock layer is live customer-facing code, not scaffolding, and this is already true today (create has landed for containers, list has not): a partner could open a virtual repository and see upstreams they never configured. The mock surface is also being retired (master is down to one @client field), and a server-bound read shortens the path to a working page.

The cost: until AR serves the route, this page renders not-found or the service-unavailable alert, and exercising it needs the AR boundary stubbed. Two amendments are needed: the monolith S07 virtual CRUD plan's Step 12 should have its own Step 7 own a local read instead of depending on this one, and this plan's Step 8 needs its file list corrected.

What comes next

This step unblocks Steps 5, 6, and 7: "Virtual sidebar stats" (#628666 (closed)), the empty state (#628667 (closed)), and the upstream row actions (#628668 (closed)), all sibling-parallel off this branch.

Step 8 (#628669 (closed)) no longer has a directive to drop or a local layer to delete; what is left is the generated fixture and the routed browser example. See Difference from the plan.

Artifact Registry remains the gate: the upstream association operations are still unserved, per the status in Evidence.

Related to #628475 (closed)

🤖 Generated with Claude Code

Edited by Fiona McCawley

Merge request reports

Loading
Loading