@skyhook-io/radar-app 1.13.4 → 1.14.0

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 (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -1,6 +1,6 @@
1
1
  import { createElement, useState, useRef, useEffect, useMemo, useCallback } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
- import { X, File, Link2, ChevronRight, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
3
+ import { X, File, Link2, ChevronRight, ArrowLeft, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
4
4
  import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
5
5
  import { clsx } from 'clsx'
6
6
  import type { FileNode } from '../../types'
@@ -11,6 +11,7 @@ import { isDesktopApp } from '../../utils/desktop-download'
11
11
  import { openFile, openFolder } from '../../utils/desktop-open-folder'
12
12
  import { useToast } from '../ui/Toast'
13
13
  import { Tooltip } from '../ui/Tooltip'
14
+ import { PodFilePreview, PREVIEW_BYTE_CAP } from './PodFilePreview'
14
15
 
15
16
  interface PodFilesystem {
16
17
  root: FileNode
@@ -68,6 +69,56 @@ async function savePodFileToDisk(
68
69
  return body.path
69
70
  }
70
71
 
72
+ type PodFileToast = Pick<ReturnType<typeof useToast>, 'showSuccess' | 'showError'>
73
+
74
+ // One download path for the listing row and the preview header. Desktop saves
75
+ // straight from the backend; the browser gets a blob.
76
+ async function downloadPodFile(
77
+ namespace: string,
78
+ podName: string,
79
+ container: string,
80
+ node: Pick<FileNode, 'path' | 'name'>,
81
+ { showSuccess, showError }: PodFileToast,
82
+ ) {
83
+ try {
84
+ if (await isDesktopApp()) {
85
+ const savedPath = await savePodFileToDisk(namespace, podName, container, node.path)
86
+ showSuccess(
87
+ 'File saved',
88
+ savedPath,
89
+ {
90
+ label: 'Show in Finder',
91
+ icon: createElement(FolderOpen, { className: 'w-3.5 h-3.5' }),
92
+ onClick: () => openFolder(savedPath),
93
+ },
94
+ () => openFile(savedPath),
95
+ )
96
+ return
97
+ }
98
+
99
+ const params = new URLSearchParams()
100
+ params.set('container', container)
101
+ params.set('path', node.path)
102
+
103
+ const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files/download?${params.toString()}`), {
104
+ credentials: getCredentialsMode(),
105
+ headers: getAuthHeaders(),
106
+ })
107
+ if (!response.ok) {
108
+ const err = await response.json().catch(() => ({ error: 'Download failed' }))
109
+ throw new Error(err.error || `HTTP ${response.status}`)
110
+ }
111
+
112
+ const blob = await response.blob()
113
+ await downloadBlob(blob, node.name)
114
+ } catch (err) {
115
+ const message = err instanceof Error ? err.message : String(err)
116
+ if (message !== 'cancelled') {
117
+ showError(`Could not download ${node.name}`, message)
118
+ }
119
+ }
120
+ }
121
+
71
122
  interface PodFilesystemModalProps {
72
123
  open: boolean
73
124
  onClose: () => void
@@ -94,8 +145,12 @@ export function PodFilesystemModal({
94
145
  const [filesystem, setFilesystem] = useState<PodFilesystem | null>(null)
95
146
  const [isLoading, setIsLoading] = useState(false)
96
147
  const [error, setError] = useState<string | null>(null)
148
+ // The file being viewed in place of the listing; null shows the directory.
149
+ const [previewFile, setPreviewFile] = useState<FileNode | null>(null)
150
+ const toast = useToast()
97
151
 
98
152
  const loadDirectory = useCallback(async (dirPath: string) => {
153
+ setPreviewFile(null)
99
154
  setIsLoading(true)
100
155
  setError(null)
101
156
  try {
@@ -124,19 +179,24 @@ export function PodFilesystemModal({
124
179
  setError(null)
125
180
  setIsLoading(false)
126
181
  setCurrentPath('/')
182
+ setPreviewFile(null)
127
183
  setSelectedContainer(initialContainer || containers[0] || '')
128
184
  }
129
185
  }, [open, initialContainer, containers])
130
186
 
131
- // Handle ESC key
187
+ // ESC steps back one level: out of a file to its listing, then out of the
188
+ // dialog. One listener owns both so the two can never race.
132
189
  useEffect(() => {
133
190
  if (!open) return
134
191
  const handleKeyDown = (e: KeyboardEvent) => {
135
- if (e.key === 'Escape') { e.stopPropagation(); onClose() }
192
+ if (e.key !== 'Escape') return
193
+ e.stopPropagation()
194
+ if (previewFile) setPreviewFile(null)
195
+ else onClose()
136
196
  }
137
197
  document.addEventListener('keydown', handleKeyDown, true)
138
198
  return () => document.removeEventListener('keydown', handleKeyDown, true)
139
- }, [open, onClose])
199
+ }, [open, onClose, previewFile])
140
200
 
141
201
  // Focus trap
142
202
  useEffect(() => {
@@ -149,10 +209,19 @@ export function PodFilesystemModal({
149
209
 
150
210
  const showFilesystem = filesystem && filesystem.root
151
211
 
152
- // Build breadcrumb segments
153
- const pathSegments = currentPath === '/'
212
+ // Breadcrumb: the directory path, plus the file when one is open. Clicking
213
+ // the current directory while viewing a file just returns to its listing.
214
+ const breadcrumbPath = previewFile ? previewFile.path : currentPath
215
+ const pathSegments = breadcrumbPath === '/'
154
216
  ? ['/']
155
- : ['/', ...currentPath.split('/').filter(Boolean)]
217
+ : ['/', ...breadcrumbPath.split('/').filter(Boolean)]
218
+ const goToDirectory = (dirPath: string) => {
219
+ if (dirPath === currentPath) setPreviewFile(null)
220
+ else loadDirectory(dirPath)
221
+ }
222
+ const downloadPreviewFile = () => {
223
+ if (previewFile) downloadPodFile(namespace, podName, selectedContainer, previewFile, toast)
224
+ }
156
225
 
157
226
  return createPortal(
158
227
  <div className="fixed inset-0 z-[100] flex items-center justify-center">
@@ -163,19 +232,47 @@ export function PodFilesystemModal({
163
232
  <div
164
233
  ref={dialogRef}
165
234
  tabIndex={-1}
166
- className="relative dialog w-full max-w-4xl mx-4 max-h-[85vh] flex flex-col outline-none"
235
+ className="relative dialog w-full max-w-4xl mx-4 h-[85vh] flex flex-col outline-none"
167
236
  >
168
237
  {/* Header */}
169
238
  <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
170
- <div className="flex-1 min-w-0">
171
- <h3 className="text-lg font-semibold text-theme-text-primary">Pod Files</h3>
172
- <p className="text-sm text-theme-text-secondary truncate mt-0.5">
173
- {namespace}/{podName}
174
- </p>
175
- </div>
239
+ {previewFile ? (
240
+ <>
241
+ <Tooltip content="Back to files">
242
+ <button
243
+ onClick={() => setPreviewFile(null)}
244
+ className="p-2 mr-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
245
+ >
246
+ <ArrowLeft className="w-5 h-5" />
247
+ </button>
248
+ </Tooltip>
249
+ <div className="flex-1 min-w-0">
250
+ <h3 className="text-lg font-semibold text-theme-text-primary truncate">{previewFile.name}</h3>
251
+ <p className="text-sm text-theme-text-secondary truncate mt-0.5">
252
+ {namespace}/{podName} · {selectedContainer}
253
+ {previewFile.size !== undefined ? ` · ${formatBytes(previewFile.size)}` : null}
254
+ </p>
255
+ </div>
256
+ <Tooltip content="Download file">
257
+ <button
258
+ onClick={downloadPreviewFile}
259
+ className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
260
+ >
261
+ <Download className="w-5 h-5" />
262
+ </button>
263
+ </Tooltip>
264
+ </>
265
+ ) : (
266
+ <div className="flex-1 min-w-0">
267
+ <h3 className="text-lg font-semibold text-theme-text-primary">Pod Files</h3>
268
+ <p className="text-sm text-theme-text-secondary truncate mt-0.5">
269
+ {namespace}/{podName}
270
+ </p>
271
+ </div>
272
+ )}
176
273
 
177
274
  {/* Container selector */}
178
- {containers.length > 1 && (
275
+ {!previewFile && containers.length > 1 && (
179
276
  <select
180
277
  value={selectedContainer}
181
278
  onChange={(e) => setSelectedContainer(e.target.value)}
@@ -206,7 +303,7 @@ export function PodFilesystemModal({
206
303
  <span key={segmentPath} className="flex items-center gap-1">
207
304
  {i > 0 && <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />}
208
305
  <button
209
- onClick={() => !isLast && loadDirectory(segmentPath)}
306
+ onClick={() => !isLast && goToDirectory(segmentPath)}
210
307
  className={clsx(
211
308
  'truncate',
212
309
  isLast
@@ -222,7 +319,7 @@ export function PodFilesystemModal({
222
319
  </div>
223
320
 
224
321
  {/* Search */}
225
- {showFilesystem && (
322
+ {showFilesystem && !previewFile && (
226
323
  <div className="relative flex-1 min-w-[200px]">
227
324
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
228
325
  <Input
@@ -236,9 +333,22 @@ export function PodFilesystemModal({
236
333
  </div>
237
334
 
238
335
  {/* Content */}
239
- <div className="flex-1 overflow-y-auto p-4">
336
+ {previewFile ? (
337
+ <div className="flex-1 min-h-0 flex flex-col">
338
+ <PodFilePreview
339
+ key={previewFile.path}
340
+ namespace={namespace}
341
+ podName={podName}
342
+ container={selectedContainer}
343
+ filePath={previewFile.path}
344
+ fileName={previewFile.name}
345
+ onDownload={downloadPreviewFile}
346
+ />
347
+ </div>
348
+ ) : (
349
+ <div className="flex-1 min-h-0 overflow-y-auto p-4">
240
350
  {/* Loading */}
241
- {isLoading && <PaneLoader label="Loading files…" className="h-64" />}
351
+ {isLoading && <PaneLoader label="Loading files…" className="h-full" />}
242
352
 
243
353
  {/* Error */}
244
354
  {error && !isLoading && (
@@ -262,9 +372,11 @@ export function PodFilesystemModal({
262
372
  podName={podName}
263
373
  container={selectedContainer}
264
374
  onNavigate={loadDirectory}
375
+ onOpenFile={setPreviewFile}
265
376
  />
266
377
  )}
267
378
  </div>
379
+ )}
268
380
 
269
381
  {/* Footer with stats */}
270
382
  <div className="p-3 border-t border-theme-border text-xs text-theme-text-tertiary flex items-center gap-4 shrink-0">
@@ -300,9 +412,10 @@ interface PodFileTreeViewProps {
300
412
  podName: string
301
413
  container: string
302
414
  onNavigate: (path: string) => void
415
+ onOpenFile: (node: FileNode) => void
303
416
  }
304
417
 
305
- function PodFileTreeView({ root, searchQuery, namespace, podName, container, onNavigate }: PodFileTreeViewProps) {
418
+ function PodFileTreeView({ root, searchQuery, namespace, podName, container, onNavigate, onOpenFile }: PodFileTreeViewProps) {
306
419
  const filteredRoot = useMemo(() => {
307
420
  if (!searchQuery.trim()) return root
308
421
  return filterTree(root, searchQuery.toLowerCase())
@@ -326,6 +439,7 @@ function PodFileTreeView({ root, searchQuery, namespace, podName, container, onN
326
439
  podName={podName}
327
440
  container={container}
328
441
  onNavigate={onNavigate}
442
+ onOpenFile={onOpenFile}
329
443
  />
330
444
  ))}
331
445
  </div>
@@ -338,72 +452,48 @@ interface PodFileTreeNodeProps {
338
452
  podName: string
339
453
  container: string
340
454
  onNavigate: (path: string) => void
455
+ onOpenFile: (node: FileNode) => void
341
456
  }
342
457
 
343
- function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: PodFileTreeNodeProps) {
458
+ function PodFileTreeNode({ node, namespace, podName, container, onNavigate, onOpenFile }: PodFileTreeNodeProps) {
344
459
  const [downloading, setDownloading] = useState(false)
345
- const { showSuccess, showError } = useToast()
460
+ const toast = useToast()
346
461
  const isDir = node.type === 'dir'
347
462
  const isSymlink = node.type === 'symlink'
348
- const isDownloadable = !isDir // files and symlinks can be downloaded
463
+ // Size is the one preview rejection the listing can see coming. Whether a
464
+ // file is binary or readable is only known once its bytes are read.
465
+ const tooLarge = !isDir && (node.size ?? 0) > PREVIEW_BYTE_CAP
349
466
 
350
467
  const handleDownload = async (e: React.MouseEvent) => {
351
468
  e.stopPropagation()
352
469
  if (downloading) return
353
-
354
470
  setDownloading(true)
355
471
  try {
356
- if (await isDesktopApp()) {
357
- const savedPath = await savePodFileToDisk(namespace, podName, container, node.path)
358
- showSuccess(
359
- 'File saved',
360
- savedPath,
361
- {
362
- label: 'Show in Finder',
363
- icon: createElement(FolderOpen, { className: 'w-3.5 h-3.5' }),
364
- onClick: () => openFolder(savedPath),
365
- },
366
- () => openFile(savedPath),
367
- )
368
- return
369
- }
370
-
371
- const params = new URLSearchParams()
372
- params.set('container', container)
373
- params.set('path', node.path)
374
-
375
- const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files/download?${params.toString()}`), {
376
- credentials: getCredentialsMode(),
377
- headers: getAuthHeaders(),
378
- })
379
- if (!response.ok) {
380
- const err = await response.json().catch(() => ({ error: 'Download failed' }))
381
- throw new Error(err.error || `HTTP ${response.status}`)
382
- }
383
-
384
- const blob = await response.blob()
385
- await downloadBlob(blob, node.name)
386
- } catch (err) {
387
- const message = err instanceof Error ? err.message : String(err)
388
- if (message !== 'cancelled') {
389
- showError(`Could not download ${node.name}`, message)
390
- }
472
+ await downloadPodFile(namespace, podName, container, node, toast)
391
473
  } finally {
392
474
  setDownloading(false)
393
475
  }
394
476
  }
395
477
 
478
+ // Directories open in place; anything else opens in the viewer, which
479
+ // reports a symlink to a directory as "not a regular file" itself.
396
480
  const handleClick = () => {
397
- if (isDir) {
398
- onNavigate(node.path)
399
- }
481
+ if (isDir) onNavigate(node.path)
482
+ else if (!tooLarge) onOpenFile(node)
400
483
  }
401
484
 
402
485
  return (
486
+ <Tooltip
487
+ content={`Too large to preview (${formatBytes(node.size ?? 0)}) — download instead`}
488
+ disabled={!tooLarge}
489
+ wrapperClassName="flex w-full"
490
+ position="bottom"
491
+ >
403
492
  <div
404
493
  className={clsx(
405
- 'flex items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
406
- isDir && 'font-medium cursor-pointer'
494
+ 'flex flex-1 min-w-0 items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
495
+ isDir && 'font-medium',
496
+ tooLarge ? 'cursor-default' : 'cursor-pointer'
407
497
  )}
408
498
  onClick={handleClick}
409
499
  >
@@ -415,7 +505,7 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
415
505
  <File className="w-4 h-4 text-theme-text-tertiary shrink-0" />
416
506
  )}
417
507
 
418
- <span className="text-theme-text-primary truncate flex-1">{node.name}</span>
508
+ <span className={clsx('truncate flex-1', tooLarge ? 'text-theme-text-secondary' : 'text-theme-text-primary')}>{node.name}</span>
419
509
 
420
510
  {isSymlink && node.linkTarget && (
421
511
  <span className="text-xs text-cyan-400 truncate max-w-48">
@@ -435,7 +525,7 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
435
525
  </span>
436
526
  )}
437
527
 
438
- {isDownloadable && (
528
+ {!isDir && (
439
529
  <Tooltip content="Download file" wrapperClassName="ml-1">
440
530
  <button
441
531
  onClick={handleDownload}
@@ -451,6 +541,6 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
451
541
  </Tooltip>
452
542
  )}
453
543
  </div>
544
+ </Tooltip>
454
545
  )
455
546
  }
456
-
@@ -1,7 +1,7 @@
1
1
  import { useState, useMemo, useCallback, useEffect } from 'react'
2
2
  import { useLocation, useNavigate } from 'react-router-dom'
3
3
  import { useQuery } from '@tanstack/react-query'
4
- import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
4
+ import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, isKindSyncFailed, isStillLoadingError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
5
5
  import { isBadgeWorthy } from '../../utils/auditBadges'
6
6
  import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
7
7
  import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
@@ -143,7 +143,18 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
143
143
  }
