monolith/S05 virtual Step 7: Upstream row actions
What this does
Step 7 of the monolith/S05 virtual repository detail slice. It adds the per-row kebab to the upstream table on a virtual repository's detail page: Edit repository on every row, and Clear cache on remote-source rows only. Clear cache is this slice's own write. It targets the upstream's own name (a different repository from the virtual one you are looking at), evicts that repository's cached details from the Apollo cache rather than refetching the virtual page (which the clear leaves unchanged), and reports acceptance without a count. The virtual page kebab needs no change and is asserted (Copy repository URL and Delete repository only). Everything ships dark behind the disabled artifact_registry_ui flag.
Closes #628668 (closed)
Plan step (source of truth): docs/plans/monolith/2026-09-10-repository-detail-virtual.md → "Step 7: Upstream row actions".
Changes
- New
repositories/detail/upstream_repository_actions.vue: a disclosure menu mirroring the list'srow_actions_menu.vue. Edit repository (every row) links to the shipped edit route with the upstream's name. Clear cache (v-if="isRemote") issuesclearRepositoryCacheMutationagainst the upstream's name, with norefetchQueriesandupdate: evictRepositoryDetails(name), toasts an acceptance message, and alerts on failure with no retry (matching the shipped remote kebab while the remote plan's failure-handling step is unmerged). graphql/utils/cache_update.js: new payload-agnosticevictRepositoryDetails(name)besideevictUpdatedRepositoryDetails, whose guard readsdata.updateRepositoryand so cannot serve the clear-cache document.repositories/detail/upstream_repositories_table.vue+constants.js: an actions column on the upstream table (Repository width 7/10 to 3/5, actions 1/10).- Accessibility: the menu toggle carries the upstream's name in its accessible name (e.g. "More actions for npmjs-proxy"), so a screen reader user can tell each row's menu apart. The Clear cache item does not repeat it in a per-item
aria-label:GlDisclosureDropdownalready labels the item list by the toggle, and the shippedrepository_actions.vuemenu carries no per-item label either (dropped after review; an earlier revision had one). - One new string (
ArtifactRegistry|Cache clear successfully scheduled for %{name}.) with the regeneratedlocale/gitlab.pot. No changelog (dark). - Specs: new
upstream_repository_actions_spec.js;upstream_repositories_table_spec.js,repository_actions_spec.js(virtual page kebab regression case),cache_update_spec.js, andmock_data.jsupdated.
Design notes and deviations from the plan
- Menu order is Clear cache then Edit repository, matching Figma
589-37933. The plan text lists Edit then Clear; this MR now follows the design instead. - Remove repository is not mounted (S07 Phase 3 owns the dissociate mutation). Its position is marked as the menu's last item and its absence is asserted.
- The new strings live in the component's
$options.i18n, notconstants.js. The plan listsconstants.jsfor the strings, but moving them there would mean refactoring the shippedrepository_actions.vueandrow_actions_menu.vue, which is beyond a minimal fix, and the i18n guidance prefers component-local strings.constants.jschanges only for the actions column field. repository_detail.stories.jsis unchanged. The menus render automatically once the table mounts them, and the existing virtual stories already provide a vue-router (Edit's router link resolves), aclearRepositoryCacheMutationhandler, and pinnedArtifactRegistryRepositoryDetailstypenames. The CItest-storybookjob runs the a11y check over these stories.
Permissions: both row items render ungated, by design
Master now gates the detail page's own controls on verdicts ("Gate the detail header, connection test, and kebab", part of #628427 (closed)), so this MR is the one place on the page that renders a control without a verdict check. That is the spec's instruction, not an omission.
Why no gate is possible here yet
Both items act on the upstream, a sibling repository this page never reads, so none of this page's reads carries a verdict for them. monolith/S15 (Approved) puts them in its Out of scope section, calls them cross-resource, and records the bindings for the day a verdict exists: the row's Edit repository reads the upstream's own update_repository, and the row's Clear cache reads the upstream's own delete_artifact. Inferring either from the virtual repository's own verdicts is forbidden there, so this MR does not do it.
The AR change that produces the verdicts is artifact-registry#1174, which adds the existing include_permissions opt-in to the upstream-association list read and a RepositoryPermissions object on each row's upstream summary. It is open and unscheduled. The monolith follow-up for the wiring (client, UpstreamRepositorySummaryType, resolver, query selection, and the gate in upstream_repository_actions.vue) is #629674 (closed), a task under this slice's parent #628475 (closed), blocked by AR#1174.
Remove repository, the third item planned for this kebab (S07 Phase 3), asks about the virtual repository itself, so it gates from the detail read the page already performs and needs nothing new.
Verdicts are advisory in any case: Artifact Registry authorizes every request, so a user who cannot clear an upstream's cache is refused at the service, and the gate only spares them the attempt.
How to set up and validate locally
The Jest suite and the lint and pot checks below are the automated evidence. The behaviour was also driven end to end in the GDK against a real Artifact Registry (see Step 4 and the recording), which replaces the Storybook screenshots this MR carried before: Storybook renders Artifact Registry icons broken, so it is a poor basis for judging the rendered menu.
Step 1 - Jest specs for the changed files
node_modules/.bin/jest --config jest.config.js \
ee/spec/frontend/packages_and_registries/artifact_registry/repositories/detail/upstream_repository_actions_spec.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_actions_spec.js \
ee/spec/frontend/packages_and_registries/artifact_registry/graphql/cache_update_spec.jsOutput (✅ )
PASS ee/spec/frontend/packages_and_registries/artifact_registry/graphql/cache_update_spec.js
PASS ee/spec/frontend/packages_and_registries/artifact_registry/repositories/detail/upstream_repository_actions_spec.js
PASS ee/spec/frontend/packages_and_registries/artifact_registry/repositories/detail/upstream_repositories_table_spec.js
PASS ee/spec/frontend/packages_and_registries/artifact_registry/repositories/detail/repository_actions_spec.js
Test Suites: 4 passed, 4 total
Tests: 87 passed, 87 totalCount rose from 76 to 83 on the rebase onto master (repository_actions_spec.js picked up master's verdict-gating cases), then to 87 addressing review: two toggle-reset regression tests added (one per failure path) and the stale aria-label test removed.
Step 2 - ESLint and Prettier on the changed files
node_modules/.bin/eslint <changed .vue/.js files>
node_modules/.bin/prettier --check <changed files>Output (✅ )
ESLint: clean
Prettier: All matched files use Prettier code style!Step 3 - Regenerated pot carries only the two new strings
tooling/bin/gettext_extractor locale/gitlab.pot
git diff --stat locale/gitlab.potOutput (✅ )
+ msgid "ArtifactRegistry|Cache clear successfully scheduled for %{name}."Step 4 - The virtual page in the GDK against a real Artifact Registry
A virtual repository can now be driven for real, which was not true when this MR was opened. AR's main creates a virtual repository of any of the four formats, and the npm upstream association routes (associate, list, get, update, dissociate) are served by the open artifact-registry!2735, Step 40 of artifact-registry#314. Maven and container upstream routes are still unserved (!2780, !2778 are drafts), so npm is the one format this rig can use today.
Rig: AR built from hswimelar/s17-p6-step-40 rebased on AR main, on :8080 against the GDK's JWKS, with the GDK on this branch and artifact_registry_ui, org_stage_experimental and org_stage_ga enabled.
Seeding the three repositories and the two associations
# rails runner
org = Organizations::Organization.find(1)
user = User.find_by_username('root')
client = ArtifactRegistry::Client.new(current_user: user, organization: org)
slug = 'gdk-local-test'
client.create_repository(slug: slug, name: 'npmjs-proxy', format: 'npm', kind: 'remote',
settings: { url: 'https://registry.npmjs.org' })
client.create_repository(slug: slug, name: 'team-internal-npm', format: 'npm', kind: 'hosted')
client.create_repository(slug: slug, name: 'npm-virtual-demo', format: 'npm', kind: 'virtual')# one POST per upstream, in the order the rows should carry
BASE=http://localhost:8080/api/v1/gdk-local-test/repositories/npm-virtual-demo/npm/upstream_repositories
curl -X POST "$BASE" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"upstream_repository_id":"<remote id>"}'
curl -X POST "$BASE" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"upstream_repository_id":"<hosted id>"}'Confirmed through the Ruby client (
upstreams: [[1, "npmjs-proxy", "remote"], [2, "team-internal-npm", "hosted"]]
virtual repo kind=virtual format=npmWhat the run asserted (✅ )
page rows=["npmjs-proxy","team-internal-npm"] kinds=["Remote","Hosted"] positions=["1","2"]
remote-row-menu items=["Clear cache","Edit repository"]
editHref="/o/default/-/artifact_registry/gdk-local-test/repositories/npmjs-proxy/edit"
hosted-row-menu items=["Edit repository"]
editHref="/o/default/-/artifact_registry/gdk-local-test/repositories/team-internal-npm/edit"
page-kebab copyUrl=1 clearCache=0 deleteRepository=1
clear-cache request clearArtifactRegistryRepositoryCache {"input":{"name":"npmjs-proxy"}}
response 200 {"clearRepositoryCache":{"errors":[]}}
toast "Cache clear successfully scheduled for npmjs-proxy."
operations issued after the click: ["clearArtifactRegistryRepositoryCache"] only
edit-navigation url=".../repositories/npmjs-proxy/edit"
accessible names "More actions for npmjs-proxy" and "More actions for team-internal-npm" each match one buttonFour of those answer the design decisions in this MR directly: the mutation's variables name the upstream and not npm-virtual-demo; the clear issues no refetch of the virtual page; Edit repository navigates to the upstream's own edit route; and the page kebab is unchanged.
Re-recorded after review to pick up the Clear cache/Edit repository menu order, the dropped per-item aria-label, and the reworded toast (see the Screen recording section below).
Evidence
Rails console
N/A for the change itself (no ee/lib client method changed). The client calls above are rig setup, not the change under test.
GraphiQL
N/A. No resolver, mutation, type, or schema changed. The Clear cache item calls the already-shipped artifactRegistryRepositoryArtifactsDelete mutation, and the recording below shows that call and its response.
Screen recording
Recorded in the GDK against the real Artifact Registry described in Step 4, in this order: the virtual page, the remote row's menu, the hosted row's menu, the virtual page kebab, Clear cache with its toast, and Edit repository navigating to the upstream. The black strip at the bottom is a Playwright caption, not page content.
The same six moments as screenshots
The virtual repository detail page: two upstreams, npmjs-proxy (remote, position 1) and team-internal-npm (hosted, position 2).
Remote-source row menu: Edit repository and Clear cache.
Hosted-source row menu: Edit repository only.
Virtual page kebab, unchanged by this MR: Copy repository URL and Delete repository, no Clear cache.
Clear cache on the remote row: the toast names the upstream.
Edit repository on the remote row lands on the upstream's own edit page, not the virtual repository's.





