mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
67478bbc80
commit
2bc11a70ba
@@ -31,7 +31,8 @@ export function Admin() {
|
||||
|
||||
const [workflowId, setWorkflowId] = useState('')
|
||||
const [usersOffset, setUsersOffset] = useState(0)
|
||||
const [usersEnabled, setUsersEnabled] = useState(false)
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [banUserId, setBanUserId] = useState<string | null>(null)
|
||||
const [banReason, setBanReason] = useState('')
|
||||
|
||||
@@ -39,8 +40,12 @@ export function Admin() {
|
||||
data: usersData,
|
||||
isLoading: usersLoading,
|
||||
error: usersError,
|
||||
refetch: refetchUsers,
|
||||
} = useAdminUsers(usersOffset, PAGE_SIZE, usersEnabled)
|
||||
} = useAdminUsers(usersOffset, PAGE_SIZE, searchQuery)
|
||||
|
||||
const handleSearch = () => {
|
||||
setUsersOffset(0)
|
||||
setSearchQuery(searchInput.trim())
|
||||
}
|
||||
|
||||
const totalPages = useMemo(
|
||||
() => Math.ceil((usersData?.total ?? 0) / PAGE_SIZE),
|
||||
@@ -62,14 +67,6 @@ export function Admin() {
|
||||
)
|
||||
}
|
||||
|
||||
const handleLoadUsers = () => {
|
||||
if (usersEnabled) {
|
||||
refetchUsers()
|
||||
} else {
|
||||
setUsersEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
const pendingUserIds = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId)
|
||||
@@ -136,10 +133,16 @@ export function Admin() {
|
||||
<div className='h-px bg-[var(--border-secondary)]' />
|
||||
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<p className='font-medium text-[14px] text-[var(--text-primary)]'>User Management</p>
|
||||
<Button variant='active' onClick={handleLoadUsers} disabled={usersLoading}>
|
||||
{usersLoading ? 'Loading...' : usersEnabled ? 'Refresh' : 'Load Users'}
|
||||
<p className='font-medium text-[14px] text-[var(--text-primary)]'>User Management</p>
|
||||
<div className='flex gap-[8px]'>
|
||||
<EmcnInput
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder='Search by email or paste a user ID...'
|
||||
/>
|
||||
<Button variant='primary' onClick={handleSearch} disabled={usersLoading}>
|
||||
{usersLoading ? 'Searching...' : 'Search'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -164,9 +167,9 @@ export function Admin() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usersData && (
|
||||
{searchQuery.length > 0 && usersData && (
|
||||
<>
|
||||
<div className='flex flex-col gap-[2px] rounded-[8px] border border-[var(--border-secondary)]'>
|
||||
<div className='flex flex-col gap-[2px]'>
|
||||
<div className='flex items-center gap-[12px] border-[var(--border-secondary)] border-b px-[12px] py-[8px] text-[12px] text-[var(--text-tertiary)]'>
|
||||
<span className='w-[200px]'>Name</span>
|
||||
<span className='flex-1'>Email</span>
|
||||
@@ -176,7 +179,7 @@ export function Admin() {
|
||||
</div>
|
||||
|
||||
{usersData.users.length === 0 && (
|
||||
<div className='px-[12px] py-[16px] text-center text-[13px] text-[var(--text-tertiary)]'>
|
||||
<div className='py-[16px] text-center text-[13px] text-[var(--text-tertiary)]'>
|
||||
No users found.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,8 @@ const logger = createLogger('AdminUsersQuery')
|
||||
export const adminUserKeys = {
|
||||
all: ['adminUsers'] as const,
|
||||
lists: () => [...adminUserKeys.all, 'list'] as const,
|
||||
list: (offset: number, limit: number) => [...adminUserKeys.lists(), offset, limit] as const,
|
||||
list: (offset: number, limit: number, searchQuery: string) =>
|
||||
[...adminUserKeys.lists(), offset, limit, searchQuery] as const,
|
||||
}
|
||||
|
||||
interface AdminUser {
|
||||
@@ -24,31 +25,59 @@ interface AdminUsersResponse {
|
||||
total: number
|
||||
}
|
||||
|
||||
async function fetchAdminUsers(offset: number, limit: number): Promise<AdminUsersResponse> {
|
||||
const { data, error } = await client.admin.listUsers({
|
||||
query: { limit, offset },
|
||||
})
|
||||
if (error) {
|
||||
throw new Error(error.message ?? 'Failed to fetch users')
|
||||
}
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
function mapUser(u: {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
role?: string | null
|
||||
banned?: boolean | null
|
||||
banReason?: string | null
|
||||
}): AdminUser {
|
||||
return {
|
||||
users: (data?.users ?? []).map((u) => ({
|
||||
id: u.id,
|
||||
name: u.name || '',
|
||||
email: u.email,
|
||||
role: u.role ?? 'user',
|
||||
banned: u.banned ?? false,
|
||||
banReason: u.banReason ?? null,
|
||||
})),
|
||||
id: u.id,
|
||||
name: u.name || '',
|
||||
email: u.email,
|
||||
role: u.role ?? 'user',
|
||||
banned: u.banned ?? false,
|
||||
banReason: u.banReason ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAdminUsers(
|
||||
offset: number,
|
||||
limit: number,
|
||||
searchQuery: string
|
||||
): Promise<AdminUsersResponse> {
|
||||
if (UUID_REGEX.test(searchQuery.trim())) {
|
||||
const { data, error } = await client.admin.getUser({ query: { id: searchQuery.trim() } })
|
||||
if (error) throw new Error(error.message ?? 'Failed to fetch user')
|
||||
if (!data) return { users: [], total: 0 }
|
||||
return { users: [mapUser(data)], total: 1 }
|
||||
}
|
||||
|
||||
const { data, error } = await client.admin.listUsers({
|
||||
query: {
|
||||
limit,
|
||||
offset,
|
||||
searchField: 'email',
|
||||
searchValue: searchQuery,
|
||||
searchOperator: 'contains',
|
||||
},
|
||||
})
|
||||
if (error) throw new Error(error.message ?? 'Failed to fetch users')
|
||||
return {
|
||||
users: (data?.users ?? []).map(mapUser),
|
||||
total: data?.total ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function useAdminUsers(offset: number, limit: number, enabled: boolean) {
|
||||
export function useAdminUsers(offset: number, limit: number, searchQuery: string) {
|
||||
return useQuery({
|
||||
queryKey: adminUserKeys.list(offset, limit),
|
||||
queryFn: () => fetchAdminUsers(offset, limit),
|
||||
enabled,
|
||||
queryKey: adminUserKeys.list(offset, limit, searchQuery),
|
||||
queryFn: () => fetchAdminUsers(offset, limit, searchQuery),
|
||||
enabled: searchQuery.length > 0,
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user