Skip to content

Commit e881f35

Browse files
roylibmanclaude
authored andcommitted
Search findings after the table's other filters and tighten a11y and tokens
Rows hidden by column, problem, label or owner filters no longer spend the findings budget; other kinds keep the name filter first. A spaced finding match is capped at 256 UTF-8 bytes like server search. The run view's image hint is announced to screen readers, and the new notices use theme tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b9e926b commit e881f35

6 files changed

Lines changed: 50 additions & 20 deletions

File tree

‎packages/k8s-ui/src/components/resources/ResourcesView.tsx‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ import {
148148
SEVERITY_DOT_COLOR,
149149
healthColors,
150150
} from './resource-utils'
151-
import { TRIVY_GROUP, searchTrivyFindings, type TrivyFindingMatch } from './resource-utils-trivy'
151+
import { TRIVY_GROUP, searchTrivyFindings, type TrivyFindingMatch, type TrivyFindingsSearch } from './resource-utils-trivy'
152152
import { getGenericResourceStatus } from './generic-status'
153153
import { ActiveFilterBar } from './ActiveFilterBar'
154154
import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
@@ -5463,21 +5463,18 @@ export function ResourcesView({
54635463
}
54645464
}, [regexMode, deferredSearchTerm])
54655465

5466-
// Regex mode stays on names: a pattern run over every finding could
5467-
// backtrack for seconds on the main thread.
54685466
const isTrivyKind = selectedKind.group === TRIVY_GROUP
5469-
const findingsSearch = useMemo(() => {
5470-
if (!tableActive || !resources || !deferredSearchTerm || regexMode || !isTrivyKind) return null
5471-
return searchTrivyFindings(resources, deferredSearchTerm.toLowerCase())
5472-
}, [tableActive, resources, deferredSearchTerm, regexMode, isTrivyKind])
54735467

