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, andUPSTREAM_REPOSITORIES_ORDER_HELP.graphql/queries/get_repository_upstream_repositories.query.graphql(new): selectsid position upstreamRepository { id name kind }, no@client, so the whole document goes to the server;formatis deliberately omitted.graphql/cache_config.js: the association keys on its ownid, 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; theupstreamRepositoriesreplace-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-operableHelpPopoveron 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 thetest-storybookaxe 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 absentcreatedAt. 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/graphqlOutput (✅ 9 suites, 332 tests)
Test Suites: 9 passed, 9 total
Tests: 332 passed, 332 totalStep 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_registryOutput (⚠️ 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 totalZero 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 --checkOutput (✅ 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.potOutput (✅ 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" --verboseOutput (✅ )
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 unpaginatedStripping 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
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
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-odefault icon; the design namesinformation-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/5rather than inlinethStylepercentages, per the style guide's preference for utility classes, and the Order header picked up the centring its cells already had.thStyleappears 21 times across the codebase against manythClass, 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.vueandattributed_timestamp.vueon 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)

