@skyhook-io/k8s-ui 1.17.1 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +13 -0
  2. package/package.json +4 -4
  3. package/src/components/compare/CompareResourcePicker.tsx +18 -4
  4. package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
  5. package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
  6. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -2
  7. package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
  8. package/src/components/gitops/GitOpsTableView.tsx +33 -4
  9. package/src/components/gitops/detail-helpers.test.ts +58 -13
  10. package/src/components/gitops/detail-helpers.ts +37 -6
  11. package/src/components/gitops/index.ts +1 -0
  12. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +27 -0
  13. package/src/components/gitops/insights/GitOpsInsightViews.tsx +16 -10
  14. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  15. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  16. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +73 -9
  17. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +45 -28
  18. package/src/components/gitops/tree/tree-helpers.test.ts +6 -0
  19. package/src/components/gitops/tree/tree-helpers.ts +5 -1
  20. package/src/components/issues/IssuesView.tsx +18 -10
  21. package/src/components/issues/ResourceIssuesSection.tsx +5 -2
  22. package/src/components/issues/diagnostic.ts +2 -0
  23. package/src/components/issues/index.ts +1 -1
  24. package/src/components/issues/issues.test.ts +26 -0
  25. package/src/components/issues/severity.ts +9 -2
  26. package/src/components/issues/types.ts +32 -4
  27. package/src/components/logs/LogCore.theme.test.tsx +126 -1
  28. package/src/components/logs/LogCore.tsx +159 -40
  29. package/src/components/logs/LogsViewer.test.tsx +183 -60
  30. package/src/components/logs/LogsViewer.tsx +49 -12
  31. package/src/components/logs/StructuredLogLine.tsx +1 -1
  32. package/src/components/logs/WorkloadLogsViewer.tsx +7 -1
  33. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  34. package/src/components/logs/useLogBuffer.ts +48 -18
  35. package/src/components/logs/useLogSearch.ts +3 -1
  36. package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
  37. package/src/components/namespace-switcher/NamespacePicker.tsx +42 -8
  38. package/src/components/resources/ColumnPickerList.test.tsx +46 -0
  39. package/src/components/resources/ColumnPickerList.tsx +174 -0
  40. package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
  41. package/src/components/resources/ResourcesSidebar.tsx +36 -21
  42. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -2
  43. package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
  44. package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
  45. package/src/components/resources/ResourcesView.tsx +198 -93
  46. package/src/components/resources/column-order.test.ts +104 -0
  47. package/src/components/resources/column-order.ts +64 -0
  48. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  49. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  50. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  51. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  52. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  53. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  54. package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
  55. package/src/components/resources/renderers/PodRenderer.tsx +72 -20
  56. package/src/components/resources/resource-utils-trivy.test.ts +87 -0
  57. package/src/components/resources/resource-utils-trivy.ts +122 -0
  58. package/src/components/resources/resource-utils.ts +15 -2
  59. package/src/components/resources/summarize-scheduler-message.test.ts +1 -0
  60. package/src/components/shared/DetailShell.tsx +9 -7
  61. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  62. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  63. package/src/components/shared/ManagedByChip.test.tsx +22 -0
  64. package/src/components/shared/ManagedByChip.tsx +24 -4
  65. package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
  66. package/src/components/shared/ResourceActionsBar.tsx +12 -6
  67. package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
  68. package/src/components/shared/ResourceRendererDispatch.tsx +48 -31
  69. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  70. package/src/components/timeline/DiffViewer.tsx +113 -4
  71. package/src/components/timeline/TimelineList.tsx +5 -1
  72. package/src/components/timeline/shared.test.tsx +48 -0
  73. package/src/components/timeline/shared.tsx +14 -19
  74. package/src/components/ui/FetchResult.test.tsx +19 -2
  75. package/src/components/ui/FetchResult.tsx +13 -1
  76. package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
  77. package/src/components/ui/MultiSelectPicker.tsx +20 -2
  78. package/src/components/ui/PageHeader.tsx +6 -4
  79. package/src/components/ui/Toast.tsx +48 -30
  80. package/src/components/ui/drawer-components.test.tsx +253 -0
  81. package/src/components/ui/drawer-components.tsx +315 -11
  82. package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
  83. package/src/components/ui/index.ts +1 -1
  84. package/src/components/workload/WorkloadView.relationships.test.tsx +33 -0
  85. package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
  86. package/src/components/workload/WorkloadView.tsx +68 -14
  87. package/src/hooks/index.ts +2 -0
  88. package/src/hooks/useMediaQuery.ts +21 -0
  89. package/src/hooks/usePhoneLayout.ts +12 -0
  90. package/src/theme/tailwind-theme.css +5 -0
  91. package/src/types/core.ts +91 -3
  92. package/src/types/gitops-flux-inventory.test.ts +24 -0
  93. package/src/types/gitops-insights.ts +1 -0
  94. package/src/types/gitops-tree.ts +2 -0
  95. package/src/types/gitops.ts +5 -5
  96. package/src/types/index.ts +3 -0
  97. package/src/types/read-coverage.ts +65 -0
  98. package/src/types/resource-coverage.ts +40 -0
  99. package/src/utils/format.test.ts +7 -0
  100. package/src/utils/format.ts +6 -1
  101. package/src/utils/gitops-owner.test.ts +42 -1
  102. package/src/utils/gitops-owner.ts +22 -0
  103. package/src/utils/index.ts +2 -0
  104. package/src/utils/read-actions.test.ts +22 -0
  105. package/src/utils/read-actions.ts +56 -0
  106. package/src/utils/resource-coverage.test.ts +121 -0
  107. package/src/utils/resource-coverage.ts +117 -0
@@ -1,10 +1,15 @@
1
+ import type { ResourceCoverage } from '../../types/resource-coverage'
2
+ import { resourceCoverageNotice, resourceEmptyMessage, resourceNarrowedNotice } from '../../utils/resource-coverage'
1
3
  import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
2
4
  import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
3
5
  import { useDebouncedValue } from '../../hooks/useDebouncedValue'
6
+ import { Badge } from '../ui/Badge'
4
7
  import { PaneLoader } from '../ui/PaneLoader'
5
8
  import { RestrictedState } from '../ui/RestrictedState'
6
9
  import { FetchResult } from '../ui/FetchResult'
7
10
  import { Input } from '../ui/Input'
