@skyhook-io/radar-app 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +125 -87
  4. package/src/api/actions.errorCode.test.ts +40 -0
  5. package/src/api/actions.test.ts +48 -0
  6. package/src/api/actions.ts +79 -0
  7. package/src/api/apiResources.navigation.test.tsx +232 -0
  8. package/src/api/apiResources.ts +46 -3
  9. package/src/api/client.helmSource.test.ts +39 -0
  10. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  11. package/src/api/client.namespaceSwitch.test.ts +107 -0
  12. package/src/api/client.ts +266 -39
  13. package/src/api/cnpg-ha.live.test.ts +140 -0
  14. package/src/api/cnpg-ha.test.ts +78 -0
  15. package/src/api/cnpg-ha.ts +19 -0
  16. package/src/api/cnpg-history.ts +123 -0
  17. package/src/api/cnpg-inspect.ts +102 -0
  18. package/src/api/cnpg-protection.ts +112 -0
  19. package/src/api/cnpg-recovery.test.tsx +60 -0
  20. package/src/api/cnpg-recovery.ts +262 -0
  21. package/src/api/cnpg-sessions.ts +175 -0
  22. package/src/api/cnpg-storage.ts +138 -0
  23. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  24. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  25. package/src/api/cnpg.ts +428 -10
  26. package/src/api/radarFeatures.render.test.tsx +47 -3
  27. package/src/api/radarFeatures.ts +4 -1
  28. package/src/api/reviewedActionMutation.test.tsx +164 -0
  29. package/src/api/reviewedActionMutation.ts +66 -0
  30. package/src/api/timelineSource.test.ts +9 -0
  31. package/src/api/timelineSource.ts +8 -0
  32. package/src/components/audit/AuditView.tsx +9 -0
  33. package/src/components/capacity/CapacityActivity.tsx +3 -31
  34. package/src/components/capacity/CapacityDemand.tsx +4 -36
  35. package/src/components/capacity/CapacityOverview.tsx +217 -56
  36. package/src/components/capacity/CapacityPoolDetail.tsx +6 -55
  37. package/src/components/capacity/CapacityView.test.tsx +323 -0
  38. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  39. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  40. package/src/components/capacity/routes.ts +42 -0
  41. package/src/components/capacity/shared.tsx +11 -129
  42. package/src/components/capacity/workspaceRoute.ts +18 -0
  43. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  44. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  45. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  46. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  47. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  48. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  49. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  50. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  51. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  52. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  53. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  54. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  55. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  56. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  57. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  58. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  59. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  60. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  61. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  62. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  63. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  64. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  65. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  66. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  67. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  68. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  69. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  70. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  71. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  72. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  73. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  74. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  75. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  76. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  77. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  78. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  79. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  80. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  81. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  82. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  83. package/src/components/cnpg/CNPGProtection.tsx +228 -122
  84. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  85. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  86. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  87. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  88. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  89. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  90. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  91. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  92. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  93. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  94. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  95. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  96. package/src/components/cnpg/CNPGView.tsx +34 -21
  97. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  98. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  99. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  100. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  101. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  102. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  103. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  104. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  105. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  106. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  107. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  108. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  109. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  110. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  111. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  112. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  113. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  114. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  115. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  116. package/src/components/cnpg/actions/actionModel.ts +127 -0
  117. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  118. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  119. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  120. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  121. package/src/components/cnpg/archivingRepair.ts +145 -0
  122. package/src/components/cnpg/baseBackup.test.ts +36 -0
  123. package/src/components/cnpg/baseBackup.ts +36 -0
  124. package/src/components/cnpg/blocking.test.ts +59 -0
  125. package/src/components/cnpg/blocking.ts +131 -0
  126. package/src/components/cnpg/coverageText.test.ts +27 -0
  127. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  128. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  129. package/src/components/cnpg/creation/targetModel.ts +149 -0
  130. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  131. package/src/components/cnpg/fleetStatus.ts +43 -0
  132. package/src/components/cnpg/formFields.ts +2 -0
  133. package/src/components/cnpg/grantText.test.tsx +11 -0
  134. package/src/components/cnpg/host.test.tsx +24 -0
  135. package/src/components/cnpg/host.tsx +101 -0
  136. package/src/components/cnpg/inspectModel.test.ts +120 -0
  137. package/src/components/cnpg/inspectModel.ts +140 -0
  138. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  139. package/src/components/cnpg/logicalSlots.ts +29 -0
  140. package/src/components/cnpg/lsn.test.ts +40 -0
  141. package/src/components/cnpg/lsn.ts +43 -0
  142. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  143. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  144. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  145. package/src/components/cnpg/operations/model.test.ts +510 -0
  146. package/src/components/cnpg/operations/model.ts +499 -0
  147. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  148. package/src/components/cnpg/operations/presentation.ts +84 -0
  149. package/src/components/cnpg/operations/store.ts +98 -0
  150. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  151. package/src/components/cnpg/operatorStatus.ts +194 -0
  152. package/src/components/cnpg/paths.test.ts +54 -0
  153. package/src/components/cnpg/paths.ts +47 -4
  154. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  155. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  156. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  157. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  158. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  159. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  160. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  161. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  162. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  163. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  164. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  165. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  166. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  167. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  168. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  169. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  172. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  173. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  174. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  175. package/src/components/cnpg/refresh.test.ts +38 -0
  176. package/src/components/cnpg/refresh.ts +17 -0
  177. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  178. package/src/components/cnpg/routes.test.ts +34 -3
  179. package/src/components/cnpg/routes.ts +60 -48
  180. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  181. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  182. package/src/components/cnpg/runtimeModel.ts +196 -0
  183. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  184. package/src/components/cnpg/runtimeParts.tsx +79 -0
  185. package/src/components/cnpg/shared.test.tsx +59 -0
  186. package/src/components/cnpg/shared.tsx +100 -182
  187. package/src/components/cnpg/storageModel.test.ts +107 -0
  188. package/src/components/cnpg/storageModel.ts +101 -0
  189. package/src/components/cnpg/trendSamples.test.ts +148 -0
  190. package/src/components/cnpg/trendSamples.ts +222 -0
  191. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  192. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  193. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  194. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  195. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  196. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  197. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  198. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  199. package/src/components/cnpg/workspaceRoute.ts +23 -0
  200. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  201. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  202. package/src/components/cost/CostView.test.ts +2 -1
  203. package/src/components/cost/CostView.tsx +0 -2
  204. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  205. package/src/components/cost/errors.ts +3 -1
  206. package/src/components/cost/source.test.ts +10 -0
  207. package/src/components/cost/source.ts +5 -1
  208. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  209. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  210. package/src/components/dock/BottomDock.tsx +2 -0
  211. package/src/components/dock/TerminalTab.test.tsx +17 -0
  212. package/src/components/dock/TerminalTab.tsx +7 -3
  213. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  214. package/src/components/gitops/GitOpsView.tsx +16 -23
  215. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  216. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  217. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  218. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  219. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  220. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  221. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  222. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  223. package/src/components/helm/ValuesViewer.tsx +169 -78
  224. package/src/components/home/ActivitySummary.tsx +2 -1
  225. package/src/components/home/HomeView.tsx +20 -9
  226. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  227. package/src/components/home/TopologyPreview.tsx +2 -2
  228. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  229. package/src/components/issues/IssuesPane.test.ts +65 -1
  230. package/src/components/issues/IssuesPane.tsx +188 -25
  231. package/src/components/issues/uncheckedSources.ts +21 -0
  232. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  233. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  234. package/src/components/resource/PVCUsageBar.tsx +15 -6
  235. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  236. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  237. package/src/components/resources/CompositeRenderer.tsx +4 -4
  238. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  239. package/src/components/resources/ResourcesView.tsx +26 -12
  240. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  241. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  242. package/src/components/settings/SettingsDialog.tsx +28 -15
  243. package/src/components/timeline/TimelineList.tsx +22 -24
  244. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  245. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  246. package/src/components/timeline/TimelineView.tsx +18 -45
  247. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  248. package/src/components/timeline/useStickyError.test.tsx +47 -0
  249. package/src/components/timeline/useStickyError.ts +15 -0
  250. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  251. package/src/components/ui/Markdown.test.tsx +65 -0
  252. package/src/components/ui/Markdown.tsx +144 -127
  253. package/src/components/whats-new/releaseNotes.ts +63 -1
  254. package/src/components/workload/WorkloadView.test.ts +10 -1
  255. package/src/components/workload/WorkloadView.tsx +27 -239
  256. package/src/components/workspace/layout.test.tsx +29 -0
  257. package/src/components/workspace/layout.tsx +259 -0
  258. package/src/components/workspace/table.ts +7 -0
  259. package/src/contexts/CapabilitiesContext.tsx +1 -1
  260. package/src/integrations/resourceHost.test.tsx +55 -0
  261. package/src/integrations/resourceHost.ts +99 -0
  262. package/src/integrations/resourceHosts.test.tsx +141 -0
  263. package/src/integrations/resourceHosts.tsx +119 -0
  264. package/src/integrations/workspaceRoute.ts +20 -0
  265. package/src/integrations/workspaceRoutes.test.ts +40 -0
  266. package/src/integrations/workspaceRoutes.ts +22 -0
  267. package/src/integrations/workspaceScreens.tsx +14 -0
  268. package/src/utils/drawer-trail.ts +35 -0
  269. package/src/utils/kindFilterStore.test.ts +147 -0
  270. package/src/utils/kindFilterStore.ts +91 -0
  271. package/src/utils/navigation.test.ts +13 -1
  272. package/src/utils/navigation.ts +39 -2
  273. package/src/utils/page-links.ts +21 -0
  274. package/src/utils/tabStorage.ts +40 -0
  275. package/src/utils/viewMemory.test.ts +78 -0
  276. package/src/utils/viewMemory.ts +54 -0
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect } from 'react'
1
+ import { useState, useCallback, useEffect, useMemo } from 'react'
2
2
  import { Copy, Check, Settings, Pencil, X, Eye, Play, Loader2 } from 'lucide-react'
