Add a view options popover to the artifact version list
What does this MR do and why?
This change is behind the feature flag :artifact_registry_ui, which is default-off.
This is Step 20 of the monolith/S14 "version list" slice. It adds a view-options popover to the artifact version list, with a Columns section: one toggle per optional column that the active format renders. The selection persists in local storage, since it's a presentation preference with no server surface and no schema change to justify one. No new flag, and deliberately no changelog entry, since :artifact_registry_ui stays default-off.
The design proposal's popover shows 5 toggles for Maven and 8 for containers. This MR ships fewer: Tags, Downloads, Platforms, Referrers, and the Maven/npm Size column are all Phase 2, gated on Artifact Registry contract extensions that don't exist yet. So Phase 1's Columns section is Published + Source for Maven/npm, and Type + Size + Published for containers. Two toggles for a package artifact is correct for this phase, not an oversight. The row-identity column gets no toggle either way — Version for Maven/npm, Digest for containers — matching the design proposal, which also withholds a switch for Version.
Changes
- The stored value is the set of hidden columns, not the visible ones. A column added in a later phase is then absent from a stored selection and arrives visible, which is the right default. Storing visible keys would leave every newly shipped column hidden for anyone who'd ever opened the popover. It also makes a first visit the empty array rather than a seeded list, so nothing needs writing until the user actually changes something.
- The storage key is scoped to the format family and to nothing else, matching
REPOSITORIES_SORT_STORAGE_KEYbeside it. Key shape:artifact-registry-version-list-columns-packages. This departs from the spec, which asks for a key "scoped per user". The reasoning: a column preference is not sensitive, two accounts sharing a browser profile sharing one is a cosmetic non-event, and the monolith has no precedent for scoping aLocalStorageSynckey by user — per-user keys exist only in rawlocalStoragecode. Carrying two conventions in one directory costs more than the scoping buys. Happy to restore the per-user segment if the spec author would rather amend the code than the spec. The family segment does stay: the two optional sets overlap only on Published, so a single key would let container activity clobber a package selection. - The selection is read when the key settles, not on mount. Because the key carries the format family and the format arrives with the artifact read, there is nothing to read at mount — which is why this cannot use the
data()read thatrepositories_list.vueuses, whose key is static. It is read again if the key changes, because the route component is reused across artifacts and a package-to-container navigation flipsformaton a live instance; without that, the package selection would be written under the container key. A spec drives that navigation and asserts both the selection and the key change.LocalStorageSyncis also keyed on the storage key, so one instance can never span a family change. - The read intersects the stored value with the columns that family offers a switch for. A stale entry, a removed column, or a hand-edited value can't hide a column the popover has no way to restore — including the row-identity and row-actions columns. Three specs cover it, one per case.
- The popover sits on the page, not inside the table region.
versions_section.vueswaps wholesale between skeleton, alert, and table, so hosting the control there would take it away exactly when a read is in flight or has failed — and the spec requires the shell to stay rendered when only the connection fails. Verified in the browser that the control is a sibling of the table region rather than inside it. GlDisclosureDropdown, notGlPopover. The monolith usesGlPopoveronly for transient dismissible callouts; a settings panel hanging off a toolbar button is alwaysGlDisclosureDropdown. This followsapp/assets/javascripts/boards/components/board_options.vueclosely, including:auto-close="false"so flipping one toggle doesn't dismiss the panel the user is still working in, and the hover tooltip that names the icon-only button for a sighted pointer user, sincetext-sr-onlyrenders that name for assistive technology only.- The optional-column set is declared explicitly, rather than derived as "every field except the row identity." An open MR, !250529 (merged), is adding an
actionscolumn to the version table, and a derived list would wrongly offer a toggle for it. Toggle labels come from the existing table field definitions, so the popover can't drift out of order with the table and adds no duplicate strings. - One new i18n string,
ArtifactRegistry|Columns.View optionsalready existed in the pot and is reused. - Tests.
view_options_spec.jsis new: 22 cases over all four formats, covering the switch set per format, the row-identity and actions columns having no switch, every switch on when nothing is hidden, a hidden column's switch reading off, the emitted key on switching off and back on, the panel staying open, and the accessible naming of the icon button, by both its screen-reader name and its tooltip.version_list_spec.jsgains a column-selection block: first visit, restore, per-family read, the key carrying user and family, the format-change re-read, and three stale-value cases. Both table specs gain a hidden-column case. 1380 Jest tests pass on both Vue 2 and Vue 3; two failures inrepository_detail_spec.jsare pre-existing on unmodifiedmaster, confirmed by running that spec onorigin/master.
One accessibility trade-off
GlDisclosureDropdownItem renders its #list-item slot inside a button, and GlToggle renders a button with role="switch", so each row nests an interactive control inside another one — the shape axe's nested-interactive rule flags. This is inherited from the board_options.vue idiom that the monolith standardises on, and the panel content is in the DOM whether the panel is open or closed, so this isn't something an open-panel story introduces here. axe-core isn't installed in this checkout, so it couldn't be confirmed locally. The alternative is GlDisclosureDropdownItem's default slot, which drops the inner button at the cost of re-adding the item padding by hand and re-checking the click wiring. Raising it rather than quietly shipping it; happy to switch if a reviewer prefers.
Screenshots or screen recordings
A Maven artifact. Version has no switch, because it identifies the row.
A container artifact, driven from the keyboard: Type switched off and gone from the table, the panel still open, focus still on the row.
How to set up and validate locally
- Enable the flag:
Feature.enable(:artifact_registry_ui). - Open the Artifact Registry SPA at
/o/<organization>/-/artifact_registry/acme/repositories. - Open a Maven or npm repository and click into an artifact.
- Open the popover via the icon button at the top right of the table.
- Switch a column off. Confirm it vanishes from the table and the panel stays open.
- Reload the page and confirm the column is still off.
- Open a Docker or OCI artifact and confirm its column switches are independent of the package selection made above.
- Keyboard only: press Enter to open the panel, ArrowDown to move to the first row, Enter to flip it. Confirm the column disappears, the panel stays open, and focus stays on the row.
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.

