feat: extend search (Ctrl+F and Ctrl+K) to include pending devices
Both SearchBar and SearchModal now fetch and search pending devices by IP, hostname, and service name. Selecting a pending result opens the sidebar to the Pending tab and highlights the matching device.
This commit is contained in:
@@ -2,15 +2,27 @@ import { useState, useEffect, useRef } from 'react'
|
||||
import { useReactFlow } from '@xyflow/react'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import { useCanvasStore } from '@/stores/canvasStore'
|
||||
import { scanApi } from '@/api/client'
|
||||
import { NODE_TYPE_LABELS } from '@/types'
|
||||
import type { PendingDevice } from '@/components/modals/PendingDeviceModal'
|
||||
|
||||
export function SearchBar() {
|
||||
interface SearchBarProps {
|
||||
onOpenPending?: (deviceId: string) => void
|
||||
}
|
||||
|
||||
export function SearchBar({ onOpenPending }: SearchBarProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [pendingDevices, setPendingDevices] = useState<PendingDevice[]>([])
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const { nodes, setSelectedNode } = useCanvasStore()
|
||||
const { setCenter } = useReactFlow()
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
scanApi.pending().then((res) => setPendingDevices(res.data)).catch(() => {})
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||
@@ -31,7 +43,7 @@ export function SearchBar() {
|
||||
}, [open])
|
||||
|
||||
const q = query.toLowerCase().trim()
|
||||
const results = q
|
||||
const nodeResults = q
|
||||
? nodes.filter((n) => {
|
||||
if (n.data.type === 'groupRect') return false
|
||||
return (
|
||||
@@ -43,6 +55,19 @@ export function SearchBar() {
|
||||
})
|
||||
: []
|
||||
|
||||
const pendingResults = q
|
||||
? pendingDevices.filter((d) =>
|
||||
d.ip.toLowerCase().includes(q) ||
|
||||
d.hostname?.toLowerCase().includes(q) ||
|
||||
d.services.some((s) =>
|
||||
s.service_name?.toLowerCase().includes(q) ||
|
||||
s.category?.toLowerCase().includes(q)
|
||||
)
|
||||
).slice(0, 4)
|
||||
: []
|
||||
|
||||
const totalResults = nodeResults.length + pendingResults.length
|
||||
|
||||
const goToNode = (id: string) => {
|
||||
const node = nodes.find((n) => n.id === id)
|
||||
if (!node) return
|
||||
@@ -101,7 +126,7 @@ export function SearchBar() {
|
||||
/>
|
||||
{query && (
|
||||
<span style={{ fontSize: 11, color: '#6e7681', flexShrink: 0 }}>
|
||||
{results.length} result{results.length !== 1 ? 's' : ''}
|
||||
{totalResults} result{totalResults !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
@@ -113,9 +138,9 @@ export function SearchBar() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{results.length > 0 && (
|
||||
{totalResults > 0 && (
|
||||
<div style={{ borderTop: '1px solid #30363d', maxHeight: 260, overflowY: 'auto' }}>
|
||||
{results.map((n) => (
|
||||
{nodeResults.map((n) => (
|
||||
<button
|
||||
key={n.id}
|
||||
onClick={() => goToNode(n.id)}
|
||||
@@ -146,10 +171,43 @@ export function SearchBar() {
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{pendingResults.length > 0 && nodeResults.length > 0 && (
|
||||
<div style={{ height: 1, background: '#30363d', margin: '2px 0' }} />
|
||||
)}
|
||||
{pendingResults.map((d) => {
|
||||
const serviceName = d.services.find((s) => s.service_name)?.service_name
|
||||
return (
|
||||
<button
|
||||
key={d.id}
|
||||
onClick={() => { onOpenPending?.(d.id); setOpen(false); setQuery('') }}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
padding: '7px 12px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#21262d')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'none')}
|
||||
>
|
||||
<span style={{ fontSize: 10, color: '#e3b341', fontFamily: 'JetBrains Mono, monospace', flexShrink: 0 }}>pending</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#e6edf3', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{d.hostname ?? d.ip}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: '#8b949e', fontFamily: 'JetBrains Mono, monospace', flexShrink: 0 }}>
|
||||
{serviceName ?? d.ip}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{q && results.length === 0 && (
|
||||
{q && totalResults === 0 && (
|
||||
<div style={{ borderTop: '1px solid #30363d', padding: '10px 12px', fontSize: 12, color: '#6e7681', textAlign: 'center' }}>
|
||||
No results for “{query}”
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user