3
3
  import { PaneLoader } from '@skyhook-io/k8s-ui'
4
4
  import { clsx } from 'clsx'
@@ -6,7 +6,7 @@ import yaml from 'yaml'
6
6
  import type { HelmValues, ValuesPreviewResponse } from '../../types'
7
7
  import { CodeViewer } from '../ui/CodeViewer'
8
8
  import { YamlEditor } from '../ui/YamlEditor'
9
- import { useHelmPreviewValues, useHelmApplyValues } from '../../api/client'
9
+ import { useHelmPreviewValues, useHelmApplyValues, useHelmValues } from '../../api/client'
10
10
  import { useCanHelmAct } from '../../api/client'
11
11
  import { ValuesDiffPreview } from './ValuesDiffPreview'
12
12
  import { Tooltip } from '../ui/Tooltip'
@@ -14,8 +14,9 @@ import { Tooltip } from '../ui/Tooltip'
14
14
  interface ValuesViewerProps {
15
15
  values?: HelmValues
16
16
  isLoading: boolean
17
- showAllValues: boolean
18
- onToggleAllValues: (show: boolean) => void
17
+ error?: unknown
18
+ showEffectiveValues: boolean
19
+ onToggleEffectiveValues: (show: boolean) => void
19
20
  onCopy: (text: string) => void
20
21
  copied: boolean
21
22
  // Required for editing
@@ -24,12 +25,14 @@ interface ValuesViewerProps {
24
25
  revision?: number
25
26
  currentRevision?: number
26
27
  onApplySuccess?: () => void
28
+ onRetry?: () => void
27
29
  }
28
30
  export function ValuesViewer({
29
31
  values,
30
32
  isLoading,
31
- showAllValues,
32
- onToggleAllValues,
33
+ error,
34
+ showEffectiveValues,
35
+ onToggleEffectiveValues,
33
36
  onCopy,
34
37
  copied,
35
38
  namespace,
@@ -37,6 +40,7 @@ export function ValuesViewer({
37
40
  revision,
38
41
  currentRevision,
39
42
  onApplySuccess,
43
+ onRetry,
40
44
  }: ValuesViewerProps) {
41
45
  const [isEditing, setIsEditing] = useState(false)
42
46
  const [editedYaml, setEditedYaml] = useState('')
@@ -49,23 +53,32 @@ export function ValuesViewer({
49
53
  const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct(namespace)
50
54
  const isHistoricalRevision = typeof revision === 'number' && typeof currentRevision === 'number' && revision !== currentRevision
51
55
 
52
- const canEdit = Boolean(namespace && name) && canHelmWrite && !isHistoricalRevision
56
+ // Edits replace every stored override on apply, so they start from, and wait for, a fresh
57
+ // overrides-only read. An older Radar's all=true response can carry empty overrides after a
58
+ // failed second read, and a cached copy can predate the last apply.
59
+ const overridesQuery = useHelmValues(namespace ?? '', name ?? '', false, Boolean(namespace && name), revision)
60
+ const overridesData = overridesQuery.data
61
+ const overridesBaseline = useMemo(() => (overridesData ? (overridesData.userSupplied ?? {}) : undefined), [overridesData])
62
+ const overridesReady = overridesBaseline !== undefined && !overridesQuery.error && !overridesQuery.isFetching
63
+ const canOfferEdit = Boolean(namespace && name) && canHelmWrite && !isHistoricalRevision
64
+ const canEdit = canOfferEdit && overridesReady
53
65
 
54
- const displayValues = showAllValues && values?.computed ? values.computed : values?.userSupplied
66
+ const displayValues = showEffectiveValues ? values?.computed : overridesBaseline
55
67
  const isEmpty = !displayValues || Object.keys(displayValues).length === 0
56
68
 
57
69
  // Start editing mode
58
70
  const handleStartEdit = useCallback(() => {
71
+ if (!overridesReady) return
59
72
  // Allow editing even with no user-supplied values (start with empty YAML)
60
- const yamlStr = values?.userSupplied ? jsonToYaml(values.userSupplied) : ''
73
+ const yamlStr = overridesBaseline && Object.keys(overridesBaseline).length > 0 ? jsonToYaml(overridesBaseline) : ''
61
74
  setEditedYaml(yamlStr)
62
75
  setYamlError(null)
63
76
  setIsEditing(true)
64
- // Switch to user-supplied view when editing
65
- if (showAllValues) {
66
- onToggleAllValues(false)
77
+ // Only user overrides are editable.
78
+ if (showEffectiveValues) {
79
+ onToggleEffectiveValues(false)
67
80
  }
68
- }, [values?.userSupplied, showAllValues, onToggleAllValues])
81
+ }, [overridesBaseline, overridesReady, showEffectiveValues, onToggleEffectiveValues])
69
82
 
70
83
  // Cancel editing
71
84
  const handleCancelEdit = useCallback(() => {
@@ -96,7 +109,7 @@ export function ValuesViewer({
96
109
 
97
110
  // Preview changes
98
111
  const handlePreview = useCallback(async () => {
99
- if (!namespace || !name || isHistoricalRevision) return
112
+ if (!namespace || !name || isHistoricalRevision || !overridesReady) return
100
113
  const parsed = parseYaml(editedYaml)
101
114
  if (!parsed) return
102
115
 
@@ -111,11 +124,11 @@ export function ValuesViewer({
111
124
  } catch {
112
125
  // Error is handled by mutation
113
126
  }
114
- }, [namespace, name, isHistoricalRevision, editedYaml, parseYaml, previewMutation])
127
+ }, [namespace, name, isHistoricalRevision, overridesReady, editedYaml, parseYaml, previewMutation])
115
128
 
116
129
  // Apply changes
117
130
  const handleApply = useCallback(async () => {
118
- if (!namespace || !name || isHistoricalRevision) return
131
+ if (!namespace || !name || isHistoricalRevision || !overridesReady) return
119
132
  const parsed = parseYaml(editedYaml)
120
133
  if (!parsed) return
121
134
 
@@ -130,11 +143,11 @@ export function ValuesViewer({
130
143
  } catch {
131
144
  // Error is handled by mutation
132
145
  }
133
- }, [namespace, name, isHistoricalRevision, editedYaml, parseYaml, applyMutation, handleCancelEdit, onApplySuccess])
146
+ }, [namespace, name, isHistoricalRevision, overridesReady, editedYaml, parseYaml, applyMutation, handleCancelEdit, onApplySuccess])
134
147
 
135
148
  // Apply from preview modal
136
149
  const handleApplyFromPreview = useCallback(async () => {
137
- if (!previewData || !namespace || !name || isHistoricalRevision) return
150
+ if (!previewData || !namespace || !name || isHistoricalRevision || !overridesReady) return
138
151
  try {
139
152
  await applyMutation.mutateAsync({
140
153
  namespace,
@@ -147,48 +160,21 @@ export function ValuesViewer({
147
160
  } catch {
148
161
  // Error is handled by mutation
149
162
  }
150
- }, [previewData, namespace, name, isHistoricalRevision, applyMutation, handleCancelEdit, onApplySuccess])
163
+ }, [previewData, namespace, name, isHistoricalRevision, overridesReady, applyMutation, handleCancelEdit, onApplySuccess])
151
164
 
152
- if (isLoading) {
165
+ if (!isEditing && (showEffectiveValues ? isLoading : overridesQuery.isLoading)) {
153
166
  return <PaneLoader label="Loading values…" className="h-32" />
154
167
  }
155
168
 
156
- if (isEmpty && !isEditing) {
157
- return (
158
- <div className="p-4">
159
- <div className="flex items-center justify-between mb-3">
160
- <div className="flex items-center gap-2">
161
- <span className="text-sm font-medium text-theme-text-secondary">Values</span>
162
- {isHistoricalRevision && (
163
- <span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">revision {revision}</span>
164
- )}
165
- </div>
166
- <div className="flex items-center gap-2">
167
- <ToggleButton showAll={showAllValues} onToggle={onToggleAllValues} disabled={isEditing} />
168
- {canEdit && (
169
- <button
170
- onClick={handleStartEdit}
171
- className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
172
- >
173
- <Pencil className="w-3.5 h-3.5" />
174
- Edit
175
- </button>
176
- )}
177
- </div>
178
- </div>
179
- {isHistoricalRevision && (
180
- <div className="mb-3 rounded border border-theme-border bg-theme-elevated/40 px-3 py-2 text-xs text-theme-text-secondary">
181
- Viewing historical values. Switch back to the latest revision before editing or applying changes.
182
- </div>
183
- )}
184
- <div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
185
- <Settings className="w-8 h-8 text-theme-text-disabled" />
186
- <span>{showAllValues ? 'No computed values' : 'No user-supplied values'}</span>
187
- </div>
188
- </div>
189
- )
190
- }
191
-
169
+ const readProblems = valuesReadProblems({
170
+ showEffectiveValues,
171
+ effectiveLoaded: values !== undefined,
172
+ effectiveFailed: Boolean(error),
173
+ overridesLoaded: overridesBaseline !== undefined,
174
+ overridesFailed: Boolean(overridesQuery.error),
175
+ editingAffected: canOfferEdit,
176
+ })
177
+ const viewLoadFailed = showEffectiveValues ? Boolean(error) && !values : Boolean(overridesQuery.error) && !overridesBaseline
192
178
  const yamlContent = isEditing ? editedYaml : jsonToYaml(displayValues || {})
193
179
 
194
180
  return (
@@ -197,7 +183,7 @@ export function ValuesViewer({
197
183
  <div className="flex items-center justify-between mb-3">
198
184
  <div className="flex items-center gap-2">
199
185
  <span className="text-sm font-medium text-theme-text-secondary">
200
- {isEditing ? 'Editing Values' : showAllValues ? 'All Values (Computed)' : 'User-Supplied Values'}
186
+ {isEditing ? 'Editing User Overrides' : showEffectiveValues ? 'Effective Values' : 'User Overrides'}
201
187
  </span>
202
188
  {isHistoricalRevision && !isEditing && (
203
189
  <span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">revision {revision}</span>
@@ -211,21 +197,24 @@ export function ValuesViewer({
211
197
  <div className="flex items-center gap-2">
212
198
  {!isEditing && (
213
199
  <>
214
- <ToggleButton showAll={showAllValues} onToggle={onToggleAllValues} disabled={isEditing} />
215
- <button
216
- onClick={() => onCopy(yamlContent)}
217
- className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
218
- >
219
- {copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
220
- Copy
221
- </button>
222
- {canEdit && (
200
+ <ValuesViewToggle showEffective={showEffectiveValues} onToggle={onToggleEffectiveValues} disabled={isEditing} />
201
+ {!isEmpty && (
202
+ <button
203
+ onClick={() => onCopy(yamlContent)}
204
+ className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
205
+ >
206
+ {copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
207
+ Copy
208
+ </button>
209
+ )}
210
+ {canOfferEdit && (
223
211
  <button
224
212
  onClick={handleStartEdit}
225
- className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 rounded border border-blue-500/30"
213
+ disabled={!canEdit}
214
+ className="flex items-center gap-1 px-2 py-1 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 rounded border border-blue-500/30 disabled:opacity-50 disabled:cursor-not-allowed"
226
215
  >
227
216
  <Pencil className="w-3.5 h-3.5" />
228
- Edit
217
+ Edit Overrides
229
218
  </button>
230
219
  )}
231
220
  </>
@@ -241,7 +230,7 @@ export function ValuesViewer({
241
230
  </button>
242
231
  <button
243
232
  onClick={handlePreview}
244
- disabled={!!yamlError || previewMutation.isPending || isHistoricalRevision}
233
+ disabled={!!yamlError || previewMutation.isPending || isHistoricalRevision || !overridesReady}
245
234
  className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded border border-theme-border disabled:opacity-50 disabled:cursor-not-allowed"
246
235
  >
247
236
  {previewMutation.isPending ? (
@@ -254,7 +243,7 @@ export function ValuesViewer({
254
243
  <Tooltip content={!canHelmWrite ? helmActReason : ''}>
255
244
  <button
256
245
  onClick={handleApply}
257
- disabled={!!yamlError || applyMutation.isPending || !canHelmWrite || isHistoricalRevision}
246
+ disabled={!!yamlError || applyMutation.isPending || !canHelmWrite || isHistoricalRevision || !overridesReady}
258
247
  className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed disabled:pointer-events-none"
259
248
  >
260
249
  {applyMutation.isPending ? (
@@ -276,6 +265,44 @@ export function ValuesViewer({
276
265
  </div>
277
266
  )}
278
267
 
268
+ {!isEditing && readProblems.map((problem) => (
269
+ <div key={problem.read} role="alert" className="mb-3 rounded border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-400">
270
+ {problem.message}{' '}
271
+ {(problem.read === 'overrides' || onRetry) && (
272
+ <button
273
+ type="button"
274
+ onClick={() => (problem.read === 'overrides' ? void overridesQuery.refetch() : onRetry?.())}
275
+ className="underline hover:text-red-300"
276
+ >
277
+ Try again
278
+ </button>
279
+ )}
280
+ </div>
281
+ ))}
282
+
283
+ {isEditing && !overridesReady && (
284
+ <div
285
+ role={overridesQuery.error ? 'alert' : undefined}
286
+ className={clsx(
287
+ 'mb-3 rounded border px-3 py-2 text-xs',
288
+ overridesQuery.error
289
+ ? 'border-red-500/30 bg-red-500/10 text-red-400'
290
+ : 'border-theme-border bg-theme-elevated/40 text-theme-text-secondary',
291
+ )}
292
+ >
293
+ {overridesQuery.error ? (
294
+ <>
295
+ Couldn't reload user overrides, so Apply is off. Your edits are kept.{' '}
296
+ <button type="button" onClick={() => void overridesQuery.refetch()} className="underline hover:text-red-300">
297
+ Try again
298
+ </button>
299
+ </>
300
+ ) : (
301
+ 'Reloading user overrides. Your edits are kept.'
302
+ )}
303
+ </div>
304
+ )}
305
+
279
306
  {/* Error message */}
280
307
  {yamlError && (
281
308
  <div className="mb-3 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
@@ -292,7 +319,12 @@ export function ValuesViewer({
292
319
 
293
320
  {/* Editor / Viewer */}
294
321
  <div className="flex-1 min-h-0">
295
- {isEditing ? (
322
+ {!isEditing && viewLoadFailed ? null : !isEditing && isEmpty ? (
323
+ <div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
324
+ <Settings className="w-8 h-8 text-theme-text-disabled" />
325
+ <span>{showEffectiveValues ? 'No effective values' : 'No user overrides'}</span>
326
+ </div>
327
+ ) : isEditing ? (
296
328
  <YamlEditor
297
329
  value={editedYaml}
298
330
  onChange={setEditedYaml}
@@ -325,28 +357,87 @@ export function ValuesViewer({
325
357
  )
326
358
  }
327
359
 
328
- function ToggleButton({ showAll, onToggle, disabled }: { showAll: boolean; onToggle: (show: boolean) => void; disabled?: boolean }) {
360
+ export interface ValuesReadProblem {
361
+ read: 'effective' | 'overrides'
362
+ message: string
363
+ }
364
+
365
+ // The two reads fail independently, so each view only reports its own read,
366
+ // plus the overrides read wherever it switches editing off. "Refresh" wording
367
+ // is only used when the last loaded data for that view is still on screen.
368
+ export function valuesReadProblems({
369
+ showEffectiveValues,
370
+ effectiveLoaded,
371
+ effectiveFailed,
372
+ overridesLoaded,
373
+ overridesFailed,
374
+ editingAffected,
375
+ }: {
376
+ showEffectiveValues: boolean
377
+ effectiveLoaded: boolean
378
+ effectiveFailed: boolean
379
+ overridesLoaded: boolean
380
+ overridesFailed: boolean
381
+ editingAffected: boolean
382
+ }): ValuesReadProblem[] {
383
+ const problems: ValuesReadProblem[] = []
384
+ if (showEffectiveValues && effectiveFailed) {
385
+ problems.push({
386
+ read: 'effective',
387
+ message: effectiveLoaded
388
+ ? "Couldn't refresh effective values. Showing the last ones that loaded."
389
+ : "Couldn't load effective values.",
390
+ })
391
+ }
392
+ if (overridesFailed && !showEffectiveValues) {
393
+ problems.push({
394
+ read: 'overrides',
395
+ message: overridesLoaded
396
+ ? "Couldn't refresh your overrides. Showing the last ones that loaded."
397
+ + (editingAffected ? ' Editing is off until they load again.' : '')
398
+ : "Couldn't load your overrides." + (editingAffected ? ' Editing is off until they load.' : ''),
399
+ })
400
+ } else if (overridesFailed && editingAffected) {
401
+ problems.push({
402
+ read: 'overrides',
403
+ message: overridesLoaded
404
+ ? "Couldn't refresh your overrides, so editing is off."
405
+ : "Couldn't load your overrides, so editing is off.",
406
+ })
407
+ }
408
+ return problems
409
+ }
410
+
411
+ function ValuesViewToggle({
412
+ showEffective,
413
+ onToggle,
414
+ disabled,
415
+ }: {
416
+ showEffective: boolean
417
+ onToggle: (show: boolean) => void
418
+ disabled?: boolean
419
+ }) {
329
420
  return (
330
421
  <div className={clsx('flex items-center bg-theme-elevated/50 rounded-md p-0.5 text-xs', disabled && 'opacity-50 pointer-events-none')}>
331
422
  <button
332
- onClick={() => onToggle(false)}
423
+ onClick={() => onToggle(true)}
333
424
  disabled={disabled}
334
425
  className={clsx(
335
426
  'px-2 py-1 rounded transition-colors',
336
- !showAll ? 'bg-theme-hover text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary'
427
+ showEffective ? 'bg-theme-hover text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary'
337
428
  )}
338
429
  >
339
- User
430
+ Effective Values
340
431
  </button>
341
432
  <button
342
- onClick={() => onToggle(true)}
433
+ onClick={() => onToggle(false)}
343
434
  disabled={disabled}
344
435
  className={clsx(
345
436
  'px-2 py-1 rounded transition-colors',
346
- showAll ? 'bg-theme-hover text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary'
437
+ !showEffective ? 'bg-theme-hover text-theme-text-primary' : 'text-theme-text-secondary hover:text-theme-text-primary'
347
438
  )}
348
439
  >
349
- All
440
+ User Overrides
350
441
  </button>
351
442
  </div>
352
443
  )
@@ -33,7 +33,7 @@ function scoreLane(lane: ResourceLane): number {
33
33
  const kindScores: Record<string, number> = {
34
34
  Deployment: 50, Rollout: 50, StatefulSet: 50, DaemonSet: 50,
35
35
  Service: 45, Ingress: 45, Gateway: 45,
36
- HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42,
36
+ HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42, UDPRoute: 42,
37
37
  Job: 40, CronJob: 40,
38
38
  Pod: 30, ReplicaSet: 20,
39
39
  }
@@ -69,6 +69,7 @@ const KIND_SHORT: Record<string, string> = {
69
69
  HTTPRoute: 'HR',
70
70
  GRPCRoute: 'gRPC',
71
71
  TCPRoute: 'TCP',
72
+ UDPRoute: 'UDP',
72
73
  TLSRoute: 'TLS',
73
74
  }
74
75
 
@@ -20,7 +20,7 @@ import {
20
20
  FreshnessControl,
21
21
  PaneLoader,
22
22
  StatusDot,
23
- categoryLabel,
23
+ issueTitle,
24
24
  groupLabel,
25
25
  issueFirstSeenTitle,
26
26
  issueTimingForDisplay,
@@ -30,6 +30,7 @@ import {
30
30
  import { formatCompactAge } from '@skyhook-io/k8s-ui/utils/format'
31
31
  import { ClusterHealthCard } from './ClusterHealthCard'
32
32
  import { AlertTriangle, CheckCircle, Loader2, Shield } from 'lucide-react'
33
+ import { uncheckedSourceLines } from '../issues/uncheckedSources'
33
34
  import { clsx } from 'clsx'
34
35
  import { getVersionUpdateStatus } from '../../utils/version'
35
36
  import { RadarVersionLine } from './RadarVersionLine'
@@ -271,6 +272,7 @@ export function HomeView({ namespaces, topology, fallbackClusterLoadState, onNav
271
272
  issues={issues}
272
273
  issueCount={issueCount}
273
274
  visibility={issuesData?.visibility}
275
+ uncheckedLines={uncheckedSourceLines(issuesData?.unchecked_sources)}
274
276
  hasData={!!issuesData}
275
277
  isLoading={issuesLoading && !issuesData}
276
278
  isRefreshing={issuesFetching && !!issuesData}
@@ -300,6 +302,9 @@ interface ProblemsPanelProps {
300
302
  issues: Issue[]
301
303
  issueCount: number
302
304
  visibility?: IssuesResponse['visibility']
305
+ /** One line per issue source Radar couldn't check. Any line means an
306
+ * empty list isn't an all-clear. */
307
+ uncheckedLines?: string[]
303
308
  hasData: boolean
304
309
  isLoading: boolean
305
310
  isRefreshing: boolean
@@ -309,11 +314,11 @@ interface ProblemsPanelProps {
309
314
  onResourceClick: (resource: SelectedResource) => void
310
315
  }
311
316
 
312
-
313
- function ProblemsPanel({
317
+ export function ProblemsPanel({
314
318
  issues,
315
319
  issueCount,
316
320
  visibility,
321
+ uncheckedLines = [],
317
322
  hasData,
318
323
  isLoading,
319
324
  isRefreshing,
@@ -322,6 +327,7 @@ function ProblemsPanel({
322
327
  onNavigateToIssues,
323
328
  onResourceClick,
324
329
  }: ProblemsPanelProps) {
330
+ const unchecked = uncheckedLines.length > 0
325
331
  const hasCriticalIssues = issues.some((issue) => issue.severity === 'critical')
326
332
  const hasIssues = issueCount > 0
327
333
  const hasHardError = !!error && !hasData
@@ -329,14 +335,14 @@ function ProblemsPanel({
329
335
  const isTruncated = issueCount > totalReturned
330
336
  const titleClass = hasCriticalIssues
331
337
  ? 'text-red-500'
332
- : hasIssues || hasHardError || hasLimitedVisibility
338
+ : hasIssues || hasHardError || hasLimitedVisibility || unchecked
333
339
  ? 'text-amber-500'
334
340
  : 'text-theme-text-secondary'
335
341
  const countClass = hasHardError
336
342
  ? 'status-unknown'
337
343
  : hasCriticalIssues
338
344
  ? 'status-unhealthy'
339
- : hasIssues || hasLimitedVisibility
345
+ : hasIssues || hasLimitedVisibility || unchecked
340
346
  ? 'status-degraded'
341
347
  : 'status-healthy'
342
348
  const countLabel = isLoading ? '...' : hasHardError ? 'error' : String(issueCount)
@@ -387,6 +393,9 @@ function ProblemsPanel({
387
393
  Limited visibility - {visibility.impact} Results may be incomplete.
388
394
  </ProblemsPanelNotice>
389
395
  )}
396
+ {(issues.length > 0 || hasLimitedVisibility) && uncheckedLines.map((line) => (
397
+ <ProblemsPanelNotice key={line} tone="warning">{line}</ProblemsPanelNotice>
398
+ ))}
390
399
  {isTruncated && (
391
400
  <ProblemsPanelNotice tone="neutral">
392
401
  Showing {totalReturned} of {issueCount} issues. Narrow by namespace to see the rest.
@@ -396,15 +405,17 @@ function ProblemsPanel({
396
405
  {issues.length === 0 ? (
397
406
  <ProblemsPanelState
398
407
  icon={
399
- hasLimitedVisibility
408
+ hasLimitedVisibility || unchecked
400
409
  ? <AlertTriangle className="h-5 w-5 text-amber-500" />
401
410
  : <CheckCircle className="h-5 w-5 text-green-500" />
402
411
  }
403
- title={hasLimitedVisibility ? 'No visible active issues' : 'No active issues'}
412
+ title={hasLimitedVisibility ? 'No visible active issues' : unchecked ? 'No issues found' : 'No active issues'}
404
413
  body={
405
414
  hasLimitedVisibility
406
415
  ? 'Radar could not read every workload source in this scope.'
407
- : 'No critical or warning issues across the selected scope.'
416
+ : unchecked
417
+ ? uncheckedLines.join(' ')
418
+ : 'No critical or warning issues across the selected scope.'
408
419
  }
409
420
  />
410
421
  ) : (
@@ -448,7 +459,7 @@ function ProblemsPanel({
448
459
  )}
449
460
  </div>
450
461
  <div className="flex items-center gap-1.5 mt-0.5">
451
- <span className="text-[11px] text-theme-text-secondary truncate">{categoryLabel(issue.category)}</span>
462
+ <span className="text-[11px] text-theme-text-secondary truncate">{issueTitle(issue)}</span>
452
463
  <span className="text-[10px] text-theme-text-tertiary shrink-0">{groupLabel(issue.category_group)}</span>
453
464
  {issue.namespace && <span className="text-[10px] text-theme-text-tertiary shrink-0">{issue.namespace}</span>}
454
465
  </div>
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import type { Issue } from '@skyhook-io/k8s-ui'
4
+ import { ProblemsPanel } from './HomeView'
5
+ import { uncheckedSourceLines } from '../issues/uncheckedSources'
6
+ import type { IssuesResponse } from '../../api/client'
7
+
8
+ const line = "Helm releases couldn't be checked, so failed or stuck releases may be missing."
9
+ const generic = "Some issue checks couldn't run, so issues may be missing."
10
+ const helm = [{ source: 'helm', reason: 'read_failed' }]
11
+ const issue: Issue = {
12
+ id: 'pod-failed', severity: 'critical' as const, source: 'problem', category: 'workload', category_group: 'workload', grouping_scope: 'resource',
13
+ kind: 'Pod', group: '', namespace: 'db', name: 'pg-1', reason: 'CrashLoopBackOff', message: 'The selected Pod keeps crashing',
14
+ }
15
+
16
+ function render(props: { issues?: Issue[]; unchecked?: IssuesResponse['unchecked_sources']; visibility?: { impact?: string } }) {
17
+ const issues = props.issues ?? []
18
+ return renderToString(
19
+ <ProblemsPanel
20
+ issues={issues}
21
+ issueCount={issues.length}
22
+ uncheckedLines={uncheckedSourceLines(props.unchecked)}
23
+ visibility={props.visibility}
24
+ hasData
25
+ isLoading={false}
26
+ isRefreshing={false}
27
+ error={null}
28
+ totalReturned={issues.length}
29
+ onNavigateToIssues={() => {}}
30
+ onResourceClick={() => {}}
31
+ />,
32
+ ).replaceAll('<!-- -->', '').replace(/&#x27;|&#39;/g, "'")
33
+ }
34
+
35
+ describe('Home Active Issues when Radar could not read Helm releases', () => {
36
+ it('gives no all-clear when nothing else was found', () => {
37
+ const html = render({ unchecked: helm })
38
+ expect(html).toContain('No issues found')
39
+ expect(html).toContain(line)
40
+ expect(html).not.toContain('No active issues')
41
+ expect(html).not.toContain('status-healthy')
42
+ expect(html).not.toContain('text-green-500')
43
+ })
44
+
45
+ it('keeps the line when limited visibility owns the empty state', () => {
46
+ const html = render({ unchecked: helm, visibility: { impact: 'Pods are not readable.' } })
47
+ expect(html).toContain('No visible active issues')
48
+ expect(html).toContain(line)
49
+ })
50
+
51
+ it('says so above the rows when there are other issues', () => {
52
+ const html = render({ issues: [issue], unchecked: helm })
53
+ expect(html).toContain(line)
54
+ })
55
+
56
+ it('keeps the all-clear when the field is absent', () => {
57
+ const html = render({})
58
+ expect(html).toContain('No active issues')
59
+ expect(html).toContain('status-healthy')
60
+ expect(html).not.toContain('couldn')
61
+ })
62
+
63
+ it('gives no all-clear for a source this version does not know, and says so generically', () => {
64
+ const html = render({ unchecked: [{ source: 'some-new-source', reason: 'read_failed' }] })
65
+ expect(html).toContain('No issues found')
66
+ expect(html).toContain(generic)
67
+ expect(html).not.toContain('some-new-source')
68
+ expect(html).not.toContain('No active issues')
69
+ expect(html).not.toContain('status-healthy')
70
+ expect(html).not.toContain('text-green-500')
71
+ })
72
+
73
+ it('shows one line per source above the rows, without repeats', () => {
74
+ const html = render({
75
+ issues: [issue],
76
+ unchecked: [...helm, { source: 'a', reason: 'x' }, { source: 'b', reason: 'y' }, ...helm],
77
+ })
78
+ expect(html.split(line).length - 1).toBe(1)
79
+ expect(html.split(generic).length - 1).toBe(1)
80
+ })
81
+ })
@@ -108,7 +108,7 @@ function useTopologyStats(topology: Topology | null) {
108
108
  const kindPriority: Record<string, number> = {
109
109
  Deployment: 1, Rollout: 1, StatefulSet: 2, DaemonSet: 2,
110
110
  Service: 3, Ingress: 4, Gateway: 4,
111
- HTTPRoute: 4, GRPCRoute: 4, TCPRoute: 4, TLSRoute: 4,
111
+ HTTPRoute: 4, GRPCRoute: 4, TCPRoute: 4, TLSRoute: 4, UDPRoute: 4,
112
112
  Pod: 5, PodGroup: 5,
113
113
  Job: 6, CronJob: 6, ConfigMap: 7, Secret: 7,
114
114
  }
@@ -125,7 +125,7 @@ const kindDotColors: Record<string, string> = {
125
125
  Deployment: 'bg-emerald-400', Rollout: 'bg-emerald-400',
126
126
  StatefulSet: 'bg-cyan-400', DaemonSet: 'bg-teal-400',
127
127
  Service: 'bg-blue-400', Ingress: 'bg-violet-400', Gateway: 'bg-violet-400',
128
- HTTPRoute: 'bg-purple-400', GRPCRoute: 'bg-purple-400', TCPRoute: 'bg-purple-400', TLSRoute: 'bg-purple-400',
128
+ HTTPRoute: 'bg-purple-400', GRPCRoute: 'bg-purple-400', TCPRoute: 'bg-purple-400', TLSRoute: 'bg-purple-400', UDPRoute: 'bg-purple-400',
129
129
  Pod: 'bg-lime-500', PodGroup: 'bg-lime-500',
130
130
  Job: 'bg-purple-400', CronJob: 'bg-purple-400',
131
131
  ConfigMap: 'bg-amber-400', Secret: 'bg-red-400',