@@ -10,10 +10,13 @@ import {
1010 adminParsers ,
1111 adminUrlKeys ,
1212} from '@/app/workspace/[workspaceId]/settings/components/admin/search-params'
13+ import { useRecentImpersonations } from '@/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations'
1314import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1415import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
1516import {
17+ type AdminUser ,
1618 useAdminUsers ,
19+ useAdminUsersByEmails ,
1720 useBanUser ,
1821 useImpersonateUser ,
1922 useSetUserRole ,
@@ -25,6 +28,16 @@ import { clearUserData } from '@/stores'
2528
2629const PAGE_SIZE = 20 as const
2730
31+ const USER_TABLE_HEADER = (
32+ < div className = 'flex items-center gap-3 px-3 pb-1 text-[var(--text-tertiary)] text-caption' >
33+ < span className = 'w-[170px]' > Name</ span >
34+ < span className = 'flex-1' > Email</ span >
35+ < span className = 'w-[60px]' > Role</ span >
36+ < span className = 'w-[55px]' > Status</ span >
37+ < span className = 'w-[200px] text-right' > Actions</ span >
38+ </ div >
39+ )
40+
2841const MOTHERSHIP_ENV_OPTIONS : { value : MothershipEnvironment ; label : string } [ ] = [
2942 { value : 'default' , label : 'Default' } ,
3043 { value : 'dev' , label : 'Dev' } ,
@@ -43,6 +56,8 @@ export function Admin() {
4356 const banUser = useBanUser ( )
4457 const unbanUser = useUnbanUser ( )
4558 const impersonateUser = useImpersonateUser ( )
59+ const { recentEmails, recordImpersonation } = useRecentImpersonations ( )
60+ const { data : recentUsers } = useAdminUsersByEmails ( recentEmails )
4661
4762 const [ workflowId , setWorkflowId ] = useState ( '' )
4863 const [ targetWorkspaceId , setTargetWorkspaceId ] = useState ( '' )
@@ -94,7 +109,7 @@ export function Admin() {
94109 }
95110 }
96111
97- const handleImpersonate = ( userId : string ) => {
112+ const handleImpersonate = ( userId : string , email : string ) => {
98113 setImpersonationGuardError ( null )
99114 if ( session ?. user ?. role !== 'admin' ) {
100115 setImpersonatingUserId ( null )
@@ -111,6 +126,7 @@ export function Admin() {
111126 setImpersonatingUserId ( null )
112127 } ,
113128 onSuccess : async ( ) => {
129+ recordImpersonation ( email )
114130 await clearUserData ( )
115131 window . location . assign ( '/workspace' )
116132 } ,
@@ -156,6 +172,119 @@ export function Admin() {
156172 impersonateUser . variables ,
157173 impersonatingUserId ,
158174 ] )
175+
176+ const renderUserRow = ( u : AdminUser ) => (
177+ < div key = { u . id } className = 'flex flex-col gap-2 px-3 py-2 text-small' >
178+ < div className = 'flex items-center gap-3' >
179+ < span className = 'w-[170px] truncate text-[var(--text-primary)]' > { u . name || '—' } </ span >
180+ < span className = 'flex-1 truncate text-[var(--text-secondary)]' > { u . email } </ span >
181+ < span className = 'w-[60px]' >
182+ < Badge variant = { u . role === 'admin' ? 'blue' : 'gray' } > { u . role || 'user' } </ Badge >
183+ </ span >
184+ < span className = 'w-[55px]' >
185+ { u . banned ? < Badge variant = 'red' > Banned</ Badge > : < Badge variant = 'green' > Active</ Badge > }
186+ </ span >
187+ < span className = 'flex w-[200px] justify-end gap-1' >
188+ { u . id !== session ?. user ?. id && (
189+ < >
190+ < Button
191+ variant = 'active'
192+ className = 'h-[28px] px-2 text-caption'
193+ onClick = { ( ) => handleImpersonate ( u . id , u . email ) }
194+ disabled = { pendingUserIds . has ( u . id ) }
195+ >
196+ { impersonatingUserId === u . id ||
197+ ( impersonateUser . isPending &&
198+ ( impersonateUser . variables as { userId ?: string } | undefined ) ?. userId === u . id )
199+ ? 'Switching...'
200+ : 'Impersonate' }
201+ </ Button >
202+ < Button
203+ variant = 'active'
204+ className = 'h-[28px] px-2 text-caption'
205+ onClick = { ( ) => {
206+ setUserRole . reset ( )
207+ setUserRole . mutate ( {
208+ userId : u . id ,
209+ role : u . role === 'admin' ? 'user' : 'admin' ,
210+ } )
211+ } }
212+ disabled = { pendingUserIds . has ( u . id ) }
213+ >
214+ { u . role === 'admin' ? 'Demote' : 'Promote' }
215+ </ Button >
216+ { u . banned ? (
217+ < Button
218+ variant = 'active'
219+ className = 'h-[28px] px-2 text-caption'
220+ onClick = { ( ) => {
221+ unbanUser . reset ( )
222+ unbanUser . mutate ( { userId : u . id } )
223+ } }
224+ disabled = { pendingUserIds . has ( u . id ) }
225+ >
226+ Unban
227+ </ Button >
228+ ) : (
229+ < Button
230+ variant = 'active'
231+ className = { cn (
232+ 'h-[28px] px-2 text-caption' ,
233+ banUserId === u . id ? 'text-[var(--text-primary)]' : 'text-[var(--text-error)]'
234+ ) }
235+ onClick = { ( ) => {
236+ if ( banUserId === u . id ) {
237+ setBanUserId ( null )
238+ setBanReason ( '' )
239+ } else {
240+ setBanUserId ( u . id )
241+ setBanReason ( '' )
242+ }
243+ } }
244+ disabled = { pendingUserIds . has ( u . id ) }
245+ >
246+ { banUserId === u . id ? 'Cancel' : 'Ban' }
247+ </ Button >
248+ ) }
249+ </ >
250+ ) }
251+ </ span >
252+ </ div >
253+ { banUserId === u . id && ! u . banned && (
254+ < div className = 'flex items-center gap-2 pl-[170px]' >
255+ < ChipInput
256+ value = { banReason }
257+ onChange = { ( e ) => setBanReason ( e . target . value ) }
258+ placeholder = 'Reason (optional)'
259+ className = 'flex-1'
260+ />
261+ < Button
262+ variant = 'primary'
263+ className = 'h-[28px] px-3 text-caption'
264+ onClick = { ( ) => {
265+ banUser . reset ( )
266+ banUser . mutate (
267+ {
268+ userId : u . id ,
269+ ...( banReason . trim ( ) ? { banReason : banReason . trim ( ) } : { } ) ,
270+ } ,
271+ {
272+ onSuccess : ( ) => {
273+ setBanUserId ( null )
274+ setBanReason ( '' )
275+ } ,
276+ }
277+ )
278+ } }
279+ disabled = { pendingUserIds . has ( u . id ) }
280+ >
281+ Confirm Ban
282+ </ Button >
283+ </ div >
284+ ) }
285+ </ div >
286+ )
287+
159288 return (
160289 < SettingsPanel >
161290 < div className = 'flex flex-col gap-4' >
@@ -275,149 +404,16 @@ export function Admin() {
275404 </ p >
276405 ) }
277406
278- { searchQuery . length > 0 && usersData && (
407+ { searchQuery . length > 0 && usersData ? (
279408 < >
280409 < div className = 'flex flex-col gap-0.5' >
281- < div className = 'flex items-center gap-3 border-[var(--border-secondary)] border-b px-3 py-2 text-[var(--text-tertiary)] text-caption' >
282- < span className = 'w-[200px]' > Name</ span >
283- < span className = 'flex-1' > Email</ span >
284- < span className = 'w-[80px]' > Role</ span >
285- < span className = 'w-[80px]' > Status</ span >
286- < span className = 'w-[250px] text-right' > Actions</ span >
287- </ div >
410+ { USER_TABLE_HEADER }
288411
289412 { usersData . users . length === 0 && (
290413 < SettingsEmptyState variant = 'inline' > No users found.</ SettingsEmptyState >
291414 ) }
292415
293- { usersData . users . map ( ( u ) => (
294- < div
295- key = { u . id }
296- className = { cn (
297- 'flex flex-col gap-2 px-3 py-2 text-small' ,
298- 'border-[var(--border-secondary)] border-b last:border-b-0'
299- ) }
300- >
301- < div className = 'flex items-center gap-3' >
302- < span className = 'w-[200px] truncate text-[var(--text-primary)]' >
303- { u . name || '—' }
304- </ span >
305- < span className = 'flex-1 truncate text-[var(--text-secondary)]' > { u . email } </ span >
306- < span className = 'w-[80px]' >
307- < Badge variant = { u . role === 'admin' ? 'blue' : 'gray' } >
308- { u . role || 'user' }
309- </ Badge >
310- </ span >
311- < span className = 'w-[80px]' >
312- { u . banned ? (
313- < Badge variant = 'red' > Banned</ Badge >
314- ) : (
315- < Badge variant = 'green' > Active</ Badge >
316- ) }
317- </ span >
318- < span className = 'flex w-[250px] justify-end gap-1' >
319- { u . id !== session ?. user ?. id && (
320- < >
321- < Button
322- variant = 'active'
323- className = 'h-[28px] px-2 text-caption'
324- onClick = { ( ) => handleImpersonate ( u . id ) }
325- disabled = { pendingUserIds . has ( u . id ) }
326- >
327- { impersonatingUserId === u . id ||
328- ( impersonateUser . isPending &&
329- ( impersonateUser . variables as { userId ?: string } | undefined )
330- ?. userId === u . id )
331- ? 'Switching...'
332- : 'Impersonate' }
333- </ Button >
334- < Button
335- variant = 'active'
336- className = 'h-[28px] px-2 text-caption'
337- onClick = { ( ) => {
338- setUserRole . reset ( )
339- setUserRole . mutate ( {
340- userId : u . id ,
341- role : u . role === 'admin' ? 'user' : 'admin' ,
342- } )
343- } }
344- disabled = { pendingUserIds . has ( u . id ) }
345- >
346- { u . role === 'admin' ? 'Demote' : 'Promote' }
347- </ Button >
348- { u . banned ? (
349- < Button
350- variant = 'active'
351- className = 'h-[28px] px-2 text-caption'
352- onClick = { ( ) => {
353- unbanUser . reset ( )
354- unbanUser . mutate ( { userId : u . id } )
355- } }
356- disabled = { pendingUserIds . has ( u . id ) }
357- >
358- Unban
359- </ Button >
360- ) : (
361- < Button
362- variant = 'active'
363- className = { cn (
364- 'h-[28px] px-2 text-caption' ,
365- banUserId === u . id
366- ? 'text-[var(--text-primary)]'
367- : 'text-[var(--text-error)]'
368- ) }
369- onClick = { ( ) => {
370- if ( banUserId === u . id ) {
371- setBanUserId ( null )
372- setBanReason ( '' )
373- } else {
374- setBanUserId ( u . id )
375- setBanReason ( '' )
376- }
377- } }
378- disabled = { pendingUserIds . has ( u . id ) }
379- >
380- { banUserId === u . id ? 'Cancel' : 'Ban' }
381- </ Button >
382- ) }
383- </ >
384- ) }
385- </ span >
386- </ div >
387- { banUserId === u . id && ! u . banned && (
388- < div className = 'flex items-center gap-2 pl-[200px]' >
389- < ChipInput
390- value = { banReason }
391- onChange = { ( e ) => setBanReason ( e . target . value ) }
392- placeholder = 'Reason (optional)'
393- className = 'flex-1'
394- />
395- < Button
396- variant = 'primary'
397- className = 'h-[28px] px-3 text-caption'
398- onClick = { ( ) => {
399- banUser . reset ( )
400- banUser . mutate (
401- {
402- userId : u . id ,
403- ...( banReason . trim ( ) ? { banReason : banReason . trim ( ) } : { } ) ,
404- } ,
405- {
406- onSuccess : ( ) => {
407- setBanUserId ( null )
408- setBanReason ( '' )
409- } ,
410- }
411- )
412- } }
413- disabled = { pendingUserIds . has ( u . id ) }
414- >
415- Confirm Ban
416- </ Button >
417- </ div >
418- ) }
419- </ div >
420- ) ) }
416+ { usersData . users . map ( ( u ) => renderUserRow ( u ) ) }
421417 </ div >
422418
423419 { totalPages > 1 && (
@@ -450,6 +446,15 @@ export function Admin() {
450446 </ div >
451447 ) }
452448 </ >
449+ ) : (
450+ searchQuery . length === 0 &&
451+ recentUsers &&
452+ recentUsers . length > 0 && (
453+ < div className = 'flex flex-col gap-0.5' >
454+ { USER_TABLE_HEADER }
455+ { recentUsers . map ( ( u ) => renderUserRow ( u ) ) }
456+ </ div >
457+ )
453458 ) }
454459 </ div >
455460 </ SettingsPanel >
0 commit comments