@@ -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,6 +522,36 @@ function AddIntegrationModal({
495522 ) : (
496523 < >
497524 < p > Add a new code, data, or compute integration.</ p >
525+ < div className = "mb-4" >
526+ < Label for = "search" > Search</ Label >
527+ < InputGroup >
528+ < Input
529+ className = "lg"
530+ data-cy = "add-integration-search-input"
531+ id = "search"
532+ innerRef = { searchInputRef }
533+ placeholder = "Search by name or url"
534+ type = "text"
535+ value = { userSearchInput }
536+ onChange = { ( e ) => setUserSearchInput ( e . target . value ) }
537+ onKeyDown = { ( e ) => {
538+ if ( e . key === "Enter" ) {
539+ e . preventDefault ( ) ;
540+ }
541+ } }
542+ />
543+ < Button
544+ color = "outline-secondary"
545+ className = "border-secondary-subtle"
546+ data-cy = "search-clear-button"
547+ onClick = { onResetSearch }
548+ id = "search-button"
549+ type = "button"
550+ >
551+ < XCircleFill className = { cx ( "bi" ) } />
552+ </ Button >
553+ </ InputGroup >
554+ </ div >
498555 < ListGroup
499556 className = { cx (
500557 "bg-white" ,
@@ -521,7 +578,7 @@ function AddIntegrationModal({
521578 </ div >
522579 </ ListGroupItem >
523580 ) ) }
524- { providers . length > DEFAULT_MODAL_PROVIDERS_COUNT && (
581+ { filteredProviders . length > DEFAULT_MODAL_PROVIDERS_COUNT && (
525582 < button
526583 onClick = { ( ) => setShowAllIntegrations ( ! showAllIntegrations ) }
527584 className = { cx (
0 commit comments