fix(search): clean up node/property search matches
- add tests for notes, property key/value, hidden-property exclusion, matched-value display - fix flatMap indentation and stray semicolon - use stable match key instead of display_value (avoids dup-key collisions) - keep label searchable but not repeated in matched-values column
This commit is contained in:
@@ -52,8 +52,11 @@ export function SearchBar({ onOpenPending }: SearchBarProps) {
|
|||||||
|
|
||||||
const matches: Array<{ key: string; display_value: string }> = []
|
const matches: Array<{ key: string; display_value: string }> = []
|
||||||
|
|
||||||
|
// Label matches make the node a result, but it is already shown in the
|
||||||
|
// header — don't repeat it in the matched-values column.
|
||||||
|
const labelMatch = contains(n.data.label)
|
||||||
|
|
||||||
for (const [key, value] of [
|
for (const [key, value] of [
|
||||||
['label', n.data.label],
|
|
||||||
['notes', n.data.notes],
|
['notes', n.data.notes],
|
||||||
['ip', n.data.ip],
|
['ip', n.data.ip],
|
||||||
['hostname', n.data.hostname],
|
['hostname', n.data.hostname],
|
||||||
@@ -78,10 +81,9 @@ export function SearchBar({ onOpenPending }: SearchBarProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches.length > 0 ? [{ node: n, matches: matches }] : []
|
return labelMatch || matches.length > 0 ? [{ node: n, matches }] : []
|
||||||
})
|
})
|
||||||
: [];
|
: []
|
||||||
|
|
||||||
|
|
||||||
const pendingResults = q
|
const pendingResults = q
|
||||||
? pendingDevices.filter((d) =>
|
? pendingDevices.filter((d) =>
|
||||||
@@ -193,8 +195,8 @@ export function SearchBar({ onOpenPending }: SearchBarProps) {
|
|||||||
</span>
|
</span>
|
||||||
{n.matches.length > 0 && (
|
{n.matches.length > 0 && (
|
||||||
<span style={{ maxWidth: "50%", display: 'flex', flexDirection: 'column', gap: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
<span style={{ maxWidth: "50%", display: 'flex', flexDirection: 'column', gap: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
{n.matches.map((m) => (
|
{n.matches.map((m, mIdx) => (
|
||||||
<span key={m.display_value} style={{ fontSize: 11, color: '#8b949e', fontFamily: 'JetBrains Mono, monospace', flexShrink: 0, display: 'inline-block' }}>
|
<span key={`${m.key}-${mIdx}`} style={{ fontSize: 11, color: '#8b949e', fontFamily: 'JetBrains Mono, monospace', flexShrink: 0, display: 'inline-block' }}>
|
||||||
{m.display_value}
|
{m.display_value}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -103,6 +103,50 @@ describe('SearchBar', () => {
|
|||||||
expect(screen.queryByText('DB Server')).toBeNull()
|
expect(screen.queryByText('DB Server')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('filters by notes', () => {
|
||||||
|
setupStore([
|
||||||
|
makeNode('n1', { data: { label: 'Server A', type: 'server', status: 'online', services: [], ip: null, hostname: null, notes: 'backup target every night' } }),
|
||||||
|
makeNode('n2', { data: { label: 'Server B', type: 'server', status: 'online', services: [], ip: null, hostname: null, notes: 'primary web host' } }),
|
||||||
|
])
|
||||||
|
render(<SearchBar />)
|
||||||
|
openSearch()
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'backup' } })
|
||||||
|
expect(screen.getByText('Server A')).toBeDefined()
|
||||||
|
expect(screen.queryByText('Server B')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filters by visible property key or value', () => {
|
||||||
|
setupStore([
|
||||||
|
makeNode('n1', { data: { label: 'Server A', type: 'server', status: 'online', services: [], ip: null, hostname: null, properties: [{ key: 'rack', value: 'B12', icon: null, visible: true }] } }),
|
||||||
|
makeNode('n2', { data: { label: 'Server B', type: 'server', status: 'online', services: [], ip: null, hostname: null, properties: [{ key: 'rack', value: 'A01', icon: null, visible: true }] } }),
|
||||||
|
])
|
||||||
|
render(<SearchBar />)
|
||||||
|
openSearch()
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'b12' } })
|
||||||
|
expect(screen.getByText('Server A')).toBeDefined()
|
||||||
|
expect(screen.queryByText('Server B')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores hidden properties', () => {
|
||||||
|
setupStore([
|
||||||
|
makeNode('n1', { data: { label: 'Server A', type: 'server', status: 'online', services: [], ip: null, hostname: null, properties: [{ key: 'secret', value: 'hidden-val', icon: null, visible: false }] } }),
|
||||||
|
])
|
||||||
|
render(<SearchBar />)
|
||||||
|
openSearch()
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'hidden-val' } })
|
||||||
|
expect(screen.queryByText('Server A')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the matched value alongside the node label', () => {
|
||||||
|
setupStore([
|
||||||
|
makeNode('n1', { data: { label: 'Server A', type: 'server', status: 'online', services: [], ip: null, hostname: null, properties: [{ key: 'rack', value: 'B12', icon: null, visible: true }] } }),
|
||||||
|
])
|
||||||
|
render(<SearchBar />)
|
||||||
|
openSearch()
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/search/i), { target: { value: 'b12' } })
|
||||||
|
expect(screen.getByText('rack = B12')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('excludes groupRect nodes from results', () => {
|
it('excludes groupRect nodes from results', () => {
|
||||||
setupStore([
|
setupStore([
|
||||||
makeNode('gr1', { data: { label: 'DMZ Zone', type: 'groupRect', status: 'unknown', services: [], ip: null, hostname: null } }),
|
makeNode('gr1', { data: { label: 'DMZ Zone', type: 'groupRect', status: 'unknown', services: [], ip: null, hostname: null } }),
|
||||||
|
|||||||
Reference in New Issue
Block a user