144
144
  },
145
145
  staleTime: 10000,
146
- refetchInterval: 60000, // Safety net — SSE k8s_event drives near-real-time invalidation
146
+ // SSE invalidation isn't running while connecting, and mid-sync counts
147
+ // are what unlatch guarded kinds as their informers finish — poll fast
148
+ // during the shell, settle to the safety net once connected.
149
+ refetchInterval: connection.state === 'connecting' ? 3000 : 60000,
150
+ // During the first seconds of the progressive shell the endpoint 503s
151
+ // (cluster_connecting) until the mid-sync cache handle exists; keep the
152
+ // query pending rather than parking it in error state, which would
153
+ // unlatch the large-list guard at the connected flip.
154
+ retry: (failureCount: number, error: Error) =>
155
+ isStillLoadingError(error) ? true : failureCount < 3,
156
+ retryDelay: (failureCount: number, error: Error) =>
157
+ isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
147
158
  })
148
159
 
149
160
  // Determine if selected kind is a CRD (only CRDs should send ?group= to backend)
@@ -200,8 +211,29 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
200
211
  const isSelectedKindGuarded = selectedCountKey !== '' && LARGE_RESOURCE_LIST_GUARD_KEYS.has(selectedCountKey)
201
212
  const selectedKindSummaryServed = SUMMARY_LIST_KINDS.has(selectedCountKey)
