Add the artifact registry repository empty state

What this does

A repository with no artifacts rendered an empty GlTable reading "This repository has no artifacts." — a dead end for the one user who most needs guidance: someone who has just created a repository. Step 10 already built the content that would help them; nothing surfaced it.

This renders the designed empty state instead — the illustration, wording that follows the format, and the publish snippets inline as numbered steps with the build-tool selector, so an empty repository needs no drawer. The sidebar drops its stats block, since three zeroed counters beside "there is nothing here yet" only repeat it.

Closes acceptance criterion 11 of monolith/S05. Step 12 of the repository detail plan.

A Maven repository in the GDK, against the mock resolvers The container wording
gdk-empty-maven story-empty-docker

Note the sidebar in the left-hand shot: the stats block is gone, and Created on stays, because it is true of an empty repository too.

Why the trigger is the connection, not the counter

The empty state keys off an empty first page of the artifact connection, never the repository's buffered artifactsCount. Artifact Registry buffers that counter, so it can disagree with what the connection returns, and the connection is what the view is actually rendering.

Step 8 was specified to derive this signal and pass it up for this step. It never didartifacts_section.vue had no emits and repository_detail.vue tracked no such state — so the derivation lands here.

Two readers answer to that connection: the section renders it, and the sidebar hides its stats when it comes back empty. So repository_detail.vue owns the query, as the nearest ancestor of both, and hands the result down:

File Role
repository_detail.vue reads the connection, derives artifacts / isLoadingArtifacts / hasArtifactsError / hasNoArtifacts
artifacts_section.vue presentational — renders skeleton, alert, empty state, or table from props
repository_sidebar.vue hides the stats block on hideStats

Moving the query up is the substantive change in the second commit. It replaced an earlier cut where the section owned the query and pushed emptiness up through a watcher on a computed, which made the parent track its child's state — the wrong direction. Nothing emits now.

Two consequences a reviewer should look at:

  • The query skips until the repository has loaded, because the document to issue follows the repository's format. A skipped query never reports itself as loading, so the gap before it starts has to count as loading too, or the page would call a repository empty before it had asked.
  • A failed read never assigns the connection, so the error state has to end the loading state or that gap never closes and the section waits on a skeleton for good. isLoadingArtifacts checks the error first. There is a test for exactly this, and I mutation-checked it.

The plan's file list is stale here

It puts the table/empty-state swap in repository_detail.vue. The swap itself is in artifacts_section.vue, which is what owns that region's four states; repository_detail.vue owns the data behind it. (Step 11 flagged the same list naming repository_header.vue for its kebab item.)

Deviations from the prototype, all deliberate

The prototype (&empty=true) is the SSoT and this matches its structure — illustration, format-aware heading, support line, then a centered "CLI commands" label plus tool selector over left-aligned numbered steps. Where it differs:

  • One <ol>, with each code block inside its own <li>. The prototype uses separate lists juggling a start attribute because its code blocks sit between them. A single list is valid markup, needs no arithmetic, and lets a screen reader hear each snippet as part of the step it belongs to. Cost: code indents with its list item instead of sitting flush left. Modelled on integrations/edit/components/google_artifact_management/configuration_instructions.vue.
  • The support text follows the format too, not only the heading. The prototype says "package" even on a Docker repository, which reads like a gap rather than a decision.
  • Dropped: "You can create a new package each time the default branch is updated." This slice wires up no CI integration, so the line promises something that does not exist.
  • Omitted: the More information link (href="#" in the prototype). As in Steps 10 and 11 — Artifact Registry has no docs page and require_valid_help_page_path rejects a fabricated path. Follow-up when docs land.
  • Section headings are flattened away. snippets.js groups blocks under "Repository setup" / "Registry setup"; numbered steps read as one run and each block's prose already says what it is for. Matches the prototype, which shows a flat 1..n list.
  • Three tools per format, not the prototype's two — the spec's sets, already built as SETUP_TOOLS.
  • Spacing between the support line and "CLI commands" is 36px against the prototype's 24px. That is GlEmptyState's own rhythm (its always-rendered actions slot carries gl-mt-5), so I left it rather than cancel it with a negative margin.

Illustration is @gitlab/svgs/dist/illustrations/empty-state/empty-package-md.svg — rendered side by side, it is a pixel match for the prototype's inline SVG, and it is already what repositories_list.vue and not_found.vue use in this domain.

