feat(ui): add delete button to fleet table rows

Trash icon appears on row hover. Confirms before deleting.
Both virtual and non-virtual row renderers have group/row
class for the hover-to-show pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-22 08:21:51 -05:00
parent 1cb869a195
commit f47438a5a5

View File

@@ -2,7 +2,7 @@ import { useRef, useState, useCallback } from 'react'
import { Link, useNavigate } from '@tanstack/react-router' import { Link, useNavigate } from '@tanstack/react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useVirtualizer } from '@tanstack/react-virtual' import { useVirtualizer } from '@tanstack/react-virtual'
import { ChevronUp, ChevronDown, ChevronsUpDown, Monitor, MapPin, Router, Network } from 'lucide-react' import { ChevronUp, ChevronDown, ChevronsUpDown, Monitor, MapPin, Router, Network, Trash2 } from 'lucide-react'
import { devicesApi, sitesApi, type DeviceResponse } from '@/lib/api' import { devicesApi, sitesApi, type DeviceResponse } from '@/lib/api'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { useShortcut } from '@/hooks/useShortcut' import { useShortcut } from '@/hooks/useShortcut'
@@ -165,6 +165,20 @@ export function FleetTable({
}, },
}) })
const deleteMutation = useMutation({
mutationFn: (deviceId: string) => devicesApi.delete(tenantId, deviceId),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['devices'] })
},
})
function handleDeleteDevice(e: React.MouseEvent, deviceId: string, hostname: string) {
e.stopPropagation()
if (confirm(`Delete ${hostname}? This cannot be undone.`)) {
deleteMutation.mutate(deviceId)
}
}
const { data, isLoading, isFetching } = useQuery({ const { data, isLoading, isFetching } = useQuery({
queryKey: ['devices', tenantId, { search, status, deviceType, sortBy, sortDir, page, pageSize }], queryKey: ['devices', tenantId, { search, status, deviceType, sortBy, sortDir, page, pageSize }],
queryFn: () => queryFn: () =>
@@ -322,6 +336,15 @@ export function FleetTable({
))} ))}
</div> </div>
</td> </td>
<td className="px-2 py-1.5 w-8">
<button
className="opacity-0 group-hover/row:opacity-100 transition-opacity text-text-muted hover:text-error p-0.5"
onClick={(e) => handleDeleteDevice(e, device.id, device.hostname)}
title="Delete device"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</td>
</> </>
) )
} }
@@ -348,6 +371,7 @@ export function FleetTable({
<SortHeader column="uptime_seconds" label="Uptime" {...sortProps} className="text-right" /> <SortHeader column="uptime_seconds" label="Uptime" {...sortProps} className="text-right" />
<SortHeader column="last_seen" label="Last Seen" {...sortProps} className="text-left" /> <SortHeader column="last_seen" label="Last Seen" {...sortProps} className="text-left" />
<th scope="col" className="px-2 py-2 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-left">Tags</th> <th scope="col" className="px-2 py-2 text-[10px] uppercase tracking-wider font-semibold text-text-muted text-left">Tags</th>
<th scope="col" className="px-2 py-2 w-8"><span className="sr-only">Actions</span></th>
</tr> </tr>
</thead> </thead>
) )
@@ -417,7 +441,7 @@ export function FleetTable({
data-index={virtualRow.index} data-index={virtualRow.index}
ref={virtualizer.measureElement} ref={virtualizer.measureElement}
className={cn( className={cn(
'border-b border-border/50 hover:bg-elevated/30 transition-[background-color] duration-[50ms]', 'border-b border-border/50 hover:bg-elevated/30 transition-[background-color] duration-[50ms] group/row',
selectedIndex === virtualRow.index && 'bg-elevated/50', selectedIndex === virtualRow.index && 'bg-elevated/50',
)} )}
style={{ style={{
@@ -475,7 +499,7 @@ export function FleetTable({
key={device.id} key={device.id}
data-testid={`device-row-${device.hostname}`} data-testid={`device-row-${device.hostname}`}
className={cn( className={cn(
'border-b border-border/50 hover:bg-elevated/30 transition-[background-color] duration-[50ms]', 'border-b border-border/50 hover:bg-elevated/30 transition-[background-color] duration-[50ms] group/row',
selectedIndex === idx && 'bg-elevated/50', selectedIndex === idx && 'bg-elevated/50',
)} )}
> >