Pilotiq
DocsGitHub

User menu

The panel's top-right user-menu dropdown surfaces the current user plus optional links (profile, billing, docs, …) and a sign-out endpoint. It mounts whenever Pilotiq.user(req => …) resolves a non-null user.

#Quick start

import { Pilotiq, UserMenuItem } from '@pilotiq/pilotiq'
import { Auth } from '@rudderjs/auth'

Pilotiq.make('admin')
  .user(req => Auth.user())
  .userMenuItems([
    UserMenuItem.make('billing').label('Billing').icon('credit-card').url('/billing'),
    UserMenuItem.make('docs')
      .label('Documentation')
      .icon('book-open')
      .url('https://docs.example.com')
      .openUrlInNewTab(),
  ])
  .signOut('/logout')

The dropdown shows:

┌─────────────────────────┐
│  <render hook: before>  │
│  Demo Admin             │  ← identity (name + email + avatar)
[email protected]
│ ─────────────────────── │
│  Edit profile           │  ← auto-injected by .profile(P)
│  Billing                │  ← from .userMenuItems([...])
│  Documentation          │
│  <render hook: after>   │
│ ─────────────────────── │
│  Sign out               │  ← from .signOut(...)
└─────────────────────────┘

Anonymous requests never see the dropdown — when Pilotiq.user(...) returns null, the trigger is suppressed entirely.

#UserMenuItem

UserMenuItem.make('profile')
  .label('My profile')                 // string or ({ user }) => string
  .icon('user')                        // icon-registry key
  .url('/profile')                     // string or ({ user }) => string
  .openUrlInNewTab()                   // external link
  .color('destructive')                // 'default' (omit) or 'destructive'
  .sort(10)                            // ascending; registration order breaks ties
  .visible(({ user }) => isAdmin(user))// boolean or ({ user }) => boolean | Promise<boolean>

#Visibility

.visible(rule) mirrors Action.visible(...) — receives an ActionVisibilityContext with just { user } populated. Throwing rules fail closed (item hidden), matching the canAccess posture elsewhere.

#Dynamic label / URL

Labels and URLs accept callbacks so items can be personalised:

UserMenuItem.make('profile')
  .label(({ user }) => `Signed in as ${(user as any).name}`)
  .url(({ user }) => `/users/${(user as any).id}/profile`)

Returning a Promise is supported. The callback fires server-side inside panelInfo().

#.signOut(...)

.signOut('/logout')                                          // POST /logout, label "Sign out"
.signOut({ url: '/auth/logout', method: 'GET', label: 'Log out' })

The dropdown's sign-out entry renders as a <form method="POST"> (not a bare <a href>) so CSRF middleware downstream can validate the request. Set method: 'GET' for traditional logout endpoints that redirect on a normal navigation.

Without .signOut(...), the menu shows custom items (if any) and the user identity, but no sign-out affordance.

#.profile(P) — auto-inject "Edit profile"

import { Page, Form, TextField } from '@pilotiq/pilotiq'

class ProfilePage extends Page {
  static override slug  = 'profile'
  static override label = 'My profile'
  static override icon  = 'user-circle'
  static override schema(ctx) {
    return [Form.make().schema([
      TextField.make('name').required(),
      TextField.make('email').email().required(),
    ])]
  }
}

Pilotiq.make('admin').profile(ProfilePage)

Pilotiq.profile(P) registers the page in cfg.pages (so routing and canAccess wire up) AND auto-prepends an "Edit profile" entry to the user menu pointing at it. The entry's label and icon read from Page.label / Page.icon with 'Edit profile' and 'user-circle' defaults.

Author the page schema against your own auth model — pilotiq treats the user object as opaque, so the form is just a normal Page subclass.

#Render hooks

Two splice points let plugins add to the menu without owning the slot:

Slot Position
panels::user-menu.before Top of the dropdown (above the identity row)
panels::user-menu.after Above the separator before sign-out
Pilotiq.make('admin')
  .renderHook('panels::user-menu.after', () => [
    Alert.make('You have 3 unread invites').info(),
  ])

#User identity row

The identity row at the top of the dropdown reads name, email, and avatar from your user object. Pilotiq tries common shapes (user.name / user.fullName; user.email; user.avatar / user.profilePhotoUrl) — anything else is invisible unless you inject it via render hook.

#What's not in v1

  • Nested sub-menus inside the user menu (use .url() to a custom page if you need a multi-step settings flow).
  • Per-item authorisation against Resource.canAccess — items rely on .visible(rule) only; cross-resource policy gates aren't inferred.
  • A built-in language switcher / theme override inside the menu; use a render hook or a custom Pilotiq.components({ nav }) component.

#Reference

  • Pilotiq.userMenuItems(items) / Pilotiq.signOut(config) / Pilotiq.profile(P)src/Pilotiq.ts
  • UserMenuItemsrc/UserMenuItem.ts
  • <UserMenu> renderer — src/react/UserMenu.tsx
  • See also: Render hooks, Pages