feat: add 'With services' filter to Device Inventory
ha-relevant: maybe
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Globe, Router, Server, Layers, Box, Container, HardDrive, Cpu, Wifi, Circle, Network,
|
Globe, Router, Server, Layers, Box, Container, HardDrive, Cpu, Wifi, Circle, Network,
|
||||||
Search, RefreshCw, X, CheckCircle2, EyeOff, Trash2, Loader2,
|
Search, RefreshCw, X, CheckCircle2, EyeOff, Trash2, Loader2, ServerCog,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { scanApi } from '@/api/client'
|
import { scanApi } from '@/api/client'
|
||||||
@@ -121,6 +121,8 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
|||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>(initialStatus)
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>(initialStatus)
|
||||||
// Inventory shows on-canvas devices by default; toggle off to hide them.
|
// Inventory shows on-canvas devices by default; toggle off to hide them.
|
||||||
const [showOnCanvas, setShowOnCanvas] = useState(true)
|
const [showOnCanvas, setShowOnCanvas] = useState(true)
|
||||||
|
// Optionally restrict to devices that have at least one detected service.
|
||||||
|
const [withServicesOnly, setWithServicesOnly] = useState(false)
|
||||||
const { addNode, scanEventTs } = useCanvasStore()
|
const { addNode, scanEventTs } = useCanvasStore()
|
||||||
const highlightRef = useRef<HTMLButtonElement>(null)
|
const highlightRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
@@ -163,6 +165,7 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
|||||||
if (typeFilter !== 'all' && d.suggested_type !== typeFilter) return false
|
if (typeFilter !== 'all' && d.suggested_type !== typeFilter) return false
|
||||||
// Inventory-only: optionally hide devices already placed on a canvas.
|
// Inventory-only: optionally hide devices already placed on a canvas.
|
||||||
if (statusFilter === 'pending' && !showOnCanvas && (d.canvas_count ?? 0) > 0) return false
|
if (statusFilter === 'pending' && !showOnCanvas && (d.canvas_count ?? 0) > 0) return false
|
||||||
|
if (withServicesOnly && (d.services?.length ?? 0) === 0) return false
|
||||||
if (q) {
|
if (q) {
|
||||||
const hay = [
|
const hay = [
|
||||||
d.friendly_name, d.hostname, d.ip, d.mac, d.ieee_address, d.vendor, d.model,
|
d.friendly_name, d.hostname, d.ip, d.mac, d.ieee_address, d.vendor, d.model,
|
||||||
@@ -172,7 +175,7 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}, [devices, search, sourceFilter, typeFilter, statusFilter, showOnCanvas])
|
}, [devices, search, sourceFilter, typeFilter, statusFilter, showOnCanvas, withServicesOnly])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!highlightId || loading || !open) return
|
if (!highlightId || loading || !open) return
|
||||||
@@ -505,6 +508,15 @@ export function PendingDevicesModal({ open, onClose, highlightId, initialStatus
|
|||||||
{showOnCanvas ? 'Hide on-canvas' : 'Show on-canvas'}
|
{showOnCanvas ? 'Hide on-canvas' : 'Show on-canvas'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => setWithServicesOnly((v) => !v)}
|
||||||
|
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded border transition-colors ${withServicesOnly ? 'bg-[#00d4ff]/20 text-[#00d4ff] border-[#00d4ff]/50' : 'bg-[#0d1117] text-muted-foreground border-border hover:text-foreground'}`}
|
||||||
|
title="Only show devices with at least one detected service"
|
||||||
|
aria-pressed={withServicesOnly}
|
||||||
|
>
|
||||||
|
<ServerCog size={12} />
|
||||||
|
With services
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => selectMode ? exitSelectMode() : enterSelectMode()}
|
onClick={() => selectMode ? exitSelectMode() : enterSelectMode()}
|
||||||
className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${selectMode ? 'bg-[#00d4ff]/20 text-[#00d4ff] border-[#00d4ff]/50' : 'bg-[#0d1117] text-muted-foreground border-border hover:text-foreground'}`}
|
className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${selectMode ? 'bg-[#00d4ff]/20 text-[#00d4ff] border-[#00d4ff]/50' : 'bg-[#0d1117] text-muted-foreground border-border hover:text-foreground'}`}
|
||||||
|
|||||||
@@ -309,6 +309,15 @@ describe('PendingDevicesModal', () => {
|
|||||||
expect(screen.queryByLabelText(/On \d+ canvas/)).not.toBeInTheDocument()
|
expect(screen.queryByLabelText(/On \d+ canvas/)).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('filters to devices with detected services when "With services" is on', async () => {
|
||||||
|
// dev-a has an http service; dev-b (zigbee) has none.
|
||||||
|
render(<PendingDevicesModal {...baseProps} />)
|
||||||
|
await waitFor(() => expect(screen.getByTestId('pending-card-dev-b')).toBeInTheDocument())
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /With services/ }))
|
||||||
|
expect(screen.getByTestId('pending-card-dev-a')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('pending-card-dev-b')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('shows on-canvas devices by default and hides them when toggled off', async () => {
|
it('shows on-canvas devices by default and hides them when toggled off', async () => {
|
||||||
mockPending.mockResolvedValue({
|
mockPending.mockResolvedValue({
|
||||||
data: [DEVICE_IP, { ...DEVICE_ZIGBEE, canvas_count: 1 }],
|
data: [DEVICE_IP, { ...DEVICE_ZIGBEE, canvas_count: 1 }],
|
||||||
|
|||||||
Reference in New Issue
Block a user