@@ -40,9 +40,11 @@ import {
40
40
import { RunnerGroupCard } from "components/runners/RunnerGroupCard" ;
41
41
import { ParamSelector } from "lib/ParamSelector" ;
42
42
import { RunnersApiResponse , unknownGoesLast } from "lib/runnerUtils" ;
43
+ import debounce from "lodash/debounce" ;
43
44
import { useSession } from "next-auth/react" ;
44
45
import { 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" ;
46
48
import useSWR from "swr" ;
47
49
48
50
// Define sort order constants to prevent typos
@@ -71,10 +73,58 @@ export default function RunnersPage() {
71
73
const orgParam = typeof org === "string" ? org : null ;
72
74
73
75
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
+ ) ;
75
86
const [ sortOrder , setSortOrder ] = useState < SortOrder > ( SORT_ALPHABETICAL ) ;
76
87
const [ expandedGroup , setExpandedGroup ] = useState < string | null > ( null ) ;
77
88
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
+
78
128
// Handle URL editing for organization
79
129
const handleOrgSubmit = ( newOrg : string ) => {
80
130
if ( ! newOrg ) return ;
@@ -192,7 +242,11 @@ export default function RunnersPage() {
192
242
variant = "outlined"
193
243
placeholder = "Search runners by name, ID, OS, or labels..."
194
244
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
+ } }
196
250
sx = { { maxWidth : 600 , mb : 2 } }
197
251
/>
198
252
0 commit comments