22
33import { useState } from 'react'
44import { Button } from '@sim/emcn/button'
5- import { EmptyState } from '@sim/emcn/empty-state'
65import { TextField } from '@sim/emcn/field'
76import { ActionMenu } from '@sim/emcn/menu/action-menu'
87import { Modal } from '@sim/emcn/modal'
98import { Pagination } from '@sim/emcn/pagination'
109import { SearchField } from '@sim/emcn/search-field'
11- import { SettingsRow , SettingsSection } from '@sim/emcn/settings-section'
12- import { InlineNotice , StatusTag } from '@sim/emcn/status'
10+ import { SettingsRow } from '@sim/emcn/settings-section'
11+ import { FailureNotice , InlineNotice , StatusTag } from '@sim/emcn/status'
12+ import { SettingsList } from '@/app/(product)/_components/settings-parts/settings-list'
1313import {
1414 ActionRow ,
15- FieldStack ,
16- Muted ,
15+ SectionStack ,
1716 SectionToolbar ,
1817} from '@/app/(product)/_components/settings-parts/settings-parts'
1918import { AddUserDialog } from '@/app/(product)/o/[organizationId]/settings/_components/sections/admin/add-user-dialog'
@@ -47,107 +46,112 @@ export function AdminDirectory({ directory, onImpersonate }: AdminDirectoryProps
4746 return directory . setRole ( pendingUser , pending . action === 'Promote' ? 'admin' : 'user' )
4847 }
4948 const action = pending ?. action
49+ const userRows = rows . map ( ( user ) => {
50+ const busy = directory . busy ( user . id )
51+ return (
52+ < SettingsRow key = { user . id } label = { user . name || '—' } description = { user . email } >
53+ < ActionRow >
54+ < StatusTag
55+ label = { user . role === 'admin' ? 'Admin' : 'User' }
56+ intent = { user . role === 'admin' ? 'info' : 'neutral' }
57+ />
58+ { user . banned && < StatusTag label = 'Banned' intent = 'danger' /> }
59+ { user . id !== directory . operatorId && (
60+ < >
61+ < Button
62+ size = 'compact'
63+ disabled = { directory . impersonating || busy }
64+ onClick = { ( ) => onImpersonate ( user ) }
65+ >
66+ Impersonate
67+ </ Button >
68+ < ActionMenu
69+ label = { `${ user . email } actions` }
70+ icon = 'I108'
71+ align = 'end'
72+ actions = { [
73+ {
74+ id : 'reset' ,
75+ label : 'Reset password' ,
76+ disabled : busy ,
77+ onSelect : ( ) => directory . resetPassword ( user ) ,
78+ } ,
79+ {
80+ id : 'role' ,
81+ label : user . role === 'admin' ? 'Demote' : 'Promote' ,
82+ destructive : user . role === 'admin' ,
83+ disabled : busy ,
84+ onSelect : ( ) =>
85+ setPending ( {
86+ id : user . id ,
87+ action : user . role === 'admin' ? 'Demote' : 'Promote' ,
88+ } ) ,
89+ } ,
90+ user . banned
91+ ? {
92+ id : 'unban' ,
93+ label : 'Unban' ,
94+ disabled : busy ,
95+ onSelect : ( ) => directory . unban ( user ) ,
96+ }
97+ : {
98+ id : 'ban' ,
99+ label : 'Ban' ,
100+ separatorBefore : true ,
101+ destructive : true ,
102+ disabled : busy ,
103+ onSelect : ( ) => {
104+ setReason ( '' )
105+ setPending ( { id : user . id , action : 'Ban' } )
106+ } ,
107+ } ,
108+ ] }
109+ />
110+ </ >
111+ ) }
112+ </ ActionRow >
113+ </ SettingsRow >
114+ )
115+ } )
50116 return (
51- < SettingsSection title = 'User management' >
52- < FieldStack >
53- < DirectorySearch
54- key = { q }
55- initial = { q }
56- searching = { directory . searching }
57- onSearch = { ( term ) => void setSearch ( { q : term || null , offset : null } ) }
58- onAdd = { ( ) => setAdding ( true ) }
117+ < SectionStack >
118+ < DirectorySearch
119+ key = { q }
120+ initial = { q }
121+ searching = { directory . searching }
122+ onSearch = { ( term ) => void setSearch ( { q : term || null , offset : null } ) }
123+ onAdd = { ( ) => setAdding ( true ) }
124+ />
125+ { directory . warning && < InlineNotice intent = 'warning' message = { directory . warning } /> }
126+ { q ? (
127+ directory . error ? (
128+ < FailureNotice
129+ message = { directory . error }
130+ retrying = { directory . searching }
131+ onRetry = { directory . retry }
132+ />
133+ ) : (
134+ results && (
135+ < SettingsList
136+ title = 'Users'
137+ empty = { rows . length === 0 ? { title : 'No matching users' } : undefined }
138+ >
139+ { userRows }
140+ </ SettingsList >
141+ )
142+ )
143+ ) : (
144+ rows . length > 0 && < SettingsList title = 'Recently impersonated' > { userRows } </ SettingsList >
145+ ) }
146+ { q && pages > 1 && (
147+ < Pagination
148+ label = 'User search pages'
149+ page = { page }
150+ pageCount = { pages }
151+ disabled = { directory . searching }
152+ onPageChange = { ( next ) => void setSearch ( { offset : ( next - 1 ) * ADMIN_PAGE_SIZE } ) }
59153 />
60- { directory . error && < InlineNotice intent = 'danger' message = { directory . error } /> }
61- { directory . warning && < InlineNotice intent = 'warning' message = { directory . warning } /> }
62- { q && results && rows . length === 0 && < EmptyState title = 'No matching users' /> }
63- { rows . length > 0 && (
64- < div >
65- { ! q && < Muted > Recently impersonated</ Muted > }
66- { rows . map ( ( user ) => {
67- const busy = directory . busy ( user . id )
68- return (
69- < SettingsRow key = { user . id } field label = { user . name || '—' } description = { user . email } >
70- < ActionRow >
71- < StatusTag
72- label = { user . role }
73- intent = { user . role === 'admin' ? 'info' : 'neutral' }
74- />
75- < StatusTag
76- label = { user . banned ? 'Banned' : 'Active' }
77- intent = { user . banned ? 'danger' : 'success' }
78- />
79- { user . id !== directory . operatorId && (
80- < >
81- < Button
82- size = 'compact'
83- disabled = { directory . impersonating || busy }
84- onClick = { ( ) => onImpersonate ( user ) }
85- >
86- Impersonate
87- </ Button >
88- < ActionMenu
89- label = { `${ user . email } actions` }
90- icon = 'I108'
91- align = 'end'
92- actions = { [
93- {
94- id : 'reset' ,
95- label : 'Reset password' ,
96- disabled : busy ,
97- onSelect : ( ) => directory . resetPassword ( user ) ,
98- } ,
99- {
100- id : 'role' ,
101- label : user . role === 'admin' ? 'Demote' : 'Promote' ,
102- destructive : user . role === 'admin' ,
103- disabled : busy ,
104- onSelect : ( ) =>
105- setPending ( {
106- id : user . id ,
107- action : user . role === 'admin' ? 'Demote' : 'Promote' ,
108- } ) ,
109- } ,
110- user . banned
111- ? {
112- id : 'unban' ,
113- label : 'Unban' ,
114- disabled : busy ,
115- onSelect : ( ) => directory . unban ( user ) ,
116- }
117- : {
118- id : 'ban' ,
119- label : 'Ban' ,
120- separatorBefore : true ,
121- destructive : true ,
122- disabled : busy ,
123- onSelect : ( ) => {
124- setReason ( '' )
125- setPending ( { id : user . id , action : 'Ban' } )
126- } ,
127- } ,
128- ] }
129- />
130- </ >
131- ) }
132- </ ActionRow >
133- </ SettingsRow >
134- )
135- } ) }
136- </ div >
137- ) }
138- { q && pages > 1 && (
139- < >
140- < Muted > { `Page ${ page } of ${ pages } (${ results ?. total ?? 0 } users)` } </ Muted >
141- < Pagination
142- label = 'User search pages'
143- page = { page }
144- pageCount = { pages }
145- disabled = { directory . searching }
146- onPageChange = { ( next ) => void setSearch ( { offset : ( next - 1 ) * ADMIN_PAGE_SIZE } ) }
147- />
148- </ >
149- ) }
150- </ FieldStack >
154+ ) }
151155 { adding && (
152156 < AddUserDialog
153157 onClose = { ( ) => setAdding ( false ) }
@@ -164,13 +168,13 @@ export function AdminDirectory({ directory, onImpersonate }: AdminDirectoryProps
164168 onOpenChange = { ( open ) => {
165169 if ( ! open ) setPending ( undefined )
166170 } }
167- title = { `${ action } user ` }
171+ title = { `${ action } ${ pendingUser . name || pendingUser . email } ` }
168172 description = {
169173 action === 'Ban'
170- ? `Banning ${ pendingUser . email } signs them out everywhere and blocks them from signing back in. You can unban them later. `
174+ ? `Banning ${ pendingUser . email } signs them out everywhere and blocks them from signing back in. You can unban them later`
171175 : action === 'Demote'
172176 ? `Demoting ${ pendingUser . email } removes their platform admin access.`
173- : `Promoting ${ pendingUser . email } grants full platform admin access, including impersonating any user. `
177+ : `Promoting ${ pendingUser . email } grants full platform admin access, including impersonating any user`
174178 }
175179 confirmLabel = { pending . action }
176180 destructive = { action !== 'Promote' }
@@ -187,7 +191,7 @@ export function AdminDirectory({ directory, onImpersonate }: AdminDirectoryProps
187191 ) }
188192 </ Modal >
189193 ) }
190- </ SettingsSection >
194+ </ SectionStack >
191195 )
192196}
193197
0 commit comments