202
213
  const selectedKindRowLimit = selectedKindSummaryServed ? SUMMARY_LIST_LIMIT : LARGE_RESOURCE_LIST_LIMIT
203
- const waitingForGuardCount = isSelectedKindGuarded && !countsData && !countsIsError
204
- const largeListBlocked = isSelectedKindGuarded && countsData != null && (selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > selectedKindRowLimit))
214
+ // While still 'connecting', /resource-counts is unavailable — its error
215
+ // must NOT unlatch the large-list guard, or a huge Pods list could fetch
216
+ // unguarded on exactly the clusters the guard protects. Counts arrive
217
+ // right after 'connected' and settle the guard then.
218
+ const syncShellActive = connection.state === 'connecting'
219
+ // A kind whose sync deadline fired is unavailable-with-a-reason: hand it to
220
+ // the list endpoint, whose 503 kind_sync_failed renders the honest error —
221
+ // the guard would otherwise trap it on "count unavailable" forever. Safe to
222
+ // unblock: the server serves no rows for a failed kind.
223
+ const selectedCountFailedSync =
224
+ selectedCountUnavailable && countsData?.reasons?.[selectedCountKey] === 'kind_sync_failed'
225
+ // A guarded kind whose informer hasn't finished reports unavailable with a
226
+ // kind_sync_pending reason — "count not known YET", so keep the loading
227
+ // state. Keyed on the reason, not on connection.state: deferred kinds sync
228
+ // after connect. Reasonless unavailable stays a real verification failure
229
+ // and blocks the view as before.
230
+ const selectedCountPendingSync =
231
+ (selectedCountUnavailable && countsData?.reasons?.[selectedCountKey] === 'kind_sync_pending') ||
232
+ (syncShellActive && selectedCountUnavailable && !selectedCountFailedSync)
233
+ const waitingForGuardCount = isSelectedKindGuarded &&
234
+ ((!countsData && (!countsIsError || syncShellActive)) || selectedCountPendingSync)
235
+ const largeListBlocked = isSelectedKindGuarded && countsData != null && !selectedCountPendingSync && !selectedCountFailedSync &&
236
+ (selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > selectedKindRowLimit))
205
237
  const selectedKindQueryBlocked = waitingForGuardCount || largeListBlocked