Reuse, and one duplication on purpose

The empty state calls the pure setupSnippetSections() from Step 10 and renders its own <ol>, reusing snippet_code_block.vue and tool_selector.vue as components. It does not add a numbered variant prop to setup_snippets.vue: the shared thing is the content model, which Step 10 already extracted, and a prop that swaps a whole template is worse than two templates over one model.

The ~8-line "resolve the active tool for this format, falling back to its first" computed is duplicated from setup_drawer.vue deliberately, so this MR touches no file !248991 (merged) touches and the two can merge in either order. Worth extracting into a shared helper once both land.

GlEmptyState's #description slot renders inside a <p>, so the <ol> cannot live there; it is a sibling. No GlEmptyState consumer in the repo currently contains an <ol>, so there was no precedent either way.

Also in here

The component is artifacts_empty_state.vue, not repository_empty_state.vue: it is the artifact list that is empty, not the repository, which still has a header, a sidebar, and its own metadata.

artifacts_table.vue loses show-empty and :empty-text. The table now only renders when there is at least one row, so that string was unreachable — and a dead translated string is worse than a two-line cleanup. It comes out of locale/gitlab.pot.

repository_detail.stories.js needed no change: its existing WithoutArtifacts story already drives this path and now renders the empty state on its own.

Stacking

Branches off and targets zcuddy-s05-step10-setup-snippets (!248880 (merged)), whose snippets.js, snippet_code_block.vue, and tool_selector.vue this consumes. It is a sibling of Step 11 (!248991 (merged)), not stacked on it, and shares no changed file with it. Retarget to master once Step 10 merges.

Behind the artifact_registry_ui feature flag, dark.

How I verified

  • 615 Jest tests pass (up from 609), 28 of them new for the empty state.
  • Mutation-checked the guards that matter: removing the error-wins branch from isLoadingArtifacts fails "stops loading when the read fails outright"; removing the sidebar's v-if fails "drops the counters". Neither assertion is decorative.
  • The criterion-11 assertion is the contradiction case: the fixture's artifactsCount is '3' while the connection returns an empty page. The empty state renders and the stats hide anyway.
  • In the GDK against the mock resolvers: created a repository (seeded with artifacts: []), confirmed the empty state, the real composed client URL, the hidden stats with Created on retained, and that switching the tool to Gradle (Kotlin) swaps the steps to ./gradlew publish plus the build.gradle.kts config. Confirmed a populated repository is unchanged — stats present, three table rows, no empty state.
  • Sampled 120 animation frames through an SPA route change to check the states in order: skeleton while the connection loads, then the table — no flash of the empty state before the connection answers, and the empty repository goes skeleton → empty state with the stats hidden.
  • Compared all four formats against the prototype side by side in Storybook.
  • eslint and prettier clean; the only eslint failures in the tree are the pre-existing .graphql schema-dump parse errors, which need a generated local schema and are unrelated.
One thing I could not fully explain

On the local GDK, driving any page.evaluate through the Playwright MCP surfaces ~92 [Vue warn]: Property or method "toJSON" is not defined messages on AR detail pages, and this branch is what makes them appear (verified by stashing and restoring, twice each way).

I do not believe it is a defect in this change, and I want to be straight that I stopped short of a root cause:

  • Every captured trace names GlBreadcrumbItemGlBreadcrumbArtifactRegistryBreadcrumbs. This MR touches none of them, and cannot influence their props.
  • They appear on a populated repository, where all of this MR's new logic is inert — so the trigger is the module graph (the new GlEmptyState/GlCollapsibleListbox imports), not any template here.
  • They never appear on page load, and force-updating every Vue root on the page produces zero of them. An in-page console.error hook never sees them either; only the MCP reports them.
  • toJSON appears nowhere in the AR tree.
  • Jest's console watcher fails a test on any console error, and all 615 pass with full mount of every component changed here.

Vue 2 only emits this class of warning for render functions compiled at runtime, which is a dev-server concern (@gitlab/ui is served from src/ under Vite here) and not something a production build does. Flagging it in case a reviewer sees it locally and wonders.


Written by GitLab Duo

Edited by Zack Cuddy

Merge request reports

Loading
Loading