feat: add About Me dialog showing user info, roles, and groups
- Add GET /api/v1/auth/me endpoint returning current user's UserDetail - Add AboutMeDialog component with role badges and group memberships - Add userMenuItems prop to TopBar via design-system update - Wire "About Me" menu item into user dropdown above Logout Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
ui/src/components/AboutMeDialog.module.css
Normal file
57
ui/src/components/AboutMeDialog.module.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metaGrid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 6px 16px;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-body);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metaLabel {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metaValue {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tagList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sourceList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sourceNote {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
74
ui/src/components/AboutMeDialog.tsx
Normal file
74
ui/src/components/AboutMeDialog.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Modal, Badge, MonoText, Spinner, SectionHeader } from '@cameleer/design-system';
|
||||
import { useMe } from '../api/queries/auth';
|
||||
import styles from './AboutMeDialog.module.css';
|
||||
|
||||
interface AboutMeDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function AboutMeDialog({ open, onClose }: AboutMeDialogProps) {
|
||||
const { data: user, isLoading } = useMe(open);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title="About Me" size="sm">
|
||||
{isLoading || !user ? (
|
||||
<div className={styles.loading}><Spinner size="md" /></div>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.metaGrid}>
|
||||
<span className={styles.metaLabel}>Name</span>
|
||||
<span className={styles.metaValue}>{user.displayName}</span>
|
||||
<span className={styles.metaLabel}>User ID</span>
|
||||
<MonoText size="xs">{user.userId}</MonoText>
|
||||
<span className={styles.metaLabel}>Provider</span>
|
||||
<span className={styles.metaValue}>{user.provider}</span>
|
||||
{user.email && (
|
||||
<>
|
||||
<span className={styles.metaLabel}>Email</span>
|
||||
<span className={styles.metaValue}>{user.email}</span>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.metaLabel}>Created</span>
|
||||
<span className={styles.metaValue}>
|
||||
{new Date(user.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<SectionHeader>Roles</SectionHeader>
|
||||
<div className={styles.tagList}>
|
||||
{user.effectiveRoles.length === 0 && (
|
||||
<span className={styles.empty}>No roles assigned</span>
|
||||
)}
|
||||
{user.effectiveRoles.map((role) => (
|
||||
<Badge
|
||||
key={role.id}
|
||||
label={role.name}
|
||||
color={role.name === 'ADMIN' ? 'error' : role.name === 'OPERATOR' ? 'warning' : 'auto'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{user.effectiveRoles.some((r) => r.source !== 'direct') && (
|
||||
<div className={styles.sourceList}>
|
||||
{user.effectiveRoles.map((role) => (
|
||||
<span key={role.id} className={styles.sourceNote}>
|
||||
{role.name}: {role.source}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SectionHeader>Groups</SectionHeader>
|
||||
<div className={styles.tagList}>
|
||||
{user.effectiveGroups.length === 0 && (
|
||||
<span className={styles.empty}>No group memberships</span>
|
||||
)}
|
||||
{user.effectiveGroups.map((group) => (
|
||||
<Badge key={group.id} label={group.name} color="auto" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -15,9 +15,10 @@ import {
|
||||
useGlobalFilters,
|
||||
useStarred,
|
||||
} from '@cameleer/design-system';
|
||||
import type { SearchResult, SidebarTreeNode } from '@cameleer/design-system';
|
||||
import type { SearchResult, SidebarTreeNode, DropdownItem } from '@cameleer/design-system';
|
||||
import sidebarLogo from '@cameleer/design-system/assets/cameleer3-logo.svg';
|
||||
import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X } from 'lucide-react';
|
||||
import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User } from 'lucide-react';
|
||||
import { AboutMeDialog } from './AboutMeDialog';
|
||||
import css from './LayoutShell.module.css';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouteCatalog } from '../api/queries/catalog';
|
||||
@@ -437,6 +438,12 @@ function LayoutContent() {
|
||||
|
||||
const effectiveSelectedPath = sidebarRevealPath ?? location.pathname;
|
||||
|
||||
// --- About Me dialog -----------------------------------------------
|
||||
const [aboutMeOpen, setAboutMeOpen] = useState(false);
|
||||
const userMenuItems: DropdownItem[] = useMemo(() => [
|
||||
{ label: 'About Me', icon: createElement(User, { size: 14 }), onClick: () => setAboutMeOpen(true) },
|
||||
], []);
|
||||
|
||||
// --- Exchange full-text search via command palette -----------------
|
||||
const [paletteQuery, setPaletteQuery] = useState('');
|
||||
const debouncedQuery = useDebouncedValue(paletteQuery, 300);
|
||||
@@ -724,8 +731,10 @@ function LayoutContent() {
|
||||
/>
|
||||
}
|
||||
user={username ? { name: username } : undefined}
|
||||
userMenuItems={userMenuItems}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
<AboutMeDialog open={aboutMeOpen} onClose={() => setAboutMeOpen(false)} />
|
||||
<CommandPalette
|
||||
key={isAdminPage ? 'admin' : 'ops'}
|
||||
open={paletteOpen}
|
||||
|
||||
Reference in New Issue
Block a user