206
238
  const podCount = countsData?.counts.Pod
207
239
  const podCountKnown = hasResourceCount(countsData?.counts, 'Pod')
@@ -285,9 +317,15 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
285
317
  staleTime: 30000,
286
318
  refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
287
319
  retry: (failureCount: number, error: Error) => {
288
- if (isForbiddenError(error)) return false
320
+ if (isForbiddenError(error) || isKindSyncFailed(error)) return false
321
+ // Cluster still connecting, or this kind's informer still completing
322
+ // its initial sync: keep the query in its loading state and retry —
323
+ // the header's sync-progress label explains the wait.
324
+ if (isStillLoadingError(error)) return true
289
325
  return failureCount < 3
290
326
  },
327
+ retryDelay: (failureCount: number, error: Error) =>
328
+ isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
291
329
  })
292
330
 
293
331
  // Map to ResourceQueryResult shape
@@ -398,7 +436,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
398
436
  resourceUnavailable={countsData?.unavailable}
399
437
  selectedKindQuery={selectedKindQueryResult}
400
438
  printerTable={selectedKindQueryBlocked ? null : selectedKindQuery.data?.printerTable ?? null}
401
- connectionState={connection.state}
439
+ connectionState={connection.state === 'connecting' && connection.syncStatus ? 'syncing' : connection.state}
402
440
  largeListGuard={largeListGuard}
