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's row_actions_menu.vue. Edit repository (every row) links to the shipped edit route with the upstream's name. Clear cache (v-if="isRemote") issues clearRepositoryCacheMutation against the upstream's name, with no refetchQueries and update: 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-agnostic evictRepositoryDetails(name) beside evictUpdatedRepositoryDetails, whose guard reads data.updateRepository and 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: GlDisclosureDropdown already labels the item list by the toggle, and the shipped repository_actions.vue menu 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 regenerated locale/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, and mock_data.js updated.

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, not constants.js. The plan lists constants.js for the strings, but moving them there would mean refactoring the shipped repository_actions.vue and row_actions_menu.vue, which is beyond a minimal fix, and the i18n guidance prefers component-local strings. constants.js changes only for the actions column field.
  • repository_detail.stories.js is 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), a clearRepositoryCacheMutation handler, and pinned ArtifactRegistryRepositoryDetails typenames. The CI test-storybook job 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.js
Output (✅)
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 total

Count 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.pot
Output (✅)
+ 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=npm
What 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 button

Four 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).

01-virtual-detail

Remote-source row menu: Edit repository and Clear cache.

02-remote-row-menu

Hosted-source row menu: Edit repository only.

03-hosted-row-menu

Virtual page kebab, unchanged by this MR: Copy repository URL and Delete repository, no Clear cache.

04-page-kebab

Clear cache on the remote row: the toast names the upstream.

05-clear-cache-toast

Edit repository on the remote row lands on the upstream's own edit page, not the virtual repository's.

06-edit-upstream

Edited by Fiona McCawley

Merge request reports

Loading
Loading