Artifact registry: Add the setup instructions drawer
What does this MR do and why?
monolith/S05 Step 11, per the repository detail plan. This change is behind the feature flag :artifact_registry_ui.
Mounts the Step 10 setup content behind the repository kebab's View setup instructions item: a drawer with Install and Publish tabs and the build-tool selector on the tab row. It is the first surface a reader can actually reach that content from, and it closes acceptance criterion 10 plus the drawer half of criterion 15.
⚠️ Stacked on !248880 (merged) (Step 10) and targeting that branch, because the drawer importssetup_snippets.vueandtool_selector.vue, neither of which is onmasteryet. Retarget tomasteronce !248880 (merged) merges. Review Step 10 first.
Changes
setup_instructions/setup_drawer.vue—GlDrawerwithGlTabs, one tab per section, and the tool selector in the#tabs-endslot. Owns tab and tool state. Wrapped in aMountingPortaltargeting the body, for the reason in the notes.repository_actions.vue— theView setup instructionskebab item and the drawer. The kebab now offers Copy repository URL → View setup instructions → Delete repository; the prototype's Manage retention and Manage access are deferred tomonolith/S08/S09/S12per the spec.setup_instructions/setup_drawer.stories.js— one story per format, which is also what gets the component under the axe run.- Specs: 13 new for the drawer, 5 new for the kebab.
The tool is shared across both tabs and resets to the format's default each time the drawer opens, matching the prototype. A tool the current format does not offer reads as no selection, so a format change cannot leave Podman selected for a Maven repository.
The kebab item is gated on clientUrl, on the same terms as the copy item — without it the snippets cannot be composed and the drawer would open empty.
Screenshots or screen recordings
All captured in the GDK behind the flag, against the local Apollo mock resolvers.
The kebab. Copy repository URL → View setup instructions → Delete repository. The prototype's Manage retention and Manage access are deferred to monolith/S08/S09/S12 per the spec.
Maven, Install tab. Compare with the prototype's drawer (kebab → View setup instructions). Snippets are composed from the real mount data, so the URL is the local localhost:8080 origin rather than a placeholder.
The tool selector, and what switching it does. Selecting Gradle (Kotlin) on the Publish tab swaps both the command and the config it belongs in. The prototype's own drawer still shows mvn deploy here — see the notes.
| Selector open | Publish + Gradle (Kotlin) |
|---|---|
![]() |
![]() |
A container repository, showing the per-format tool set (Docker CLI / Podman) and the token placeholder in the sign-in command.
How to set up and validate locally
- Enable the flag:
Feature.enable(:artifact_registry_ui)inrails c. - Visit a repository detail page, e.g.
/o/default/-/artifact_registry/acme/repositories/payment-core. Data comes from the local Apollo mock resolvers. - Open the kebab and confirm three items in order: Copy repository URL, View setup instructions, Delete repository.
- Choose View setup instructions. Switch tabs and switch build tools; confirm the snippets change for both, and that the tool choice persists across a tab switch.
- Escape closes the drawer. Note that focus is not trapped and is not returned to the kebab on close — see the accessibility note below.
yarn storybook:start→ee/artifact_registry/repositories/detail/setup_instructions/setup_drawerfor the per-format variants.
Agent Notes
Verified locally: 598 AR frontend tests pass (18 new), eslint and prettier clean, locale/gitlab.pot regenerated. Also driven by hand in Storybook against the prototype and end-to-end in the GDK behind the flag.
GlDrawer supplies almost no dialog behaviour, and this MR deliberately does not paper over that. Its entire behaviour is a document keydown that emits close on Escape: no focus trap, no focus on open, no focus restore, and a bare <aside> with no role. I first built all of that here — a Tab trap, focus-on-open, focus return to the kebab, role="dialog", aria-modal — and then removed it, because a per-consumer workaround is the wrong place for it. Of the ~60 GlDrawer consumers in the monolith exactly one restores focus to its opener, and that behaviour is untested: the clearest evidence that asking each caller to solve this does not work.
GlDrawer proper dialog semantics — focus trap while open, focus into the panel on open, focus restored to the opener on close, and role="dialog"/aria-modal. One fix there serves every consumer. Not filed yet.
What remains here is not custom behaviour: the drawer names itself via aria-labelledby on its own heading — a plain attribute, valid for the aside's landmark role, claiming nothing untrue about focus. aria-modal went for the same reason it came: it tells assistive technology the rest of the page is inert, which would be a lie without a trap.
The drawer is mounted at the body, and that is load-bearing. .panel-content sets contain: layout (app/assets/stylesheets/layouts/panel.scss), which makes it the containing block for position: fixed descendants — so a GlDrawer inside the paneled layout is scoped to the panel rather than the viewport. Measured: the drawer sat at top 124, height 768 against a 900px viewport; moving the node to the body put it at top 0, height 900. This is not specific to Artifact Registry — GitLab's own CI/CD variable drawer renders at exactly the same top 124 / height 768 on a paneled page, so every in-panel GlDrawer is affected, and the comment beside that contain: layout rule shows the containment is deliberate. Wrapping in MountingPortal mount-to="body" follows ee/.../vulnerability_details_enrichment/false_positive_analysis_drawer.vue, whose own comment describes this problem. #js-drawer-container would not have worked (it sits inside the panel body, so it changes the DOM position without changing the containing block), and converting to DynamicPanel over #contextual-panel-portal would have replaced the overlay with a half-width sibling panel. The panel-scoped drawer problem affects the ~60 other consumers too, and is worth rolling into the same GitLab UI issue.
Also worth knowing: getContentWrapperHeight() is effectively vestigial on paneled pages. .content-wrapper is now the static panel itself, so its offsetTop is 0 and the helper returns "0px" — it no longer measures the topbar. Harmless for us at the body, but the many drawers still calling it are not getting the offset they expect.
Deviations from the plan, both deliberate:
- The plan's file list names
repository_header.vuefor the kebab item. That is stale — Step 7 landed the kebab inrepository_actions.vue, which is where this goes. - No
repository_detail.stories.jsedit. The plan wants an open-drawer story there, but the drawer is nested inside the kebab inside the header, so a page-level story cannot open it declaratively. A component-level story is what axe needs and it touches no shared file.
The per-format documentation link is omitted. The spec's drawer carries one, but no Artifact Registry docs page exists (doc/user/packages/ has no entry) and the require_valid_help_page_path ESLint rule rejects a fabricated path. Follow-up when docs land.
Judgement calls a reviewer may want to push back on:
- Switching tools genuinely swaps the snippets, which the prototype's drawer only does for npm and Docker — selecting Gradle there still shows
mvn install. Criterion 10 requires the swap, so the Gradle variants were authored in Step 10. - Both tab panels stay mounted rather than using
GlTabs'lazy. That is what makes a chosen tool apply to both tabs;lazywould remount on every tab switch and lose that. - The drawer takes
nameandformatrather than the wholerepositoryobject, so it depends on the two fields it actually uses.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.
This MR was authored with the assistance of an AI coding agent and reviewed by @zcuddy before submission.