403
441
  onSelectedKindChange={setSelectedKind}
404
442
  topPodMetrics={topPodMetrics}
@@ -1,7 +1,7 @@
1
1
  import { NamespaceRenderer as BaseNamespaceRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/NamespaceRenderer'
2
2
  import type { ResourceRef } from '@skyhook-io/k8s-ui'
3
3
  import { useRBACNamespace } from '../../../api/rbac'
4
- import { useNamespaceQuotas } from '../../../api/quotas'
4
+ import { useNamespaceLimitRanges, useNamespaceQuotas } from '../../../api/quotas'
5
5
  import { isForbiddenError } from '../../../api/client'
6
6
 
7
7
  interface NamespaceRendererProps {
@@ -17,6 +17,9 @@ export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps)
17
17
  // RBAC sections). Surface other errors (500/503) so a quota-constrained
18
18
  // namespace doesn't silently render as quota-free.
19
19
  const quotaErr = quotaError && !isForbiddenError(quotaError) ? (quotaError as Error) : null
20
+ // LimitRanges keep their 403: the section states the denial rather than
21
+ // hiding, because "no rules apply here" is the wrong thing to imply.
22
+ const { data: limitRangeData, isLoading: limitRangeLoading, error: limitRangeError } = useNamespaceLimitRanges(name, !!name)
20
23
  return (
21
24
  <BaseNamespaceRenderer
22
25
  data={data}
@@ -25,6 +28,9 @@ export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps)
25
28
  rbacError={error as Error | null}
26
29
  quotaData={quotaData}
27
30
  quotaError={quotaErr}
31
+ limitRangeData={limitRangeData}
32
+ limitRangeLoading={limitRangeLoading}
33
+ limitRangeError={limitRangeError}
28
34
  onNavigate={onNavigate}
29
35
  />
30
36
  )
