monolith/S05 virtual: render upstream size, URL, and connection status in the upstream table
What this does
Renders each upstream's size, URL, and connection status in the virtual repository detail page's upstream table, as the design (Figma AR-concepting, 589-36301) draws them. It also passes each stored upstream's size and URL through to the Edit page's upstream list, which until now rendered them only for upstreams added through the picker. The fields come from the widened upstream summary that !258049 (merged) brings into GraphQL, so they all arrive on the one upstream-list call with no per-row repository read.
Visibility is dropped for now. The design also draws a visibility icon on each row, but the closed beta creates Private repositories only, so every row would show the same lock that the page heading already shows. The repositories list dropped its per-row icon for the same reason in !248512 (merged). The query does not select visibility either, so the Edit page, which shares the query, does not fetch it. It can come back when Internal and Public visibility ship. See the review thread.
Connection status renders as an icon after the URL, as the design draws it, not as the sidebar's badge. It uses the sidebar's icon and label for each health value, and has a tooltip and an accessible name that names the upstream.
Artifact Registry serves sizeBytes and settings only to a viewer who can read that upstream, so a row can arrive without them. Such a row keeps its name, its Remote badge, and its position, and renders no URL line, no connection status, and a blank Size cell. A hosted upstream has no settings, so its row renders no URL line and no connection status either. The Edit page follows the same rule: an upstream the viewer cannot read keeps its place in the list, with no URL and a blank Size.
The surface is dark behind the artifact_registry_ui feature flag, so there is no changelog entry.
Builds on !258049 (merged) (merged). The spec is gitlab-org/ops/artifact-registry!2998 (merged) (docs/specs/monolith/S05-repository-detail.md, Upstream table, Accessibility, and the acceptance criteria), and this is Step 10 of the amended virtual detail plan in the same MR. Following review, this MR departs from that amendment in two places: rows render no visibility icon, and connection status is an icon rather than the sidebar's badge. The amendment needs updating to match.
Closes #631075 (closed)
Changes
get_repository_upstream_repositories.query.graphql: selectsizeBytesandsettings { url lastHealthStatus }onupstreamRepository.visibilityandformatstay unselected.constants.js:- Add a Size column after Type in
UPSTREAM_REPOSITORIES_TABLE_FIELDS, and rebalance the column widths (Order 1/10, Repository 1/2, Type 3/20, Size 3/20, Actions 1/10). - Add
UPSTREAM_HEALTH_STATUS_ICONS, the icon, icon variant, and label for each health value:status-success/success/ Healthy,status-failed/danger/ Unhealthy, andseverity-unknown/subtle/ Unknown. The icons and labels match the sidebar indicator's. Unknown usessubtlebecauseGlIconhas noneutralvariant.
- Add a Size column after Type in
upstream_repositories_table.vue:- Repository cell: the name, then, on a row whose
settingsis present, a second line holding the upstream URL as plain text (no link, no copy action) and the connection status icon. The icon sits in a compact tertiary button so its tooltip (the verdict) is focusable, and the button's accessible name isConnection status for %{name}: %{verdict}. A row with nullsettingsrenders no second line. No visibility icon. - Size cell:
sizeBytesthroughhumanSize.0renders as0 B, and null renders blank.
- Repository cell: the name, then, on a row whose
repositories_edit_form.vue:upstreamSourcespasses each stored upstream'ssizeBytesandsettings.urlthrough, the same shape the picker gives an upstream it adds, so the order table's URL line and Size cell render for stored rows too. The save still sends upstream ids only, and the "did the list change" check still compares ids only.- Mock data, the
repository_upstream_repositories.rbfrontend fixture (a readable remote, a readable hosted with size0, and an unreadable remote whose verdicts all deny, as Artifact Registry sends them), aVirtualRepositoryWithUnreadableUpstreamstory, andlocale/gitlab.pot. - Specs:
upstream_repositories_table_spec.js: each source kind, each health value (icon, variant, tooltip, and accessible name), an unreadable remote row and an unreadable hosted row, Size0and null, and the five headers.repositories_edit_form_spec.js: stored sources carry their URL and size and render them, a stored source and an added one render alike, an unreadable upstream keeps its position with no URL and a blank Size, and a reorder still saves by id.repository_detail_spec.jsover the regenerated fixture.
connection_indicator.vue is not changed. An earlier revision gave it a table-row mode, which review replaced with the inline icon.
References
- Work item: #631075 (closed)
- Client and GraphQL fields (Step 9, merged): !258049 (merged)
- Spec and plan amendment: gitlab-org/ops/artifact-registry!2998 (merged)
- Artifact Registry contract and handler (merged): gitlab-org/ops/artifact-registry!2959 (merged)
How to set up and validate locally
Check out 631075-s05-render-upstream-visibility-size-url. Steps 1 to 6 need no Artifact Registry. Steps 7 to 9 run against a real Artifact Registry on the caproni rig with real role grants: Step 7 checks that it serves sizeBytes and settings only for an upstream the viewer can read, Step 8 covers the three health values, and Step 9 covers the Edit page. All three are recorded under Evidence. Nothing is stubbed.
Step 1 - Regenerate the frontend fixture
The fixture holds a readable remote, a readable hosted upstream with size 0, and a remote upstream the user cannot read.
bin/rspec ee/spec/frontend/fixtures/artifact_registry/repository_upstream_repositories.rbOutput (✅ )
Finished in 12.59 seconds (files took 29.28 seconds to load)
2 examples, 0 failuresStep 2 - The Jest suites pass
The changed specs, plus the Edit page's form, upstream section, order table, and picker, and the unchanged connection indicator and sidebar specs, as regression checks.
D=ee/spec/frontend/packages_and_registries/artifact_registry
node_modules/.bin/jest --config jest.config.js \
$D/repositories/detail/upstream_repositories_table_spec.js \
$D/repositories/detail/repository_detail_spec.js \
$D/repositories/detail/upstream_repository_actions_spec.js \
$D/repositories/detail/connection_section_spec.js \
$D/repositories/edit/repositories_edit_form_spec.js \
$D/repositories/components/repository_form_spec.js \
$D/repositories/components/upstream_repositories_section_spec.js \
$D/repositories/components/upstream_repositories_order_table_spec.js \
$D/repositories/components/upstream_repository_picker_spec.js \
$D/repositories/components/connection_indicator_spec.js \
$D/repositories/components/remote_source_section_spec.js \
$D/graphql/cache_config_spec.jsOutput (✅ )
PASS .../detail/connection_section_spec.js
PASS .../detail/upstream_repository_actions_spec.js
PASS .../components/upstream_repositories_order_table_spec.js
PASS .../detail/upstream_repositories_table_spec.js
PASS .../components/connection_indicator_spec.js
PASS .../components/upstream_repository_picker_spec.js
PASS .../graphql/cache_config_spec.js
PASS .../components/upstream_repositories_section_spec.js
PASS .../components/remote_source_section_spec.js
PASS .../detail/repository_detail_spec.js
PASS .../components/repository_form_spec.js
PASS .../edit/repositories_edit_form_spec.js
Test Suites: 12 passed, 12 total
Tests: 789 passed, 789 totalThe four new repositories_edit_form_spec.js examples fail against the old { id, name, kind } mapping.
Step 3 - The query document validates against the schema
bundle exec rake gitlab:graphql:validate 2>&1 | grep -E "get_repository_upstream_repositories|ERROR|FAIL"Output (✅ )
OK ee/app/assets/javascripts/packages_and_registries/artifact_registry/graphql/queries/get_repository_upstream_repositories.query.graphqlThe rake task exits 0 with no ERROR or FAIL lines.
Step 4 - Prettier is clean on the changed files
git diff --name-only origin/master...HEAD \
| grep -E '\.(js|vue|graphql)$' | xargs node_modules/.bin/prettier --checkOutput (✅ )
Checking formatting...
All matched files use Prettier code style!Step 5 - gitlab.pot is in sync
tooling/bin/gettext_extractor locale/gitlab.pot >/dev/null && git diff --exit-code --stat locale/gitlab.pot && echo "gitlab.pot in sync"Output (✅ )
gitlab.pot in syncStep 6 - RuboCop is clean on the fixture
bundle exec rubocop --no-server ee/spec/frontend/fixtures/artifact_registry/repository_upstream_repositories.rbOutput (✅ )
1 file inspected, no offenses detectedStep 7 - Against a real Artifact Registry, an upstream the viewer cannot read arrives without its fields
A local Artifact Registry runs with authorization.unenforced: true, so every upstream is readable there. This step uses the caproni rig with the Artifact Registry and IAM Data Access fragments instead, so every read is decided by a real role grant in IAM Data Access, evaluated by the GLAZ sidecar as on GitLab.com.
Extend both fragments (IAM last) in caproni.local.yaml, enable monolith edit mode, put repos/gitlab on this branch, then caproni up and caproni run:
extends:
- fragments/caproni.artifact-registry.yaml
- fragments/caproni.iam-data-access.yaml
repositories:
gitlab:
edit_mode: true
reloaders:
gitlab:
edit_mode: trueArtifact Registry must be v2.125.0 or later. v2.119.0 is the first release with the widened upstream summary (gitlab-org/ops/artifact-registry!2959 (merged)), and v2.125.0 is the first that accepts upstream_repository_ids on an update, which Step 9's save needs (an older one refuses it with unknown field "upstream_repository_ids"). The recordings ran on v2.129.0. Enable org_stage_experimental, org_stage_ga, and artifact_registry_ui in the edit-mode database. The virtual npm repository npm-virtual-demo has two upstreams, the remote npmjs-proxy and the hosted team-internal-npm.
Then create non-admin organization members and grant each a role on the namespace or on single repositories, as root:
mutation($id: UserID!, $r: String!, $role: ArtifactRegistryRole!) {
artifactRegistryRoleGrant(input: { assigneeId: $id, resourceId: $r, role: $role }) { errors }
}| User | Namespace default |
Repository grants | Cannot read | Used in |
|---|---|---|---|---|
ar-e2e-viewer |
Artifact Viewer | none | nothing | Steps 7 and 8 |
ar-e2e-reads-hosted |
none | Artifact Viewer on npm-virtual-demo and team-internal-npm |
npmjs-proxy |
Step 7 |
ar-e2e-reads-remote |
none | Artifact Viewer on npm-virtual-demo and npmjs-proxy |
team-internal-npm |
Step 7 |
ar-e2e-admin |
Artifact Admin | none | nothing | Steps 8 and 9 |
ar-e2e-edits-virtual |
none | Artifact Manager on npm-virtual-demo, Artifact Viewer on team-internal-npm |
npmjs-proxy |
Step 9 |
Output (✅ )
ar-e2e-reads-hosted: id=8 admin=false org_member=true
Artifact Viewer on npm-virtual-demo: {"data":{"artifactRegistryRoleGrant":{"errors":[]}}}
Artifact Viewer on team-internal-npm: {"data":{"artifactRegistryRoleGrant":{"errors":[]}}}
ar-e2e-reads-remote: id=9 admin=false org_member=true
Artifact Viewer on npm-virtual-demo: {"data":{"artifactRegistryRoleGrant":{"errors":[]}}}
Artifact Viewer on npmjs-proxy: {"data":{"artifactRegistryRoleGrant":{"errors":[]}}}
ar-e2e-edits-virtual: id=10 admin=false org_member=true
ARTIFACT_MANAGER on npm-virtual-demo: {"data":{"artifactRegistryRoleGrant":{"errors":[]}}}
ARTIFACT_VIEWER on team-internal-npm: {"data":{"artifactRegistryRoleGrant":{"errors":[]}}}Read the upstream list as each user, first through the client to see which keys Artifact Registry sends, then through this branch's query document. Run it with .gitlab/caproni/exec.sh rails runner from repos/gitlab:
org = Organizations::Organization.find_by!(path: 'default')
query = File.read('ee/app/assets/javascripts/packages_and_registries/artifact_registry/graphql/queries/get_repository_upstream_repositories.query.graphql')
%w[ar-e2e-viewer ar-e2e-reads-hosted ar-e2e-reads-remote ar-e2e-edits-virtual].each do |username|
user = User.find_by!(username: username)
puts "== #{username}"
client = ArtifactRegistry::Client.new(current_user: user, organization: org)
client.upstream_repositories(slug: 'default', repository_name: 'npm-virtual-demo', format: 'npm',
include_permissions: true).each do |association|
upstream = association.upstream_repository
keys = upstream.instance_variable_get(:@attributes).keys.sort
puts " AR #{upstream.name} (#{upstream.kind}) keys: #{keys.join(', ')}"
end
result = GitlabSchema.execute(query, context: { current_user: user, current_organization: org },
variables: { organizationId: org.to_global_id.to_s, name: 'npm-virtual-demo' }).to_h
puts " GraphQL errors: #{result['errors'].inspect}"
result.dig('data', 'organization', 'artifactRegistryRepository', 'upstreamRepositories').each do |association|
summary = association['upstreamRepository']
puts " GraphQL #{summary['name']} (#{summary['kind']}): " \
"#{summary.except('__typename', 'id', 'name', 'kind', 'userPermissions').to_json}"
end
endOutput (✅ )
Artifact Registry still sends visibility to a viewer who can read the upstream. The query no longer selects it.
== ar-e2e-viewer
AR npmjs-proxy (remote) keys: format, id, kind, name, permissions, settings, size_bytes, visibility
AR team-internal-npm (hosted) keys: format, id, kind, name, permissions, size_bytes, visibility
GraphQL errors: nil
GraphQL npmjs-proxy (REMOTE): {"sizeBytes":"0","settings":{"url":"https://registry.npmjs.org","lastHealthStatus":"HEALTHY"}}
GraphQL team-internal-npm (HOSTED): {"sizeBytes":"0","settings":null}
== ar-e2e-reads-hosted
AR npmjs-proxy (remote) keys: format, id, kind, name, permissions
AR team-internal-npm (hosted) keys: format, id, kind, name, permissions, size_bytes, visibility
GraphQL errors: nil
GraphQL npmjs-proxy (REMOTE): {"sizeBytes":null,"settings":null}
GraphQL team-internal-npm (HOSTED): {"sizeBytes":"0","settings":null}
== ar-e2e-reads-remote
AR npmjs-proxy (remote) keys: format, id, kind, name, permissions, settings, size_bytes, visibility
AR team-internal-npm (hosted) keys: format, id, kind, name, permissions
GraphQL errors: nil
GraphQL npmjs-proxy (REMOTE): {"sizeBytes":"0","settings":{"url":"https://registry.npmjs.org","lastHealthStatus":"HEALTHY"}}
GraphQL team-internal-npm (HOSTED): {"sizeBytes":null,"settings":null}
== ar-e2e-edits-virtual
AR npmjs-proxy (remote) keys: format, id, kind, name, permissions
AR team-internal-npm (hosted) keys: format, id, kind, name, permissions, size_bytes, visibility
GraphQL errors: nil
GraphQL npmjs-proxy (REMOTE): {"sizeBytes":null,"settings":null}
GraphQL team-internal-npm (HOSTED): {"sizeBytes":"0","settings":null}Sign in as ar-e2e-viewer, ar-e2e-reads-hosted, and ar-e2e-reads-remote in turn and open /o/default/-/artifact_registry/default/repositories/npm-virtual-demo.
Output (✅ )
Read from the rendered page (visible nodes only), next to the upstream list the page's own GraphQL request returned:
ar-e2e-viewer
headers: Order | Repository | Type | Size
1 npmjs-proxy https://registry.npmjs.org [status-success] "Connection status for npmjs-proxy: Healthy" Remote 0 B
2 team-internal-npm (no second line) Hosted 0 B
ar-e2e-reads-hosted
headers: Order | Repository | Type | Size
1 npmjs-proxy (no second line) Remote (blank)
2 team-internal-npm (no second line) Hosted 0 B
GraphQL npmjs-proxy: sizeBytes null, settings null
ar-e2e-reads-remote
headers: Order | Repository | Type | Size
1 npmjs-proxy https://registry.npmjs.org [status-success] "Connection status for npmjs-proxy: Healthy" Remote 0 B
2 team-internal-npm (no second line) Hosted (blank)
GraphQL team-internal-npm: sizeBytes null, settings null
every run: no icon in any Repository cell except the status icon, tbody [role="status"] count 0,
links in the Repository cells 0No user here holds update_repository, delete_artifact, or delete_repository_upstream, so the Actions column is dropped on every run.
Recordings: Real role grants.
Step 8 - The Unhealthy and Unknown health values, and a size above zero
A remote upstream's stored verdict is whatever its connection tests left, and npmjs-proxy only reaches Healthy and 0 B. On the same rig, as ar-e2e-admin, create two more npm remotes and a second virtual repository through GraphQL:
npm-health-unreachable, URLhttps://example.com:81/npm/. Run the connection test on it three times. The port does not answer, and Artifact Registry records Unhealthy after its defaultunhealthy_thresholdof three consecutive failures. A URL that answers with any HTTP status, even404, counts as reachable, so it needs a port that does not answer.npm-health-untested, URLhttps://registry.npmmirror.com/. It is never tested, so it stays Unknown.npm-virtual-health, a virtual npm repository whose upstreams are, in order,npmjs-proxy,npm-health-unreachable,npm-health-untested,remote-npm(an existing remote holding 2.91 MiB), andteam-internal-npm.
mutation($input: ArtifactRegistryRepositoryCreateInput!) {
artifactRegistryRepositoryCreate(input: $input) { repository { id name kind } errors }
}
mutation($input: ArtifactRegistryRepositoryTestConnectionInput!) {
artifactRegistryRepositoryTestConnection(input: $input) { passed httpStatus lastHealthStatus lastHealthCheckedAt errors }
}Output (✅ )
The three connection tests on npm-health-unreachable, then each upstream's stored verdict:
{"passed":false,"httpStatus":null,"lastHealthStatus":"UNKNOWN","lastHealthCheckedAt":"2026-10-01T04:09:04+00:00","errors":[]}
{"passed":false,"httpStatus":null,"lastHealthStatus":"UNKNOWN","lastHealthCheckedAt":"2026-10-01T04:09:29+00:00","errors":[]}
{"passed":false,"httpStatus":null,"lastHealthStatus":"UNHEALTHY","lastHealthCheckedAt":"2026-10-01T04:09:34+00:00","errors":[]}
npmjs-proxy REMOTE sizeBytes 0 https://registry.npmjs.org HEALTHY
npm-health-unreachable REMOTE sizeBytes 0 https://example.com:81/npm UNHEALTHY
npm-health-untested REMOTE sizeBytes 0 https://registry.npmmirror.com UNKNOWN (never checked)
remote-npm REMOTE sizeBytes 3048126 https://registry.npmjs.org HEALTHY
team-internal-npm HOSTED sizeBytes 0 (no settings)Then sign in as ar-e2e-viewer and open /o/default/-/artifact_registry/default/repositories/npm-virtual-health.
Output (✅ )
headers: Order | Repository | Type | Size
1 npmjs-proxy https://registry.npmjs.org [status-success success] "Connection status for npmjs-proxy: Healthy" Remote 0 B
2 npm-health-unreachable https://example.com:81/npm [status-failed danger] "Connection status for npm-health-unreachable: Unhealthy" Remote 0 B
3 npm-health-untested https://registry.npmmirror.com [severity-unknown subtle] "Connection status for npm-health-untested: Unknown" Remote 0 B
4 remote-npm https://registry.npmjs.org [status-success success] "Connection status for remote-npm: Healthy" Remote 2.91 MiB
5 team-internal-npm (no second line) Hosted 0 B
each status icon is a <button> whose tooltip shows the verdict, and has no visible text
tbody [role="status"] count: 0Recording: Health values.
Step 9 - The Edit page renders each stored upstream's URL and size, and still saves by id
As ar-e2e-admin, open /o/default/-/artifact_registry/default/repositories/npm-virtual-demo/edit. Both stored upstreams show their size, and the remote shows its URL. Add remote-npm through Add repository, which renders the same way, then remove it again without saving. Move team-internal-npm up and select Save changes: the detail page shows the new order. Edit again, move it back, and save.
Then, as ar-e2e-edits-virtual, open the same page and select Save changes without changing anything.
Output (✅ )
Read from the order table (visible nodes only), with the updateArtifactRegistryRepository input each save sent:
ar-e2e-admin, on load
npmjs-proxy https://registry.npmjs.org Remote 0 B
team-internal-npm (no URL) Hosted 0 B
after adding remote-npm through the picker
remote-npm https://registry.npmjs.org Remote 2.91 MiB
after removing it, moving team-internal-npm up, and saving
input: {"name":"npm-virtual-demo","description":"","upstreamRepositoryIds":["01a0ebda-e8fb-7b65-ad69-a4dd6d30b97b","01a0ebda-e888-7571-90e6-d9435e16a1c8"]}
detail page: 1 team-internal-npm, 2 npmjs-proxy
after moving it back and saving
input: {"name":"npm-virtual-demo","description":"","upstreamRepositoryIds":["01a0ebda-e888-7571-90e6-d9435e16a1c8","01a0ebda-e8fb-7b65-ad69-a4dd6d30b97b"]}
detail page: 1 npmjs-proxy, 2 team-internal-npm
ar-e2e-edits-virtual, on load
npmjs-proxy (no URL) Remote (blank)
team-internal-npm (no URL) Hosted 0 B
saving the unchanged list
input: {"name":"npm-virtual-demo","description":""}
detail page: 1 npmjs-proxy, 2 team-internal-npmThe upstream ids are the only upstream data a save sends, and an unchanged list sends none.
Recordings: Edit page.
Evidence
Change type: UI only (the upstream query document, two Vue components, and constants). Every recording ran on the caproni rig with Artifact Registry v2.129.0, the GLAZ v10.1.0 sidecar, and IAM Data Access, with the monolith in edit mode at this branch's head, 316e843e5d818. Nothing is stubbed. Every recording is as a non-admin organization member.
Rails console
The Step 7 runner script is the console evidence: it shows the keys Artifact Registry sends per user, and what the query resolves. No client change. The client fields are !258049 (merged).
GraphiQL
N/A. No GraphQL schema change. !258049 (merged) carries the GraphiQL recordings for these fields.
Screen recordings
Real role grants: ar-e2e-viewer, Artifact Viewer on the namespace. Both upstreams are readable. The remote npmjs-proxy row shows https://registry.npmjs.org as plain text, then the Healthy status icon (hovered, so its tooltip shows), and 0 B. The hosted team-internal-npm row shows no second line and 0 B. No row shows a visibility icon.
Real role grants: ar-e2e-reads-hosted, Artifact Viewer on npm-virtual-demo and team-internal-npm only. Artifact Registry sends no size_bytes or settings for npmjs-proxy, so its row keeps its position, name, and Remote badge, and renders no URL, no status icon, and a blank Size cell. The hosted row still shows 0 B.
Real role grants: ar-e2e-reads-remote, Artifact Viewer on npm-virtual-demo and npmjs-proxy only. The reverse: the remote row renders in full, and the hosted team-internal-npm row renders a blank Size cell.
Health values: ar-e2e-viewer on npm-virtual-health (Step 8). Healthy, Unhealthy, and Unknown each render their own icon, each hovered in turn so its tooltip shows, and remote-npm renders 2.91 MiB.
Edit page: ar-e2e-admin, Artifact Admin on the namespace (Step 9). The stored upstreams show their URL and size, remote-npm added through the picker renders the same way and is removed again, and a reorder saves and shows on the detail page before the original order is restored.
Edit page: ar-e2e-edits-virtual, Artifact Manager on npm-virtual-demo and Artifact Viewer on team-internal-npm only (Step 9). npmjs-proxy keeps its place with no URL and a blank Size, and saving the unchanged list returns to the detail page.
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.