feat: show Z-Wave scan runs in Scan History
Scan History now recognises kind=zwave: dedicated Z-Wave filter chip, badge (RadioTower, orange) and import-done toast, instead of falling back to the generic IP type. ha-relevant: yes
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { RefreshCw, X, Loader2, StopCircle, Clock, ScanLine, Network, Inbox } from 'lucide-react'
|
import { RefreshCw, X, Loader2, StopCircle, Clock, ScanLine, Network, RadioTower, Inbox } from 'lucide-react'
|
||||||
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { scanApi } from '@/api/client'
|
import { scanApi } from '@/api/client'
|
||||||
@@ -22,7 +22,18 @@ interface ScanHistoryModalProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type KindFilter = 'all' | 'ip' | 'zigbee'
|
type KindFilter = 'all' | 'ip' | 'zigbee' | 'zwave'
|
||||||
|
|
||||||
|
/** Normalise a ScanRun.kind into one of the known display kinds. */
|
||||||
|
function runKind(kind: string | undefined): 'ip' | 'zigbee' | 'zwave' {
|
||||||
|
return kind === 'zigbee' ? 'zigbee' : kind === 'zwave' ? 'zwave' : 'ip'
|
||||||
|
}
|
||||||
|
|
||||||
|
const KIND_META = {
|
||||||
|
ip: { label: 'IP', color: '#a855f7' },
|
||||||
|
zigbee: { label: 'Zigbee', color: '#00d4ff' },
|
||||||
|
zwave: { label: 'Z-Wave', color: '#ff6e00' },
|
||||||
|
} as const
|
||||||
type StatusFilter = 'all' | 'running' | 'done' | 'error' | 'cancelled'
|
type StatusFilter = 'all' | 'running' | 'done' | 'error' | 'cancelled'
|
||||||
|
|
||||||
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
|
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
|
||||||
@@ -37,6 +48,7 @@ const KIND_FILTERS: { key: KindFilter; label: string }[] = [
|
|||||||
{ key: 'all', label: 'All' },
|
{ key: 'all', label: 'All' },
|
||||||
{ key: 'ip', label: 'IP' },
|
{ key: 'ip', label: 'IP' },
|
||||||
{ key: 'zigbee', label: 'Zigbee' },
|
{ key: 'zigbee', label: 'Zigbee' },
|
||||||
|
{ key: 'zwave', label: 'Z-Wave' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function statusColor(s: string): string {
|
function statusColor(s: string): string {
|
||||||
@@ -90,8 +102,9 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
|
|||||||
toast.error(`Scan failed: ${run.error ?? 'unknown error'}`)
|
toast.error(`Scan failed: ${run.error ?? 'unknown error'}`)
|
||||||
}
|
}
|
||||||
if (prev?.status === 'running' && run.status === 'done') {
|
if (prev?.status === 'running' && run.status === 'done') {
|
||||||
if (run.kind === 'zigbee') {
|
if (run.kind === 'zigbee' || run.kind === 'zwave') {
|
||||||
toast.success(`Zigbee import done — ${run.devices_found} device${run.devices_found !== 1 ? 's' : ''}`)
|
const label = run.kind === 'zwave' ? 'Z-Wave' : 'Zigbee'
|
||||||
|
toast.success(`${label} import done — ${run.devices_found} device${run.devices_found !== 1 ? 's' : ''}`)
|
||||||
}
|
}
|
||||||
useCanvasStore.getState().notifyScanDeviceFound()
|
useCanvasStore.getState().notifyScanDeviceFound()
|
||||||
}
|
}
|
||||||
@@ -143,7 +156,7 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filtered = runs.filter((r) => {
|
const filtered = runs.filter((r) => {
|
||||||
const k = r.kind === 'zigbee' ? 'zigbee' : 'ip'
|
const k = runKind(r.kind)
|
||||||
if (kindFilter !== 'all' && k !== kindFilter) return false
|
if (kindFilter !== 'all' && k !== kindFilter) return false
|
||||||
if (statusFilter !== 'all' && r.status !== statusFilter) return false
|
if (statusFilter !== 'all' && r.status !== statusFilter) return false
|
||||||
return true
|
return true
|
||||||
@@ -216,7 +229,9 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{filtered.map((r) => {
|
{filtered.map((r) => {
|
||||||
const isZigbee = r.kind === 'zigbee'
|
const kind = runKind(r.kind)
|
||||||
|
const meta = KIND_META[kind]
|
||||||
|
const KindIcon = kind === 'zigbee' ? Network : kind === 'zwave' ? RadioTower : ScanLine
|
||||||
return (
|
return (
|
||||||
<div key={r.id} className="rounded-lg border border-border bg-[#161b22] p-3">
|
<div key={r.id} className="rounded-lg border border-border bg-[#161b22] p-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -225,12 +240,10 @@ export function ScanHistoryModal({ open, onClose }: ScanHistoryModalProps) {
|
|||||||
{r.status === 'running' && <Loader2 size={12} className="animate-spin text-[#e3b341]" />}
|
{r.status === 'running' && <Loader2 size={12} className="animate-spin text-[#e3b341]" />}
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1 text-[10px] font-mono px-1.5 py-0.5 rounded uppercase tracking-wider"
|
className="inline-flex items-center gap-1 text-[10px] font-mono px-1.5 py-0.5 rounded uppercase tracking-wider"
|
||||||
style={isZigbee
|
style={{ background: `${meta.color}22`, color: meta.color }}
|
||||||
? { background: '#00d4ff22', color: '#00d4ff' }
|
|
||||||
: { background: '#a855f722', color: '#a855f7' }}
|
|
||||||
>
|
>
|
||||||
{isZigbee ? <Network size={10} /> : <ScanLine size={10} />}
|
<KindIcon size={10} />
|
||||||
{isZigbee ? 'Zigbee' : 'IP'}
|
{meta.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto text-xs text-muted-foreground font-mono">
|
<span className="ml-auto text-xs text-muted-foreground font-mono">
|
||||||
{r.devices_found} found
|
{r.devices_found} found
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ const ZIGBEE_RUN = {
|
|||||||
error: null,
|
error: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ZWAVE_RUN = {
|
||||||
|
id: 'run-5',
|
||||||
|
status: 'done',
|
||||||
|
kind: 'zwave',
|
||||||
|
ranges: [],
|
||||||
|
devices_found: 5,
|
||||||
|
started_at: new Date().toISOString(),
|
||||||
|
finished_at: new Date().toISOString(),
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
function renderModal() {
|
function renderModal() {
|
||||||
return render(
|
return render(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -155,4 +166,14 @@ describe('ScanHistoryModal', () => {
|
|||||||
expect(screen.getByText('7 found')).toBeDefined()
|
expect(screen.getByText('7 found')).toBeDefined()
|
||||||
expect(screen.queryByText('3 found')).toBeNull()
|
expect(screen.queryByText('3 found')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('filters by zwave kind', async () => {
|
||||||
|
vi.mocked(scanApi.runs).mockResolvedValue({ data: [DONE_RUN, ZWAVE_RUN] } as never)
|
||||||
|
renderModal()
|
||||||
|
await waitFor(() => expect(screen.getAllByText('done').length).toBe(2))
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Z-Wave' }))
|
||||||
|
// Only the zwave run (5 found) remains
|
||||||
|
expect(screen.getByText('5 found')).toBeDefined()
|
||||||
|
expect(screen.queryByText('3 found')).toBeNull()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user