Skip to content

Commit 2bc11a7

Browse files
waleedlatif1claude
andauthored
waleedlatif1/hangzhou v2 (#3647)
* feat(admin): add user search by email and ID, remove table border - Replace Load Users button with a live search input; query fires on any input - Email search uses listUsers with contains operator - User ID search (UUID format) uses admin.getUser directly for exact lookup - Remove outer border on user table that rendered white in dark mode - Reset pagination to page 0 on new search Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(admin): replace live search with explicit search button - Split searchInput (controlled input) from searchQuery (committed value) so the hook only fires on Search click or Enter, not every keystroke - Gate table render on searchQuery.length > 0 to prevent stale results showing after input is cleared Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 67478bb commit 2bc11a7

File tree

2 files changed

+68
-36
lines changed

2 files changed

+68
-36
lines changed

apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,21 @@ export function Admin() {
3131

3232
const [workflowId, setWorkflowId] = useState('')
3333
const [usersOffset, setUsersOffset] = useState(0)
34-
const [usersEnabled, setUsersEnabled] = useState(false)
34+
const [searchInput, setSearchInput] = useState('')
35+
const [searchQuery, setSearchQuery] = useState('')
3536
const [banUserId, setBanUserId] = useState<string | null>(null)
3637
const [banReason, setBanReason] = useState('')
3738

3839
const {
3940
data: usersData,
4041
isLoading: usersLoading,
4142
error: usersError,
42-
refetch: refetchUsers,
43-
} = useAdminUsers(usersOffset, PAGE_SIZE, usersEnabled)
43+
} = useAdminUsers(usersOffset, PAGE_SIZE, searchQuery)
44+
45+
const handleSearch = () => {
46+
setUsersOffset(0)
47+
setSearchQuery(searchInput.trim())
48+
}
4449

4550
const totalPages = useMemo(
4651
() => Math.ceil((usersData?.total ?? 0) / PAGE_SIZE),
@@ -62,14 +67,6 @@ export function Admin() {
6267
)
6368
}
6469

65-
const handleLoadUsers = () => {
66-
if (usersEnabled) {
67-
refetchUsers()
68-
} else {
69-
setUsersEnabled(true)
70-
}
71-
}
72-
7370
const pendingUserIds = useMemo(() => {
7471
const ids = new Set<string>()
7572
if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId)
@@ -136,10 +133,16 @@ export function Admin() {
136133
<div className='h-px bg-[var(--border-secondary)]' />
137134

138135
<div className='flex flex-col gap-[12px]'>
139-
<div className='flex items-center justify-between'>
140-
<p className='font-medium text-[14px] text-[var(--text-primary)]'>User Management</p>
141-
<Button variant='active' onClick={handleLoadUsers} disabled={usersLoading}>
142-
{usersLoading ? 'Loading...' : usersEnabled ? 'Refresh' : 'Load Users'}
136+
<p className='font-medium text-[14px] text-[var(--text-primary)]'>User Management</p>
137+
<div className='flex gap-[8px]'>
138+
<EmcnInput
139+
value={searchInput}
140+
onChange={(e) => setSearchInput(e.target.value)}
141+
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
142+
placeholder='Search by email or paste a user ID...'
143+
/>
144+
<Button variant='primary' onClick={handleSearch} disabled={usersLoading}>
145+
{usersLoading ? 'Searching...' : 'Search'}
143146
</Button>
144147
</div>
145148

@@ -164,9 +167,9 @@ export function Admin() {
164167
</div>
165168
)}
166169

167-
{usersData && (
170+
{searchQuery.length > 0 && usersData && (
168171
<>
169-
<div className='flex flex-col gap-[2px] rounded-[8px] border border-[var(--border-secondary)]'>
172+
<div className='flex flex-col gap-[2px]'>
170173
<div className='flex items-center gap-[12px] border-[var(--border-secondary)] border-b px-[12px] py-[8px] text-[12px] text-[var(--text-tertiary)]'>
171174
<span className='w-[200px]'>Name</span>
172175
<span className='flex-1'>Email</span>
@@ -176,7 +179,7 @@ export function Admin() {
176179
</div>
177180

178181
{usersData.users.length === 0 && (
179-
<div className='px-[12px] py-[16px] text-center text-[13px] text-[var(--text-tertiary)]'>
182+
<div className='py-[16px] text-center text-[13px] text-[var(--text-tertiary)]'>
180183
No users found.
181184
</div>
182185
)}

apps/sim/hooks/queries/admin-users.ts

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const logger = createLogger('AdminUsersQuery')
77
export const adminUserKeys = {
88
all: ['adminUsers'] as const,
99
lists: () => [...adminUserKeys.all, 'list'] as const,
10-
list: (offset: number, limit: number) => [...adminUserKeys.lists(), offset, limit] as const,
10+
list: (offset: number, limit: number, searchQuery: string) =>
11+
[...adminUserKeys.lists(), offset, limit, searchQuery] as const,
1112
}
1213

1314
interface AdminUser {
@@ -24,31 +25,59 @@ interface AdminUsersResponse {
2425
total: number
2526
}
2627

27-
async function fetchAdminUsers(offset: number, limit: number): Promise<AdminUsersResponse> {
28+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
29+
30+
function mapUser(u: {
31+
id: string
32+
name: string
33+
email: string
34+
role?: string | null
35+
banned?: boolean | null
36+
banReason?: string | null
37+
}): AdminUser {
38+
return {
39+
id: u.id,
40+
name: u.name || '',
41+
email: u.email,
42+
role: u.role ?? 'user',
43+
banned: u.banned ?? false,
44+
banReason: u.banReason ?? null,
45+
}
46+
}
47+
48+
async function fetchAdminUsers(
49+
offset: number,
50+
limit: number,
51+
searchQuery: string
52+
): Promise<AdminUsersResponse> {
53+
if (UUID_REGEX.test(searchQuery.trim())) {
54+
const { data, error } = await client.admin.getUser({ query: { id: searchQuery.trim() } })
55+
if (error) throw new Error(error.message ?? 'Failed to fetch user')
56+
if (!data) return { users: [], total: 0 }
57+
return { users: [mapUser(data)], total: 1 }
58+
}
59+
2860
const { data, error } = await client.admin.listUsers({
29-
query: { limit, offset },
61+
query: {
62+
limit,
63+
offset,
64+
searchField: 'email',
65+
searchValue: searchQuery,
66+
searchOperator: 'contains',
67+
},
3068
})
31-
if (error) {
32-
throw new Error(error.message ?? 'Failed to fetch users')
33-
}
69+
if (error) throw new Error(error.message ?? 'Failed to fetch users')
3470
return {
35-
users: (data?.users ?? []).map((u) => ({
36-
id: u.id,
37-
name: u.name || '',
38-
email: u.email,
39-
role: u.role ?? 'user',
40-
banned: u.banned ?? false,
41-
banReason: u.banReason ?? null,
42-
})),
71+
users: (data?.users ?? []).map(mapUser),
4372
total: data?.total ?? 0,
4473
}
4574
}
4675

47-
export function useAdminUsers(offset: number, limit: number, enabled: boolean) {
76+
export function useAdminUsers(offset: number, limit: number, searchQuery: string) {
4877
return useQuery({
49-
queryKey: adminUserKeys.list(offset, limit),
50-
queryFn: () => fetchAdminUsers(offset, limit),
51-
enabled,
78+
queryKey: adminUserKeys.list(offset, limit, searchQuery),
79+
queryFn: () => fetchAdminUsers(offset, limit, searchQuery),
80+
enabled: searchQuery.length > 0,
5281
staleTime: 30 * 1000,
5382
placeholderData: keepPreviousData,
5483
})

0 commit comments

Comments
 (0)