54745468
// Filter resources by search term, status, problems, and sort
5475-
const filteredResources = useMemo(() => {
5476-
if (!tableActive || !resources) return []
5469+
const { list: filteredResources, findings: findingsSearch } = useMemo((): { list: any[]; findings: TrivyFindingsSearch | null } => {
5470+
if (!tableActive || !resources) return { list: [], findings: null }
54775471

54785472
let result = resources
5473+
let findings: TrivyFindingsSearch | null = null
54795474

5480-
// Apply search filter
5475+
// Apply search filter. On Trivy report kinds a plain search also matches
5476+
// findings and runs below, after the other filters. Regex mode stays on
5477+
// names: a pattern run over every finding could backtrack for seconds.
54815478
if (deferredSearchTerm) {
54825479
if (regexMode) {
54835480
const re = searchRegex.re
@@ -5487,12 +5484,11 @@ export function ResourcesView({
54875484
re.test(r.metadata?.namespace ?? '')
54885485
)
54895486
}
5490-
} else {
5487+
} else if (!isTrivyKind) {
54915488
const term = deferredSearchTerm.toLowerCase()
54925489
result = result.filter((r: any) =>
54935490
r.metadata?.name?.toLowerCase().includes(term) ||
5494-
r.metadata?.namespace?.toLowerCase().includes(term) ||
5495-
!!findingsSearch?.matches.has(r)
5491+
r.metadata?.namespace?.toLowerCase().includes(term)
54965492
)
54975493
}
54985494
}
@@ -5564,6 +5560,19 @@ export function ResourcesView({
55645560
})
55655561
}
55665562

5563+
// Trivy findings are searched after the other filters, so rows they hide
5564+
// don't spend the findings budget.
5565+
if (deferredSearchTerm && !regexMode && isTrivyKind) {
5566+
const term = deferredSearchTerm.toLowerCase()
5567+
const matched = searchTrivyFindings(result, term)
5568+
findings = matched
5569+
result = result.filter((r: any) =>
5570+
r.metadata?.name?.toLowerCase().includes(term) ||
5571+
r.metadata?.namespace?.toLowerCase().includes(term) ||
5572+
matched.matches.has(r)
5573+
)
5574+
}
5575+
55675576
// Apply custom sorting if set. Extra columns override
55685577
// the built-in getSortValue for their key.
55695578
if (effectiveSortColumn && sortDirection) {
@@ -5659,8 +5668,8 @@ export function ResourcesView({
56595668
}
56605669
}
56615670

5662-
return result
5663-
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, findingsSearch, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
5671+
return { list: result, findings }
5672+
}, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, isTrivyKind, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
56645673

56655674
// For nodes table: compute the majority minor version so outliers can be highlighted
56665675
const majorityNodeMinorVersion = useMemo(() => {
@@ -6630,7 +6639,7 @@ export function ResourcesView({
66306639
</button>
66316640
)}
66326641
{searchTerm && isTrivyKind && (
6633-
<p className="text-xs mt-1 text-theme-text-disabled">
6642+
<p className="text-xs mt-1 text-theme-text-tertiary">
66346643
{regexMode
66356644
? 'Regex search matches names and namespaces only.'
66366645
: 'Searched names, namespaces and report findings (CVE, package, check ID, image).'}

‎packages/k8s-ui/src/components/resources/resource-utils-trivy.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ describe('searchTrivyFindings', () => {
5151
expect(searchTrivyFindings([vuln], 'php8.').matches.size).toBe(0)
5252
})
5353

54+
it('caps a finding at 256 UTF-8 bytes for phrases, like server search', () => {
55+
const wide = { metadata: { name: 'r', namespace: 'n' }, report: { components: { components: [{ name: 'é'.repeat(128), version: 'needle' }] } } }
56+
expect(searchTrivyFindings([wide], 'é needle').matches.size).toBe(0)
57+
expect(searchTrivyFindings([wide], 'needle').matches.size).toBe(1)
58+
})
59+
5460
it('matches a phrase across one finding, never across findings', () => {
5561
expect(searchTrivyFindings([vuln], 'php83 8.3.20').matches.has(vuln)).toBe(true)
5662
expect(searchTrivyFindings([vuln], 'libssl3 8.3.20').matches.has(vuln)).toBe(false)

‎packages/k8s-ui/src/components/resources/resource-utils-trivy.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,23 @@ function asArray(v: unknown): any[] {
8787
return Array.isArray(v) ? v : []
8888
}
8989

90+
// findingLabel joins a finding's parts and caps it at MAX_FINDING_LABEL
91+
// UTF-8 bytes on a character boundary, as server search does, so a phrase
92+
// that falls past the cap matches in neither place.
9093
function findingLabel(...parts: string[]): string {
91-
return parts.filter(Boolean).join(' ').slice(0, MAX_FINDING_LABEL)
94+
const label = parts.filter(Boolean).join(' ')
95+
// A UTF-16 unit is at most 3 UTF-8 bytes, so short labels need no count.
96+
if (label.length * 3 <= MAX_FINDING_LABEL) return label
97+
let bytes = 0
98+
let end = 0
99+
for (const ch of label) {
100+
const cp = ch.codePointAt(0) ?? 0
101+
const size = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4
102+
if (bytes + size > MAX_FINDING_LABEL) break
103+
bytes += size
104+
end += ch.length
105+
}
106+
return label.slice(0, end)
92107
}
93108

94109
// trivyReportImageRef renders a report's scanned image the way search

‎web/src/components/execution/BatchExecutionView.image-browse.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ describe('template image in the run view', () => {
4141
auth.enabled = true
4242
const html = render()
4343
expect(html).not.toMatch(new RegExp(`<button[^>]*>${image}</button>`))
44-
expect(html).toMatch(new RegExp(`<span class="[^"]*decoration-dotted[^"]*" tabindex="0">${image}</span>`))
44+
expect(html).toMatch(new RegExp(`<span class="[^"]*decoration-dotted[^"]*" tabindex="0">${image}<span class="sr-only">. With sign-in on, browse an image&#x27;s files from one of its Pods.</span></span>`))
4545
})
4646
})

‎web/src/components/execution/BatchExecutionView.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,7 @@ function DefinitionFact({ label, value, mono = false, onClick, tooltip, hint = f
770770
<Tooltip content={tooltip ?? value} delay={300} wrapperClassName="min-w-0 text-right">
771771
{onClick
772772
? <button type="button" onClick={onClick} className={clsx(valueClassName, 'text-right')}>{value}</button>
773-
: <span className={valueClassName} tabIndex={hint ? 0 : undefined}>{value}</span>}
773+
: <span className={valueClassName} tabIndex={hint ? 0 : undefined}>{value}{hint && tooltip && <span className="sr-only">. {tooltip}</span>}</span>}
774774
</Tooltip>
775775
</div>
776776
)

‎web/src/components/ui/Omnibar.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
498498
))
499499
)}
500500
{!isFetching && !isError && partialNotices.map((notice) => (
501-
<div key={notice} className="flex items-start gap-2 px-3 py-1.5 text-[11px] text-amber-600 dark:text-amber-400">
501+
<div key={notice} className="flex items-start gap-2 px-3 py-1.5 text-xs text-warning-text">
502502
<AlertTriangle className="w-3 h-3 mt-0.5 shrink-0" /> {notice}
503503
</div>
504504
))}

0 commit comments

Comments
 (0)