11'use client'
22
33import { useEffect , useMemo , useRef , useState } from 'react'
4- import { Badge , Button , ChipInput , ChipSelect , cn , Label , Search , Switch } from '@sim/emcn'
4+ import { Badge , Button , Chip , ChipInput , ChipSelect , cn , Label , Search , Switch } from '@sim/emcn'
55import { getErrorMessage } from '@sim/utils/errors'
66import { useQueryStates } from 'nuqs'
77import type { MothershipEnvironment } from '@/lib/api/contracts'
@@ -14,6 +14,7 @@ import {
1414import { useRecentImpersonations } from '@/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations'
1515import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
1616import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
17+ import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
1718import {
1819 type AdminUser ,
1920 useAdminUsers ,
@@ -371,99 +372,100 @@ export function Admin() {
371372
372373 < div className = 'h-px bg-[var(--border)]' />
373374
374- < div className = 'flex flex-col gap-3' >
375- < div className = 'flex items-center justify-between gap-3' >
376- < p className = 'font-medium text-[var(--text-muted)] text-small' > User Management</ p >
377- < Button variant = 'primary' onClick = { ( ) => setIsAddUserOpen ( true ) } >
378- Add user
379- </ Button >
380- </ div >
381- < div className = 'flex gap-2' >
382- < ChipInput
383- icon = { Search }
384- value = { searchInput }
385- onChange = { ( e ) => setSearchInput ( e . target . value ) }
386- onKeyDown = { ( e ) => e . key === 'Enter' && handleSearch ( ) }
387- placeholder = 'Search by email or paste a user ID...'
388- className = 'min-w-0 flex-1'
389- />
390- < Button variant = 'primary' onClick = { handleSearch } disabled = { usersLoading } >
391- { usersLoading ? 'Searching...' : 'Search' }
392- </ Button >
393- </ div >
394-
395- { usersError && (
396- < p className = 'text-[var(--text-error)] text-small' >
397- { getErrorMessage ( usersError , 'Failed to fetch users' ) }
398- </ p >
399- ) }
375+ < SettingsSection
376+ label = 'User management'
377+ action = { < Chip onClick = { ( ) => setIsAddUserOpen ( true ) } > Add user</ Chip > }
378+ >
379+ < div className = 'flex flex-col gap-3' >
380+ < div className = 'flex gap-2' >
381+ < ChipInput
382+ icon = { Search }
383+ value = { searchInput }
384+ onChange = { ( e ) => setSearchInput ( e . target . value ) }
385+ onKeyDown = { ( e ) => e . key === 'Enter' && handleSearch ( ) }
386+ placeholder = 'Search by email or paste a user ID...'
387+ className = 'min-w-0 flex-1'
388+ />
389+ < Button variant = 'primary' onClick = { handleSearch } disabled = { usersLoading } >
390+ { usersLoading ? 'Searching...' : 'Search' }
391+ </ Button >
392+ </ div >
393+
394+ { usersError && (
395+ < p className = 'text-[var(--text-error)] text-small' >
396+ { getErrorMessage ( usersError , 'Failed to fetch users' ) }
397+ </ p >
398+ ) }
400399
401- { ( setUserRole . error ||
402- banUser . error ||
403- unbanUser . error ||
404- impersonateUser . error ||
405- impersonationGuardError ) && (
406- < p className = 'text-[var(--text-error)] text-small' >
407- { impersonationGuardError ||
408- ( setUserRole . error || banUser . error || unbanUser . error || impersonateUser . error )
409- ?. message ||
410- 'Action failed. Please try again.' }
411- </ p >
412- ) }
400+ { ( setUserRole . error ||
401+ banUser . error ||
402+ unbanUser . error ||
403+ impersonateUser . error ||
404+ impersonationGuardError ) && (
405+ < p className = 'text-[var(--text-error)] text-small' >
406+ { impersonationGuardError ||
407+ ( setUserRole . error || banUser . error || unbanUser . error || impersonateUser . error )
408+ ?. message ||
409+ 'Action failed. Please try again.' }
410+ </ p >
411+ ) }
413412
414- { searchQuery . length > 0 && usersData ? (
415- < >
416- < div className = 'flex flex-col gap-0.5' >
417- { USER_TABLE_HEADER }
413+ { searchQuery . length > 0 && usersData ? (
414+ < >
415+ < div className = 'flex flex-col gap-0.5' >
416+ { USER_TABLE_HEADER }
418417
419- { usersData . users . length === 0 && (
420- < SettingsEmptyState variant = 'inline' > No users found.</ SettingsEmptyState >
421- ) }
418+ { usersData . users . length === 0 && (
419+ < SettingsEmptyState variant = 'inline' > No users found.</ SettingsEmptyState >
420+ ) }
422421
423- { usersData . users . map ( ( u ) => renderUserRow ( u ) ) }
424- </ div >
422+ { usersData . users . map ( ( u ) => renderUserRow ( u ) ) }
423+ </ div >
425424
426- { totalPages > 1 && (
427- < div className = 'flex items-center justify-between text-[var(--text-secondary)] text-small' >
428- < span >
429- Page { currentPage } of { totalPages } ({ usersData . total } users)
430- </ span >
431- < div className = 'flex gap-1' >
432- < Button
433- variant = 'active'
434- className = 'h-[28px] px-2 text-caption'
435- onClick = { ( ) =>
436- setAdminParams ( ( prev ) => ( {
437- offset : Math . max ( 0 , prev . offset - PAGE_SIZE ) ,
438- } ) )
439- }
440- disabled = { usersOffset === 0 || usersLoading }
441- >
442- Previous
443- </ Button >
444- < Button
445- variant = 'active'
446- className = 'h-[28px] px-2 text-caption'
447- onClick = { ( ) => setAdminParams ( ( prev ) => ( { offset : prev . offset + PAGE_SIZE } ) ) }
448- disabled = { usersOffset + PAGE_SIZE >= ( usersData ?. total ?? 0 ) || usersLoading }
449- >
450- Next
451- </ Button >
425+ { totalPages > 1 && (
426+ < div className = 'flex items-center justify-between text-[var(--text-secondary)] text-small' >
427+ < span >
428+ Page { currentPage } of { totalPages } ({ usersData . total } users)
429+ </ span >
430+ < div className = 'flex gap-1' >
431+ < Button
432+ variant = 'active'
433+ className = 'h-[28px] px-2 text-caption'
434+ onClick = { ( ) =>
435+ setAdminParams ( ( prev ) => ( {
436+ offset : Math . max ( 0 , prev . offset - PAGE_SIZE ) ,
437+ } ) )
438+ }
439+ disabled = { usersOffset === 0 || usersLoading }
440+ >
441+ Previous
442+ </ Button >
443+ < Button
444+ variant = 'active'
445+ className = 'h-[28px] px-2 text-caption'
446+ onClick = { ( ) =>
447+ setAdminParams ( ( prev ) => ( { offset : prev . offset + PAGE_SIZE } ) )
448+ }
449+ disabled = { usersOffset + PAGE_SIZE >= ( usersData ?. total ?? 0 ) || usersLoading }
450+ >
451+ Next
452+ </ Button >
453+ </ div >
452454 </ div >
455+ ) }
456+ </ >
457+ ) : (
458+ searchQuery . length === 0 &&
459+ recentUsers &&
460+ recentUsers . length > 0 && (
461+ < div className = 'flex flex-col gap-0.5' >
462+ { USER_TABLE_HEADER }
463+ { recentUsers . map ( ( u ) => renderUserRow ( u ) ) }
453464 </ div >
454- ) }
455- </ >
456- ) : (
457- searchQuery . length === 0 &&
458- recentUsers &&
459- recentUsers . length > 0 && (
460- < div className = 'flex flex-col gap-0.5' >
461- { USER_TABLE_HEADER }
462- { recentUsers . map ( ( u ) => renderUserRow ( u ) ) }
463- </ div >
464- )
465- ) }
466- </ div >
465+ )
466+ ) }
467+ </ div >
468+ </ SettingsSection >
467469 < AddUserModal
468470 open = { isAddUserOpen }
469471 onOpenChange = { setIsAddUserOpen }
0 commit comments