@@ -23,6 +23,7 @@ import {
2323 useContext ,
2424 useEffect ,
2525 useMemo ,
26+ useRef ,
2627 useState ,
2728 type ReactNode ,
2829} from "react" ;
@@ -32,6 +33,7 @@ import {
3233 Plugin ,
3334 PlusLg ,
3435 Send ,
36+ XCircleFill ,
3537 XLg ,
3638} from "react-bootstrap-icons" ;
3739import { Link , useSearchParams } from "react-router" ;
@@ -43,6 +45,9 @@ import {
4345 CardHeader ,
4446 CardText ,
4547 Col ,
48+ Input ,
49+ InputGroup ,
50+ Label ,
4651 ListGroup ,
4752 ListGroupItem ,
4853 Modal ,
@@ -457,16 +462,38 @@ function AddIntegrationModal({
457462} : AddIntegrationModalProps ) {
458463 const [ showAllIntegrations , setShowAllIntegrations ] = useState ( false ) ;
459464 const isListExpanded = isOpen && showAllIntegrations ;
465+ const searchInputRef = useRef < HTMLInputElement | null > ( null ) ;
466+ const [ userSearchInput , setUserSearchInput ] = useState ( "" ) ;
467+
468+ const normalizedSearchInput = useMemo (
469+ ( ) => userSearchInput . trim ( ) . toLowerCase ( ) ,
470+ [ userSearchInput ]
471+ ) ;
472+
473+ const filteredProviders = useMemo ( ( ) => {
474+ if ( ! normalizedSearchInput ) return providers ;
475+
476+ return providers . filter ( ( { provider } ) =>
477+ [ provider . display_name , provider . url ] . some ( ( value ) =>
478+ value . toLowerCase ( ) . includes ( normalizedSearchInput )
479+ )
480+ ) ;
481+ } , [ normalizedSearchInput , providers ] ) ;
460482
461483 const visibleProviders = useMemo ( ( ) => {
462- if ( isListExpanded ) return providers ;
463- return providers . slice ( 0 , DEFAULT_MODAL_PROVIDERS_COUNT ) ;
464- } , [ isListExpanded , providers ] ) ;
484+ if ( isListExpanded ) return filteredProviders ;
485+ return filteredProviders . slice ( 0 , DEFAULT_MODAL_PROVIDERS_COUNT ) ;
486+ } , [ filteredProviders , isListExpanded ] ) ;
465487
466488 const toggleShowAllIntegrations = useCallback ( ( ) => {
467489 setShowAllIntegrations ( ( open ) => ! open ) ;
468490 } , [ ] ) ;
469491
492+ const onResetSearch = useCallback ( ( ) => {
493+ setUserSearchInput ( "" ) ;
494+ searchInputRef . current ?. focus ( ) ;
495+ } , [ ] ) ;
496+
470497 return (
471498 < Modal
472499 centered
@@ -487,50 +514,83 @@ function AddIntegrationModal({
487514 ) : (
488515 < >
489516 < p > Add a new code, data, or compute integration.</ p >
490- < ListGroup
491- className = { cx (
492- "bg-white" ,
493- "rounded-3" ,
494- "border" ,
495- "border-rk-green"
496- ) }
497- >
498- { visibleProviders . map ( ( { provider, connection } ) => (
499- < ListGroupItem
500- key = { provider . id }
501- action = { true }
502- data-cy = "provider-item"
517+ < div className = "mb-4" >
518+ < Label for = "add-integration-search-input" > Search</ Label >
519+ < InputGroup >
520+ < Input
521+ className = "lg"
522+ data-cy = "add-integration-search-input"
523+ id = "add-integration-search-input"
524+ innerRef = { searchInputRef }
525+ placeholder = "Search by name or url"
526+ type = "text"
527+ value = { userSearchInput }
528+ onChange = { ( e ) => setUserSearchInput ( e . target . value ) }
529+ />
530+ < Button
531+ color = "outline-secondary"
532+ className = "border-secondary-subtle"
533+ data-cy = "search-clear-button"
534+ onClick = { onResetSearch }
535+ id = "search-button"
536+ type = "button"
503537 >
504- < div className = { cx ( "d-flex" , "align-items-center" , "gap-3" ) } >
505- < div className = { cx ( "flex-grow-1" ) } >
506- < ProviderRowHeader provider = { provider } />
538+ < XCircleFill className = { cx ( "bi" ) } />
539+ </ Button >
540+ </ InputGroup >
541+ </ div >
542+ { normalizedSearchInput && filteredProviders . length === 0 ? (
543+ < span className = { cx ( "small" , "text-muted" ) } >
544+ No integrations found for "{ userSearchInput . trim ( ) } ".
545+ </ span >
546+ ) : (
547+ < ListGroup
548+ className = { cx (
549+ "bg-white" ,
550+ "rounded-3" ,
551+ "border" ,
552+ "border-rk-green"
553+ ) }
554+ >
555+ { visibleProviders . map ( ( { provider, connection } ) => (
556+ < ListGroupItem
557+ key = { provider . id }
558+ action = { true }
559+ data-cy = "provider-item"
560+ >
561+ < div
562+ className = { cx ( "d-flex" , "align-items-center" , "gap-3" ) }
563+ >
564+ < div className = { cx ( "flex-grow-1" ) } >
565+ < ProviderRowHeader provider = { provider } />
566+ </ div >
567+ < ConnectButton
568+ provider = { provider }
569+ connectionStatus = { connection ?. status }
570+ onConnectStart = { onToggle }
571+ />
507572 </ div >
508- < ConnectButton
509- provider = { provider }
510- connectionStatus = { connection ?. status }
511- onConnectStart = { onToggle }
573+ </ ListGroupItem >
574+ ) ) }
575+ { filteredProviders . length > DEFAULT_MODAL_PROVIDERS_COUNT && (
576+ < button
577+ onClick = { toggleShowAllIntegrations }
578+ className = { cx (
579+ "text-primary" ,
580+ "list-group-item" ,
581+ "text-start" ,
582+ "text-decoration-underline"
583+ ) }
584+ >
585+ { ! isListExpanded ? "See all integrations" : "Show less " }
586+ < ChevronFlippedIcon
587+ className = "ms-1"
588+ flipped = { showAllIntegrations }
512589 />
513- </ div >
514- </ ListGroupItem >
515- ) ) }
516- { providers . length > DEFAULT_MODAL_PROVIDERS_COUNT && (
517- < button
518- onClick = { toggleShowAllIntegrations }
519- className = { cx (
520- "text-primary" ,
521- "list-group-item" ,
522- "text-start" ,
523- "text-decoration-underline"
524- ) }
525- >
526- { ! isListExpanded ? "See all integrations" : "Show less " }
527- < ChevronFlippedIcon
528- className = "ms-1"
529- flipped = { showAllIntegrations }
530- />
531- </ button >
532- ) }
533- </ ListGroup >
590+ </ button >
591+ ) }
592+ </ ListGroup >
593+ ) }
534594 </ >
535595 ) }
536596 </ ModalBody >
0 commit comments