11
+ import { ColumnPickerList } from './ColumnPickerList'
12
+ import { applyColumnOrder, sanitizeColumnOrder } from './column-order'
8
13
  import type { TopPodMetrics, TopNodeMetrics, ContainerResourceMetrics } from '../../types'
9
14
  import {
10
15
  Search,
@@ -145,6 +150,7 @@ import {
145
150
  SEVERITY_DOT_COLOR,
146
151
  healthColors,
147
152
  } from './resource-utils'
153
+ import { TRIVY_GROUP, searchTrivyFindings, type TrivyFindingMatch, type TrivyFindingsSearch } from './resource-utils-trivy'
148
154
  import { getGenericResourceStatus } from './generic-status'
149
155
  import { ActiveFilterBar } from './ActiveFilterBar'
150
156
  import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
@@ -153,7 +159,7 @@ import {
153
159
  printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
154
160
  } from './printer-columns'
155
161
  import { SEVERITY_BADGE, SEVERITY_TEXT, healthToSeverity, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
156
- import { pluralize } from '../../utils/pluralize'
162
+ import { englishPlural, isEnglishPlural, pluralize } from '../../utils/pluralize'
157
163
  import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
158
164
  import { formatRelativeAgeTime, parseQuantityToNumber } from '../../utils/format'
159
165
  import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
@@ -161,6 +167,8 @@ import { isRolloutActivityVisible } from '../../utils/workload-rollout'
161
167
  import { FreshnessControl, type FreshnessConnection } from '../ui/FreshnessControl'
162
168
  import { Tooltip } from '../ui/Tooltip'
163
169
  import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
170
+ import type { Issue } from '../issues/types'
171
+ import { issueTitle } from '../issues/severity'
164
172
  import { SEVERITY_TEXT_CLASS } from '../checks/severity'
165
173
  // CRD-specific cell components (extracted)
166
174
  import { GitRepositoryCell, OCIRepositoryCell, HelmRepositoryCell, KustomizationCell, FluxHelmReleaseCell, FluxAlertCell } from './renderers/flux-cells'
@@ -3255,6 +3263,7 @@ function filterHostExtras(
3255
3263
 
3256
3264
  // localStorage helpers for column settings
3257
3265
  const COLUMN_SETTINGS_PREFIX = 'radar-columns-'
3266
+ const CHECKBOX_VISIBILITY_KEY = 'radar-resource-checkboxes'
3258
3267
 
3259
3268
  // Storage identity for a kind's column settings. normalizeKindToPlural falls
3260
3269
  // through to the bare plural for anything outside the curated collision
@@ -3280,6 +3289,8 @@ interface ColumnSettings {
3280
3289
  * load tell a column they hid from one that did not exist yet. Absent on
3281
3290
  * blobs written before this field. */
3282
3291
  known?: string[]
3292
+ /** Optional display order for movable columns, including hidden ones. */
3293
+ order?: string[]
3283
3294
  }
3284
3295
 
3285
3296
  function loadColumnSettings(kind: string, group?: string): ColumnSettings | null {
@@ -3342,6 +3353,9 @@ interface ResourcesViewData {
3342
3353
  // cluster-scoped kind. An ExternalSecret names a store; only the store
3343
3354
  // knows which backend it reads from.
3344
3355
  storeProviders?: Record<string, string>
3356
+ /** Current CronJob Issues keyed by "namespace/name", supplied by the host. */
3357
+ operationalIssuesIncomplete?: boolean
3358
+ operationalIssues?: Record<string, Issue>
3345
3359
  /** Cluster Audit findings keyed by "namespace/name". Raw compatibility
3346
3360
  * counts render danger as High and warning as Medium. */
3347
3361
  auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
@@ -3352,6 +3366,7 @@ interface ResourcesViewData {
3352
3366
  export const ResourcesViewDataContext = React.createContext<ResourcesViewData>({})
3353
3367
 
3354
3368
  export interface ResourceQueryResult {
3369
+ coverage?: ResourceCoverage
3355
3370
  resourceName?: string
3356
3371
  group?: string
3357
3372
  data?: any[]
@@ -3387,6 +3402,7 @@ interface ResourcesViewProps {
3387
3402
  resourceQueries?: ResourceQueryResult[]
3388
3403
  // Lightweight counts for sidebar badges (from /api/resource-counts)
3389
3404
  resourceCounts?: Record<string, number | null>
3405
+ resourceCoverage?: Record<string, ResourceCoverage>
3390
3406
  resourceForbidden?: string[]
3391
3407
  /** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
3392
3408
  * keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
@@ -3406,6 +3422,9 @@ interface ResourcesViewProps {
3406
3422
  // cluster-scoped kind. An ExternalSecret names a store; only the store
3407
3423
  // knows which backend it reads from.
3408
3424
  storeProviders?: Record<string, string>
3425
+ /** Current CronJob Issues keyed by "namespace/name", supplied by the host. */
3426
+ operationalIssuesIncomplete?: boolean
3427
+ operationalIssues?: Record<string, Issue>
3409
3428
  /** Cluster Audit findings keyed by "namespace/name". Raw compatibility
3410
3429
  * counts render danger as High and warning as Medium. */
3411
3430
  auditBadges?: Record<string, { danger: number; warning: number; messages?: AuditBadgeMessage[] }>
@@ -3537,6 +3556,7 @@ const DEFAULT_KIND_INFO: SelectedKindInfo = { name: 'pods', kind: 'Pod', group:
3537
3556
  export const LOADED_RESOURCE_COUNT_TTL_MS = 5 * 60 * 1000
3538
3557
 
3539
3558
  interface LoadedResourceCount {
3559
+ coverage?: ResourceCoverage
3540
3560
  count: number
3541
3561
  expiresAt: number
3542
3562
  }
@@ -3646,6 +3666,22 @@ export function deriveSidebarResourceCounts(
3646
3666
  return results
3647
3667
  }
3648
3668
 
3669
+ export function deriveSidebarResourceCoverage(
3670
+ resourceCounts: Record<string, number | null> | undefined,
3671
+ resourceCoverage: Record<string, ResourceCoverage> | undefined,
3672
+ loadedCountCache: LoadedResourceCountCache,
3673
+ now = Date.now(),
3674
+ ): Record<string, ResourceCoverage> {
3675
+ const result: Record<string, ResourceCoverage> = { ...resourceCoverage }
3676
+ for (const key of Object.keys(resourceCounts ?? {})) {
3677
+ if (!(key in result)) result[key] = { status: 'unknown' }
3678
+ }
3679
+ for (const [key, cached] of Object.entries(loadedCountCache)) {
3680
+ if (!(key in result) && cached.expiresAt > now) result[key] = cached.coverage ?? { status: 'unknown' }
3681
+ }
3682
+ return result
3683
+ }
3684
+
3649
3685
  const FILTER_INTENT_PARAMS = [...RESOURCE_LIST_FILTER_PARAMS, 'resource']
3650
3686
 
3651
3687
  /**
@@ -3680,6 +3716,7 @@ export function ResourcesView({
3680
3716
  apiResources: apiResourcesProp,
3681
3717
  resourceQueries: resourceQueriesProp,
3682
3718
  resourceCounts: resourceCountsProp,
3719
+ resourceCoverage: resourceCoverageProp,
3683
3720
  resourceForbidden: resourceForbiddenProp,
3684
3721
  resourceReasons,
3685
3722
  resourceUnavailable: resourceUnavailableProp,
@@ -3692,6 +3729,8 @@ export function ResourcesView({
3692
3729
  certExpiryError,
3693
3730
  storeProviders,
3694
3731
  auditBadges,
3732
+ operationalIssues,
3733
+ operationalIssuesIncomplete,
3695
3734
  pinned = [],
3696
3735
  togglePin = () => {},
3697
3736
  isPinned = () => false,
@@ -3747,7 +3786,6 @@ export function ResourcesView({
3747
3786
  }, [selectedKind.name, selectedKind.group]) // eslint-disable-line react-hooks/exhaustive-deps
3748
3787
 
3749
3788
  useEffect(() => {
3750
- setBulkMode(false)
3751
3789
  setCheckedResources(new Set())
3752
3790
  setShowBulkDeleteConfirm(false)
3753
3791
  setShowBulkRestartConfirm(false)
@@ -3808,6 +3846,7 @@ export function ResourcesView({
3808
3846
  // Column visibility and resize state
3809
3847
  const [visibleColumns, setVisibleColumns] = useState<Set<string>>(new Set())
3810
3848
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
3849
+ const [columnOrder, setColumnOrder] = useState<string[] | null>(null)
3811
3850
  const [showColumnPicker, setShowColumnPicker] = useState(false)
3812
3851
  const [customColumns, setCustomColumns] = useState<CustomColumnDef[]>([])
3813
3852
  const [customColumnDraft, setCustomColumnDraft] = useState<CustomColumnDef>({ source: 'label', path: '' })
@@ -3820,10 +3859,10 @@ export function ResourcesView({
3820
3859
  const [ownerKind, setOwnerKind] = useState<string>(initialFilters.ownerKind)
3821
3860
  const [ownerName, setOwnerName] = useState<string>(initialFilters.ownerName)
3822
3861
 
3823
- // Multi-select state for bulk operations. Checkboxes only render while
3824
- // bulk mode is active — entered via the toolbar toggle — so mutating
3825
- // actions stay out of the way during normal browsing.
3826
- const [bulkMode, setBulkMode] = useState(false)
3862
+ const [showCheckboxes, setShowCheckboxes] = useState(() => {
3863
+ try { return localStorage.getItem(CHECKBOX_VISIBILITY_KEY) === 'true' }
3864
+ catch { return false }
3865
+ })
3827
3866
  const [checkedResources, setCheckedResources] = useState<Set<string>>(new Set())
3828
3867
  const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
3829
3868
  const [showBulkRestartConfirm, setShowBulkRestartConfirm] = useState(false)
@@ -3831,8 +3870,7 @@ export function ResourcesView({
3831
3870
  const [bulkForceDelete, setBulkForceDelete] = useState(false)
3832
3871
  const [bulkScaleReplicas, setBulkScaleReplicas] = useState(0)
3833
3872
 
3834
- const exitBulkMode = useCallback(() => {
3835
- setBulkMode(false)
3873
+ const clearBulkSelection = useCallback(() => {
3836
3874
  setCheckedResources(new Set())
3837
3875
  }, [])
3838
3876
 
@@ -3949,13 +3987,16 @@ export function ResourcesView({
3949
3987
  [printerTable, builtPrinterColumns],
3950
3988
  )
3951
3989
 
3952
- const allColumns = useMemo(() => {
3990
+ const definedColumns = useMemo(() => {
3953
3991
  const kindColumns = columnsForKindWithPrinter(selectedKind.name, selectedKind.group, builtPrinterColumns)
3954
3992
  if (!extraLeadingColumns?.length && !builtCustomColumns.length) return kindColumns
3955
3993
  const builtinKeys = new Set(kindColumns.map(c => c.key))
3956
3994
  const filteredExtras = filterHostExtras(extraLeadingColumns, builtinKeys, selectedKind.name)
3957
3995
  return [...filteredExtras, ...kindColumns, ...builtCustomColumns]
3958
3996
  }, [selectedKind.name, selectedKind.group, extraLeadingColumns, builtCustomColumns, builtPrinterColumns])
3997
+ // Display order. Everything downstream reads this, so the picker, the header
3998
+ // and the resize width snapshot (which maps <th> index to key) agree.
3999
+ const allColumns = useMemo(() => applyColumnOrder(definedColumns, columnOrder), [definedColumns, columnOrder])
3959
4000
 
3960
4001
  // The offered key set, and a stable signature for it. allColumns is a new
3961
4002
  // identity on every printer-column refetch; depending on the array itself
@@ -4090,15 +4131,18 @@ export function ResourcesView({
4090
4131
  hadSavedColumnSettings.current = false
4091
4132
  setVisibleColumns(getDefaultVisibleColumns(effective))
4092
4133
  setColumnWidths({})
4134
+ setColumnOrder(null)
4093
4135
  } else {
4094
4136
  setVisibleColumns(mergeSavedVisibleColumns(
4095
4137
  saved.visible, extraKeys, builtPrinterColumns, effective, savedKnown,
4096
4138
  ))
4097
4139
  setColumnWidths(saved.widths || {})
4140
+ setColumnOrder(sanitizeColumnOrder(saved.order))
4098
4141
  }
4099
4142
  } else {
4100
4143
  setVisibleColumns(getDefaultVisibleColumns(effective))
4101
4144
  setColumnWidths({})
4145
+ setColumnOrder(null)
4102
4146
  }
4103
4147
  // printerColumnsKey, not builtPrinterColumns: the built array is a new
4104
4148
  // identity on every refetch, and re-running this effect would reset the
@@ -4120,12 +4164,13 @@ export function ResourcesView({
4120
4164
  widths: columnWidths,
4121
4165
  custom: customColumns,
4122
4166
  known: Array.from(knownColumnKeys.current),
4167
+ order: columnOrder ?? undefined,
4123
4168
  })
4124
4169
  // A persisted blob blocks GPU auto-show from here on — same rule in-session
4125
4170
  // as across sessions, so a later data refresh can't override user choices.
4126
4171
  hadSavedColumnSettings.current = true
4127
4172
  // allColumnKeysSig, not allColumnKeys: see the memo above.
4128
- }, [visibleColumns, columnWidths, customColumns, selectedKind.name, selectedKind.group, allColumnKeysSig, tableActive])
4173
+ }, [visibleColumns, columnWidths, customColumns, columnOrder, selectedKind.name, selectedKind.group, allColumnKeysSig, tableActive])
4129
4174
 
4130
4175
  // Close column picker on outside click or Escape
4131
4176
  useEffect(() => {
@@ -4233,6 +4278,7 @@ export function ResourcesView({
4233
4278
  const customKeys = new Set(builtCustomColumns.map(c => c.key))
4234
4279
  setVisibleColumns(getDefaultVisibleColumns(allColumns.filter(c => !customKeys.has(c.key))))
4235
4280
  setColumnWidths({})
4281
+ setColumnOrder(null)
4236
4282
  setSortColumn(null)
4237
4283
  setSortDirection(null)
4238
4284
  saveSavedSort(selectedKind.name, selectedKind.group, null)
@@ -4552,14 +4598,11 @@ export function ResourcesView({
4552
4598
  {
4553
4599
  id: 'resources-bulk-select',
4554
4600
  keys: 'b',
4555
- description: 'Toggle bulk select',
4601
+ description: 'Show or hide selection checkboxes',
4556
4602
  category: 'Table',
4557
4603
  scope: 'resources',
4558
4604
  handler: () => {
4559
- if (bulkMode) { exitBulkMode(); return }
4560
- if (!canBulkSelect) return
4561
- exitCompareMode()
4562
- setBulkMode(true)
4605
+ if (canBulkSelect) toggleCheckboxVisibility()
4563
4606
  },
4564
4607
  },
4565
4608
  {
@@ -4574,7 +4617,7 @@ export function ResourcesView({
4574
4617
  if (showProblemsDropdown) { setShowProblemsDropdown(false); return }
4575
4618
  if (showLabelsDropdown) { setShowLabelsDropdown(false); return }
4576
4619
  if (compareMode) { exitCompareMode(); return }
4577
- if (bulkMode) { exitBulkMode(); return }
4620
+ if (checkedResources.size > 0) { clearBulkSelection(); return }
4578
4621
  if (highlightedIndex >= 0) setHighlightedIndex(-1)
4579
4622
  else searchInputRef.current?.blur()
4580
4623
  },
@@ -5145,16 +5188,16 @@ export function ResourcesView({
5145
5188
  }
5146
5189
 
5147
5190
  const existing = next[selectedKindCountKey]
5148
- if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt) {
5191
+ if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt && existing.coverage === selectedQuery?.coverage) {
5149
5192
  return changed ? next : prev
5150
5193
  }
5151
5194
 
5152
5195
  return {
5153
5196
  ...next,
5154
- [selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt },
5197
+ [selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt, coverage: selectedQuery?.coverage },
5155
5198
  }
5156
5199
  })
5157
- }, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt])
5200
+ }, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt, selectedQuery?.coverage])
5158
5201
 
5159
5202
  // Derive counts — prefer lightweight resourceCounts prop over full query data
5160
5203
  const counts = useMemo(() => {
@@ -5175,6 +5218,12 @@ export function ResourcesView({
5175
5218
  return results
5176
5219
  }, [useNewCountsMode, resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache, selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, resourceQueries])
5177
5220
 
5221
+ const sidebarResourceCoverage = useMemo(() => {
5222
+ const result = deriveSidebarResourceCoverage(resourceCountsProp, resourceCoverageProp, loadedCountCache)
5223
+ if (selectedQueryHasLoadedCount) result[selectedKindCountKey] = selectedQuery?.coverage ?? { status: 'unknown' }
5224
+ return result
5225
+ }, [resourceCountsProp, resourceCoverageProp, loadedCountCache, selectedQueryHasLoadedCount, selectedKindCountKey, selectedQuery?.coverage])
5226
+
5178
5227
  const sidebarResourceUnavailable = useMemo(() => {
5179
5228
  if (!resourceUnavailableProp?.length) {
5180
5229
  return resourceUnavailableProp
@@ -5292,6 +5341,15 @@ export function ResourcesView({
5292
5341
  saveSavedSort(selectedKind.name, selectedKind.group, newColumn && newDirection ? { column: newColumn, direction: newDirection } : null)
5293
5342
  }, [sortColumn, sortDirection, canSortBy, selectedKind.name, selectedKind.group])
5294
5343
 
5344
+ const getDisplayFilterValue = useCallback((resource: { metadata?: { namespace?: string; name?: string } }, column: string, kind: string): string => {
5345
+ if (column === 'status' && selectedKind.group === 'batch' && kind === 'cronjobs') {
5346
+ const issue = operationalIssues?.[`${resource.metadata?.namespace ?? ''}/${resource.metadata?.name ?? ''}`]
5347
+ if (issue) return issueTitle(issue)
5348
+ if (operationalIssuesIncomplete) return 'Unknown'
5349
+ }
5350
+ return getCellFilterValue(resource, column, kind)
5351
+ }, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
5352
+
5295
5353
  // Get sortable value from a resource for a given column
5296
5354
  const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
5297
5355
  const meta = resource.metadata || {}
@@ -5320,7 +5378,7 @@ export function ResourcesView({
5320
5378
  if (hasCuratedColumns(selectedKind.name, selectedKind.group) && status.phase) {
5321
5379
  return status.phase
5322
5380
  }
5323
- return getCellFilterValue(resource, 'status', kindLower)
5381
+ return getDisplayFilterValue(resource, 'status', kindLower)
5324
5382
  case 'servers':
5325
5383
  // Numeric so 10 does not sort before 9.
5326
5384
  if (kindLower === 'istiogateways') return getIstioGatewayServerCount(resource)
@@ -5410,7 +5468,7 @@ export function ResourcesView({
5410
5468
  default:
5411
5469
  return ''
5412
5470
  }
5413
- }, [metricsLookup, selectedKind.name, selectedKind.group])
5471
+ }, [metricsLookup, selectedKind.name, selectedKind.group, getDisplayFilterValue])
5414
5472
 
5415
5473
  // Helper to check if a pod matches problem filters
5416
5474
  const podMatchesProblemFilter = useCallback((pod: any, filters: string[]): boolean => {
@@ -5433,13 +5491,18 @@ export function ResourcesView({
5433
5491
  }
5434
5492
  }, [regexMode, deferredSearchTerm])
5435
5493
 
5494
+ const isTrivyKind = selectedKind.group === TRIVY_GROUP
5495
+
5436
5496
  // Filter resources by search term, status, problems, and sort
5437
- const filteredResources = useMemo(() => {
5438
- if (!tableActive || !resources) return []
5497
+ const { list: filteredResources, findings: findingsSearch } = useMemo((): { list: any[]; findings: TrivyFindingsSearch | null } => {
5498
+ if (!tableActive || !resources) return { list: [], findings: null }
5439
5499
 
5440
5500
  let result = resources
5501
+ let findings: TrivyFindingsSearch | null = null
5441
5502
 
5442
- // Apply search filter
5503
+ // Apply search filter. On Trivy report kinds a plain search also matches
5504
+ // findings and runs below, after the other filters. Regex mode stays on
5505
+ // names: a pattern run over every finding could backtrack for seconds.
5443
5506
  if (deferredSearchTerm) {
5444
5507
  if (regexMode) {
5445
5508
  const re = searchRegex.re
@@ -5449,7 +5512,7 @@ export function ResourcesView({
5449
5512
  re.test(r.metadata?.namespace ?? '')
5450
5513
  )
5451
5514
  }
5452
- } else {
5515
+ } else if (!isTrivyKind) {
5453
5516
  const term = deferredSearchTerm.toLowerCase()
5454
5517
  result = result.filter((r: any) =>
5455
5518
  r.metadata?.name?.toLowerCase().includes(term) ||
@@ -5467,7 +5530,7 @@ export function ResourcesView({
5467
5530
  result = result.filter((r: any) =>
5468
5531
  activeColFilters.every(([col, vals]) => {
5469
5532
  const extra = extraColumnsByKey.get(col)
5470
- const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col, cellFilterKind)
5533
+ const cellVal = extra?.getFilterValue ? extra.getFilterValue(r) : getDisplayFilterValue(r, col, cellFilterKind)
5471
5534
  const match = vals.includes(cellVal)
5472
5535
  return columnFilterExcludes[col] ? !match : match
5473
5536
  })
@@ -5525,6 +5588,19 @@ export function ResourcesView({
5525
5588
  })
5526
5589
  }
5527
5590
 
5591
+ // Trivy findings are searched after the other filters, so rows they hide
5592
+ // don't spend the findings budget.
5593
+ if (deferredSearchTerm && !regexMode && isTrivyKind) {
5594
+ const term = deferredSearchTerm.toLowerCase()
5595
+ const matched = searchTrivyFindings(result, term)
5596
+ findings = matched
5597
+ result = result.filter((r: any) =>
5598
+ r.metadata?.name?.toLowerCase().includes(term) ||
5599
+ r.metadata?.namespace?.toLowerCase().includes(term) ||
5600
+ matched.matches.has(r)
5601
+ )
5602
+ }
5603
+
5528
5604
  // Apply custom sorting if set. Extra columns override
5529
5605
  // the built-in getSortValue for their key.
5530
5606
  if (effectiveSortColumn && sortDirection) {
@@ -5620,8 +5696,8 @@ export function ResourcesView({
5620
5696
  }
5621
5697
  }
5622
5698
 
5623
- return result
5624
- }, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, extraColumnsByKey, podMatchesProblemFilter])
5699
+ return { list: result, findings }
5700
+ }, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, isTrivyKind, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
5625
5701
 
5626
5702
  // For nodes table: compute the majority minor version so outliers can be highlighted
5627
5703
  const majorityNodeMinorVersion = useMemo(() => {
@@ -5746,16 +5822,25 @@ export function ResourcesView({
5746
5822
  setCheckedResources(allVisibleChecked ? new Set() : new Set(filteredResources.map(getResourceKey)))
5747
5823
  }, [allVisibleChecked, filteredResources, getResourceKey])
5748
5824
 
5749
- const isCheckboxMode = canBulkSelect && bulkMode
5825
+ const toggleCheckboxVisibility = useCallback(() => {
5826
+ const next = compareMode || !showCheckboxes
5827
+ exitCompareMode()
5828
+ clearBulkSelection()
5829
+ setShowCheckboxes(next)
5830
+ try { localStorage.setItem(CHECKBOX_VISIBILITY_KEY, String(next)) }
5831
+ catch { /* Keep the choice for this session when storage is unavailable. */ }
5832
+ }, [compareMode, showCheckboxes, exitCompareMode, clearBulkSelection])
5833
+
5834
+ const isCheckboxMode = canBulkSelect && !compareMode && showCheckboxes
5835
+ const showBulkBar = isCheckboxMode && checkedItems.length > 0
5750
5836
 
5751
5837
  // Stable row handlers so the memoized ResourceRowCells skips re-rendering
5752
5838
  // rows whose data didn't change on a refetch (React Query's structural
5753
5839
  // sharing keeps unchanged resources referentially identical).
5754
5840
  const handleRowClick = useCallback((resource: any, isSelected: boolean) => {
5755
5841
  if (compareMode) toggleComparePick(resource)
5756
- else if (isCheckboxMode) toggleChecked(resource)
5757
5842
  else selectResource(resource, isSelected)
5758
- }, [compareMode, isCheckboxMode, toggleComparePick, toggleChecked, selectResource])
5843
+ }, [compareMode, toggleComparePick, selectResource])
5759
5844
  const handleRowMouseEnter = useCallback(() => setHighlightedIndex(-1), [])
5760
5845
 
5761
5846
  // Filter columns by visibility
@@ -5847,7 +5932,7 @@ export function ResourcesView({
5847
5932
  // Count distinct values for this column
5848
5933
  const valueCounts: Record<string, number> = {}
5849
5934
  for (const r of resources) {
5850
- const val = extra?.getFilterValue ? extra.getFilterValue(r) : getCellFilterValue(r, col.key, cellFilterKind)
5935
+ const val = extra?.getFilterValue ? extra.getFilterValue(r) : getDisplayFilterValue(r, col.key, cellFilterKind)
5851
5936
  if (val) {
5852
5937
  valueCounts[val] = (valueCounts[val] || 0) + 1
5853
5938
  }
@@ -5930,7 +6015,7 @@ export function ResourcesView({
5930
6015
 
5931
6016
  if (filterableColumns.length === 0 && !problems && labelValues.length === 0) return null
5932
6017
  return { columns: filterableColumns, problems, labels: labelValues }
5933
- }, [tableActive, resources, selectedKind.name, selectedKind.group, allColumns])
6018
+ }, [tableActive, resources, selectedKind.name, selectedKind.group, allColumns, getDisplayFilterValue])
5934
6019
 
5935
6020
  // Map filterable columns by key for O(1) lookup in header rendering
5936
6021
  const filterableColumnMap = useMemo(() => {
@@ -6005,9 +6090,11 @@ export function ResourcesView({
6005
6090
  certExpiry,
6006
6091
  certExpiryError,
6007
6092
  auditBadges,
6093
+ operationalIssues,
6094
+ operationalIssuesIncomplete,
6008
6095
  onOpenLogs,
6009
6096
  onOpenWorkloadLogs,
6010
- }), [onNavigate, certExpiry, certExpiryError, auditBadges, onOpenLogs, onOpenWorkloadLogs])
6097
+ }), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
6011
6098
 
6012
6099
  return (
6013
6100
  <ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
@@ -6023,6 +6110,7 @@ export function ResourcesView({
6023
6110
  onKindChange={onKindChange}
6024
6111
  apiResources={apiResourcesProp}
6025
6112
  resourceCounts={counts}
6113
+ resourceCoverage={sidebarResourceCoverage}
6026
6114
  resourceForbidden={Array.from(forbiddenKinds)}
6027
6115
  resourceUnavailable={sidebarResourceUnavailable}
6028
6116
  pinned={pinned}
@@ -6048,7 +6136,7 @@ export function ResourcesView({
6048
6136
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
6049
6137
  <Input
6050
6138
  ref={searchInputRef}
6051
- placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
6139
+ placeholder={regexMode ? 'Search by regex... (press /)' : isTrivyKind ? 'Search names and findings... (press /)' : 'Search... (press /)'}
6052
6140
  value={searchTerm}
6053
6141
  onChange={(e) => setSearchTerm(e.target.value)}
6054
6142
  onKeyDown={(e) => {
@@ -6110,6 +6198,11 @@ export function ResourcesView({
6110
6198
  <X className="w-3.5 h-3.5" />
6111
6199
  </button>
6112
6200
  )}
6201
+ {!searchRegex.error && findingsSearch?.truncated && (
6202
+ <div className="absolute left-0 top-full mt-1 z-10 px-2 py-1 rounded bg-theme-elevated border border-theme-border text-xs text-theme-text-secondary shadow-theme-sm">
6203
+ Too many findings to search them all here; some weren't searched.
6204
+ </div>
6205
+ )}
6113
6206
  {searchRegex.error && (
6114
6207
  <div
6115
6208
  title={searchRegex.error}
@@ -6316,41 +6409,14 @@ export function ResourcesView({
6316
6409
  {/* Column list scrolls; the add-column footer stays pinned so
6317
6410
  its input isn't pushed below the fold on kinds with many columns. */}
6318
6411
  <div className="overflow-y-auto flex-1 min-h-0">
6319
- {allColumns.map(col => {
6320
- const isCustom = customColumnKeySet.has(col.key)
6321
- return (
6322
- <div
6323
- key={col.key}
6324
- className="group flex items-center gap-2 px-3 py-1.5 hover:bg-theme-elevated"
6325
- >
6326
- <label className="flex items-center gap-2 flex-1 min-w-0 cursor-pointer">
6327
- <input
6328
- type="checkbox"
6329
- checked={visibleColumns.has(col.key)}
6330
- onChange={() => toggleColumnVisibility(col.key)}
6331
- disabled={col.key === 'name'}
6332
- className="rounded border-theme-border"
6333
- />
6334
- <span className={clsx(
6335
- 'text-sm truncate',
6336
- col.key === 'name' ? 'text-theme-text-tertiary' : 'text-theme-text-primary'
6337
- )} title={col.tooltip || col.label}>
6338
- {col.label}
6339
- </span>
6340
- </label>
6341
- {isCustom && (
6342
- <button
6343
- type="button"
6344
- onClick={() => removeCustomColumn(col.key)}
6345
- className="shrink-0 p-0.5 text-theme-text-tertiary hover:text-red-500 opacity-0 group-hover:opacity-100 focus:opacity-100"
6346
- title="Remove column"
6347
- >
6348
- <X className="w-3.5 h-3.5" />
6349
- </button>
6350
- )}
6351
- </div>
6352
- )
6353
- })}
6412
+ <ColumnPickerList
6413
+ columns={allColumns}
6414
+ visibleColumns={visibleColumns}
6415
+ customColumnKeys={customColumnKeySet}
6416
+ onToggle={toggleColumnVisibility}
6417
+ onRemoveCustom={removeCustomColumn}
6418
+ onReorder={setColumnOrder}
6419
+ />
6354
6420
  </div>
6355
6421
  {/* Add a label/annotation column */}
6356
6422
  <div className="shrink-0 border-t border-theme-border pt-2 px-3 pb-1">
@@ -6420,7 +6486,7 @@ export function ResourcesView({
6420
6486
  <button
6421
6487
  onClick={() => {
6422
6488
  if (compareMode) { exitCompareMode(); return }
6423
- exitBulkMode()
6489
+ clearBulkSelection()
6424
6490
  setCompareMode(true)
6425
6491
  }}
6426
6492
  aria-pressed={compareMode}
@@ -6436,17 +6502,14 @@ export function ResourcesView({
6436
6502
  </Tooltip>
6437
6503
  )}
6438
6504
  {canBulkSelect && (
6439
- <Tooltip content={bulkMode ? 'Exit bulk select mode' : 'Select multiple resources'}>
6505
+ <Tooltip content={isCheckboxMode ? 'Hide selection checkboxes' : 'Show selection checkboxes'}>
6440
6506
  <button
6441
- onClick={() => {
6442
- if (bulkMode) { exitBulkMode(); return }
6443
- exitCompareMode()
6444
- setBulkMode(true)
6445
- }}
6446
- aria-pressed={bulkMode}
6507
+ onClick={toggleCheckboxVisibility}
6508
+ aria-label={isCheckboxMode ? 'Hide selection checkboxes' : 'Show selection checkboxes'}
6509
+ aria-pressed={isCheckboxMode}
6447
6510
  className={clsx(
6448
6511
  'p-2 rounded-lg transition-colors',
6449
- bulkMode
6512
+ isCheckboxMode
6450
6513
  ? 'bg-skyhook-500/15 text-skyhook-300 border border-skyhook-400/50'
6451
6514
  : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated',
6452
6515
  )}
@@ -6466,7 +6529,7 @@ export function ResourcesView({
6466
6529
  </div>
6467
6530
 
6468
6531
  {/* Bulk actions bar */}
6469
- {isCheckboxMode && (
6532
+ {showBulkBar && (
6470
6533
  <div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
6471
6534
  <span className="text-sm font-medium text-theme-text-primary">
6472
6535
  {checkedItems.length} selected
@@ -6506,14 +6569,26 @@ export function ResourcesView({
6506
6569
  )}
6507
6570
  <button
6508
6571
  type="button"
6509
- onClick={exitBulkMode}
6572
+ onClick={clearBulkSelection}
6510
6573
  className="px-3 py-1.5 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors"
6511
6574
  >
6512
- Cancel
6575
+ Clear selection
6513
6576
  </button>
6514
6577
  </div>
6515
6578
  )}
6516
6579
 
6580
+ {!isLoading && !selectedQueryError && resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind)) && (
6581
+ <div role="status" className="px-4 py-2 text-xs text-warning-text border-b border-theme-border bg-theme-surface">
6582
+ {resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind))}
6583
+ </div>
6584
+ )}
6585
+
6586
+ {!isLoading && !selectedQueryError && resourceNarrowedNotice(selectedQuery?.coverage) && (
6587
+ <div role="status" className="px-4 py-2 text-xs text-theme-text-tertiary border-b border-theme-border bg-theme-surface">
6588
+ {resourceNarrowedNotice(selectedQuery?.coverage)}
6589
+ </div>
6590
+ )}
6591
+
6517
6592
  {!isLoading && !isSelectedForbidden && !largeListGuard && filteredResources.length > 0 && (
6518
6593
  <ActiveFilterBar
6519
6594
  columnFilters={columnFilters}
@@ -6555,7 +6630,7 @@ export function ResourcesView({
6555
6630
  />
6556
6631
  </div>
6557
6632
  </div>
6558
- ) : selectedQueryError && !Array.isArray(resources) ? (
6633
+ ) : selectedQueryError && !Array.isArray(resources) && !largeListGuard ? (
6559
6634
  // A failed fetch with no rows to fall back on must not reach the
6560
6635
  // empty state below — it would tell the user the cluster has none.
6561
6636
  <FetchResult
@@ -6575,7 +6650,9 @@ export function ResourcesView({
6575
6650
  <p className="text-sm mt-1 max-w-xl">
6576
6651
  {largeListGuard.reason === 'count-unavailable'
6577
6652
  ? 'Radar could not verify this list is small enough to load. Choose a namespace or smaller namespace set to try again.'
6578
- : `${largeListGuard.count?.toLocaleString()} ${largeListGuard.kind.toLowerCase()} are in the current scope. Choose a namespace or smaller namespace set to load this view.`}
6653
+ : largeListGuard.count == null
6654
+ ? 'This list exceeds the resource limit. Choose a namespace or smaller namespace set to load this view.'
6655
+ : `${largeListGuard.count.toLocaleString()} ${largeListGuard.kind.toLowerCase()} were read in the current scope. Choose a namespace or smaller namespace set to load this view.`}
6579
6656
  </p>
6580
6657
  <p className="text-xs mt-2 text-theme-text-disabled">
6581
6658
  Radar limits full table loads to {largeListGuard.limit.toLocaleString()} resources to keep the UI responsive.
@@ -6586,8 +6663,8 @@ export function ResourcesView({
6586
6663
  </div>
6587
6664
  ) : filteredResources.length === 0 ? (
6588
6665
  <div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary">
6589
- <p>No {selectedKind.kind} found</p>
6590
- {emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
6666
+ <p>{resourceEmptyMessage(selectedKind.kind, selectedQuery?.coverage, (resources?.length ?? 0) > 0)}</p>
6667
+ {selectedQuery?.coverage?.status !== 'partial' && emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
6591
6668
  <p className="text-xs mt-2 max-w-md text-center text-theme-text-disabled">
6592
6669
  {emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0)}
6593
6670
  </p>
@@ -6604,6 +6681,13 @@ export function ResourcesView({
6604
6681
  <X className="w-3.5 h-3.5" />
6605
6682
  </button>
6606
6683
  )}
6684
+ {searchTerm && isTrivyKind && (
6685
+ <p className="text-xs mt-1 text-theme-text-tertiary">
6686
+ {regexMode
6687
+ ? 'Regex search matches names and namespaces only.'
6688
+ : 'Searched names, namespaces and report findings (CVE, package, check ID, image).'}
6689
+ </p>
6690
+ )}
6607
6691
  {namespaces.length > 0 && <p className="text-sm mt-1 text-theme-text-disabled">Searching in {namespaces.length === 1 ? `namespace: ${namespaces[0]}` : `${namespaces.length} namespaces`}</p>}
6608
6692
  <ActiveFilterBar
6609
6693
  columnFilters={columnFilters}
@@ -6912,6 +6996,7 @@ export function ResourcesView({
6912
6996
  comparePickIndex={pickIdx}
6913
6997
  rowHref={rowHrefFor?.(resource)}
6914
6998
  onRowCheckToggle={toggleChecked}
6999
+ findingMatch={findingsSearch?.matches.get(resource)}
6915
7000
  />
6916
7001
  )
6917
7002
  }}
@@ -6948,7 +7033,7 @@ export function ResourcesView({
6948
7033
  onBulkDelete?.(checkedBulkItems, {
6949
7034
  force: bulkForceDelete,
6950
7035
  onSuccess: () => {
6951
- exitBulkMode()
7036
+ clearBulkSelection()
6952
7037
  setShowBulkDeleteConfirm(false)
6953
7038
  setBulkForceDelete(false)
6954
7039
  },
@@ -6978,7 +7063,7 @@ export function ResourcesView({
6978
7063
  onConfirm={() => {
6979
7064
  onBulkRestart?.(checkedBulkItems, {
6980
7065
  onSuccess: () => {
6981
- exitBulkMode()
7066
+ clearBulkSelection()
6982
7067
  setShowBulkRestartConfirm(false)
6983
7068
  },
6984
7069
  })
@@ -6997,7 +7082,7 @@ export function ResourcesView({
6997
7082
  onConfirm={() => {
6998
7083
  onBulkScale?.(checkedBulkItems, bulkScaleReplicas, {
6999
7084
  onSuccess: () => {
7000
- exitBulkMode()
7085
+ clearBulkSelection()
7001
7086
  setShowBulkScaleDialog(false)
7002
7087
  },
7003
7088
  })
@@ -7079,6 +7164,9 @@ interface ResourceRowCellsProps {
7079
7164
  * is unaffected (still toggles picks). */
7080
7165
  rowHref?: string
7081
7166
  onRowCheckToggle?: (resource: any) => void
7167
+ /** What a Trivy report matched the table search through, when its name
7168
+ * and namespace didn't. */
7169
+ findingMatch?: TrivyFindingMatch
7082
7170
  }
7083
7171
 
7084
7172
  function rowHighlightClass(
@@ -7100,7 +7188,7 @@ function rowHighlightClass(
7100
7188
  return 'group-hover/row:bg-theme-surface/50'
7101
7189
  }
7102
7190
 
7103
- const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, isChecked, showCheckbox, majorityNodeMinorVersion, storeProviders, onRowClick, onRowMouseEnter, compareMode, comparePickIndex = -1, rowHref, onRowCheckToggle }: ResourceRowCellsProps) {
7191
+ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind, group, columns, extraColumnsByKey, hasSpacerColumn, isSelected, isHighlighted, isChecked, showCheckbox, majorityNodeMinorVersion, storeProviders, onRowClick, onRowMouseEnter, compareMode, comparePickIndex = -1, rowHref, onRowCheckToggle, findingMatch }: ResourceRowCellsProps) {
7104
7192
  const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted, isChecked)
7105
7193
  const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
7106
7194
  const onClick = onRowClick ? () => onRowClick(resource, !!isSelected) : undefined
@@ -7170,6 +7258,7 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7170
7258
  storeProviders={storeProviders}
7171
7259
  extraColumn={extraColumnsByKey?.get(col.key)}
7172
7260
  nameHref={col.key === 'name' ? rowHref : undefined}
7261
+ findingMatch={col.key === 'name' ? findingMatch : undefined}
7173
7262
  />
7174
7263
  </td>
7175
7264
  ))}
@@ -7219,9 +7308,10 @@ interface CellContentProps {
7219
7308
  /** When set on the name column, the resource name renders as `<a href>`
7220
7309
  * so ⌘-click / copy-link / hover-URL all work. */
7221
7310
  nameHref?: string
7311
+ findingMatch?: TrivyFindingMatch
7222
7312
  }
7223
7313
 
7224
- function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref }: CellContentProps) {
7314
+ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref, findingMatch }: CellContentProps) {
7225
7315
  const { auditBadges } = useContext(ResourcesViewDataContext)
7226
7316
  // Parent-injected extra columns short-circuit the built-in switch.
7227
7317
  // Used by hosts that inject leading columns (e.g. a multi-cluster Cluster column).
@@ -7239,7 +7329,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7239
7329
  const auditTotal = audit ? audit.danger + audit.warning : 0
7240
7330
  return (
7241
7331
  <div className="flex items-center gap-1.5 min-w-0">
7242
- <Tooltip content={meta.name}>
7332
+ <Tooltip content={meta.name} wrapperClassName={findingMatch ? 'min-w-0' : undefined}>
7243
7333
  {nameHref ? (
7244
7334
  <a
7245
7335
  href={nameHref}
@@ -7254,6 +7344,13 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7254
7344
  )}
7255
7345
  </Tooltip>
7256
7346
  <CopyNameButton name={meta.name} />
7347
+ {findingMatch && (
7348
+ <Tooltip content={findingMatch.text}>
7349
+ <span className="shrink-0 text-xs italic text-theme-text-tertiary cursor-help">
7350
+ {findingMatch.site === 'image' ? 'in image' : 'in findings'}
7351
+ </span>
7352
+ </Tooltip>
7353
+ )}
7257
7354
  {auditTotal > 0 && audit && (
7258
7355
  <Tooltip content={audit.messages && audit.messages.length > 0
7259
7356
  ? <AuditBadgeTooltip messages={audit.messages} />
@@ -7390,7 +7487,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7390
7487
  // Disambiguate Gateway API vs Istio Gateway by apiVersion. Retained
7391
7488
  // alongside the case above because not every call site normalizes the
7392
7489
  // kind through GROUP_QUALIFIED_COLUMN_KEYS before dispatching.
7393
- if (resource.apiVersion?.includes('networking.istio.io')) {
7490
+ if (isApiGroup(resource.apiVersion, 'networking.istio.io')) {
7394
7491
  return <IstioGatewayCell resource={resource} column={column} />
7395
7492
  }
7396
7493
  return <GatewayCell resource={resource} column={column} />
@@ -8629,6 +8726,7 @@ function JobCell({ resource, column }: { resource: any; column: string }) {
8629
8726
  }
8630
8727
 
8631
8728
  function CronJobCell({ resource, column }: { resource: any; column: string }) {
8729
+ const { operationalIssues, operationalIssuesIncomplete } = useContext(ResourcesViewDataContext)
8632
8730
  switch (column) {
8633
8731
  case 'schedule': {
8634
8732
  const { cron, readable } = getCronJobSchedule(resource)
@@ -8640,6 +8738,13 @@ function CronJobCell({ resource, column }: { resource: any; column: string }) {
8640
8738
  )
8641
8739
  }
8642
8740
  case 'status': {
8741
+ const issue = operationalIssues?.[`${resource.metadata?.namespace ?? ''}/${resource.metadata?.name ?? ''}`]
8742
+ if (issue) {
8743
+ return <Tooltip content={issue.message}><Badge severity={issue.severity === 'critical' ? 'error' : 'warning'}>{issueTitle(issue)}</Badge></Tooltip>
8744
+ }
8745
+ if (operationalIssuesIncomplete) {
8746
+ return <Tooltip content="The issue list is incomplete. This CronJob's status could not be checked."><Badge severity="neutral">Unknown</Badge></Tooltip>
8747
+ }
8643
8748
  const status = getCronJobStatus(resource)
8644
8749
  return (
8645
8750
  <span className={clsx('badge', status.color)}>