@@ -10,35 +10,33 @@ import {
1010 DropdownMenuSeparator ,
1111 DropdownMenuTrigger ,
1212} from "@/components/ui/dropdown-menu"
13- import { Input } from "@/components/ui/input"
13+ import { InputGroup , InputGroupAddon , InputGroupInput } from "@/components/ui/input-group "
1414import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from "@/components/ui/select"
1515import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table"
1616import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
1717import { cn , getCodeHostCommitUrl , getCodeHostIcon , getCodeHostInfoForRepo , getRepoImageSrc } from "@/lib/utils"
1818import {
1919 type ColumnDef ,
20- type ColumnFiltersState ,
2120 type SortingState ,
2221 type VisibilityState ,
2322 flexRender ,
2423 getCoreRowModel ,
25- getFilteredRowModel ,
26- getPaginationRowModel ,
2724 getSortedRowModel ,
2825 useReactTable ,
2926} from "@tanstack/react-table"
3027import { cva } from "class-variance-authority"
31- import { ArrowUpDown , ExternalLink , MoreHorizontal , RefreshCwIcon } from "lucide-react"
28+ import { ArrowUpDown , ExternalLink , Loader2 , MoreHorizontal , RefreshCwIcon } from "lucide-react"
3229import Image from "next/image"
3330import Link from "next/link"
34- import { useMemo , useState } from "react"
31+ import { useEffect , useRef , useState } from "react"
3532import { getBrowsePath } from "../../browse/hooks/utils"
36- import { useRouter } from "next/navigation"
33+ import { useRouter , useSearchParams , usePathname } from "next/navigation"
3734import { useToast } from "@/components/hooks/use-toast" ;
3835import { DisplayDate } from "../../components/DisplayDate"
3936import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip"
4037import { NotificationDot } from "../../components/notificationDot"
4138import { CodeHostType } from "@sourcebot/db"
39+ import { useHotkeys } from "react-hotkeys-hook"
4240
4341// @see : https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
4442
@@ -271,46 +269,87 @@ export const columns: ColumnDef<Repo>[] = [
271269 } ,
272270]
273271
274- export const ReposTable = ( { data } : { data : Repo [ ] } ) => {
272+ interface ReposTableProps {
273+ data : Repo [ ] ;
274+ currentPage : number ;
275+ pageSize : number ;
276+ totalCount : number ;
277+ initialSearch : string ;
278+ initialStatus : string ;
279+ }
280+
281+ export const ReposTable = ( {
282+ data,
283+ currentPage,
284+ pageSize,
285+ totalCount,
286+ initialSearch,
287+ initialStatus,
288+ } : ReposTableProps ) => {
275289 const [ sorting , setSorting ] = useState < SortingState > ( [ ] )
276- const [ columnFilters , setColumnFilters ] = useState < ColumnFiltersState > ( [ ] )
277290 const [ columnVisibility , setColumnVisibility ] = useState < VisibilityState > ( { } )
278291 const [ rowSelection , setRowSelection ] = useState ( { } )
292+ const [ searchValue , setSearchValue ] = useState ( initialSearch )
293+ const [ isPendingSearch , setIsPendingSearch ] = useState ( false )
294+ const searchInputRef = useRef < HTMLInputElement > ( null )
279295 const router = useRouter ( ) ;
296+ const searchParams = useSearchParams ( ) ;
297+ const pathname = usePathname ( ) ;
280298 const { toast } = useToast ( ) ;
281299
282- const {
283- numCompleted,
284- numInProgress,
285- numPending,
286- numFailed,
287- numNoJobs,
288- } = useMemo ( ( ) => {
289- return {
290- numCompleted : data . filter ( ( repo ) => repo . latestJobStatus === "COMPLETED" ) . length ,
291- numInProgress : data . filter ( ( repo ) => repo . latestJobStatus === "IN_PROGRESS" ) . length ,
292- numPending : data . filter ( ( repo ) => repo . latestJobStatus === "PENDING" ) . length ,
293- numFailed : data . filter ( ( repo ) => repo . latestJobStatus === "FAILED" ) . length ,
294- numNoJobs : data . filter ( ( repo ) => repo . latestJobStatus === null ) . length ,
300+ // Focus search box when '/' is pressed
301+ useHotkeys ( '/' , ( event ) => {
302+ event . preventDefault ( ) ;
303+ searchInputRef . current ?. focus ( ) ;
304+ } ) ;
305+
306+ // Debounced search effect - only runs when searchValue changes
307+ useEffect ( ( ) => {
308+ setIsPendingSearch ( true ) ;
309+ const timer = setTimeout ( ( ) => {
310+ const params = new URLSearchParams ( searchParams . toString ( ) ) ;
311+ if ( searchValue ) {
312+ params . set ( 'search' , searchValue ) ;
313+ } else {
314+ params . delete ( 'search' ) ;
315+ }
316+ params . set ( 'page' , '1' ) ; // Reset to page 1 on search
317+ router . replace ( `${ pathname } ?${ params . toString ( ) } ` ) ;
318+ setIsPendingSearch ( false ) ;
319+ } , 300 ) ;
320+
321+ return ( ) => {
322+ clearTimeout ( timer ) ;
323+ setIsPendingSearch ( false ) ;
324+ } ;
325+ // eslint-disable-next-line react-hooks/exhaustive-deps
326+ } , [ searchValue ] ) ;
327+
328+ const updateStatusFilter = ( value : string ) => {
329+ const params = new URLSearchParams ( searchParams . toString ( ) ) ;
330+ if ( value === 'all' ) {
331+ params . delete ( 'status' ) ;
332+ } else {
333+ params . set ( 'status' , value ) ;
295334 }
296- } , [ data ] ) ;
335+ params . set ( 'page' , '1' ) ; // Reset to page 1 on filter change
336+ router . replace ( `${ pathname } ?${ params . toString ( ) } ` ) ;
337+ } ;
338+
339+ const totalPages = Math . ceil ( totalCount / pageSize ) ;
297340
298341 const table = useReactTable ( {
299342 data,
300343 columns,
301344 onSortingChange : setSorting ,
302- onColumnFiltersChange : setColumnFilters ,
303345 getCoreRowModel : getCoreRowModel ( ) ,
304- getPaginationRowModel : getPaginationRowModel ( ) ,
305346 getSortedRowModel : getSortedRowModel ( ) ,
306- getFilteredRowModel : getFilteredRowModel ( ) ,
307347 onColumnVisibilityChange : setColumnVisibility ,
308348 onRowSelectionChange : setRowSelection ,
309349 columnResizeMode : 'onChange' ,
310350 enableColumnResizing : false ,
311351 state : {
312352 sorting,
313- columnFilters,
314353 columnVisibility,
315354 rowSelection,
316355 } ,
@@ -319,28 +358,34 @@ export const ReposTable = ({ data }: { data: Repo[] }) => {
319358 return (
320359 < div className = "w-full" >
321360 < div className = "flex items-center gap-4 py-4" >
322- < Input
323- placeholder = "Filter repositories..."
324- value = { ( table . getColumn ( "displayName" ) ?. getFilterValue ( ) as string ) ?? "" }
325- onChange = { ( event ) => table . getColumn ( "displayName" ) ?. setFilterValue ( event . target . value ) }
326- className = "max-w-sm"
327- />
361+ < InputGroup className = "max-w-sm" >
362+ < InputGroupInput
363+ ref = { searchInputRef }
364+ placeholder = "Filter repositories..."
365+ value = { searchValue }
366+ onChange = { ( event ) => setSearchValue ( event . target . value ) }
367+ className = "ring-0"
368+ />
369+ { isPendingSearch && (
370+ < InputGroupAddon align = "inline-end" >
371+ < Loader2 className = "h-4 w-4 animate-spin" />
372+ </ InputGroupAddon >
373+ ) }
374+ </ InputGroup >
328375 < Select
329- value = { ( table . getColumn ( "latestJobStatus" ) ?. getFilterValue ( ) as string ) ?? "all" }
330- onValueChange = { ( value ) => {
331- table . getColumn ( "latestJobStatus" ) ?. setFilterValue ( value === "all" ? "" : value )
332- } }
376+ value = { initialStatus }
377+ onValueChange = { updateStatusFilter }
333378 >
334379 < SelectTrigger className = "w-[180px]" >
335380 < SelectValue placeholder = "Filter by status" />
336381 </ SelectTrigger >
337382 < SelectContent >
338383 < SelectItem value = "all" > Filter by status</ SelectItem >
339- < SelectItem value = "COMPLETED" > Completed ( { numCompleted } ) </ SelectItem >
340- < SelectItem value = "IN_PROGRESS" > In progress ( { numInProgress } ) </ SelectItem >
341- < SelectItem value = "PENDING" > Pending ( { numPending } ) </ SelectItem >
342- < SelectItem value = "FAILED" > Failed ( { numFailed } ) </ SelectItem >
343- < SelectItem value = "null" > No status ( { numNoJobs } ) </ SelectItem >
384+ < SelectItem value = "COMPLETED" > Completed</ SelectItem >
385+ < SelectItem value = "IN_PROGRESS" > In progress</ SelectItem >
386+ < SelectItem value = "PENDING" > Pending</ SelectItem >
387+ < SelectItem value = "FAILED" > Failed</ SelectItem >
388+ < SelectItem value = "null" > No status</ SelectItem >
344389 </ SelectContent >
345390 </ Select >
346391 < Button
@@ -401,18 +446,32 @@ export const ReposTable = ({ data }: { data: Repo[] }) => {
401446 </ div >
402447 < div className = "flex items-center justify-end space-x-2 py-4" >
403448 < div className = "flex-1 text-sm text-muted-foreground" >
404- { table . getFilteredRowModel ( ) . rows . length } { data . length > 1 ? 'repositories' : 'repository' } total
449+ { totalCount } { totalCount !== 1 ? 'repositories' : 'repository' } total
450+ { totalPages > 1 && ` • Page ${ currentPage } of ${ totalPages } ` }
405451 </ div >
406452 < div className = "space-x-2" >
407453 < Button
408454 variant = "outline"
409455 size = "sm"
410- onClick = { ( ) => table . previousPage ( ) }
411- disabled = { ! table . getCanPreviousPage ( ) }
456+ onClick = { ( ) => {
457+ const params = new URLSearchParams ( searchParams . toString ( ) ) ;
458+ params . set ( 'page' , String ( currentPage - 1 ) ) ;
459+ router . push ( `${ pathname } ?${ params . toString ( ) } ` ) ;
460+ } }
461+ disabled = { currentPage <= 1 }
412462 >
413463 Previous
414464 </ Button >
415- < Button variant = "outline" size = "sm" onClick = { ( ) => table . nextPage ( ) } disabled = { ! table . getCanNextPage ( ) } >
465+ < Button
466+ variant = "outline"
467+ size = "sm"
468+ onClick = { ( ) => {
469+ const params = new URLSearchParams ( searchParams . toString ( ) ) ;
470+ params . set ( 'page' , String ( currentPage + 1 ) ) ;
471+ router . push ( `${ pathname } ?${ params . toString ( ) } ` ) ;
472+ } }
473+ disabled = { currentPage >= totalPages }
474+ >
416475 Next
417476 </ Button >
418477 </ div >
0 commit comments