Skip to content

feat(console): build the user detail screen - #706

Merged
bastionstack merged 31 commits into
mainfrom
feat/console-user-detail-631
Aug 4, 2026
Merged

feat(console): build the user detail screen#706
bastionstack merged 31 commits into
mainfrom
feat/console-user-detail-631

Conversation

@bastionstack

@bastionstack bastionstack commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Which Problems Are Solved

/users/{id} renders a raw key/value dump of the API response. The design (467:44362) specifies a real screen: a header carrying the user id and status, Overview and Authentication tabs, the profile laid out from the user's schema, and a delete action.

How the Problems Are Solved

  • Header with the user id and copy-to-clipboard, the status badge, and Created — the last two from the metadata block added in feat: user metadata #721.
  • Overview: the profile rendered from the user's own $schema, so the fields are whatever that schema defines — the same rule as the users list columns and the Add-user drawer. Plus the Delete user card, wired to the dialog from Frontend: Delete user confirmation dialog #632, which returns to the list on success.
  • Authentication: the registered passkey count from GET /users/{user_id}/passkeys.
  • The schema and passkey calls are chrome for the record rather than the record itself, so neither can reject the loader — a failure costs a card, not the screen.
  • The created date renders in the viewer's locale. The design's 12 Jul 2026 is that mock's locale rather than a format to impose, so day, short month and year are requested and the order is the viewer's.

Additional Changes

Three changes reach beyond this screen, which is why the diff touches components/ui:

  • Tabs and Badge are corrected against the design system rather than the screen mock — the mock's own Tabs instance disagrees with the library it came from. Both are shared primitives, so every console screen picks the fix up.
  • The user status pill moves into one shared component. Badge already carried the icon slot the design fills with a dot; the users list and this screen were rendering two near-identical pills instead of using it. The dot uses the success colour added in chore: sync tokens from Figma #753.
  • Adds the shadcn Card primitive, which the console did not have and this design uses for every section.

Out of Scope

Five design elements are deliberately absent, recorded on #631:

Element Why
Last sign-in No such field anywhere on the user
Save on the profile form #693 — no PATCH/PUT /users/{user_id}, so the fields are read-only and the design's Save is absent rather than disabled
Password row (last changed) No password timestamp is exposed
Project permissions Removed from the design (decisions log D6)
v1 · live schema badge Schemas are not versioned — one row per URL, no history

Two deviations from the mock are intentional and worth a reviewer's agreement rather than a silent pass:

  • Content inset stays at 32px. The detail mock insets 24px and the users list mock 32px; matching each exactly would jump the layout 8px when navigating between the two screens.
  • The copy control stays a 24px button against the design's 13px glyph, which leaves the meta card 7px taller than the mock. Shrinking it would put the tap target under the WCAG 2.5.8 minimum.

Additional Context

wim07101993 and others added 6 commits July 31, 2026 16:09
# Conflicts:
#	internal/api/integration_test/user_list_test.go
Replaces the read-only key/value dump at /users/{id} with the screen from the
design (467:44362): a header carrying the user id with copy-to-clipboard, an
Overview / Authentication tab pair, the profile rendered from the user's own
schema, the registered passkey count, and the Delete user card wired to the
dialog from #632.

