Artifact registry: Add the metadata sidebar to the repository detail page

What does this MR do and why?

Fills the repository detail page's sidebar column with the repository's metadata. Until now the page rendered its header and artifact table beside an empty <aside>.

This change is behind the feature flag :artifact_registry_ui (dark, default_enabled: false).

This is monolith/S05 Step 9, a sibling of the artifact table (!248618 (merged)) off the same scaffold, and it targets that branch so the diff here is the sidebar alone.

What renders, and in what order

The design prototype draws five sections. Two of them belong to slices that have not landed — Virtual repository associations (Go-service S13) and Retention policies (monolith/S08/S09) — so three ship:

section contents
counters Size (sizeBytes), Downloads (downloadsCount), and Packages or Images (artifactsCount)
Created on createdAt plus the resolved createdBy user
Last updated lastUpdatedAt plus the resolved updatedBy user

The counters carry no section heading, matching the design: each row names itself.

The artifact counter names what the repository holds

Artifact Registry organizes artifacts per format family (ADR-009): the container formats answer with images where Maven and npm answer with packages. So the third counter's label follows the family — the same rule the page already applies to pick which connection the table reads. The icon does not change, matching the design.

The counters are buffered at Artifact Registry and may lag the table beside them. That is the contract, not a bug — but a mock that disagreed with its own table would read as one, so artifactsCount counts the rows the local connections synthesize rather than carrying an independent number.

Two date treatments, deliberately

Created on renders an exact date. Last updated renders a relative interval. That is what the design does, and the reason holds up: when a repository came into being is a fact worth reading precisely, where how long ago its content last changed is the question a reader is actually asking. So createdAt goes through localeDateFormat.asDate and lastUpdatedAt through TimeAgoTooltip, which also gives the exact timestamp on hover.

Last updated hides entirely when lastUpdatedAt is null. It reports a change to the repository's content, and nothing has been published to a fresh repository — there is no date to render and nothing to put in its place. (Note this is not the list's Never: the list has a column to fill either way.)

Attribution is one sentence, and it degrades to the date

s__('ArtifactRegistry|%{date} by %{user}') is a single GlSprintf message rather than a date and an attribution assembled side by side, so a translation can order the two however its language reads.

A user resolves null in three cases — a null Artifact Registry ID, a user that no longer exists, and a user the viewer cannot see. All three read the same way, and the date is what the section is for, so the attribution is what gives way. That falls out of the GlSprintf contract rather than needing a second branch: the component switches the message to %{date}, and GlSprintf invokes only the slots its message names, so the user slot never renders.

Changes

  • repositories/detail/repository_sidebar.vue (new) — the three counters as a list, so the icon, value, and label that belong together stay in one place. Icons are archive, download, and package, matching the design.
  • repositories/detail/attributed_timestamp.vue (new) — the titled section and the "date by user" sentence, so Created on and Last updated share one implementation instead of a copied avatar block. Takes the date as a slot, which is what lets one section pass text and the other a TimeAgoTooltip.
  • graphql/typedefs.graphql, get_repository_detail.query.graphql — appends artifactsCount, createdAt, createdBy, and updatedBy. The joins resolve into the schema's own UserCore rather than a local redeclaration; precedent is work_items/graphql/typedefs.graphql. Declaring a second user type is how the duplicate PageInfo broke the Apollo loader.
  • utils.js, repositories/list/repositories_table.vue — the list and the sidebar render the same counters, so humanSize and formattedCount move out of the table rather than being spelled twice.
  • graphql/mock_resolvers.js — see below.
  • Jest for the sidebar, the detail page's wiring, and the new resolvers; Storybook stories for the populated, container, no-attribution, and never-updated states.

The mock store is now seeded

Nothing in a browser publishes an artifact — the write flows create repositories and publishing goes through a package client — so a store holding only what those flows created reads as empty forever, and the sidebar would have had nothing but zeroes to render. The store is seeded with four published-to repositories, one per format, which the file's own comment already invited ("Add mocked repositories here to seed the cache").

Two things deliberately did not change: clearMockRepositories() still empties the store, seed included, so a spec asserts only against rows it wrote itself; and a repository created through the form still reads downloadsCount: '0', sizeBytes: '0', lastUpdatedAt: null, because a freshly created repository genuinely is empty. Only createdAt is stamped.

createdBy/updatedBy resolve the signed-in user, which is the most plausible author of repositories the mock seeded for the person reading the page. With no signed-in user they resolve null — not as a fallback, but because that is the degradation the sidebar has to render anyway.

Removing all of this is already on the file's removal checklist.

Screenshots or screen recordings

The prototype next to the GDK, both a Maven repository:

Prototype This MR (GDK)
prototype gdk

The two deferred sections account for the difference. Size reads MiB rather than the prototype's MB because numberToHumanSize is what every registry surface in the monolith formats bytes with, including the AR list this sidebar sits beside.

Container family (Images) Never updated Neither user resolves
docker never unattributed

How to set up and validate locally

  1. Enable the flag: echo "Feature.enable(:artifact_registry_ui)" | bundle exec rails c
  2. Visit /o/<your-org>/-/artifact_registry/acme/repositories. The list now opens with four seeded repositories rather than its empty state.
  3. Click payment-core (Maven). The sidebar renders 570.00 MiB / 6,910 / 3 Packages, then Created on with an exact date and Last updated with a relative one, both attributed to you.
  4. Click payment-service (Docker) — the third counter reads Images.
  5. Click oci-artifacts — its lastUpdatedAt is null, so no Last updated section renders at all.
  6. Widen the window past the panel's @lg container width to see the sidebar beside the table rather than stacked under it (the grid is the artifact table's, unchanged here).
  7. yarn jest ee/spec/frontend/packages_and_registries/artifact_registry

Agent Notes

Two judgment calls worth a look:

  1. attributed_timestamp.vue is a second new component, which the WIP plan did not name. The alternative was a ~13-line GlAvatarLink/GlAvatar block copied into both sections. I think the extraction is right, but it is the one structural decision here that is arguable.
  2. The plan's "loading state gains the sidebar skeleton" is not in this MR. Step 8 settled on a single page-level loading affordance and documented that choice in two places (including "no busy state of its own" in artifacts_table.vue), and asserts that neither page region renders while the query is in flight. Adding a skeleton for the sidebar alone, while the table has none, would be lopsided. A page-wide loading treatment is worth doing as its own change.

Also: the per-family counter labels live in the component's $options.i18n rather than constants.js as the plan sketched. Four steps in this slice are queued to touch constants.js; keeping two strings out of it drops a rebase point and reads no worse.

Local yarn eslint reports parse errors on every .graphql file in the repo because tmp/tests/graphql/gitlab_schema_apollo.graphql needs Docker to generate — pre-existing and unrelated. UserCore carrying id, name, avatarUrl, and webPath was verified against the committed schema reference instead.

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.


Edited by Zack Cuddy

Merge request reports

Loading
Loading