@skyhook-io/k8s-ui 1.18.0 → 1.18.2

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 (128) hide show
  1. package/README.md +21 -0
  2. package/package.json +1 -1
  3. package/src/components/applications/ApplicationsView.tsx +8 -7
  4. package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
  5. package/src/components/compare/CompareResourcePicker.tsx +18 -4
  6. package/src/components/compare/ResourceCompareView.tsx +6 -2
  7. package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
  9. package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
  10. package/src/components/gitops/GitOpsTableView.tsx +46 -12
  11. package/src/components/gitops/detail-helpers.test.ts +58 -13
  12. package/src/components/gitops/detail-helpers.ts +37 -6
  13. package/src/components/gitops/index.ts +1 -0
  14. package/src/components/issues/IssuesView.tsx +16 -11
  15. package/src/components/issues/diagnostic.ts +2 -0
  16. package/src/components/issues/severity.ts +1 -0
  17. package/src/components/logs/LogCore.tsx +13 -11
  18. package/src/components/logs/LogsViewer.test.tsx +104 -59
  19. package/src/components/logs/LogsViewer.tsx +28 -8
  20. package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
  21. package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
  22. package/src/components/namespace-switcher/NamespacePicker.tsx +44 -8
  23. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
  24. package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
  25. package/src/components/resources/ResourcesSidebar.tsx +62 -26
  26. package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
  27. package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
  28. package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
  29. package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
  30. package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
  31. package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
  32. package/src/components/resources/ResourcesView.tsx +342 -83
  33. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  34. package/src/components/resources/generic-status.ts +15 -0
  35. package/src/components/resources/printer-columns.test.ts +36 -0
  36. package/src/components/resources/printer-columns.ts +12 -1
  37. package/src/components/resources/readiness-gates.test.ts +95 -0
  38. package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
  39. package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
  40. package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
  41. package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
  42. package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
  43. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
  44. package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
  45. package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
  46. package/src/components/resources/renderers/PodRenderer.tsx +32 -9
  47. package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
  48. package/src/components/resources/renderers/index.ts +3 -0
  49. package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
  50. package/src/components/resources/resource-utils-crossplane.ts +44 -8
  51. package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
  52. package/src/components/resources/resource-utils-flagger.ts +76 -0
  53. package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
  54. package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
  55. package/src/components/resources/resource-utils-trivy.test.ts +87 -0
  56. package/src/components/resources/resource-utils-trivy.ts +122 -0
  57. package/src/components/resources/resource-utils.ts +80 -2
  58. package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
  59. package/src/components/shared/CreateResourceDialog.tsx +5 -0
  60. package/src/components/shared/DetailShell.phone.test.tsx +54 -0
  61. package/src/components/shared/DetailShell.tsx +21 -8
  62. package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
  63. package/src/components/shared/EditableYamlView.test.tsx +58 -0
  64. package/src/components/shared/EditableYamlView.tsx +18 -3
  65. package/src/components/shared/FilterRailPanel.test.tsx +39 -0
  66. package/src/components/shared/FilterRailPanel.tsx +62 -0
  67. package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
  68. package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
  69. package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
  70. package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
  71. package/src/components/shared/ManagedByChip.test.tsx +22 -0
  72. package/src/components/shared/ManagedByChip.tsx +158 -5
  73. package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
  74. package/src/components/shared/ResourceActionsBar.tsx +31 -9
  75. package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
  76. package/src/components/shared/ResourceRendererDispatch.tsx +69 -31
  77. package/src/components/shared/SetImageDialog.tsx +3 -0
  78. package/src/components/timeline/TimelineList.tsx +5 -1
  79. package/src/components/topology/TopologyControls.tsx +1 -1
  80. package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
  81. package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
  82. package/src/components/ui/FetchResult.test.tsx +19 -2
  83. package/src/components/ui/FetchResult.tsx +13 -1
  84. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
  85. package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
  86. package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
  87. package/src/components/ui/MultiSelectPicker.tsx +20 -2
  88. package/src/components/ui/PageHeader.tsx +7 -4
  89. package/src/components/ui/SummaryTile.tsx +13 -3
  90. package/src/components/ui/Toast.tsx +33 -26
  91. package/src/components/ui/Tooltip.touch.test.tsx +55 -0
  92. package/src/components/ui/Tooltip.tsx +11 -2
  93. package/src/components/ui/YamlReview.test.tsx +52 -0
  94. package/src/components/ui/YamlReview.tsx +7 -0
  95. package/src/components/ui/drawer-components.test.tsx +280 -0
  96. package/src/components/ui/drawer-components.tsx +302 -18
  97. package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
  98. package/src/components/workload/WorkloadView.relationships.test.tsx +73 -0
  99. package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
  100. package/src/components/workload/WorkloadView.tsx +105 -34
  101. package/src/hooks/index.ts +2 -0
  102. package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
  103. package/src/hooks/useKeepInViewport.test.ts +27 -0
  104. package/src/hooks/useKeepInViewport.ts +87 -0
  105. package/src/hooks/useMediaQuery.ts +21 -0
  106. package/src/hooks/usePhoneLayout.ts +12 -0
  107. package/src/theme/tailwind-theme.css +5 -0
  108. package/src/types/core.ts +119 -3
  109. package/src/types/gitops-flux-inventory.test.ts +24 -0
  110. package/src/types/gitops.ts +5 -5
  111. package/src/types/index.ts +3 -0
  112. package/src/types/read-coverage.ts +65 -0
  113. package/src/types/resource-coverage.ts +40 -0
  114. package/src/utils/api-resources.ts +1 -0
  115. package/src/utils/format.test.ts +7 -0
  116. package/src/utils/format.ts +6 -1
  117. package/src/utils/gitops-owner.test.ts +42 -1
  118. package/src/utils/gitops-owner.ts +158 -8
  119. package/src/utils/gitops-write-guard.test.ts +332 -0
  120. package/src/utils/gitops-write-guard.ts +175 -11
  121. package/src/utils/index.ts +3 -0
  122. package/src/utils/read-actions.test.ts +22 -0
  123. package/src/utils/read-actions.ts +90 -0
  124. package/src/utils/resource-coverage.test.ts +121 -0
  125. package/src/utils/resource-coverage.ts +117 -0
  126. package/src/utils/touch-intent.test.ts +39 -0
  127. package/src/utils/touch-intent.ts +35 -0
  128. package/vitest.config.ts +26 -0
@@ -1,8 +1,15 @@
1
+ import { getOpenTelemetryColumnValue } from './resource-utils-opentelemetry'
2
+ import { getFlaggerColumnValue } from './resource-utils-flagger'
3
+ import type { ResourceCoverage } from '../../types/resource-coverage'
4
+ import { resourceCoverageNotice, resourceEmptyMessage, resourceNarrowedNotice } from '../../utils/resource-coverage'
1
5
  import React, { useState, useMemo, useEffect, useCallback, useDeferredValue, useRef, useContext, useId } from 'react'
2
6
  import { TableVirtuoso, type TableVirtuosoHandle } from 'react-virtuoso'
3
7
  import { useDebouncedValue } from '../../hooks/useDebouncedValue'
8
+ import { usePhoneLayout } from '../../hooks/usePhoneLayout'
9
+ import { CrossplaneDeleteNotice } from '../ui/CrossplaneOperationNotice'
4
10
  import { Badge } from '../ui/Badge'
5
11
  import { PaneLoader } from '../ui/PaneLoader'
12
+ import { DialogPortal } from '../ui/DialogPortal'
6
13
  import { RestrictedState } from '../ui/RestrictedState'
7
14
  import { FetchResult } from '../ui/FetchResult'
8
15
  import { Input } from '../ui/Input'
@@ -32,6 +39,7 @@ import {
32
39
  ListChecks,
33
40
  Minus,
34
41
  Scale,
42
+ PanelLeft,
35
43
  } from 'lucide-react'
36
44
  import { clsx } from 'clsx'
37
45
  import { ResourceBar } from '../ui/ResourceBar'
@@ -148,6 +156,7 @@ import {
148
156
  SEVERITY_DOT_COLOR,
149
157
  healthColors,
150
158
  } from './resource-utils'
159
+ import { TRIVY_GROUP, searchTrivyFindings, type TrivyFindingMatch, type TrivyFindingsSearch } from './resource-utils-trivy'
151
160
  import { getGenericResourceStatus } from './generic-status'
