@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
33import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from "@/components/ui/table" ;
44import { Card , CardContent , CardHeader , CardTitle } from "@/components/ui/card" ;
55import { Button } from "@/components/ui/button" ;
6+ import { Checkbox } from "@/components/ui/checkbox" ;
67import { Input } from "@/components/ui/input" ;
78import { Badge } from "@/components/ui/badge" ;
89import { DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuSeparator , DropdownMenuTrigger } from "@/components/ui/dropdown-menu" ;
@@ -33,6 +34,9 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
3334 const [ selectedServer , setSelectedServer ] = useState < Server | null > ( null ) ;
3435 const [ isDeleting , setIsDeleting ] = useState ( false ) ;
3536 const [ pausingServers , setPausingServers ] = useState < Set < string > > ( new Set ( ) ) ;
37+ const [ selectedServerIds , setSelectedServerIds ] = useState < Set < string > > ( new Set ( ) ) ;
38+ const [ bulkDeleteDialogOpen , setBulkDeleteDialogOpen ] = useState ( false ) ;
39+ const [ isBulkDeleting , setIsBulkDeleting ] = useState ( false ) ;
3640 const navigate = useNavigate ( ) ;
3741 const { toast } = useToast ( ) ;
3842
@@ -42,6 +46,25 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
4246 server . ip_address . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
4347 ) ;
4448
49+ const allVisibleSelected = filteredServers . length > 0 && filteredServers . every ( s => selectedServerIds . has ( s . id ) ) ;
50+ const someVisibleSelected = filteredServers . some ( s => selectedServerIds . has ( s . id ) ) && ! allVisibleSelected ;
51+
52+ const toggleSelectAllVisible = ( checked : boolean ) => {
53+ const newSet = new Set ( selectedServerIds ) ;
54+ if ( checked ) {
55+ filteredServers . forEach ( s => newSet . add ( s . id ) ) ;
56+ } else {
57+ filteredServers . forEach ( s => newSet . delete ( s . id ) ) ;
58+ }
59+ setSelectedServerIds ( newSet ) ;
60+ } ;
61+
62+ const toggleSelectOne = ( serverId : string , checked : boolean ) => {
63+ const newSet = new Set ( selectedServerIds ) ;
64+ if ( checked ) newSet . add ( serverId ) ; else newSet . delete ( serverId ) ;
65+ setSelectedServerIds ( newSet ) ;
66+ } ;
67+
4568 const handleViewDetails = ( serverId : string ) => {
4669 navigate ( `/server-detail/${ serverId } ` ) ;
4770 } ;
@@ -138,6 +161,33 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
138161 }
139162 } ;
140163
164+ const confirmBulkDelete = async ( ) => {
165+ if ( selectedServerIds . size === 0 || isBulkDeleting ) return ;
166+ try {
167+ setIsBulkDeleting ( true ) ;
168+ const ids = Array . from ( selectedServerIds ) ;
169+ const deletions = ids . map ( id => pb . collection ( 'servers' ) . delete ( id ) ) ;
170+ const results = await Promise . allSettled ( deletions ) ;
171+ const failed = results . filter ( r => r . status === 'rejected' ) . length ;
172+
173+ if ( failed === 0 ) {
174+ toast ( { title : "Servers deleted" , description : `${ ids . length } server(s) have been deleted.` } ) ;
175+ } else if ( failed === ids . length ) {
176+ toast ( { variant : "destructive" , title : "Error" , description : "Failed to delete selected servers. Please try again." } ) ;
177+ } else {
178+ toast ( { variant : "destructive" , title : "Partial success" , description : `Deleted ${ ids . length - failed } , failed ${ failed } .` } ) ;
179+ }
180+
181+ onRefresh ( ) ;
182+ setSelectedServerIds ( new Set ( ) ) ;
183+ setBulkDeleteDialogOpen ( false ) ;
184+ } catch ( _e ) {
185+ toast ( { variant : "destructive" , title : "Error" , description : "Failed to delete selected servers. Please try again." } ) ;
186+ } finally {
187+ setIsBulkDeleting ( false ) ;
188+ }
189+ } ;
190+
141191 const CustomProgressBar = ( {
142192 value,
143193 label,
@@ -234,6 +284,18 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
234284 className = "pl-8"
235285 />
236286 </ div >
287+ { selectedServerIds . size > 0 && (
288+ < div className = "hidden sm:block text-sm text-muted-foreground mr-2" >
289+ { selectedServerIds . size } selected
290+ </ div >
291+ ) }
292+ < Button
293+ onClick = { ( ) => setBulkDeleteDialogOpen ( true ) }
294+ variant = "destructive"
295+ disabled = { selectedServerIds . size === 0 }
296+ >
297+ Delete Selected
298+ </ Button >
237299 < Button onClick = { onRefresh } variant = "outline" size = "icon" >
238300 < RefreshCw className = "h-4 w-4" />
239301 </ Button >
@@ -250,6 +312,16 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
250312 < Table >
251313 < TableHeader className = { `${ theme === 'dark' ? 'bg-gray-800' : 'bg-gray-50' } ` } >
252314 < TableRow className = { `${ theme === 'dark' ? 'border-gray-700 hover:bg-gray-800' : 'border-gray-200 hover:bg-gray-100' } ` } >
315+ < TableHead className = { `${ theme === 'dark' ? 'text-gray-300' : 'text-gray-700' } w-10` } >
316+ < div onClick = { ( e ) => e . stopPropagation ( ) } >
317+ < Checkbox
318+ checked = { allVisibleSelected }
319+ onCheckedChange = { ( v ) => toggleSelectAllVisible ( Boolean ( v ) ) }
320+ aria-label = "Select all"
321+ indeterminate = { someVisibleSelected }
322+ />
323+ </ div >
324+ </ TableHead >
253325 < TableHead className = { `${ theme === 'dark' ? 'text-gray-300' : 'text-gray-700' } font-medium text-base py-4` } > { t ( 'name' ) } </ TableHead >
254326 < TableHead className = { `${ theme === 'dark' ? 'text-gray-300' : 'text-gray-700' } font-medium text-base py-4` } > { t ( 'status' ) } </ TableHead >
255327 < TableHead className = { `${ theme === 'dark' ? 'text-gray-300' : 'text-gray-700' } font-medium text-base py-4` } > { t ( 'OS' ) } </ TableHead >
@@ -270,12 +342,20 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
270342 const isPaused = server . status === "paused" ;
271343 const isProcessing = pausingServers . has ( server . id ) ;
272344
345+ const isSelected = selectedServerIds . has ( server . id ) ;
273346 return (
274347 < TableRow
275348 key = { server . id }
276- className = " hover:bg-muted/50 cursor-pointer"
349+ className = { ` hover:bg-muted/50 cursor-pointer ${ isSelected ? 'bg-muted/30' : '' } ` }
277350 onClick = { ( ) => handleViewDetails ( server . id ) }
278351 >
352+ < TableCell onClick = { ( e ) => e . stopPropagation ( ) } >
353+ < Checkbox
354+ checked = { isSelected }
355+ onCheckedChange = { ( v ) => toggleSelectOne ( server . id , Boolean ( v ) ) }
356+ aria-label = { `Select ${ server . name } ` }
357+ />
358+ </ TableCell >
279359 < TableCell className = "font-medium" >
280360 < div className = "truncate" title = { server . name } >
281361 { server . name }
@@ -427,6 +507,30 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
427507 </ AlertDialogFooter >
428508 </ AlertDialogContent >
429509 </ AlertDialog >
510+
511+ { /* Bulk Delete Confirmation Dialog */ }
512+ < AlertDialog open = { bulkDeleteDialogOpen } onOpenChange = { setBulkDeleteDialogOpen } >
513+ < AlertDialogContent >
514+ < AlertDialogHeader >
515+ < AlertDialogTitle > Delete selected servers?</ AlertDialogTitle >
516+ < AlertDialogDescription >
517+ This action cannot be undone. This will permanently delete { selectedServerIds . size } server(s) and all of their monitoring data.
518+ </ AlertDialogDescription >
519+ </ AlertDialogHeader >
520+ < AlertDialogFooter >
521+ < AlertDialogCancel disabled = { isBulkDeleting } >
522+ { t ( 'cancel' ) }
523+ </ AlertDialogCancel >
524+ < AlertDialogAction
525+ onClick = { confirmBulkDelete }
526+ disabled = { isBulkDeleting }
527+ className = "bg-red-600 text-white hover:bg-red-700"
528+ >
529+ { isBulkDeleting ? t ( 'deleting' ) : 'Delete Selected' }
530+ </ AlertDialogAction >
531+ </ AlertDialogFooter >
532+ </ AlertDialogContent >
533+ </ AlertDialog >
430534 </ >
431535 ) ;
432536} ;
0 commit comments