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 arearchive,download, andpackage, 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 aTimeAgoTooltip.graphql/typedefs.graphql,get_repository_detail.query.graphql— appendsartifactsCount,createdAt,createdBy, andupdatedBy. The joins resolve into the schema's ownUserCorerather than a local redeclaration; precedent iswork_items/graphql/typedefs.graphql. Declaring a second user type is how the duplicatePageInfobroke the Apollo loader.utils.js,repositories/list/repositories_table.vue— the list and the sidebar render the same counters, sohumanSizeandformattedCountmove 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) |
|---|---|
![]() |
![]() |
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 |
|---|---|---|
![]() |
![]() |
![]() |
How to set up and validate locally
- Enable the flag:
echo "Feature.enable(:artifact_registry_ui)" | bundle exec rails c - Visit
/o/<your-org>/-/artifact_registry/acme/repositories. The list now opens with four seeded repositories rather than its empty state. - 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. - Click payment-service (Docker) — the third counter reads Images.
- Click oci-artifacts — its
lastUpdatedAtis null, so no Last updated section renders at all. - Widen the window past the panel's
@lgcontainer width to see the sidebar beside the table rather than stacked under it (the grid is the artifact table's, unchanged here). yarn jest ee/spec/frontend/packages_and_registries/artifact_registry
Agent Notes
Two judgment calls worth a look:
attributed_timestamp.vueis a second new component, which the WIP plan did not name. The alternative was a ~13-lineGlAvatarLink/GlAvatarblock copied into both sections. I think the extraction is right, but it is the one structural decision here that is arguable.- 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.