152
161
  import { ActiveFilterBar } from './ActiveFilterBar'
153
162
  import { getIstioGatewayServerCount, getIstioGatewaySelectorString } from './resource-utils-istio'
@@ -156,7 +165,7 @@ import {
156
165
  printerCellTone, printerColumnKey, printerColumnWidth, printerTableKey, printerTableMatchesKind, readPrinterCell,
157
166
  } from './printer-columns'
158
167
  import { SEVERITY_BADGE, SEVERITY_TEXT, healthToSeverity, EVENT_TYPE_COLORS } from '../../utils/badge-colors'
159
- import { pluralize } from '../../utils/pluralize'
168
+ import { englishPlural, isEnglishPlural, pluralize } from '../../utils/pluralize'
160
169
  import { getPodGpuCount, getNodeGpuCount } from '../../utils/extended-resources'
161
170
  import { formatRelativeAgeTime, parseQuantityToNumber } from '../../utils/format'
162
171
  import { type CustomColumnDef, type CustomColumnSource, customColumnKey, readCustomColumnValue, sanitizeCustomColumnDefs } from '../../utils/custom-columns'
@@ -201,8 +210,9 @@ import { BackupCell, RestoreCell, ScheduleCell, BackupStorageLocationCell, Volum
201
210
  import { isVeleroResource } from './resource-utils-velero'
202
211
  import { CNPGClusterCell, CNPGBackupCell, CNPGScheduledBackupCell, CNPGPoolerCell, CNPGObjectStoreCell, CNPGDeclarativeCell, CNPGImageCatalogCell } from './renderers/cnpg-cells'
203
212
  import { isApiGroup, CNPG_GROUP } from './resource-utils-cnpg'
204
- import { ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
205
- import { isManagedResource, isComposite } from './resource-utils-crossplane'
213
+ import { CrossplaneStatusCell, ManagedResourceCell, CompositeResourceCell, CrossplaneProviderCell, CrossplaneProviderConfigCell, CompositionCell, XRDCell } from './renderers/crossplane-cells'
214
+ import { getCrossplaneStatus, isManagedResource, isComposite, getCrossplanePauseState } from './resource-utils-crossplane'
215
+ import { CrossplanePauseChip } from './renderers/CrossplanePauseContext'
206
216
  import { VirtualServiceCell, DestinationRuleCell, IstioGatewayCell, ServiceEntryCell, PeerAuthenticationCell, AuthorizationPolicyCell } from './renderers/istio-cells'
207
217
  import { KnativeServiceCell, ConfigurationCell as KnativeConfigurationCell, RevisionCell as KnativeRevisionCell, RouteCell as KnativeRouteCell, BrokerCell, TriggerCell, EventTypeCell, PingSourceCell, ApiServerSourceCell, ContainerSourceCell, SinkBindingCell, ChannelCell, InMemoryChannelCell, SubscriptionCell, SequenceCell, ParallelCell, DomainMappingCell, ServerlessServiceCell, KnativeIngressCell, KnativeCertificateCell } from './renderers/knative-cells'
208
218
  import { IngressRouteCell, MiddlewareCell, TraefikServiceCell, ServersTransportCell, TLSOptionCell } from './renderers/traefik-cells'
@@ -215,7 +225,7 @@ import { CalicoInfraCell, CalicoPolicyCell } from './renderers/calico-cells'
215
225
  import { isCalicoPolicyResource, isCoreNetworkPolicyKind } from './resource-utils-calico'
216
226
  import { useRegisterShortcut, useRegisterShortcuts, type KeyboardShortcut } from '../../hooks/useKeyboardShortcuts'
217
227
  import { ResourcesSidebar } from './ResourcesSidebar'
218
- import type { SelectedKindInfo, SidebarCategoryWorkspace } from './ResourcesSidebar'
228
+ import type { SelectedKindInfo, SidebarCategoryWorkspace, SidebarDisplayState } from './ResourcesSidebar'
219
229
  import { CompareTray, togglePick, pickIndex, refToParam, SIDE_TONES, type CompareTrayPick, type NamespacedRef } from '../compare'
220
230
  import { ConfirmDialog } from '../ui/ConfirmDialog'
221
231
 
@@ -338,6 +348,24 @@ const COMPARE_COLUMN_STYLE: React.CSSProperties = {
338
348
  maxWidth: COMPARE_COLUMN_WIDTH,
339
349
  }
340
350
 
351
+ // Phones: the leading columns (compare marker, selection checkbox, then Name
352
+ // when it is the first column) stay in place while the rest of the table
353
+ // scrolls sideways, so every row stays identifiable. The opaque background
354
+ // keeps scrolled cells from showing through; on phones it hides the selection
355
+ // tint on these cells, where a tap opens the full-screen detail anyway.
356
+ // The offset rides a CSS variable that only the phone classes read, so the
357
+ // desktop header (position: relative) never moves.
358
+ const CHECKBOX_COLUMN_WIDTH = 40
359
+ const PHONE_PINNED_CELL = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[1] phone:bg-theme-surface'
360
+ const PHONE_PINNED_HEAD = 'phone:sticky phone:left-[var(--pin-left)] phone:z-[3]'
361
+ const PHONE_NAME_MAX_VW = 45
362
+ function phonePinnedLeft(compareMode: boolean | undefined, showCheckbox: boolean | undefined, column: 'compare' | 'checkbox' | 'name'): React.CSSProperties {
363
+ const compare = compareMode ? COMPARE_COLUMN_WIDTH : 0
364
+ const checkbox = showCheckbox ? CHECKBOX_COLUMN_WIDTH : 0
365
+ const left = column === 'compare' ? 0 : column === 'checkbox' ? compare : compare + checkbox
366
+ return { '--pin-left': `${left}px` } as React.CSSProperties
367
+ }
368
+
341
369
  // Built-in sortable keys. Hoisted so getColumnMinWidth can reserve room for the
342
370
  // sort icon without the header render and the width math drifting apart.
343
371
  const BUILTIN_SORTABLE_COLUMN_KEYS = new Set([
@@ -551,6 +579,44 @@ const DEFAULT_COLUMNS: Column[] = [
551
579
  ]
552
580
 
553
581
  const KNOWN_COLUMNS: Record<string, Column[]> = {
582
+ opentelemetrycollectors: [
583
+ { key: 'name', label: 'Name', width: 'min-w-40' },
584
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
585
+ { key: 'status', label: 'Reconciliation', width: 'w-40 shrink-0', tooltip: 'Operator reconciliation only, not Pod health or telemetry delivery.' },
586
+ { key: 'mode', label: 'Mode', width: 'w-28 shrink-0' },
587
+ { key: 'reportedPods', label: 'Reported Pods', width: 'w-32 shrink-0', tooltip: 'Last reported ready/total snapshot. Total is observed Pods for Deployment/StatefulSet, desired scheduled Pods for DaemonSet. Inspect the owned workload for current readiness.' },
588
+ { key: 'version', label: 'Version', width: 'w-24 shrink-0' },
589
+ { key: 'image', label: 'Image', width: 'min-w-48' },
590
+ { key: 'management', label: 'Management', width: 'w-28 shrink-0' },
591
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
592
+ ],
593
+ instrumentations: [
594
+ { key: 'name', label: 'Name', width: 'min-w-40' },
595
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
596
+ { key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
597
+ { key: 'endpoint', label: 'OTLP Endpoint', width: 'min-w-48' },
598
+ { key: 'sampler', label: 'Sampler', width: 'w-40 shrink-0' },
599
+ { key: 'samplerArgument', label: 'Sampler Argument', width: 'w-32 shrink-0' },
600
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
601
+ ],
602
+ canaries: [
603
+ { key: 'name', label: 'Name', width: 'min-w-40' },
604
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
605
+ { key: 'status', label: 'Rollout State', width: 'w-40 shrink-0', tooltip: 'Last recorded rollout outcome, with requested suspension separate from a failed result. Not a latest-generation guarantee.' },
606
+ { key: 'strategy', label: 'Strategy', width: 'w-36 shrink-0' },
607
+ { key: 'target', label: 'Canary Target', width: 'min-w-40' },
608
+ { key: 'canaryWeight', label: 'Reported Weight', width: 'w-36 shrink-0', tooltip: 'Last reported canary routing weight, not measured traffic. Blue/green and A/B strategies are not weighted.' },
609
+ { key: 'suspended', label: 'Suspend Requested', width: 'w-36 shrink-0' },
610
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
611
+ ],
612
+ metrictemplates: [
613
+ { key: 'name', label: 'Name', width: 'min-w-40' },
614
+ { key: 'namespace', label: 'Namespace', width: 'w-40 shrink-0' },
615
+ { key: 'status', label: 'Configuration', width: 'w-36 shrink-0' },
616
+ { key: 'provider', label: 'Provider', width: 'w-32 shrink-0' },
617
+ { key: 'address', label: 'Provider Address', width: 'min-w-48' },
618
+ { key: 'age', label: 'Age', width: 'w-24 shrink-0' },
619
+ ],
554
620
  pods: [
555
621
  { key: 'name', label: 'Name' },
556
622
  { key: 'namespace', label: 'Namespace', width: 'w-48' },
@@ -2848,6 +2914,10 @@ function isCuratedUnboundedGroup(key: string, group: string): boolean {
2848
2914
  * cannot occupy one. See CORE_API_GROUPS.
2849
2915
  */
2850
2916
  const CURATED_COLUMN_GROUPS: Record<string, readonly string[]> = {
2917
+ opentelemetrycollectors: ['opentelemetry.io'],
2918
+ instrumentations: ['opentelemetry.io'],
2919
+ canaries: ['flagger.app'],
2920
+ metrictemplates: ['flagger.app'],
2851
2921
  challenges: ['acme.cert-manager.io'],
2852
2922
  orders: ['acme.cert-manager.io'],
2853
2923
  compositeresourcedefinitions: ['apiextensions.crossplane.io'],
@@ -3129,12 +3199,12 @@ function buildPrinterColumn(def: PrinterColumnDef, index: number, table: Printer
3129
3199
  // A column the vendor named Ready/Healthy/Established carries the only
3130
3200
  // scannable signal these kinds have — they have no curated Status column.
3131
3201
  // Rendering it as grey text would leave a screen of uniform "True".
3132
- const tone = printerCellTone(def, value)
3202
+ const tone = printerCellTone(def, value, resource)
3133
3203
  if (tone) {
3134
3204
  return (
3135
3205
  // The value, not the column's description — the header tooltip
3136
3206
  // carries that. A cell tooltip exists to reveal a truncated value.
3137
- <Tooltip content={text}>
3207
+ <Tooltip content={tone === 'neutral' ? `${text} · ${String(value).trim().toLowerCase() === 'false' ? 'Controller recorded reconciliation paused' : 'Last reported readiness while reconciliation is paused'}` : text}>
3138
3208
  <span className={clsx('badge', healthColors[tone])}>{text}</span>
3139
3209
  </Tooltip>
3140
3210
  )
@@ -3363,6 +3433,7 @@ interface ResourcesViewData {
3363
3433
  export const ResourcesViewDataContext = React.createContext<ResourcesViewData>({})
3364
3434
 
3365
3435
  export interface ResourceQueryResult {
3436
+ coverage?: ResourceCoverage
3366
3437
  resourceName?: string
3367
3438
  group?: string
3368
3439
  data?: any[]
@@ -3398,6 +3469,7 @@ interface ResourcesViewProps {
3398
3469
  resourceQueries?: ResourceQueryResult[]
3399
3470
  // Lightweight counts for sidebar badges (from /api/resource-counts)
3400
3471
  resourceCounts?: Record<string, number | null>
3472
+ resourceCoverage?: Record<string, ResourceCoverage>
3401
3473
  resourceForbidden?: string[]
3402
3474
  /** Per-kind reason a forbidden kind is hidden ("rbac_denied" | "unavailable"),
3403
3475
  * keyed by the same count key as resourceForbidden. Drives RestrictedState copy. */
@@ -3551,6 +3623,7 @@ const DEFAULT_KIND_INFO: SelectedKindInfo = { name: 'pods', kind: 'Pod', group:
3551
3623
  export const LOADED_RESOURCE_COUNT_TTL_MS = 5 * 60 * 1000
3552
3624
 
3553
3625
  interface LoadedResourceCount {
3626
+ coverage?: ResourceCoverage
3554
3627
  count: number
3555
3628
  expiresAt: number
3556
3629
  }
@@ -3660,6 +3733,22 @@ export function deriveSidebarResourceCounts(
3660
3733
  return results
3661
3734
  }
3662
3735
 
3736
+ export function deriveSidebarResourceCoverage(
3737
+ resourceCounts: Record<string, number | null> | undefined,
3738
+ resourceCoverage: Record<string, ResourceCoverage> | undefined,
3739
+ loadedCountCache: LoadedResourceCountCache,
3740
+ now = Date.now(),
3741
+ ): Record<string, ResourceCoverage> {
3742
+ const result: Record<string, ResourceCoverage> = { ...resourceCoverage }
3743
+ for (const key of Object.keys(resourceCounts ?? {})) {
3744
+ if (!(key in result)) result[key] = { status: 'unknown' }
3745
+ }
3746
+ for (const [key, cached] of Object.entries(loadedCountCache)) {
3747
+ if (!(key in result) && cached.expiresAt > now) result[key] = cached.coverage ?? { status: 'unknown' }
3748
+ }
3749
+ return result
3750
+ }
3751
+
3663
3752
  const FILTER_INTENT_PARAMS = [...RESOURCE_LIST_FILTER_PARAMS, 'resource']
3664
3753
 
3665
3754
  /**
@@ -3694,6 +3783,7 @@ export function ResourcesView({
3694
3783
  apiResources: apiResourcesProp,
3695
3784
  resourceQueries: resourceQueriesProp,
3696
3785
  resourceCounts: resourceCountsProp,
3786
+ resourceCoverage: resourceCoverageProp,
3697
3787
  resourceForbidden: resourceForbiddenProp,
3698
3788
  resourceReasons,
3699
3789
  resourceUnavailable: resourceUnavailableProp,
@@ -3742,6 +3832,15 @@ export function ResourcesView({
3742
3832
  }: ResourcesViewProps) {
3743
3833
  const initialFilters = getInitialFiltersFromURL()
3744
3834
  const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
3835
+ // Phones: the 224px kind list would leave the table a sliver, so it lives in
3836
+ // a drawer over the table, opened from the toolbar and closed by a pick.
3837
+ const isPhone = usePhoneLayout()
3838
+ const [kindsDrawerOpen, setKindsDrawerOpen] = useState(false)
3839
+ // The drawer unmounts its sidebar on close; keep its display settings here.
3840
+ const [drawerSidebarState, setDrawerSidebarState] = useState<SidebarDisplayState>(() => ({ showEmptyKinds: false, favoritesExpanded: pinned.length > 0 }))
3841
+ useEffect(() => {
3842
+ if (!isPhone) setKindsDrawerOpen(false)
3843
+ }, [isPhone])
3745
3844
  // Sync selectedKind from URL when the URL changes (browser back, external sidebar navigation).
3746
3845
  // Deps are URL-derived only — including selectedKind.name/group would race against pending
3747
3846
  // navigation: a sidebar click flips state before navigate() lands, this effect re-reads the
@@ -4317,7 +4416,7 @@ export function ResourcesView({
4317
4416
  category: 'Search',
4318
4417
  scope: 'resources',
4319
4418
  handler: () => searchInputRef.current?.focus(),
4320
- enabled: tableActive,
4419
+ enabled: tableActive && !kindsDrawerOpen,
4321
4420
  })
4322
4421
 
4323
4422
  // Keyboard navigation: highlighted row state
@@ -4426,6 +4525,8 @@ export function ResourcesView({
4426
4525
  onResourceClick?.(isSelected ? null : stripped)
4427
4526
  }, [onRowSelect, onResourceClick, selectedKind.name, selectedKind.group])
4428
4527
 
4528
+ // Shortcuts here (and the search shortcut above) pause while the phone
4529
+ // kind drawer is open: it's modal, so the table behind it takes no keys.
4429
4530
  useRegisterShortcuts(([
4430
4531
  {
4431
4532
  id: 'resources-nav-down',
@@ -4599,7 +4700,7 @@ export function ResourcesView({
4599
4700
  else searchInputRef.current?.blur()
4600
4701
  },
4601
4702
  },
4602
- ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && (sc.enabled ?? true) })))
4703
+ ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: tableActive && !kindsDrawerOpen && (sc.enabled ?? true) })))
4603
4704
 
4604
4705
  // Refs for accessing filteredResources inside shortcuts (computed later in component)
4605
4706
  const filteredResourceCountRef = useRef(0)
@@ -4609,7 +4710,7 @@ export function ResourcesView({
4609
4710
  const flatKindListRef = useRef<SelectedKindInfo[]>([])
4610
4711
 
4611
4712
  // Sidebar kind navigation: [ = previous kind, ] = next kind
4612
- useRegisterShortcuts([
4713
+ useRegisterShortcuts(([
4613
4714
  {
4614
4715
  id: 'resources-prev-kind',
4615
4716
  keys: '[',
@@ -4642,7 +4743,7 @@ export function ResourcesView({
4642
4743
  onKindChange?.()
4643
4744
  },
4644
4745
  },
4645
- ])
4746
+ ] as KeyboardShortcut[]).map((sc) => ({ ...sc, enabled: !kindsDrawerOpen && (sc.enabled ?? true) })))
4646
4747
 
4647
4748
  // Close dropdowns on outside click
4648
4749
  useEffect(() => {
@@ -5165,16 +5266,16 @@ export function ResourcesView({
5165
5266
  }
5166
5267
 
5167
5268
  const existing = next[selectedKindCountKey]
5168
- if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt) {
5269
+ if (existing?.count === selectedLoadedResourceCount && existing.expiresAt === expiresAt && existing.coverage === selectedQuery?.coverage) {
5169
5270
  return changed ? next : prev
5170
5271
  }
5171
5272
 
5172
5273
  return {
5173
5274
  ...next,
5174
- [selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt },
5275
+ [selectedKindCountKey]: { count: selectedLoadedResourceCount, expiresAt, coverage: selectedQuery?.coverage },
5175
5276
  }
5176
5277
  })
5177
- }, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt])
5278
+ }, [selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, selectedQuery?.dataUpdatedAt, selectedQuery?.coverage])
5178
5279
 
5179
5280
  // Derive counts — prefer lightweight resourceCounts prop over full query data
5180
5281
  const counts = useMemo(() => {
@@ -5195,6 +5296,12 @@ export function ResourcesView({
5195
5296
  return results
5196
5297
  }, [useNewCountsMode, resourcesToCount, resourceCountsProp, resourceUnavailableProp, loadedCountCache, selectedQueryHasLoadedCount, selectedLoadedResourceCount, selectedKindCountKey, resourceQueries])
5197
5298
 
5299
+ const sidebarResourceCoverage = useMemo(() => {
5300
+ const result = deriveSidebarResourceCoverage(resourceCountsProp, resourceCoverageProp, loadedCountCache)
5301
+ if (selectedQueryHasLoadedCount) result[selectedKindCountKey] = selectedQuery?.coverage ?? { status: 'unknown' }
5302
+ return result
5303
+ }, [resourceCountsProp, resourceCoverageProp, loadedCountCache, selectedQueryHasLoadedCount, selectedKindCountKey, selectedQuery?.coverage])
5304
+
5198
5305
  const sidebarResourceUnavailable = useMemo(() => {
5199
5306
  if (!resourceUnavailableProp?.length) {
5200
5307
  return resourceUnavailableProp
@@ -5318,14 +5425,26 @@ export function ResourcesView({
5318
5425
  if (issue) return issueTitle(issue)
5319
5426
  if (operationalIssuesIncomplete) return 'Unknown'
5320
5427
  }
5428
+ if (column === 'status') {
5429
+ const managedColumns = isLikelyCrossplaneMRGroup(selectedKind.name, selectedKind.group ?? '')
5430
+ if ((managedColumns && isManagedResource(resource))
5431
+ || ((!hasCuratedColumns(selectedKind.name, selectedKind.group) || managedColumns)
5432
+ && getCrossplanePauseState(resource) === 'acknowledged')) {
5433
+ return getCrossplaneStatus(resource).text
5434
+ }
5435
+ }
5321
5436
  return getCellFilterValue(resource, column, kind)
5322
- }, [operationalIssues, operationalIssuesIncomplete, selectedKind.group])
5437
+ }, [operationalIssues, operationalIssuesIncomplete, selectedKind.name, selectedKind.group])
5323
5438
 
5324
5439
  // Get sortable value from a resource for a given column
5325
5440
  const getSortValue = useCallback((resource: any, column: string, kind?: string): string | number => {
5326
5441
  const meta = resource.metadata || {}
5327
5442
  const status = resource.status || {}
5328
5443
  const kindLower = kind?.toLowerCase() || ''
5444
+ const otelValue = getOpenTelemetryColumnValue(resource, column)
5445
+ if (otelValue !== undefined) return otelValue
5446
+ const flaggerValue = getFlaggerColumnValue(resource, column)
5447
+ if (flaggerValue !== undefined) return flaggerValue
5329
5448
 
5330
5449
  switch (column) {
5331
5450
  case 'name':
@@ -5462,13 +5581,18 @@ export function ResourcesView({
5462
5581
  }
5463
5582
  }, [regexMode, deferredSearchTerm])
5464
5583
 
5584
+ const isTrivyKind = selectedKind.group === TRIVY_GROUP
5585
+
5465
5586
  // Filter resources by search term, status, problems, and sort
5466
- const filteredResources = useMemo(() => {
5467
- if (!tableActive || !resources) return []
5587
+ const { list: filteredResources, findings: findingsSearch } = useMemo((): { list: any[]; findings: TrivyFindingsSearch | null } => {
5588
+ if (!tableActive || !resources) return { list: [], findings: null }
5468
5589
 
5469
5590
  let result = resources
5591
+ let findings: TrivyFindingsSearch | null = null
5470
5592
 
5471
- // Apply search filter
5593
+ // Apply search filter. On Trivy report kinds a plain search also matches
5594
+ // findings and runs below, after the other filters. Regex mode stays on
5595
+ // names: a pattern run over every finding could backtrack for seconds.
5472
5596
  if (deferredSearchTerm) {
5473
5597
  if (regexMode) {
5474
5598
  const re = searchRegex.re
@@ -5478,7 +5602,7 @@ export function ResourcesView({
5478
5602
  re.test(r.metadata?.namespace ?? '')
5479
5603
  )
5480
5604
  }
5481
- } else {
5605
+ } else if (!isTrivyKind) {
5482
5606
  const term = deferredSearchTerm.toLowerCase()
5483
5607
  result = result.filter((r: any) =>
5484
5608
  r.metadata?.name?.toLowerCase().includes(term) ||
@@ -5554,6 +5678,19 @@ export function ResourcesView({
5554
5678
  })
5555
5679
  }
5556
5680
 
5681
+ // Trivy findings are searched after the other filters, so rows they hide
5682
+ // don't spend the findings budget.
5683
+ if (deferredSearchTerm && !regexMode && isTrivyKind) {
5684
+ const term = deferredSearchTerm.toLowerCase()
5685
+ const matched = searchTrivyFindings(result, term)
5686
+ findings = matched
5687
+ result = result.filter((r: any) =>
5688
+ r.metadata?.name?.toLowerCase().includes(term) ||
5689
+ r.metadata?.namespace?.toLowerCase().includes(term) ||
5690
+ matched.matches.has(r)
5691
+ )
5692
+ }
5693
+
5557
5694
  // Apply custom sorting if set. Extra columns override
5558
5695
  // the built-in getSortValue for their key.
5559
5696
  if (effectiveSortColumn && sortDirection) {
@@ -5649,8 +5786,8 @@ export function ResourcesView({
5649
5786
  }
5650
5787
  }
5651
5788
 
5652
- return result
5653
- }, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
5789
+ return { list: result, findings }
5790
+ }, [tableActive, resources, deferredSearchTerm, regexMode, searchRegex, isTrivyKind, columnFilters, columnFilterExcludes, problemFilters, showInactiveReplicaSets, labelSelector, ownerKind, ownerName, selectedKind.name, effectiveSortColumn, sortDirection, getSortValue, getDisplayFilterValue, extraColumnsByKey, podMatchesProblemFilter])
5654
5791
 
5655
5792
  // For nodes table: compute the majority minor version so outliers can be highlighted
5656
5793
  const majorityNodeMinorVersion = useMemo(() => {
@@ -5754,7 +5891,9 @@ export function ResourcesView({
5754
5891
  const selectedKindName = selectedKind.name.toLowerCase()
5755
5892
  const canBulkRestartSelectedKind = onBulkRestart != null && BULK_RESTART_WORKLOAD_KINDS.has(selectedKindName)
5756
5893
  const canBulkScaleSelectedKind = onBulkScale != null && BULK_SCALE_WORKLOAD_KINDS.has(selectedKindName)
5757
- const canBulkSelect = onBulkDelete != null || canBulkRestartSelectedKind || canBulkScaleSelectedKind
5894
+ // Phones skip bulk selection and create (see the toolbar): phone triage
5895
+ // is one resource at a time.
5896
+ const canBulkSelect = !isPhone && (onBulkDelete != null || canBulkRestartSelectedKind || canBulkScaleSelectedKind)
5758
5897
  const isBulkMutating = isBulkDeleting || isBulkRestarting || isBulkScaling
5759
5898
 
5760
5899
  const openBulkScaleDialog = useCallback(() => {
@@ -5805,14 +5944,30 @@ export function ResourcesView({
5805
5944
  // Fixed-width columns can consume the table's flexible space and collapse
5806
5945
  // the required name column. Keep a real table minimum and let the container
5807
5946
  // scroll horizontally when the viewport is too narrow.
5947
+ // Phones pin a leading Name column (see PHONE_PINNED_CELL). Cap it at
5948
+ // PHONE_NAME_MAX_VW percent of the screen so a width saved on desktop can't
5949
+ // make the pinned column cover it; the saved width itself is left alone.
5950
+ // Tracked through resizes and rotation, which can stay within phone width.
5951
+ const [phoneViewportWidth, setPhoneViewportWidth] = useState(() => (typeof window === 'undefined' ? 0 : window.innerWidth))
5952
+ useEffect(() => {
5953
+ if (!isPhone) return
5954
+ const onResize = () => setPhoneViewportWidth(window.innerWidth)
5955
+ onResize()
5956
+ window.addEventListener('resize', onResize)
5957
+ return () => window.removeEventListener('resize', onResize)
5958
+ }, [isPhone])
5959
+ const phoneNameCap = isPhone && columns[0]?.key === 'name' ? Math.round(phoneViewportWidth * PHONE_NAME_MAX_VW / 100) : Infinity
5808
5960
  const tableMinWidth = useMemo(() => {
5809
5961
  const compareColumnWidth = compareMode ? COMPARE_COLUMN_WIDTH : 0
5810
- const baseMinWidth = columns.reduce((sum, col) => sum + (columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)), compareColumnWidth)
5962
+ const baseMinWidth = columns.reduce((sum, col, i) => {
5963
+ const width = columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)
5964
+ return sum + (i === 0 && col.key === 'name' ? Math.min(width, phoneNameCap) : width)
5965
+ }, compareColumnWidth)
5811
5966
  const flexibleNameColumn = columns.find(col => col.key === 'name' && !columnWidths[col.key])
5812
5967
 
5813
5968
  if (!hasResizedColumns || !flexibleNameColumn) return baseMinWidth
5814
5969
  return baseMinWidth + getColumnMinWidth(flexibleNameColumn, extraColumnsByKey)
5815
- }, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch])
5970
+ }, [columns, columnWidths, compareMode, hasResizedColumns, extraColumnsByKey, headerFontEpoch, phoneNameCap])
5816
5971
 
5817
5972
  // Stable virtuoso components — memoized to avoid remounting the table on every render
5818
5973
  const virtuosoComponents = useMemo(() => ({
@@ -5834,13 +5989,15 @@ export function ResourcesView({
5834
5989
  */}
5835
5990
  {compareMode && <col style={{ width: COMPARE_COLUMN_WIDTH }} />}
5836
5991
  {isCheckboxMode && <col style={{ width: '40px' }} />}
5837
- {columns.map(col => (
5992
+ {columns.map((col, i) => (
5838
5993
  <col
5839
5994
  key={col.key}
5840
5995
  style={{
5841
- width: columnWidths[col.key]
5842
- ? `${columnWidths[col.key]}px`
5843
- : col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
5996
+ width: i === 0 && col.key === 'name' && phoneNameCap !== Infinity
5997
+ ? `min(${columnWidths[col.key] || getColumnMinWidth(col, extraColumnsByKey)}px, ${PHONE_NAME_MAX_VW}vw)`
5998
+ : columnWidths[col.key]
5999
+ ? `${columnWidths[col.key]}px`
6000
+ : col.key === 'name' ? undefined : `${getColumnMinWidth(col, extraColumnsByKey)}px`,
5844
6001
  }}
5845
6002
  />
5846
6003
  ))}
@@ -5851,7 +6008,7 @@ export function ResourcesView({
5851
6008
  )
5852
6009
  }),
5853
6010
  TableRow: VirtuosoTableRow,
5854
- }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch])
6011
+ }), [columns, columnWidths, hasResizedColumns, compareMode, tableMinWidth, isCheckboxMode, extraColumnsByKey, headerFontEpoch, phoneNameCap])
5855
6012
 
5856
6013
  // Calculate filter options with counts based on current resources (before filtering)
5857
6014
  const filterOptions = useMemo(() => {
@@ -6049,46 +6206,86 @@ export function ResourcesView({
6049
6206
  onOpenWorkloadLogs,
6050
6207
  }), [onNavigate, certExpiry, certExpiryError, auditBadges, operationalIssues, operationalIssuesIncomplete, onOpenLogs, onOpenWorkloadLogs])
6051
6208
 
6209
+ const renderSidebar = (className?: string, inDrawer = false) => (
6210
+ <ResourcesSidebar
6211
+ className={className}
6212
+ displayState={inDrawer ? drawerSidebarState : undefined}
6213
+ onDisplayStateChange={inDrawer ? setDrawerSidebarState : undefined}
6214
+ containEscape={inDrawer}
6215
+ selectedKind={selectedKind}
6216
+ onSelectedKindChange={(kind) => {
6217
+ shouldPushHistory.current = true
6218
+ setSelectedKind(kind)
6219
+ setKindsDrawerOpen(false)
6220
+ }}
6221
+ onKindChange={onKindChange}
6222
+ apiResources={apiResourcesProp}
6223
+ resourceCounts={counts}
6224
+ resourceCoverage={sidebarResourceCoverage}
6225
+ resourceForbidden={Array.from(forbiddenKinds)}
6226
+ resourceUnavailable={sidebarResourceUnavailable}
6227
+ pinned={pinned}
6228
+ togglePin={togglePin}
6229
+ isPinned={isPinned}
6230
+ categoryWorkspaces={sidebarCategoryWorkspaces}
6231
+ onKindNavigated={() => {
6232
+ // After selecting a kind via keyboard, move focus to the table search
6233
+ // so the user can immediately filter within the selected kind.
6234
+ setTimeout(() => searchInputRef.current?.focus(), 50)
6235
+ }}
6236
+ />
6237
+ )
6238
+
6239
+ const kindsButton = (
6240
+ <button
6241
+ type="button"
6242
+ onClick={() => setKindsDrawerOpen(true)}
6243
+ aria-expanded={kindsDrawerOpen}
6244
+ aria-label={`Resource kinds (showing ${selectedKind.kind})`}
6245
+ className="flex shrink-0 items-center gap-1.5 rounded-lg border border-theme-border bg-theme-elevated px-2.5 py-1.5 text-sm font-medium text-theme-text-primary hover:bg-theme-hover"
6246
+ >
6247
+ <PanelLeft className="h-4 w-4 text-theme-text-tertiary" />
6248
+ <span className="max-w-[7rem] truncate">{selectedKind.kind}</span>
6249
+ </button>
6250
+ )
6251
+
6052
6252
  return (
6053
6253
  <ResourcesViewDataContext.Provider value={resourcesViewDataContextValue}>
6054
6254
  <div className="flex h-full w-full">
6055
- {/* Sidebar - Resource Types */}
6056
- {!hideSidebar && (
6057
- <ResourcesSidebar
6058
- selectedKind={selectedKind}
6059
- onSelectedKindChange={(kind) => {
6060
- shouldPushHistory.current = true
6061
- setSelectedKind(kind)
6062
- }}
6063
- onKindChange={onKindChange}
6064
- apiResources={apiResourcesProp}
6065
- resourceCounts={counts}
6066
- resourceForbidden={Array.from(forbiddenKinds)}
6067
- resourceUnavailable={sidebarResourceUnavailable}
6068
- pinned={pinned}
6069
- togglePin={togglePin}
6070
- isPinned={isPinned}
6071
- categoryWorkspaces={sidebarCategoryWorkspaces}
6072
- onKindNavigated={() => {
6073
- // After selecting a kind via keyboard, move focus to the table search
6074
- // so the user can immediately filter within the selected kind.
6075
- setTimeout(() => searchInputRef.current?.focus(), 50)
6076
- }}
6077
- />
6255
+ {/* Sidebar - Resource Types. On phones it is a modal drawer: the
6256
+ dialog primitive traps focus, captures Escape before the table's
6257
+ own shortcuts, and returns focus to the kinds button. */}
6258
+ {!hideSidebar && !isPhone && renderSidebar()}
6259
+ {!hideSidebar && isPhone && (
6260
+ <DialogPortal
6261
+ open={kindsDrawerOpen}
6262
+ onClose={() => setKindsDrawerOpen(false)}
6263
+ ariaLabel="Resource kinds"
6264
+ className="!ml-0 !mr-auto h-full w-72 max-w-[85vw] self-stretch overflow-hidden rounded-none border-y-0 border-l-0"
6265
+ >
6266
+ {renderSidebar('!w-full h-full border-r-0', true)}
6267
+ </DialogPortal>
6078
6268
  )}
6079
6269
 
6080
6270
  {kindView !== null ? (
6081
- <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">{kindView}</div>
6271
+ <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
6272
+ {!hideSidebar && isPhone && (
6273
+ <div className="flex items-center px-3 py-2 border-b border-theme-border bg-theme-base shrink-0">{kindsButton}</div>
6274
+ )}
6275
+ {kindView}
6276
+ </div>
6082
6277
  ) : (
6083
6278
  <div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-theme-surface">
6084
6279
  {/* Toolbar */}
6085
- <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0">
6086
- <div className="flex-1 min-w-0">
6280
+ {/* Phones: the toolbar wraps and search takes its own full row. */}
6281
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border bg-theme-base shrink-0 phone:relative phone:flex-wrap phone:gap-2 phone:px-3">
6282
+ {!hideSidebar && isPhone && kindsButton}
6283
+ <div className="flex-1 min-w-0 phone:order-last phone:basis-full">
6087
6284
  <div className="relative max-w-md">
6088
6285
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
6089
6286
  <Input
6090
6287
  ref={searchInputRef}
6091
- placeholder={regexMode ? 'Search by regex... (press /)' : 'Search... (press /)'}
6288
+ placeholder={regexMode ? 'Search by regex... (press /)' : isTrivyKind ? 'Search names and findings... (press /)' : 'Search... (press /)'}
6092
6289
  value={searchTerm}
6093
6290
  onChange={(e) => setSearchTerm(e.target.value)}
6094
6291
  onKeyDown={(e) => {
@@ -6150,6 +6347,11 @@ export function ResourcesView({
6150
6347
  <X className="w-3.5 h-3.5" />
6151
6348
  </button>
6152
6349
  )}
6350
+ {!searchRegex.error && findingsSearch?.truncated && (
6351
+ <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">
6352
+ Too many findings to search them all here; some weren't searched.
6353
+ </div>
6354
+ )}
6153
6355
  {searchRegex.error && (
6154
6356
  <div
6155
6357
  title={searchRegex.error}
@@ -6161,9 +6363,11 @@ export function ResourcesView({
6161
6363
  </div>
6162
6364
  </div>
6163
6365
 
6164
- {/* Problems dropdown (pods only) */}
6366
+ {/* Problems dropdown (pods only). On phones the toolbar wraps, so this
6367
+ menu and Labels' anchor to the toolbar (full width below it)
6368
+ rather than to a trigger that may sit near either edge. */}
6165
6369
  {filterOptions?.problems && filterOptions.problems.length > 0 && (
6166
- <div className="relative" ref={problemsDropdownRef}>
6370
+ <div className="relative phone:static" ref={problemsDropdownRef}>
6167
6371
  <button
6168
6372
  onClick={() => { setShowProblemsDropdown(!showProblemsDropdown); setShowLabelsDropdown(false) }}
6169
6373
  className={clsx(
@@ -6182,7 +6386,7 @@ export function ResourcesView({
6182
6386
  )}
6183
6387
  </button>
6184
6388
  {showProblemsDropdown && (
6185
- <div className="absolute right-0 top-full mt-1 min-w-48 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6389
+ <div className="absolute right-0 top-full mt-1 min-w-48 phone:left-3 phone:right-3 phone:min-w-0 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6186
6390
  {problemFilters.length > 0 && (
6187
6391
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-theme-border">
6188
6392
  <span className="text-xs font-medium text-theme-text-secondary">Problems</span>
@@ -6213,7 +6417,7 @@ export function ResourcesView({
6213
6417
 
6214
6418
  {/* Labels dropdown */}
6215
6419
  {filterOptions?.labels && filterOptions.labels.length > 0 && (
6216
- <div className="relative" ref={labelsDropdownRef}>
6420
+ <div className="relative phone:static" ref={labelsDropdownRef}>
6217
6421
  <button
6218
6422
  onClick={() => { setShowLabelsDropdown(!showLabelsDropdown); setShowProblemsDropdown(false); setLabelSearch('') }}
6219
6423
  className={clsx(
@@ -6237,7 +6441,7 @@ export function ResourcesView({
6237
6441
  ? labels.filter(l => l.value.toLowerCase().includes(labelSearch.toLowerCase()))
6238
6442
  : labels
6239
6443
  return (
6240
- <div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6444
+ <div className="absolute right-0 top-full mt-1 min-w-64 max-w-80 phone:left-3 phone:right-3 phone:min-w-0 phone:max-w-none bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50">
6241
6445
  <div className="flex items-center gap-2 p-2 border-b border-theme-border">
6242
6446
  <div className="relative flex-1">
6243
6447
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
@@ -6417,7 +6621,7 @@ export function ResourcesView({
6417
6621
  </div>
6418
6622
  )}
6419
6623
  </div>
6420
- {onCreateResource && (
6624
+ {onCreateResource && !isPhone && (
6421
6625
  <Tooltip content={`Create ${selectedKind.kind || 'resource'}`}>
6422
6626
  <button
6423
6627
  aria-label={`Create ${selectedKind.kind || 'resource'}`}
@@ -6477,7 +6681,7 @@ export function ResourcesView({
6477
6681
 
6478
6682
  {/* Bulk actions bar */}
6479
6683
  {showBulkBar && (
6480
- <div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0">
6684
+ <div className="flex items-center gap-3 px-4 py-2 bg-skyhook-500/10 border-b border-skyhook-400/20 shrink-0 phone:flex-wrap phone:gap-2 phone:px-3">
6481
6685
  <span className="text-sm font-medium text-theme-text-primary">
6482
6686
  {checkedItems.length} selected
6483
6687
  </span>
@@ -6524,6 +6728,18 @@ export function ResourcesView({
6524
6728
  </div>
6525
6729
  )}
6526
6730
 
6731
+ {!isLoading && !selectedQueryError && resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind)) && (
6732
+ <div role="status" className="px-4 py-2 text-xs text-warning-text border-b border-theme-border bg-theme-surface">
6733
+ {resourceCoverageNotice(selectedQuery?.coverage, isEnglishPlural(selectedKind.kind) ? selectedKind.kind : englishPlural(selectedKind.kind))}
6734
+ </div>
6735
+ )}
6736
+
6737
+ {!isLoading && !selectedQueryError && resourceNarrowedNotice(selectedQuery?.coverage) && (
6738
+ <div role="status" className="px-4 py-2 text-xs text-theme-text-tertiary border-b border-theme-border bg-theme-surface">
6739
+ {resourceNarrowedNotice(selectedQuery?.coverage)}
6740
+ </div>
6741
+ )}
6742
+
6527
6743
  {!isLoading && !isSelectedForbidden && !largeListGuard && filteredResources.length > 0 && (
6528
6744
  <ActiveFilterBar
6529
6745
  columnFilters={columnFilters}
@@ -6565,7 +6781,7 @@ export function ResourcesView({
6565
6781
  />
6566
6782
  </div>
6567
6783
  </div>
6568
- ) : selectedQueryError && !Array.isArray(resources) ? (
6784
+ ) : selectedQueryError && !Array.isArray(resources) && !largeListGuard ? (
6569
6785
  // A failed fetch with no rows to fall back on must not reach the
6570
6786
  // empty state below — it would tell the user the cluster has none.
6571
6787
  <FetchResult
@@ -6585,7 +6801,9 @@ export function ResourcesView({
6585
6801
  <p className="text-sm mt-1 max-w-xl">
6586
6802
  {largeListGuard.reason === 'count-unavailable'
6587
6803
  ? 'Radar could not verify this list is small enough to load. Choose a namespace or smaller namespace set to try again.'
6588
- : `${largeListGuard.count?.toLocaleString()} ${largeListGuard.kind.toLowerCase()} are in the current scope. Choose a namespace or smaller namespace set to load this view.`}
6804
+ : largeListGuard.count == null
6805
+ ? 'This list exceeds the resource limit. Choose a namespace or smaller namespace set to load this view.'
6806
+ : `${largeListGuard.count.toLocaleString()} ${largeListGuard.kind.toLowerCase()} were read in the current scope. Choose a namespace or smaller namespace set to load this view.`}
6589
6807
  </p>
6590
6808
  <p className="text-xs mt-2 text-theme-text-disabled">
6591
6809
  Radar limits full table loads to {largeListGuard.limit.toLocaleString()} resources to keep the UI responsive.
@@ -6596,8 +6814,8 @@ export function ResourcesView({
6596
6814
  </div>
6597
6815
  ) : filteredResources.length === 0 ? (
6598
6816
  <div className="absolute inset-0 flex flex-col items-center justify-center text-theme-text-tertiary">
6599
- <p>No {selectedKind.kind} found</p>
6600
- {emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
6817
+ <p>{resourceEmptyMessage(selectedKind.kind, selectedQuery?.coverage, (resources?.length ?? 0) > 0)}</p>
6818
+ {selectedQuery?.coverage?.status !== 'partial' && emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0) && (
6601
6819
  <p className="text-xs mt-2 max-w-md text-center text-theme-text-disabled">
6602
6820
  {emptyKindNote(selectedKind.kind, selectedKind.group, resources?.length ?? 0)}
6603
6821
  </p>
@@ -6614,6 +6832,13 @@ export function ResourcesView({
6614
6832
  <X className="w-3.5 h-3.5" />
6615
6833
  </button>
6616
6834
  )}
6835
+ {searchTerm && isTrivyKind && (
6836
+ <p className="text-xs mt-1 text-theme-text-tertiary">
6837
+ {regexMode
6838
+ ? 'Regex search matches names and namespaces only.'
6839
+ : 'Searched names, namespaces and report findings (CVE, package, check ID, image).'}
6840
+ </p>
6841
+ )}
6617
6842
  {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>}
6618
6843
  <ActiveFilterBar
6619
6844
  columnFilters={columnFilters}
@@ -6653,15 +6878,15 @@ export function ResourcesView({
6653
6878
  // Inline px width — under `table-layout:fixed`,
6654
6879
  // `w-9` is a hint the browser absorbs into leftover
6655
6880
  // row width on an icon-only column.
6656
- style={COMPARE_COLUMN_STYLE}
6657
- className="px-2 py-3 text-xs font-medium uppercase tracking-wide bg-theme-base border-b border-r-subtle border-theme-border text-center text-skyhook-400"
6881
+ style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, isCheckboxMode, 'compare') }}
6882
+ className={clsx('px-2 py-3 text-xs font-medium uppercase tracking-wide bg-theme-base border-b border-r-subtle border-theme-border text-center text-skyhook-400', PHONE_PINNED_HEAD)}
6658
6883
  title="Compare mode"
6659
6884
  >
6660
6885
  <GitCompare className="w-3.5 h-3.5 inline-block opacity-70" />
6661
6886
  </th>
6662
6887
  )}
6663
6888
  {isCheckboxMode && (
6664
- <th className="bg-theme-surface border-b border-theme-border w-10 text-center px-0 py-3">
6889
+ <th style={phonePinnedLeft(compareMode, isCheckboxMode, 'checkbox')} className={clsx('bg-theme-surface border-b border-theme-border w-10 text-center px-0 py-3', PHONE_PINNED_HEAD)}>
6665
6890
  <input
6666
6891
  type="checkbox"
6667
6892
  checked={allVisibleChecked}
@@ -6684,9 +6909,11 @@ export function ResourcesView({
6684
6909
  return (
6685
6910
  <th
6686
6911
  key={col.key}
6912
+ style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, isCheckboxMode, 'name') : undefined}
6687
6913
  className={clsx(
6688
6914
  'text-left px-4 py-3 text-xs font-medium uppercase tracking-wide relative group/th',
6689
6915
  'bg-theme-base border-b border-theme-border',
6916
+ colIdx === 0 && col.key === 'name' && PHONE_PINNED_HEAD,
6690
6917
  !isLastCol && 'border-r-subtle',
6691
6918
  isSortable ? 'text-theme-text-secondary hover:text-theme-text-primary cursor-pointer select-none' : 'text-theme-text-secondary'
6692
6919
  )}
@@ -6874,7 +7101,7 @@ export function ResourcesView({
6874
7101
  })()}
6875
7102
  {/* Resize handle with visible divider */}
6876
7103
  <div
6877
- className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center"
7104
+ className="absolute right-0 top-0 bottom-0 w-3 cursor-col-resize flex items-center justify-center phone:hidden"
6878
7105
  style={{ transform: 'translateX(50%)' , zIndex: 10 }}
6879
7106
  onMouseDown={(e) => {
6880
7107
  const th = e.currentTarget.parentElement!
@@ -6922,6 +7149,7 @@ export function ResourcesView({
6922
7149
  comparePickIndex={pickIdx}
6923
7150
  rowHref={rowHrefFor?.(resource)}
6924
7151
  onRowCheckToggle={toggleChecked}
7152
+ findingMatch={findingsSearch?.matches.get(resource)}
6925
7153
  />
6926
7154
  )
6927
7155
  }}
@@ -6955,13 +7183,12 @@ export function ResourcesView({
6955
7183
  open={tableActive && showBulkDeleteConfirm}
6956
7184
  onClose={() => { setShowBulkDeleteConfirm(false); setBulkForceDelete(false) }}
6957
7185
  onConfirm={() => {
7186
+ const submittedKeys = new Set(checkedItems.map(getResourceKey))
7187
+ setShowBulkDeleteConfirm(false)
7188
+ setBulkForceDelete(false)
6958
7189
  onBulkDelete?.(checkedBulkItems, {
6959
7190
  force: bulkForceDelete,
6960
- onSuccess: () => {
6961
- clearBulkSelection()
6962
- setShowBulkDeleteConfirm(false)
6963
- setBulkForceDelete(false)
6964
- },
7191
+ onSuccess: () => setCheckedResources(current => new Set([...current].filter(key => !submittedKeys.has(key)))),
6965
7192
  })
6966
7193
  }}
6967
7194
  title={`Delete ${checkedItems.length} ${selectedKind.kind}${checkedItems.length > 1 ? 's' : ''}?`}
@@ -6972,6 +7199,7 @@ export function ResourcesView({
6972
7199
  isLoading={isBulkDeleting}
6973
7200
  isClosable
6974
7201
  >
7202
+ <CrossplaneDeleteNotice resources={checkedItems} force={bulkForceDelete} />
6975
7203
  <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
6976
7204
  <input
6977
7205
  type="checkbox"
@@ -7089,6 +7317,9 @@ interface ResourceRowCellsProps {
7089
7317
  * is unaffected (still toggles picks). */
7090
7318
  rowHref?: string
7091
7319
  onRowCheckToggle?: (resource: any) => void
7320
+ /** What a Trivy report matched the table search through, when its name
7321
+ * and namespace didn't. */
7322
+ findingMatch?: TrivyFindingMatch
7092
7323
  }
7093
7324
 
7094
7325
  function rowHighlightClass(
@@ -7110,7 +7341,7 @@ function rowHighlightClass(
7110
7341
  return 'group-hover/row:bg-theme-surface/50'
7111
7342
  }
7112
7343
 
7113
- 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) {
7344
+ 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) {
7114
7345
  const rowHighlight = rowHighlightClass(compareMode, comparePickIndex, isSelected, isHighlighted, isChecked)
7115
7346
  const pickedSide = comparePickIndex === 0 ? 'a' : comparePickIndex === 1 ? 'b' : null
7116
7347
  const onClick = onRowClick ? () => onRowClick(resource, !!isSelected) : undefined
@@ -7125,8 +7356,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7125
7356
  <td
7126
7357
  onClick={onClick}
7127
7358
  onMouseEnter={onMouseEnter}
7128
- style={COMPARE_COLUMN_STYLE}
7129
- className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight)}
7359
+ style={{ ...COMPARE_COLUMN_STYLE, ...phonePinnedLeft(compareMode, showCheckbox, 'compare') }}
7360
+ className={clsx('px-2 py-3 border-b-subtle cursor-pointer text-center align-middle transition-colors', rowHighlight, PHONE_PINNED_CELL)}
7130
7361
  >
7131
7362
  {pickedSide ? (
7132
7363
  <span
@@ -7148,7 +7379,8 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7148
7379
  )}
7149
7380
  {showCheckbox && (
7150
7381
  <td
7151
- className={clsx('border-b-subtle text-center px-0 py-3 w-10 cursor-pointer transition-colors', rowHighlight)}
7382
+ style={phonePinnedLeft(compareMode, showCheckbox, 'checkbox')}
7383
+ className={clsx('border-b-subtle text-center px-0 py-3 w-10 cursor-pointer transition-colors', rowHighlight, PHONE_PINNED_CELL)}
7152
7384
  onClick={(e) => { e.stopPropagation(); onRowCheckToggle?.(resource) }}
7153
7385
  >
7154
7386
  <input
@@ -7159,16 +7391,18 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7159
7391
  />
7160
7392
  </td>
7161
7393
  )}
7162
- {columns.map((col) => (
7394
+ {columns.map((col, colIdx) => (
7163
7395
  <td
7164
7396
  key={col.key}
7165
7397
  onClick={cellsAreClickable ? onClick : undefined}
7166
7398
  onMouseEnter={onMouseEnter}
7399
+ style={colIdx === 0 && col.key === 'name' ? phonePinnedLeft(compareMode, showCheckbox, 'name') : undefined}
7167
7400
  className={clsx(
7168
7401
  'px-4 py-3 border-b-subtle transition-colors',
7169
7402
  cellsAreClickable && 'cursor-pointer',
7170
7403
  col.key !== 'status' && 'overflow-hidden truncate',
7171
7404
  rowHighlight,
7405
+ colIdx === 0 && col.key === 'name' && PHONE_PINNED_CELL,
7172
7406
  )}
7173
7407
  >
7174
7408
  <CellContent
@@ -7180,6 +7414,7 @@ const ResourceRowCells = React.memo(function ResourceRowCells({ resource, kind,
7180
7414
  storeProviders={storeProviders}
7181
7415
  extraColumn={extraColumnsByKey?.get(col.key)}
7182
7416
  nameHref={col.key === 'name' ? rowHref : undefined}
7417
+ findingMatch={col.key === 'name' ? findingMatch : undefined}
7183
7418
  />
7184
7419
  </td>
7185
7420
  ))}
@@ -7229,9 +7464,10 @@ interface CellContentProps {
7229
7464
  /** When set on the name column, the resource name renders as `<a href>`
7230
7465
  * so ⌘-click / copy-link / hover-URL all work. */
7231
7466
  nameHref?: string
7467
+ findingMatch?: TrivyFindingMatch
7232
7468
  }
7233
7469
 
7234
- function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref }: CellContentProps) {
7470
+ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion, storeProviders, extraColumn, nameHref, findingMatch }: CellContentProps) {
7235
7471
  const { auditBadges } = useContext(ResourcesViewDataContext)
7236
7472
  // Parent-injected extra columns short-circuit the built-in switch.
7237
7473
  // Used by hosts that inject leading columns (e.g. a multi-cluster Cluster column).
@@ -7245,11 +7481,12 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7245
7481
  if (column === 'name') {
7246
7482
  const isTerminating = !!meta.deletionTimestamp
7247
7483
  const nameClass = clsx('text-sm font-medium truncate block', isTerminating ? 'text-theme-text-tertiary line-through' : 'text-theme-text-primary')
7484
+ const pauseState = isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged' ? getCrossplanePauseState(resource) : null
7248
7485
  const audit = auditBadges?.[`${meta.namespace || ''}/${meta.name}`]
7249
7486
  const auditTotal = audit ? audit.danger + audit.warning : 0
7250
7487
  return (
7251
7488
  <div className="flex items-center gap-1.5 min-w-0">
7252
- <Tooltip content={meta.name}>
7489
+ <Tooltip content={meta.name} wrapperClassName={findingMatch || pauseState ? 'min-w-0' : undefined}>
7253
7490
  {nameHref ? (
7254
7491
  <a
7255
7492
  href={nameHref}
@@ -7264,6 +7501,14 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7264
7501
  )}
7265
7502
  </Tooltip>
7266
7503
  <CopyNameButton name={meta.name} />
7504
+ <CrossplanePauseChip state={pauseState} compact />
7505
+ {findingMatch && (
7506
+ <Tooltip content={findingMatch.text}>
7507
+ <span className="shrink-0 text-xs italic text-theme-text-tertiary cursor-help">
7508
+ {findingMatch.site === 'image' ? 'in image' : 'in findings'}
7509
+ </span>
7510
+ </Tooltip>
7511
+ )}
7267
7512
  {auditTotal > 0 && audit && (
7268
7513
  <Tooltip content={audit.messages && audit.messages.length > 0
7269
7514
  ? <AuditBadgeTooltip messages={audit.messages} />
@@ -7305,6 +7550,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7305
7550
 
7306
7551
  // Kind-specific columns (normalize CRD singular names like 'ScaledObject' → 'scaledobjects')
7307
7552
  const kindLower = normalizeKindToPlural(kind, group)
7553
+ if (column === 'status' && isLikelyCrossplaneMRGroup(kind, group ?? '') && isManagedResource(resource)) {
7554
+ return <ManagedResourceCell resource={resource} column={column} />
7555
+ }
7308
7556
  if (group && !hasCuratedColumns(kind, group)) {
7309
7557
  return <GenericCell resource={resource} column={column} />
7310
7558
  }
@@ -7331,6 +7579,14 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7331
7579
  return <KyvernoCleanupPolicyCell resource={resource} column={column} />
7332
7580
  }
7333
7581
 
7582
+ if (isApiGroup(resource.apiVersion, 'opentelemetry.io') && ['opentelemetrycollectors', 'instrumentations'].includes(kindLower)) {
7583
+ if (column === 'status') return <GenericCell resource={resource} column={column} />
7584
+ return <span className="text-sm text-theme-text-secondary break-all">{getOpenTelemetryColumnValue(resource, column) || '-'}</span>
7585
+ }
7586
+ if (isApiGroup(resource.apiVersion, 'flagger.app') && ['canaries', 'metrictemplates'].includes(kindLower)) {
7587
+ if (column === 'status') return <GenericCell resource={resource} column={column} />
7588
+ return <span className="text-sm text-theme-text-secondary break-all">{getFlaggerColumnValue(resource, column) || '-'}</span>
7589
+ }
7334
7590
  switch (kindLower) {
7335
7591
  case 'pods':
7336
7592
  return <PodCell resource={resource} column={column} />
@@ -7400,7 +7656,7 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7400
7656
  // Disambiguate Gateway API vs Istio Gateway by apiVersion. Retained
7401
7657
  // alongside the case above because not every call site normalizes the
7402
7658
  // kind through GROUP_QUALIFIED_COLUMN_KEYS before dispatching.
7403
- if (resource.apiVersion?.includes('networking.istio.io')) {
7659
+ if (isApiGroup(resource.apiVersion, 'networking.istio.io')) {
7404
7660
  return <IstioGatewayCell resource={resource} column={column} />
7405
7661
  }
7406
7662
  return <GatewayCell resource={resource} column={column} />
@@ -7890,6 +8146,9 @@ function CellContent({ resource, kind, column, group, majorityNodeMinorVersion,
7890
8146
  function GenericCell({ resource, column }: { resource: any; column: string }) {
7891
8147
  switch (column) {
7892
8148
  case 'status': {
8149
+ if (isManagedResource(resource) || isComposite(resource) || getCrossplanePauseState(resource) === 'acknowledged') {
8150
+ return <CrossplaneStatusCell resource={resource} />
8151
+ }
7893
8152
  const derived = getGenericResourceStatus(resource)
7894
8153
  if (!derived) return <span className="text-sm text-theme-text-tertiary">-</span>
7895
8154
  return (