From 83fc33a1cfdf9d2626e846eeed41cd68ee0c0d43 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 17 Mar 2026 17:38:30 -0700 Subject: [PATCH 1/2] 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 --- .../settings/components/admin/admin.tsx | 32 ++++----- apps/sim/hooks/queries/admin-users.ts | 65 ++++++++++++++----- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 2e856c39f9..2d02f7b1e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -31,7 +31,7 @@ export function Admin() { const [workflowId, setWorkflowId] = useState('') const [usersOffset, setUsersOffset] = useState(0) - const [usersEnabled, setUsersEnabled] = useState(false) + const [searchQuery, setSearchQuery] = useState('') const [banUserId, setBanUserId] = useState(null) const [banReason, setBanReason] = useState('') @@ -39,8 +39,7 @@ export function Admin() { data: usersData, isLoading: usersLoading, error: usersError, - refetch: refetchUsers, - } = useAdminUsers(usersOffset, PAGE_SIZE, usersEnabled) + } = useAdminUsers(usersOffset, PAGE_SIZE, searchQuery) const totalPages = useMemo( () => Math.ceil((usersData?.total ?? 0) / PAGE_SIZE), @@ -62,14 +61,6 @@ export function Admin() { ) } - const handleLoadUsers = () => { - if (usersEnabled) { - refetchUsers() - } else { - setUsersEnabled(true) - } - } - const pendingUserIds = useMemo(() => { const ids = new Set() if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId) @@ -136,12 +127,15 @@ export function Admin() {
-
-

User Management

- -
+

User Management

+ { + setSearchQuery(e.target.value) + setUsersOffset(0) + }} + placeholder='Search by email or paste a user ID...' + /> {usersError && (

@@ -166,7 +160,7 @@ export function Admin() { {usersData && ( <> -

+
Name Email @@ -176,7 +170,7 @@ export function Admin() {
{usersData.users.length === 0 && ( -
+
No users found.
)} diff --git a/apps/sim/hooks/queries/admin-users.ts b/apps/sim/hooks/queries/admin-users.ts index 69b74dfbaa..6f9f6bba73 100644 --- a/apps/sim/hooks/queries/admin-users.ts +++ b/apps/sim/hooks/queries/admin-users.ts @@ -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 { +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 { + 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 { + 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 }, + query: { + limit, + offset, + searchField: 'email', + searchValue: searchQuery, + searchOperator: 'contains', + }, }) - if (error) { - throw new Error(error.message ?? 'Failed to fetch users') - } + if (error) throw new Error(error.message ?? 'Failed to fetch users') 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, - })), + 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, }) From 8eb5e23532618c86aabc106b25dfc1882ab3fad3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 17 Mar 2026 17:46:32 -0700 Subject: [PATCH 2/2] 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 --- .../settings/components/admin/admin.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 2d02f7b1e2..5161bb3c5d 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -31,6 +31,7 @@ export function Admin() { const [workflowId, setWorkflowId] = useState('') const [usersOffset, setUsersOffset] = useState(0) + const [searchInput, setSearchInput] = useState('') const [searchQuery, setSearchQuery] = useState('') const [banUserId, setBanUserId] = useState(null) const [banReason, setBanReason] = useState('') @@ -41,6 +42,11 @@ export function Admin() { error: usersError, } = useAdminUsers(usersOffset, PAGE_SIZE, searchQuery) + const handleSearch = () => { + setUsersOffset(0) + setSearchQuery(searchInput.trim()) + } + const totalPages = useMemo( () => Math.ceil((usersData?.total ?? 0) / PAGE_SIZE), [usersData?.total] @@ -128,14 +134,17 @@ export function Admin() {

User Management

- { - setSearchQuery(e.target.value) - setUsersOffset(0) - }} - placeholder='Search by email or paste a user ID...' - /> +
+ setSearchInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + placeholder='Search by email or paste a user ID...' + /> + +
{usersError && (

@@ -158,7 +167,7 @@ export function Admin() {

)} - {usersData && ( + {searchQuery.length > 0 && usersData && ( <>