Elements of the design whose data does not exist are left out rather than
rendered empty, each recorded in the route's docblock: the status badge and
Created (#703), Last sign-in (no field at all), the schema version badge (#445),
and the Password row's last-changed date. The profile fields are read-only and
say so — there is no update endpoint (#693), and an editable field with no Save
promises more than a plain one.

The design's Project permissions block is not missing but removed, per the
design decisions log (D6).
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nextgen Ready Ready Preview Aug 4, 2026 8:01pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
nextgen-docs Skipped Skipped Aug 4, 2026 8:01pm
nextgen-mock-zitadel Skipped Skipped Aug 4, 2026 8:01pm

Request Review

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 60f8ef0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@zitadel/server Minor
@zitadel/cli Minor
@zitadel/testing Minor
@zitadel/server-linux-x64 Minor
@zitadel/server-linux-arm64 Minor
@zitadel/server-darwin-x64 Minor
@zitadel/server-darwin-arm64 Minor
@zitadel/server-win32-x64 Minor
@zitadel/api Minor
@zitadel/config Minor
@zitadel/components Minor
@zitadel/sdk-core Minor
@zitadel/sdk-next Minor
@zitadel/sdk-nuxt Minor
@zitadel/sdk-react Minor
@zitadel/sdk-vue Minor
@zitadel/sdk-angular Minor
@zitadel/sdk-solid Minor
@zitadel/sdk-svelte Minor
@zitadel/sdk-qwik Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

wim07101993 and others added 12 commits August 3, 2026 09:47
# Conflicts:
#	api/generated/oas_json_gen.go
#	api/generated/oas_schemas_gen.go
Local integration only, so the console work can build against the metadata
contract while #721 is still open. Resolved the one conflict — a generated ogen
file — by regenerating rather than hand-merging.
Builds the two parts of #630 that #721 unblocks.

- **Status** column from `metadata.status` (`active`, `suspended`,
  `deactivated`, `pending_purge`), rendered as a badge. A record the server has
  not stamped shows an em dash rather than an invented value.
- **Load more** (design decisions log D5), walking `next_page_token`. Pages
  append rather than replace, and a route invalidation — a delete, a create —
  resets to the first page, since the set has changed underneath. A later page
  can reference a schema the first did not, so the columns are recomputed as
  pages arrive.

`metadata` joins `id` and `$schema` as a reserved key, kept out of the
schema-driven columns so it is not rendered as an attribute.

Drops the "Showing N of the N users loaded" caveat: with a Load more button its
presence already says there is more and its absence says the list is complete,
and the search caveat restated what the rows show.

The spec stubs move from a bare array to `{ users }`, matching the new response.
Main gained a SQLite dialect (#665) after the metadata branch was cut, so the two
never met until now: the merge is clean textually and then fails to build.

`Status`, `CreatedAt` and `UpdatedAt` moved onto `User.Metadata`, `Attribute.Key`
became a typed `AttributeKey`, and `CreateAttribute` is a value rather than a
pointer — the same treatment Postgres and Spanner already had here.

Belongs to #721 and should drop out when this branch rebases onto it.
Verified the built screen against Figma `467:44362` and fixed what the
numeric readout and a per-region pixel diff turned up. Sampling the design
export rather than reading tokens off the mock is what caught most of it —
the mock's own Tabs instance disagrees with the library it came from.

Design system components, so every console screen inherits the fix:

- Tabs: the active trigger is `base/input` at full strength. Upstream
  shadcn uses `bg-input/30` in dark, which renders (16,16,16) where the
  design is (42,42,42). The resting label is muted in both themes, and the
  trigger fills the 34px the list's 3px padding leaves.
- Badge: `h-5`, the library's own height. Left to hug it rendered 22px.

The status pill moves into one shared component. `Badge` already had the
left icon slot the design fills with a dot; the list and the detail screen
were rendering two near-identical pills instead of using it. The dot is
`bg-success` — Tailwind v4's own `green-500` is oklch and renders
(0,201,80), not the (34,197,94) the design system means.

Screen fixes: field grid gaps to 18px, the panel divider inset inside the
body padding, input fill to `base/background` so it reads as inset against
the card, delete card padding and typography, the delete button to `lg`
for its 40px height, and the meta card's rule stacking flush-left on
mobile (`520:80552`) instead of indenting Created when the row wraps.

The Authentication tab's icon is `Key`, read off the node rather than
inferred, and its badge only claims `Enabled` when a passkey exists — the
design has no `None` state, and the count already says so.

Deviations left in place, deliberately:

- Content inset stays 32px. The detail mock insets 24px and the list mock
  32px; matching each would jump the layout 8px between the two screens.
- The copy control stays a 24px button against the design's 13px glyph,
  which keeps the meta card 73px against the mock's 66px. Shrinking it
  would put the tap target under WCAG 2.5.8.
@vercel
vercel Bot temporarily deployed to Preview – nextgen-docs August 4, 2026 19:04 Inactive
@vercel
vercel Bot temporarily deployed to Preview – nextgen-mock-zitadel August 4, 2026 19:04 Inactive
@bastionstack
bastionstack marked this pull request as ready for review August 4, 2026 19:08
…ated

`Load more` appends to state the route loader owns, and both the delete
dialog and the add-user drawer invalidate that loader. A page still in
flight when either fires resolves into a list that has already reset to a
fresh first page, appending rows the server no longer returns — including
the user just deleted — and recomputing the columns against them.

The loader hands back a new object per invalidation, so its identity is
the generation of the list on screen. `loadMore` captures it and checks it
after both awaits, before any state write, so the three setters are atomic
with respect to the check. The button still re-enables, leaving it ready
to fetch the current page 2.
@vercel
vercel Bot temporarily deployed to Preview – nextgen-mock-zitadel August 4, 2026 19:33 Inactive
@vercel
vercel Bot temporarily deployed to Preview – nextgen-docs August 4, 2026 19:33 Inactive
Base automatically changed from feat/console-users-list-metadata-630 to main August 4, 2026 19:44
…ail-631

# Conflicts:
#	apps/console/src/routes/_authed/users/index.tsx
Copilot AI review requested due to automatic review settings August 4, 2026 19:50
@vercel
vercel Bot temporarily deployed to Preview – nextgen-mock-zitadel August 4, 2026 19:50 Inactive
@vercel
vercel Bot temporarily deployed to Preview – nextgen-docs August 4, 2026 19:50 Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the console’s /users/$userId raw record dump with a structured user detail screen aligned to the design, including schema-driven profile rendering, metadata header, Authentication tab passkey count, and a delete action flow. It also updates shared console UI primitives (Tabs/Badge) and introduces new shared components used by both the users list and detail screen.

Changes:

  • Implement the new user detail route UI + loader behavior (best-effort schema/passkeys fetch that doesn’t fail the page).
  • Introduce UserStatusBadge and switch the users list status pill to the shared component.
  • Add shadcn Card, adjust Tabs/Badge styling to match the design system, add tests and a changeset.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
apps/console/src/routes/_authed/users/user-detail.spec.tsx Adds route-level tests covering schema-driven profile rendering, metadata display, passkey loading failure behavior, and delete→redirect flow.
apps/console/src/routes/_authed/users/index.tsx Switches the users list status rendering to the shared UserStatusBadge.
apps/console/src/routes/_authed/users/$userId.tsx Replaces the raw dump page with the designed user detail screen, including loader fetching user/schema/passkeys and new UI layout/actions.
apps/console/src/components/user-status-badge.tsx Adds a shared status pill component (dot + label) for consistent rendering across screens.
apps/console/src/components/ui/tabs.tsx Adjusts Tabs trigger styling to align with the design system (resting and active styles).
apps/console/src/components/ui/card.tsx Adds the shadcn Card primitive used for section layout.
apps/console/src/components/ui/badge.tsx Adjusts Badge sizing to match the design-system height.
.changeset/console-user-detail.md Adds release intent note for the shipped (embedded) console user detail screen behavior.

Comment thread apps/console/src/routes/_authed/users/$userId.tsx
`schemaFields` surfaces `number` and `integer` properties, and a schema may
define a boolean, but both screens read attribute values with `field`, which
returns only strings — so a real value rendered as an empty cell or field.

`displayValue` stringifies primitives and is used for schema attributes on
both the list and the detail screen. `field` keeps its string-only contract
for server-owned keys like `id` and `$schema`, where a non-string means the
record is not what was expected. Objects and arrays still render as nothing:
they have no one-line form, and `JSON.stringify` in a table cell is noise.
@vercel
vercel Bot temporarily deployed to Preview – nextgen-mock-zitadel August 4, 2026 20:00 Inactive
@vercel
vercel Bot temporarily deployed to Preview – nextgen-docs August 4, 2026 20:00 Inactive
@bastionstack
bastionstack enabled auto-merge (squash) August 4, 2026 20:02
@bastionstack
bastionstack merged commit 2fdb22e into main Aug 4, 2026
14 checks passed
@bastionstack
bastionstack deleted the feat/console-user-detail-631 branch August 4, 2026 20:11
@github-project-automation github-project-automation Bot moved this from Inbox to Done in Engineering Kanban Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants