@@ -40,9 +40,11 @@ import {
4040import { RunnerGroupCard } from "components/runners/RunnerGroupCard" ;
4141import { ParamSelector } from "lib/ParamSelector" ;
4242import { RunnersApiResponse , unknownGoesLast } from "lib/runnerUtils" ;
43+ import debounce from "lodash/debounce" ;
4344import { useSession } from "next-auth/react" ;
4445import { useRouter } from "next/router" ;
45- import { useMemo , useState } from "react" ;
46+ import type { ParsedUrlQuery } from "querystring" ;
47+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
4648import useSWR from "swr" ;
4749
4850// Define sort order constants to prevent typos
@@ -71,10 +73,58 @@ export default function RunnersPage() {
7173 const orgParam = typeof org === "string" ? org : null ;
7274
7375 const { data : _session , status : _status } = useSession ( ) ;
74- const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
76+
77+ // Utility function to extract search term from URL query
78+ const getSearchFromQuery = ( query : ParsedUrlQuery ) : string => {
79+ return typeof query . search === "string" ? query . search : "" ;
80+ } ;
81+
82+ // Get search term from URL parameters
83+ const [ searchTerm , setSearchTerm ] = useState (
84+ getSearchFromQuery ( router . query )
85+ ) ;
7586 const [ sortOrder , setSortOrder ] = useState < SortOrder > ( SORT_ALPHABETICAL ) ;
7687 const [ expandedGroup , setExpandedGroup ] = useState < string | null > ( null ) ;
7788
89+ // Use ref to access current router.query without causing debounce recreations
90+ const routerQueryRef = useRef ( router . query ) ;
91+ routerQueryRef . current = router . query ;
92+
93+ // Sync search state with URL changes
94+ useEffect ( ( ) => {
95+ const searchParam = router . query . search ;
96+ const newSearchTerm = getSearchFromQuery ( { search : searchParam } ) ;
97+ setSearchTerm ( newSearchTerm ) ;
98+ } , [ router . query . search ] ) ;
99+
100+ // Debounced function to update search in URL
101+ // Debouncing lets us limit the rate at which the
102+ // function is called, so that it doesn't get called on every
103+ // keystroke. Instead, it'll wait for DEBOUNCE_DELAY_MS
104+ // before executing.
105+ const DEBOUNCE_DELAY_MS = 300 ;
106+ const updateSearchInUrl = useCallback (
107+ debounce ( ( newSearchTerm : string ) => {
108+ const query = { ...routerQueryRef . current } ;
109+ if ( newSearchTerm ) {
110+ query . search = newSearchTerm ;
111+ } else {
112+ delete query . search ;
113+ }
114+ router . push ( { pathname : router . pathname , query } , undefined , {
115+ shallow : true ,
116+ } ) ;
117+ } , DEBOUNCE_DELAY_MS ) ,
118+ [ router . pathname ]
119+ ) ;
120+
121+ // Cleanup debounced function on unmount or when it changes
122+ useEffect ( ( ) => {
123+ return ( ) => {
124+ updateSearchInUrl . cancel ( ) ;
125+ } ;
126+ } , [ updateSearchInUrl ] ) ;
127+
78128 // Handle URL editing for organization
79129 const handleOrgSubmit = ( newOrg : string ) => {
80130 if ( ! newOrg ) return ;
@@ -192,7 +242,11 @@ export default function RunnersPage() {
192242 variant = "outlined"
193243 placeholder = "Search runners by name, ID, OS, or labels..."
194244 value = { searchTerm }
195- onChange = { ( e ) => setSearchTerm ( e . target . value ) }
245+ onChange = { ( e ) => {
246+ const value = e . target . value ;
247+ setSearchTerm ( value ) ; // Immediate UI update
248+ updateSearchInUrl ( value ) ; // Debounced URL update
249+ } }
196250 sx = { { maxWidth : 600 , mb : 2 } }
197251 />
198252
0 commit comments