@@ -7,6 +7,7 @@ import { useCapabilitiesContext, useNamespacedCapabilities, useIsLocalDeployment
7
7
  import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodEnvironment, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus, useRevealPodEnvironment } from '../../../api/client'
8
8
  import { useRBACSubject } from '../../../api/rbac'
9
9
  import { usePolicyResource } from '../../../api/policy'
10
+ import { podLimitRangeNames, useNamespaceLimitRanges } from '../../../api/quotas'
10
11
  import { podAwaitsScheduling } from '../../capacity/podDemandGate'
11
12
  import { PortForwardInlineButton } from '../../portforward/PortForwardButton'
12
13
  import { ImageFilesystemModal } from '../ImageFilesystemModal'
@@ -79,6 +80,8 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
79
80
  'pods', namespace ?? '', podName, !!namespace && !!podName,
80
81
  )
81
82
 
83
+ const { data: limitRanges, isSuccess: limitRangesReady } = useNamespaceLimitRanges(namespace ?? '', !!namespace)
84
+
82
85
  return (
83
86
  <BasePodRenderer
84
87
  data={data}
@@ -110,6 +113,7 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
110
113
  policyData={policyData ?? null}
111
114
  policyLoading={policyLoading}
112
115
  policyError={policyError as Error | null}
116
+ namespaceLimitRangeNames={limitRangesReady ? podLimitRangeNames(limitRanges) : undefined}
113
117
  canExec={canExec}
114
118
  canViewLogs={canViewLogs}
115
119
  canPortForward={showPortForward}
@@ -1,5 +1,12 @@
1
1
  import { RolloutRenderer as BaseRolloutRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RolloutRenderer'
2
- import { useRolloutAction, useRolloutCapabilities, type RolloutAction } from '../../../api/client'
2
+ import {
3
+ useRolloutAction,
4
+ useRolloutAnalysisRuns,
5
+ useRolloutCapabilities,
6
+ useWorkloadRevisions,
7
+ useWorkloadPods,
8
+ type RolloutAction,
9
+ } from '../../../api/client'
3
10
 
4
11
  interface RolloutRendererProps {
5
12
  data: any
@@ -11,6 +18,9 @@ export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
11
18
  const name = data?.metadata?.name ?? ''
12
19
  const { data: capabilities } = useRolloutCapabilities(namespace, name)
13
20
  const action = useRolloutAction()
21
+ const { data: analysisRunsResponse, error: analysisRunsError } = useRolloutAnalysisRuns(namespace, name)
22
+ const { data: revisions } = useWorkloadRevisions('rollouts', namespace, name)
23
+ const { data: podsResponse } = useWorkloadPods('rollouts', namespace, name)
14
24
 
15
25
  return (
16
26
  <BaseRolloutRenderer
@@ -19,6 +29,10 @@ export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
19
29
  capabilities={capabilities}
20
30
  onAction={(next: RolloutAction) => action.mutate({ action: next, namespace, name })}
21
31
  pendingAction={action.isPending ? action.variables?.action ?? null : null}
32
+ analysisRunHistory={analysisRunsResponse?.items}
33
+ analysisRunHistoryError={analysisRunsError}
34
+ revisions={revisions}
35
+ pods={podsResponse?.pods}
22
36
  />
23
37
  )
24
38
  }
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
3
3
  import { useScaleWorkload, fetchJSON } from '../../../api/client'
4
4
  import { useRBACSubject } from '../../../api/rbac'
5
5
  import { usePolicyResource } from '../../../api/policy'
6
+ import { podLimitRangeNames, useNamespaceLimitRanges } from '../../../api/quotas'
6
7
  import { useQueries, useQueryClient } from '@tanstack/react-query'
7
8
  import { kindToPlural, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
8
9
  import type { Relationships, ResourceRef, ResourceWithRelationships, WorkloadPodInfo } from '../../../types'
@@ -49,6 +50,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workl
49
50
  const { data: policyData, isLoading: policyLoading, error: policyError } = usePolicyResource(
50
51
  kindToPlural(kind), namespace, metadata.name || '', !!namespace && !!metadata.name,
51
52
  )
53
+ const { data: limitRanges, isSuccess: limitRangesReady } = useNamespaceLimitRanges(namespace, !!namespace)
52
54
  const hpaRefs = (scaleBlockedBy ?? []).filter(ref => {
53
55
  const refKind = ref.kind.toLowerCase()
54
56
  return refKind === 'horizontalpodautoscaler' || refKind === 'hpa'
@@ -96,6 +98,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workl
96
98
  policyData={policyData ?? null}
97
99
  policyLoading={policyLoading}
98
100
  policyError={policyError as Error | null}
101
+ namespaceLimitRangeNames={limitRangesReady ? podLimitRangeNames(limitRanges) : undefined}
99
102
  scaleBlockedBy={scaleBlockedBy}
100
103
  workloadPods={workloadPods}
101
104
  scalerDiagnostics={scalerDiagnostics}
@@ -799,6 +799,27 @@ function FitWhy({ label, row }: { label: string; row?: RightsizingRow }) {
799
799
  }
800
800
 
801
801
  function EvidenceNote({ row }: { row: RightsizingRow }) {
802
+ const primary = primaryEvidenceNote(row)
803
+ // A denied pod read qualifies whichever reason applies rather than standing in
804
+ // for it: a row can both be unable to check its pods and have a limit conflict,
805
+ // and hiding either one behind the other loses something the reader needs.
806
+ const inventory =
807
+ row.liveInventoryUnavailable && row.resource === 'memory' ? (
808
+ <p className="mt-1 text-xs text-theme-text-tertiary">
809
+ Radar could not read this workload&apos;s pods, so it could not check them for
810
+ out-of-memory restarts.
811
+ </p>
812
+ ) : null
813
+ if (!primary && !inventory) return null
814
+ return (
815
+ <>
816
+ {primary}
817
+ {inventory}
818
+ </>
819
+ )
820
+ }
821
+
822
+ function primaryEvidenceNote(row: RightsizingRow) {
802
823
  if (row.hpaManaged)
803
824
  return (
804
825
  <p className="mt-1 text-xs text-theme-text-tertiary">