22
33import { useState } from 'react'
44import { Chip , ChipConfirmModal , ChipModalTabs , toast } from '@sim/emcn'
5- import { ArrowLeft , Plus , User } from '@sim/emcn/icons'
5+ import { ArrowLeft , Plus } from '@sim/emcn/icons'
66import { getErrorMessage } from '@sim/utils/errors'
77import { useQueryState } from 'nuqs'
88import { McpIcon } from '@/components/icons'
@@ -17,11 +17,14 @@ import { getCredentialGroupProviderService } from '@/lib/credential-groups/provi
1717import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types'
1818import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
1919import {
20+ credentialGroupPeopleSearchParam ,
21+ credentialGroupPeopleSearchUrlKeys ,
2022 credentialGroupProviderSearchParam ,
2123 credentialGroupProviderSearchUrlKeys ,
2224 credentialGroupTabParam ,
2325 credentialGroupTabUrlKeys ,
2426} from '@/app/workspace/[workspaceId]/settings/[section]/search-params'
27+ import { MemberAvatar } from '@/app/workspace/[workspaceId]/settings/components/member-list'
2528import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu'
2629import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
2730import type { SettingsAction } from '@/app/workspace/[workspaceId]/settings/components/settings-header/settings-header'
@@ -125,13 +128,35 @@ export function CredentialGroupDetail({
125128 { ...credentialGroupProviderSearchParam . parser , ...credentialGroupProviderSearchUrlKeys }
126129 )
127130 const setProviderSearch = useDebouncedSearchSetter ( setProviderSearchParam )
131+ const [ peopleSearch , setPeopleSearchParam ] = useQueryState ( credentialGroupPeopleSearchParam . key , {
132+ ...credentialGroupPeopleSearchParam . parser ,
133+ ...credentialGroupPeopleSearchUrlKeys ,
134+ } )
135+ const setPeopleSearch = useDebouncedSearchSetter ( setPeopleSearchParam )
128136 const [ showInvite , setShowInvite ] = useState ( false )
129137 const [ showDelete , setShowDelete ] = useState ( false )
130138 const [ deletingEnrollmentId , setDeletingEnrollmentId ] = useState < string | null > ( null )
131139 const [ draftName , setDraftName ] = useState < string | null > ( null )
132140 const [ draftDescription , setDraftDescription ] = useState < string | null > ( null )
133141 const credentialGroup = detail . data ?. pages [ 0 ] ?. credentialGroup
134142 const enrollments = detail . data ?. pages . flatMap ( ( page ) => page . enrollments ) ?? [ ]
143+ const peopleFilter = peopleSearch . trim ( ) . toLowerCase ( )
144+ /**
145+ * Only the pages already loaded: the enrollment list is cursor-paginated with no
146+ * server-side term, so a match on a later page appears only once it is fetched.
147+ */
148+ const visibleEnrollments = peopleFilter
149+ ? enrollments . filter ( ( enrollment ) => enrollment . email . toLowerCase ( ) . includes ( peopleFilter ) )
150+ : enrollments
151+ /**
152+ * `+` means more people exist than are loaded, so it stays on the total. While
153+ * filtering, the match count is reported against that total rather than replacing
154+ * it — otherwise `People (2+)` reads as a two-person group with more to come.
155+ */
156+ const loadedTotal = `${ enrollments . length } ${ detail . hasNextPage ? '+' : '' } `
157+ const peopleLabel = peopleFilter
158+ ? `People (${ visibleEnrollments . length } of ${ loadedTotal } )`
159+ : `People (${ loadedTotal } )`
135160 const deletingEnrollment = deletingEnrollmentId
136161 ? ( enrollments . find ( ( enrollment ) => enrollment . id === deletingEnrollmentId ) ?? null )
137162 : null
@@ -290,7 +315,14 @@ export function CredentialGroupDetail({
290315 placeholder : 'Search accounts and MCP servers...' ,
291316 disabled : detail . isPending ,
292317 }
293- : undefined
318+ : activeTab === 'people'
319+ ? {
320+ value : peopleSearch ,
321+ onChange : setPeopleSearch ,
322+ placeholder : 'Search people...' ,
323+ disabled : detail . isPending ,
324+ }
325+ : undefined
294326 }
295327 >
296328 { detail . error ? (
@@ -320,7 +352,7 @@ export function CredentialGroupDetail({
320352
321353 { activeTab === 'people' && (
322354 < SettingsSection
323- label = { `People ( ${ enrollments . length } ${ detail . hasNextPage ? '+' : '' } )` }
355+ label = { peopleLabel }
324356 action = {
325357 detail . hasNextPage ? (
326358 < Chip
@@ -332,16 +364,18 @@ export function CredentialGroupDetail({
332364 ) : undefined
333365 }
334366 >
335- { enrollments . length === 0 ? (
336- < SettingsEmptyState variant = 'inline' > No people invited yet</ SettingsEmptyState >
367+ { visibleEnrollments . length === 0 ? (
368+ < SettingsEmptyState variant = 'inline' >
369+ { peopleFilter ? 'No people match your search' : 'No people invited yet' }
370+ </ SettingsEmptyState >
337371 ) : (
338372 < div className = { RESOURCE_LIST_STACK } >
339- { enrollments . map ( ( enrollment ) => {
373+ { visibleEnrollments . map ( ( enrollment ) => {
340374 return (
341375 < SettingsResourceRow
342376 key = { enrollment . id }
343- icon = { < User className = 'text-[var(--text-icon)]' /> }
344- iconFilled
377+ icon = { < MemberAvatar name = { enrollment . email } image = { null } /> }
378+ iconVariant = 'custom'
345379 title = { enrollment . email }
346380 description = {
347381 < EnrollmentConnections
0 commit comments