@@ -23,6 +23,7 @@ import {
2323 useContext ,
2424 useEffect ,
2525 useMemo ,
26+ useRef ,
2627 useState ,
2728 type ReactNode ,
2829} from "react" ;
@@ -33,6 +34,7 @@ import {
3334 Plugin ,
3435 PlusLg ,
3536 Send ,
37+ XCircleFill ,
3638 XLg ,
3739} from "react-bootstrap-icons" ;
3840import { Link , useSearchParams } from "react-router" ;
@@ -44,6 +46,9 @@ import {
4446 CardHeader ,
4547 CardText ,
4648 Col ,
49+ Input ,
50+ InputGroup ,
51+ Label ,
4752 ListGroup ,
4853 ListGroupItem ,
4954 Modal ,
@@ -465,16 +470,38 @@ function AddIntegrationModal({
465470} : AddIntegrationModalProps ) {
466471 const [ showAllIntegrations , setShowAllIntegrations ] = useState ( false ) ;
467472 const isListExpanded = isOpen && showAllIntegrations ;
473+ const searchInputRef = useRef < HTMLInputElement | null > ( null ) ;
474+ const [ userSearchInput , setUserSearchInput ] = useState ( "" ) ;
475+
476+ const normalizedSearchInput = useMemo (
477+ ( ) => userSearchInput . trim ( ) . toLowerCase ( ) ,
478+ [ userSearchInput ]
479+ ) ;
480+
481+ const filteredProviders = useMemo ( ( ) => {
482+ if ( ! normalizedSearchInput ) return providers ;
483+
484+ return providers . filter ( ( { provider } ) =>
485+ [ provider . display_name , provider . url ] . some ( ( value ) =>
486+ value . toLowerCase ( ) . includes ( normalizedSearchInput )
487+ )
488+ ) ;
489+ } , [ normalizedSearchInput , providers ] ) ;
468490
469491 const visibleProviders = useMemo ( ( ) => {
470- if ( isListExpanded ) return providers ;
471- return providers . slice ( 0 , DEFAULT_MODAL_PROVIDERS_COUNT ) ;
472- } , [ isListExpanded , providers ] ) ;
492+ if ( isListExpanded ) return filteredProviders ;
493+ return filteredProviders . slice ( 0 , DEFAULT_MODAL_PROVIDERS_COUNT ) ;
494+ } , [ filteredProviders , isListExpanded ] ) ;
473495
474496 const handleClosed = useCallback ( ( ) => {
475497 setShowAllIntegrations ( false ) ;
476498 } , [ ] ) ;
477499
500+ const onResetSearch = useCallback ( ( ) => {
501+ setUserSearchInput ( "" ) ;
502+ searchInputRef . current ?. focus ( ) ;
503+ } , [ ] ) ;
504+
478505 return (
479506 < Modal
480507 centered
@@ -495,50 +522,83 @@ function AddIntegrationModal({
495522 ) : (
496523 < >
497524 < p > Add a new code, data, or compute integration.</ p >
498- < ListGroup
499- className = { cx (
500- "bg-white" ,
501- "rounded-3" ,
502- "border" ,
503- "border-rk-green"
504- ) }
505- >
506- { visibleProviders . map ( ( { provider, connection } ) => (
507- < ListGroupItem
508- key = { provider . id }
509- action = { true }
510- data-cy = "provider-item"
525+ < div className = "mb-4" >
526+ < Label for = "add-integration-search-input" > Search</ Label >
527+ < InputGroup >
528+ < Input
529+ className = "lg"
530+ data-cy = "add-integration-search-input"
531+ id = "add-integration-search-input"
532+ innerRef = { searchInputRef }
533+ placeholder = "Search by name or url"
534+ type = "text"
535+ value = { userSearchInput }
536+ onChange = { ( e ) => setUserSearchInput ( e . target . value ) }
537+ />
538+ < Button
539+ color = "outline-secondary"
540+ className = "border-secondary-subtle"
541+ data-cy = "search-clear-button"
542+ onClick = { onResetSearch }
543+ id = "search-button"
544+ type = "button"
511545 >
512- < div className = { cx ( "d-flex" , "align-items-center" , "gap-3" ) } >
513- < div className = { cx ( "flex-grow-1" ) } >
514- < ProviderRowHeader provider = { provider } />
546+ < XCircleFill className = { cx ( "bi" ) } />
547+ </ Button >
548+ </ InputGroup >
549+ </ div >
550+ { normalizedSearchInput && filteredProviders . length === 0 ? (
551+ < span className = { cx ( "small" , "text-muted" ) } >
552+ No integrations found for "{ userSearchInput . trim ( ) } ".
553+ </ span >
554+ ) : (
555+ < ListGroup
556+ className = { cx (
557+ "bg-white" ,
558+ "rounded-3" ,
559+ "border" ,
560+ "border-rk-green"
561+ ) }
562+ >
563+ { visibleProviders . map ( ( { provider, connection } ) => (
564+ < ListGroupItem
565+ key = { provider . id }
566+ action = { true }
567+ data-cy = "provider-item"
568+ >
569+ < div
570+ className = { cx ( "d-flex" , "align-items-center" , "gap-3" ) }
571+ >
572+ < div className = { cx ( "flex-grow-1" ) } >
573+ < ProviderRowHeader provider = { provider } />
574+ </ div >
575+ < ConnectButton
576+ provider = { provider }
577+ connectionStatus = { connection ?. status }
578+ onConnectStart = { onToggle }
579+ />
515580 </ div >
516- < ConnectButton
517- provider = { provider }
518- connectionStatus = { connection ?. status }
519- onConnectStart = { onToggle }
581+ </ ListGroupItem >
582+ ) ) }
583+ { filteredProviders . length > DEFAULT_MODAL_PROVIDERS_COUNT && (
584+ < button
585+ onClick = { ( ) => setShowAllIntegrations ( ! showAllIntegrations ) }
586+ className = { cx (
587+ "text-primary" ,
588+ "list-group-item" ,
589+ "text-start" ,
590+ "text-decoration-underline"
591+ ) }
592+ >
593+ { ! isListExpanded ? "See all integrations" : "Show less " }
594+ < ChevronFlippedIcon
595+ className = "ms-1"
596+ flipped = { showAllIntegrations }
520597 />
521- </ div >
522- </ ListGroupItem >
523- ) ) }
524- { providers . length > DEFAULT_MODAL_PROVIDERS_COUNT && (
525- < button
526- onClick = { ( ) => setShowAllIntegrations ( ! showAllIntegrations ) }
527- className = { cx (
528- "text-primary" ,
529- "list-group-item" ,
530- "text-start" ,
531- "text-decoration-underline"
532- ) }
533- >
534- { ! isListExpanded ? "See all integrations" : "Show less " }
535- < ChevronFlippedIcon
536- className = "ms-1"
537- flipped = { showAllIntegrations }
538- />
539- </ button >
540- ) }
541- </ ListGroup >
598+ </ button >
599+ ) }
600+ </ ListGroup >
601+ ) }
542602 </ >
543603 ) }
544604 </ ModalBody >
0 commit comments