@skyhook-io/radar-app 1.16.3 → 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 (321) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +138 -93
  4. package/src/RadarApp.tsx +27 -17
  5. package/src/api/actions.errorCode.test.ts +40 -0
  6. package/src/api/actions.test.ts +48 -0
  7. package/src/api/actions.ts +79 -0
  8. package/src/api/apiResources.navigation.test.tsx +232 -0
  9. package/src/api/apiResources.ts +46 -3
  10. package/src/api/client.goneResource.test.ts +49 -0
  11. package/src/api/client.helmSource.test.ts +39 -0
  12. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  13. package/src/api/client.namespaceSwitch.test.ts +107 -0
  14. package/src/api/client.ts +381 -47
  15. package/src/api/cnpg-ha.live.test.ts +140 -0
  16. package/src/api/cnpg-ha.test.ts +78 -0
  17. package/src/api/cnpg-ha.ts +19 -0
  18. package/src/api/cnpg-history.ts +123 -0
  19. package/src/api/cnpg-inspect.ts +102 -0
  20. package/src/api/cnpg-protection.ts +112 -0
  21. package/src/api/cnpg-recovery.test.tsx +60 -0
  22. package/src/api/cnpg-recovery.ts +262 -0
  23. package/src/api/cnpg-sessions.ts +175 -0
  24. package/src/api/cnpg-storage.ts +138 -0
  25. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  26. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  27. package/src/api/cnpg.ts +428 -10
  28. package/src/api/diagnose.test.ts +40 -0
  29. package/src/api/diagnose.ts +12 -1
  30. package/src/api/radarFeatures.render.test.tsx +65 -3
  31. package/src/api/radarFeatures.ts +6 -0
  32. package/src/api/reviewedActionMutation.test.tsx +164 -0
  33. package/src/api/reviewedActionMutation.ts +66 -0
  34. package/src/api/timelineSource.test.ts +9 -0
  35. package/src/api/timelineSource.ts +8 -0
  36. package/src/api/traffic.sources.test.ts +55 -0
  37. package/src/api/traffic.ts +173 -14
  38. package/src/components/applications/ApplicationsView.tsx +3 -0
  39. package/src/components/audit/AuditView.tsx +9 -0
  40. package/src/components/capacity/CapacityActivity.tsx +3 -31
  41. package/src/components/capacity/CapacityDemand.tsx +4 -36
  42. package/src/components/capacity/CapacityOverview.tsx +217 -56
  43. package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
  44. package/src/components/capacity/CapacityView.test.tsx +354 -1
  45. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  46. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  47. package/src/components/capacity/routes.ts +42 -0
  48. package/src/components/capacity/shared.tsx +23 -150
  49. package/src/components/capacity/workspaceRoute.ts +18 -0
  50. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  51. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  52. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  53. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  54. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  55. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  56. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  57. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  58. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  59. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  60. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  61. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  62. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  63. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  64. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  65. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  66. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  67. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  68. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  69. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  70. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  71. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  72. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  73. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  74. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  75. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  76. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  77. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  78. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  79. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  80. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  81. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  82. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  83. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  84. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  85. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  86. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  87. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  88. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  89. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  90. package/src/components/cnpg/CNPGProtection.tsx +230 -124
  91. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  92. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  93. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  94. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  95. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  96. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  97. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  98. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  99. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  100. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  101. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  102. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  103. package/src/components/cnpg/CNPGView.tsx +34 -21
  104. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  105. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  106. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  107. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  108. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  109. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  110. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  111. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  112. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  113. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  114. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  115. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  116. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  117. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  118. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  119. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  120. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  121. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  122. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  123. package/src/components/cnpg/actions/actionModel.ts +127 -0
  124. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  125. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  126. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  127. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  128. package/src/components/cnpg/archivingRepair.ts +145 -0
  129. package/src/components/cnpg/baseBackup.test.ts +36 -0
  130. package/src/components/cnpg/baseBackup.ts +36 -0
  131. package/src/components/cnpg/blocking.test.ts +59 -0
  132. package/src/components/cnpg/blocking.ts +131 -0
  133. package/src/components/cnpg/coverageText.test.ts +27 -0
  134. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  135. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  136. package/src/components/cnpg/creation/targetModel.ts +149 -0
  137. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  138. package/src/components/cnpg/fleetStatus.ts +43 -0
  139. package/src/components/cnpg/formFields.ts +2 -0
  140. package/src/components/cnpg/grantText.test.tsx +11 -0
  141. package/src/components/cnpg/host.test.tsx +24 -0
  142. package/src/components/cnpg/host.tsx +101 -0
  143. package/src/components/cnpg/inspectModel.test.ts +120 -0
  144. package/src/components/cnpg/inspectModel.ts +140 -0
  145. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  146. package/src/components/cnpg/logicalSlots.ts +29 -0
  147. package/src/components/cnpg/lsn.test.ts +40 -0
  148. package/src/components/cnpg/lsn.ts +43 -0
  149. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  150. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  151. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  152. package/src/components/cnpg/operations/model.test.ts +510 -0
  153. package/src/components/cnpg/operations/model.ts +499 -0
  154. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  155. package/src/components/cnpg/operations/presentation.ts +84 -0
  156. package/src/components/cnpg/operations/store.ts +98 -0
  157. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  158. package/src/components/cnpg/operatorStatus.ts +194 -0
  159. package/src/components/cnpg/paths.test.ts +54 -0
  160. package/src/components/cnpg/paths.ts +47 -4
  161. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  162. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  163. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  164. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  165. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  166. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  167. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  168. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  169. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  172. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  173. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  174. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  175. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  176. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  177. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  178. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  179. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  180. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  181. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  182. package/src/components/cnpg/refresh.test.ts +38 -0
  183. package/src/components/cnpg/refresh.ts +17 -0
  184. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  185. package/src/components/cnpg/routes.test.ts +34 -3
  186. package/src/components/cnpg/routes.ts +60 -48
  187. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  188. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  189. package/src/components/cnpg/runtimeModel.ts +196 -0
  190. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  191. package/src/components/cnpg/runtimeParts.tsx +79 -0
  192. package/src/components/cnpg/shared.test.tsx +59 -0
  193. package/src/components/cnpg/shared.tsx +100 -182
  194. package/src/components/cnpg/storageModel.test.ts +107 -0
  195. package/src/components/cnpg/storageModel.ts +101 -0
  196. package/src/components/cnpg/trendSamples.test.ts +148 -0
  197. package/src/components/cnpg/trendSamples.ts +222 -0
  198. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  199. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  200. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  201. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  202. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  203. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  204. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  205. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  206. package/src/components/cnpg/workspaceRoute.ts +23 -0
  207. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  208. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  209. package/src/components/cost/CostView.test.ts +2 -1
  210. package/src/components/cost/CostView.tsx +0 -2
  211. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  212. package/src/components/cost/errors.ts +3 -1
  213. package/src/components/cost/source.test.ts +10 -0
  214. package/src/components/cost/source.ts +5 -1
  215. package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
  216. package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
  217. package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
  218. package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
  219. package/src/components/diagnose/ApplyDialog.tsx +26 -28
  220. package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
  221. package/src/components/diagnose/DiagnoseContext.tsx +136 -54
  222. package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
  223. package/src/components/diagnose/InvestigationView.tsx +39 -2
  224. package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
  225. package/src/components/diagnose/agentCatalog.ts +2 -2
  226. package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
  227. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  228. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  229. package/src/components/dock/BottomDock.tsx +2 -0
  230. package/src/components/dock/TerminalTab.test.tsx +17 -0
  231. package/src/components/dock/TerminalTab.tsx +7 -3
  232. package/src/components/dock/TrafficFlowListTab.tsx +27 -6
  233. package/src/components/execution/BatchExecutionView.tsx +11 -11
  234. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  235. package/src/components/gitops/GitOpsView.tsx +16 -23
  236. package/src/components/helm/ChartBrowser.tsx +3 -3
  237. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  238. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  239. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  240. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  241. package/src/components/helm/RevisionHistory.tsx +3 -2
  242. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  243. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  244. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  245. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  246. package/src/components/helm/ValuesViewer.tsx +169 -78
  247. package/src/components/home/ActivitySummary.tsx +2 -1
  248. package/src/components/home/ClusterHealthCard.tsx +7 -0
  249. package/src/components/home/HomeView.tsx +20 -9
  250. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  251. package/src/components/home/TopologyPreview.tsx +2 -2
  252. package/src/components/home/mcpToolCatalog.ts +1 -1
  253. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  254. package/src/components/issues/IssuesPane.test.ts +65 -1
  255. package/src/components/issues/IssuesPane.tsx +188 -25
  256. package/src/components/issues/uncheckedSources.ts +21 -0
  257. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  258. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  259. package/src/components/resource/PVCUsageBar.tsx +15 -6
  260. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  261. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  262. package/src/components/resources/CompositeRenderer.tsx +4 -4
  263. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  264. package/src/components/resources/ResourcesView.tsx +26 -12
  265. package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
  266. package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
  267. package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
  268. package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
  269. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  270. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  271. package/src/components/settings/SettingsDialog.tsx +150 -34
  272. package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
  273. package/src/components/timeline/TimelineList.tsx +25 -24
  274. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  275. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  276. package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
  277. package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
  278. package/src/components/timeline/TimelineView.tsx +25 -45
  279. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  280. package/src/components/timeline/useStickyError.test.tsx +47 -0
  281. package/src/components/timeline/useStickyError.ts +15 -0
  282. package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
  283. package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
  284. package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
  285. package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
  286. package/src/components/traffic/TrafficGraph.tsx +109 -26
  287. package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
  288. package/src/components/traffic/TrafficView.tsx +582 -149
  289. package/src/components/traffic/trafficFilters.test.ts +398 -1
  290. package/src/components/traffic/trafficFilters.ts +408 -0
  291. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  292. package/src/components/ui/Markdown.test.tsx +65 -0
  293. package/src/components/ui/Markdown.tsx +144 -127
  294. package/src/components/whats-new/releaseNotes.ts +63 -1
  295. package/src/components/workload/WorkloadView.test.ts +11 -1
  296. package/src/components/workload/WorkloadView.tsx +162 -529
  297. package/src/components/workspace/layout.test.tsx +29 -0
  298. package/src/components/workspace/layout.tsx +259 -0
  299. package/src/components/workspace/table.ts +7 -0
  300. package/src/context/TimelineCustomization.tsx +15 -0
  301. package/src/contexts/CapabilitiesContext.tsx +1 -1
  302. package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
  303. package/src/hooks/useGitOpsWriteGuard.ts +193 -0
  304. package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
  305. package/src/integrations/resourceHost.test.tsx +55 -0
  306. package/src/integrations/resourceHost.ts +99 -0
  307. package/src/integrations/resourceHosts.test.tsx +141 -0
  308. package/src/integrations/resourceHosts.tsx +119 -0
  309. package/src/integrations/workspaceRoute.ts +20 -0
  310. package/src/integrations/workspaceRoutes.test.ts +40 -0
  311. package/src/integrations/workspaceRoutes.ts +22 -0
  312. package/src/integrations/workspaceScreens.tsx +14 -0
  313. package/src/utils/drawer-trail.ts +35 -0
  314. package/src/utils/kindFilterStore.test.ts +147 -0
  315. package/src/utils/kindFilterStore.ts +91 -0
  316. package/src/utils/navigation.test.ts +13 -1
  317. package/src/utils/navigation.ts +39 -2
  318. package/src/utils/page-links.ts +21 -0
  319. package/src/utils/tabStorage.ts +40 -0
  320. package/src/utils/viewMemory.test.ts +78 -0
  321. package/src/utils/viewMemory.ts +54 -0
@@ -23,7 +23,7 @@ import { AISettingsSection, type AIDraft } from '../diagnose/AISettings'
23
23
  import { MyPermissionsContent } from './MyPermissionsDialog'
24
24
  import { PrivacySection } from './PrivacySection'
25
25
  import { ConfigToggle, SubHeading } from './controls'
26
- import { useDiagnose } from '../diagnose/DiagnoseContext'
26
+ import { useDiagnose, type DiagnoseSetup } from '../diagnose/DiagnoseContext'
27
27
  import { currencyOptionsForValue } from './currency-options'
28
28
  import { UpdateNotification } from '../ui/UpdateNotification'
29
29
  import {
@@ -856,11 +856,8 @@ export function SettingsDialog({
856
856
  anonymous={argoSectionStatus?.connected && argoSectionStatus.anonymous}
857
857
  connectedAddress={argoSectionStatus?.connected && !argoSectionStatus.anonymous ? argoSectionStatus.address : undefined}
858
858
  active={section === 'argocd'}
859
- statusReason={
860
- argoSectionStatus?.configured && !argoSectionStatus.connected
861
- ? argoSectionStatus.reason
862
- : undefined
863
- }
859
+ statusReason={argoSectionStatus?.configured ? argoSectionStatus.reason : undefined}
860
+ connecting={argoSectionStatus?.connecting}
864
861
  onChangeUrl={(v) => updateConfigField('argoCdUrl', v || undefined)}
865
862
  onChangeInsecureTls={(v) => updateConfigField('argoCdInsecureTls', v || undefined)}
866
863
  onApplied={({ url, insecureTls, tokenSet }) => {
@@ -924,7 +921,13 @@ export function SettingsDialog({
924
921
  )}
925
922
  </div>
926
923
  ) : (
927
- <AIUnavailableNotice />
924
+ <AIUnavailableNotice
925
+ setupState={diag.setupState}
926
+ cliOverride={diag.cliOverride}
927
+ checkingAgents={diag.checkingAgents}
928
+ agentsCheckFailed={diag.agentsCheckFailed}
929
+ recheckAgents={diag.recheckAgents}
930
+ />
928
931
  )}
929
932
  </div>
930
933
 
@@ -1212,7 +1215,7 @@ function OperatorSettingsSummary({ section, config }: { section: SettingsSection
1212
1215
  break
1213
1216
  case 'argocd':
1214
1217
  rows = [
1215
- ['Status', argoError ? 'Status unavailable' : !argo ? 'Checking…' : argo.connected ? 'Connected' : 'Not connected'],
1218
+ ['Status', argoError ? 'Status unavailable' : !argo ? 'Checking…' : argo.connected ? 'Connected' : argo.connecting ? 'Connecting…' : 'Not connected'],
1216
1219
  ['Endpoint', argo?.address || c.argoCdUrl || 'Auto-discovery'],
1217
1220
  ['Token', config.argoCdTokenSet ? 'Configured' : 'None configured'],
1218
1221
  ['TLS verification', c.argoCdInsecureTls ? 'Disabled' : 'Enabled'],
@@ -1311,7 +1314,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1311
1314
  : cost?.reason === 'access_denied'
1312
1315
  ? 'Your current permissions do not allow cost data.'
1313
1316
  : cost?.reason
1314
- ? costIntegrationUnavailableMessage(cost.reason) ?? 'The active cost source query failed.'
1317
+ ? costIntegrationUnavailableMessage(cost.reason) ?? 'The active cost source could not provide data.'
1315
1318
  : undefined
1316
1319
 
1317
1320
  const rows: OverviewRow[] = [
@@ -1341,9 +1344,9 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1341
1344
  },
1342
1345
  {
1343
1346
  id: 'argocd', icon: GitBranch, label: 'Argo CD',
1344
- tone: argo?.connected ? 'ok' : argo?.configured || argo?.reason ? 'warn' : 'off',
1345
- value: argo?.connected ? (argo.anonymous ? 'Connected · no token needed' : 'Connected') : 'Not connected',
1346
- detail: argo?.connected ? argo.address : argo?.reason ? 'Open Argo CD for connection details.' : undefined,
1347
+ tone: argo?.connected ? (argo.reason ? 'warn' : 'ok') : argo?.connecting ? 'unknown' : argo?.configured || argo?.reason ? 'warn' : 'off',
1348
+ value: argo?.connected ? (argo.anonymous ? 'Connected · no token needed' : 'Connected') : argo?.connecting ? 'Connecting…' : 'Not connected',
1349
+ detail: argo?.reason ? 'Open Argo CD for connection details.' : argo?.connected ? argo.address : undefined,
1347
1350
  },
1348
1351
  {
1349
1352
  id: 'advanced', icon: Zap, label: 'MCP',
@@ -1354,8 +1357,7 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1354
1357
  },
1355
1358
  {
1356
1359
  id: 'ai', icon: Sparkles, label: 'AI investigations',
1357
- tone: aiAvailable ? 'ok' : 'off',
1358
- value: aiAvailable ? 'Ready' : 'No agent CLI',
1360
+ ...aiOverviewStatus(aiAvailable, diag.setupState, diag.cliOverride, diag.agentsCheckFailed),
1359
1361
  detail: aiAvailable ? agentLabel : undefined,
1360
1362
  },
1361
1363
  ]
@@ -1408,6 +1410,33 @@ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s
1408
1410
  )
1409
1411
  }
1410
1412
 
1413
+ // aiOverviewStatus says which reason applies, for the same reason
1414
+ // AIUnavailableNotice does: "No agent CLI" is a claim about the user's machine,
1415
+ // and it is false when RADAR_AI_CLI_BIN is what's broken, when an agent is
1416
+ // reported but off, when this deployment can't run investigations at all, or
1417
+ // when the agents probe hasn't answered.
1418
+ function aiOverviewStatus(
1419
+ aiAvailable: boolean,
1420
+ setupState: DiagnoseSetup,
1421
+ cliOverride: boolean,
1422
+ checkFailed: boolean,
1423
+ ): Pick<OverviewRow, 'tone' | 'value'> {
1424
+ if (aiAvailable) return { tone: 'ok', value: 'Ready' }
1425
+ if (cliOverride && (setupState === 'needs-install' || setupState === 'needs-restart')) {
1426
+ return { tone: 'warn', value: "RADAR_AI_CLI_BIN can't be run" }
1427
+ }
1428
+ switch (setupState) {
1429
+ case 'unknown':
1430
+ return checkFailed ? { tone: 'warn', value: "Couldn't check" } : { tone: 'unknown', value: 'Checking…' }
1431
+ case 'needs-restart':
1432
+ return { tone: 'warn', value: 'Not available right now' }
1433
+ case 'off':
1434
+ return { tone: 'off', value: 'Not available in this deployment' }
1435
+ default:
1436
+ return { tone: 'off', value: 'No agent CLI' }
1437
+ }
1438
+ }
1439
+
1411
1440
  function OverviewStatus({ tone }: { tone: OverviewTone }) {
1412
1441
  const cls =
1413
1442
  tone === 'ok' ? 'bg-green-500'
@@ -1417,24 +1446,95 @@ function OverviewStatus({ tone }: { tone: OverviewTone }) {
1417
1446
  return <span className={clsx('w-2 h-2 rounded-full shrink-0', cls)} />
1418
1447
  }
1419
1448
 
1420
- // AIUnavailableNotice is the body of the AI investigations tab when no supported agent
1421
- // CLI is installed — the heading/description are provided by the tab itself, so
1422
- // this is just the enable explainer (keeping the feature discoverable to whoever
1423
- // would set it up).
1424
- function AIUnavailableNotice() {
1425
- return (
1449
+ // AIUnavailableNotice is the body of the AI investigations tab when the agent,
1450
+ // model and effort controls have nothing to configure. The tab supplies the
1451
+ // heading, so this is just the explainer.
1452
+ //
1453
+ // It must say WHICH of the reasons applies. "No supported agent CLI found" is a
1454
+ // claim about the user's machine, and when a CLI was found but investigations
1455
+ // didn't start it is false.
1456
+ function AIUnavailableNotice({
1457
+ setupState,
1458
+ cliOverride,
1459
+ checkingAgents,
1460
+ agentsCheckFailed,
1461
+ recheckAgents,
1462
+ }: {
1463
+ setupState: DiagnoseSetup
1464
+ cliOverride: boolean
1465
+ checkingAgents: boolean
1466
+ agentsCheckFailed: boolean
1467
+ recheckAgents: () => Promise<void>
1468
+ }) {
1469
+ // The state the tab was in when a check started. "Still…" is only true when
1470
+ // the finished check left it there.
1471
+ const [checkedState, setCheckedState] = useState<DiagnoseSetup | null>(null)
1472
+ const unchanged = checkedState === setupState && !checkingAgents && !agentsCheckFailed
1473
+ const checkAgain = (
1474
+ <button
1475
+ type="button"
1476
+ disabled={checkingAgents}
1477
+ onClick={() => void recheckAgents().then(() => setCheckedState(setupState))}
1478
+ className="mt-3 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium btn-brand rounded-md disabled:opacity-50"
1479
+ >
1480
+ <RotateCw className={clsx('w-3.5 h-3.5', checkingAgents && 'animate-spin')} />
1481
+ {checkingAgents ? 'Checking…' : 'Check again'}
1482
+ </button>
1483
+ )
1484
+ const panel = (title: string, body?: ReactNode, action?: ReactNode) => (
1426
1485
  <div className="rounded-md border border-theme-border bg-theme-elevated/50 p-3">
1427
- <p className="text-sm font-medium text-theme-text-primary">No supported agent CLI found</p>
1428
- <p className="mt-1 text-xs text-theme-text-tertiary">
1429
- Install <span className="text-theme-text-secondary">Claude Code</span>,{' '}
1430
- <span className="text-theme-text-secondary">Codex</span>,{' '}
1431
- <span className="text-theme-text-secondary">Cursor</span> (
1432
- <span className="font-mono">cursor-agent</span>), or{' '}
1433
- <span className="text-theme-text-secondary">OpenCode</span>, then restart Radar — this tab
1434
- will show the agent, model, and effort controls.
1435
- </p>
1486
+ <p className="text-sm font-medium text-theme-text-primary">{title}</p>
1487
+ {body && <p className="mt-1 text-xs text-theme-text-tertiary">{body}</p>}
1488
+ {action}
1436
1489
  </div>
1437
1490
  )
1491
+ if (setupState === 'unknown') {
1492
+ // The agent probe hasn't answered. Saying anything about the CLI or the
1493
+ // deployment here would be a guess.
1494
+ return agentsCheckFailed && !checkingAgents
1495
+ ? panel("Couldn't check for agent CLIs", 'Try again.', checkAgain)
1496
+ : panel('Checking…')
1497
+ }
1498
+ if (setupState === 'off') {
1499
+ return panel(
1500
+ 'Not available in this deployment',
1501
+ 'They need Radar running on your own computer, with MCP on and sign-in off.',
1502
+ )
1503
+ }
1504
+ if (cliOverride) {
1505
+ return panel(
1506
+ "Radar can't run your RADAR_AI_CLI_BIN",
1507
+ <>
1508
+ Fix the path, or remove it and restart Radar.
1509
+ {unchanged && " Still can't run it."}
1510
+ </>,
1511
+ checkAgain,
1512
+ )
1513
+ }
1514
+ if (setupState === 'needs-restart') {
1515
+ return panel(
1516
+ "AI investigations aren't available right now",
1517
+ <>
1518
+ {agentsCheckFailed && !checkingAgents ? "Couldn't check. Try again." : 'Try again in a moment.'}
1519
+ {unchanged && ' Still not available.'}
1520
+ </>,
1521
+ checkAgain,
1522
+ )
1523
+ }
1524
+ return panel(
1525
+ 'No supported agent CLI found',
1526
+ <>
1527
+ Install <span className="text-theme-text-secondary">Claude Code</span>,{' '}
1528
+ <span className="text-theme-text-secondary">Codex</span>,{' '}
1529
+ <span className="text-theme-text-secondary">Cursor</span> (
1530
+ <span className="font-mono">cursor-agent</span>), or{' '}
1531
+ <span className="text-theme-text-secondary">OpenCode</span>, and this tab will show the
1532
+ agent, model, and effort controls.
1533
+ {agentsCheckFailed && !checkingAgents && " Couldn't check. Try again."}
1534
+ {unchanged && ' Still not found.'}
1535
+ </>,
1536
+ checkAgain,
1537
+ )
1438
1538
  }
1439
1539
 
1440
1540
  // -- Startup section bodies ---------------------------------------------------
@@ -2125,6 +2225,7 @@ function ArgoCDConfigField({
2125
2225
  envError,
2126
2226
  cliSession,
2127
2227
  statusReason,
2228
+ connecting,
2128
2229
  anonymous,
2129
2230
  connectedAddress,
2130
2231
  active,
@@ -2139,6 +2240,7 @@ function ArgoCDConfigField({
2139
2240
  envError?: string
2140
2241
  cliSession?: { server: string; user: string; insecure?: boolean }
2141
2242
  statusReason?: string
2243
+ connecting?: boolean
2142
2244
  anonymous?: boolean
2143
2245
  connectedAddress?: string
2144
2246
  active?: boolean
@@ -2147,7 +2249,7 @@ function ArgoCDConfigField({
2147
2249
  onApplied?: (v: { url: string; insecureTls: boolean; tokenSet: boolean }) => void
2148
2250
  }) {
2149
2251
  if (envManaged) {
2150
- return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} connectedAddress={connectedAddress} statusReason={statusReason} />
2252
+ return <ArgoCDEnvManagedField url={url} insecureTls={insecureTls} envError={envError} connectedAddress={connectedAddress} statusReason={statusReason} connecting={connecting} />
2151
2253
  }
2152
2254
  return (
2153
2255
  <ArgoCDEditableField
@@ -2156,6 +2258,7 @@ function ArgoCDConfigField({
2156
2258
  tokenSet={tokenSet}
2157
2259
  cliSession={cliSession}
2158
2260
  statusReason={statusReason}
2261
+ connecting={connecting}
2159
2262
  anonymous={anonymous}
2160
2263
  connectedAddress={connectedAddress}
2161
2264
  active={active}
@@ -2176,6 +2279,7 @@ function ArgoCDEnvManagedField({
2176
2279
  envError,
2177
2280
  connectedAddress,
2178
2281
  statusReason,
2282
+ connecting,
2179
2283
  }: {
2180
2284
  url: string
2181
2285
  insecureTls: boolean
@@ -2185,6 +2289,7 @@ function ArgoCDEnvManagedField({
2185
2289
  // so this is the one thing on it that can change after deploy.
2186
2290
  connectedAddress?: string
2187
2291
  statusReason?: string
2292
+ connecting?: boolean
2188
2293
  }) {
2189
2294
  if (envError) {
2190
2295
  return (
@@ -2241,10 +2346,15 @@ function ArgoCDEnvManagedField({
2241
2346
  <dt className="w-24 shrink-0 text-theme-text-tertiary">Status</dt>
2242
2347
  <dd className="min-w-0 truncate text-theme-text-secondary">
2243
2348
  {connectedAddress ? (
2244
- <span className="inline-flex items-center gap-1">
2245
- <Check className="w-3 h-3 shrink-0 text-green-600 dark:text-green-400/80" />
2246
- Connected at {connectedAddress}
2247
- </span>
2349
+ <>
2350
+ <span className="inline-flex items-center gap-1">
2351
+ <Check className="w-3 h-3 shrink-0 text-green-600 dark:text-green-400/80" />
2352
+ Connected at {connectedAddress}
2353
+ </span>
2354
+ {statusReason && <span className="block whitespace-normal text-warning-text">{statusReason}</span>}
2355
+ </>
2356
+ ) : connecting ? (
2357
+ 'Connecting…'
2248
2358
  ) : statusReason ? (
2249
2359
  <span className="text-warning-text">Not connected — {statusReason}</span>
2250
2360
  ) : (
@@ -2274,6 +2384,7 @@ function ArgoCDEditableField({
2274
2384
  tokenSet,
2275
2385
  cliSession,
2276
2386
  statusReason,
2387
+ connecting: serverConnecting,
2277
2388
  anonymous,
2278
2389
  connectedAddress,
2279
2390
  active,
@@ -2286,6 +2397,7 @@ function ArgoCDEditableField({
2286
2397
  tokenSet: boolean
2287
2398
  cliSession?: { server: string; user: string; insecure?: boolean }
2288
2399
  statusReason?: string
2400
+ connecting?: boolean
2289
2401
  anonymous?: boolean
2290
2402
  connectedAddress?: string
2291
2403
  active?: boolean
@@ -2414,6 +2526,10 @@ function ArgoCDEditableField({
2414
2526
  </p>
2415
2527
  )}
2416
2528
 
2529
+ {serverConnecting && state.status === 'idle' && !anonymous && !connectedAddress && (
2530
+ <p className="mb-3 text-xs text-theme-text-secondary">Connecting to Argo CD…</p>
2531
+ )}
2532
+
2417
2533
  {statusReason && state.status !== 'connected' && (
2418
2534
  <div className="mb-3 rounded-md border border-theme-border bg-theme-elevated p-3">
2419
2535
  <p className="flex items-center gap-1.5 text-sm font-medium text-warning-text">
@@ -1,10 +1,14 @@
1
- import { useState, useRef, useEffect, useMemo } from 'react'
1
+ import { useState, useRef, useEffect, useMemo, type ReactNode } from 'react'
2
2
  import { Search } from 'lucide-react'
3
3
  import { Input } from '@skyhook-io/k8s-ui'
4
4
 
5
- export function LargeClusterNamespacePicker({ namespaces, onSelect }: {
5
+ export function LargeClusterNamespacePicker({ namespaces, onSelect, ranked = false, renderMeta }: {
6
6
  namespaces: { name: string }[] | undefined
7
7
  onSelect: (ns: string) => void
8
+ /** Keep the given order (already ranked) instead of sorting by name. */
9
+ ranked?: boolean
10
+ /** Extra detail shown at the end of a namespace's row. */
11
+ renderMeta?: (name: string) => ReactNode
8
12
  }) {
9
13
  const [search, setSearch] = useState('')
10
14
  const inputRef = useRef<HTMLInputElement>(null)
@@ -15,8 +19,9 @@ export function LargeClusterNamespacePicker({ namespaces, onSelect }: {
15
19
 
16
20
  const sorted = useMemo(() => {
17
21
  if (!namespaces) return []
22
+ if (ranked) return namespaces
18
23
  return [...namespaces].sort((a, b) => a.name.localeCompare(b.name))
19
- }, [namespaces])
24
+ }, [namespaces, ranked])
20
25
 
21
26
  const filtered = useMemo(() => {
22
27
  if (!search.trim()) return sorted
@@ -51,9 +56,10 @@ export function LargeClusterNamespacePicker({ namespaces, onSelect }: {
51
56
  key={ns.name}
52
57
  type="button"
53
58
  onClick={() => onSelect(ns.name)}
54
- className="w-full text-left px-3 py-2 text-sm text-theme-text-primary hover:bg-theme-hover transition-colors border-b border-theme-border last:border-b-0"
59
+ className="w-full flex items-center justify-between gap-3 text-left px-3 py-2 text-sm text-theme-text-primary hover:bg-theme-hover transition-colors border-b border-theme-border last:border-b-0"
55
60
  >
56
- {ns.name}
61
+ <span className="truncate">{ns.name}</span>
62
+ {renderMeta?.(ns.name)}
57
63
  </button>
58
64
  ))
59
65
  )}
@@ -1,6 +1,5 @@
1
1
  import { useState, useCallback, useMemo } from 'react'
2
2
  import {
3
- SEVERITY_TEXT,
4
3
  TimelineList as TimelineListUI,
5
4
  eventsForApplication,
6
5
  isTimelineProblem,
@@ -13,9 +12,12 @@ import {
13
12
  import type { TimelineEvent } from '../../types'
14
13
  import { useTimelineSource } from '../../context/TimelineSource'
15
14
  import { isManagedTimelineEvent } from '../../api/timelineSource'
15
+ import { TimelineRetentionBanner } from './TimelineRetentionNote'
16
16
  import { useHasLimitedAccess } from '../../contexts/CapabilitiesContext'
17
17
  import type { NavigateToResource } from '../../utils/navigation'
18
- import { AlertTriangle, RefreshCw } from 'lucide-react'
18
+ import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
19
+ import { useStickyError } from './useStickyError'
20
+ import { ViewModeToggle } from './ViewModeToggle'
19
21
 
20
22
  export type { ActivityTypeFilter, ActivityFilterKey }
21
23
 
@@ -92,9 +94,10 @@ interface TimelineListProps {
92
94
  appScoped?: boolean
93
95
  topology?: Topology
94
96
  appScopeLoading?: boolean
97
+ onOpenDiagnostics?: () => void
95
98
  }
96
99
 
97
- export function TimelineList({ namespaces, onViewChange, currentView, onResourceClick, initialFilter, initialTimeRange, showDeleted, onShowDeletedChange, showRoutine, onShowRoutineChange, search, onSearchChange, activityFilter, onActivityFilterChange, kindFilter, onKindFilterChange, selectionWindow, onVisibleWindowChange, scrollToMs, focusedAppIndex, appScoped = false, topology, appScopeLoading = false }: TimelineListProps) {
100
+ export function TimelineList({ namespaces, onViewChange, currentView, onResourceClick, initialFilter, initialTimeRange, showDeleted, onShowDeletedChange, showRoutine, onShowRoutineChange, search, onSearchChange, activityFilter, onActivityFilterChange, kindFilter, onKindFilterChange, selectionWindow, onVisibleWindowChange, scrollToMs, focusedAppIndex, appScoped = false, topology, appScopeLoading = false, onOpenDiagnostics }: TimelineListProps) {
98
101
  const hasLimitedAccess = useHasLimitedAccess()
99
102
  const timelineSource = useTimelineSource()
100
103
  const [queryParams, setQueryParams] = useState<{ timeRange: TimeRange; kinds: string[] }>({
@@ -111,7 +114,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
111
114
  const splitRows = !appScoped
112
115
  // Uncapped when splitting: each part gets its own cap below. The rows come
113
116
  // from the loaded ring, so this costs no extra fetch.
114
- const { data: fetchedEvents, isLoading, isError, error, refetch } = timelineSource.useEvents({
117
+ const { data: fetchedEvents, isLoading, isFetching, isError, error, refetch } = timelineSource.useEvents({
115
118
  namespaces,
116
119
  kinds: queryParams.kinds,
117
120
  timeRange: queryParams.timeRange,
@@ -122,6 +125,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
122
125
  fromMs: selectionWindow?.fromMs,
123
126
  toMs: selectionWindow?.toMs,
124
127
  })
128
+ const loadError = useStickyError(error, fetchedEvents != null)
125
129
  const split = useMemo(
126
130
  () => (splitRows && fetchedEvents ? splitRoutineActivity(fetchedEvents, fetchLimit) : undefined),
127
131
  [splitRows, fetchedEvents, fetchLimit],
@@ -140,21 +144,22 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
140
144
 
141
145
  // Full-screen error only when nothing is loaded; a failing background poll
142
146
  // with data on screen keeps rendering (data before error).
143
- if (isError && !fetchedEvents) {
147
+ if (loadError && !fetchedEvents) {
144
148
  return (
145
- <div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary gap-3">
146
- <AlertTriangle className="w-10 h-10 text-amber-400/70" />
147
- <p className="text-base">Failed to load timeline data</p>
148
- {error?.message?.trim() && (
149
- <p className="max-w-md px-6 text-center text-sm text-theme-text-tertiary">{error.message.trim()}</p>
149
+ <div className="flex flex-col h-full">
150
+ {currentView && onViewChange && (
151
+ <div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
152
+ <div />
153
+ <ViewModeToggle viewMode={currentView} onViewModeChange={onViewChange} />
154
+ </div>
150
155
  )}
151
- <button
152
- onClick={() => refetch()}
153
- className="flex items-center gap-2 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border-light rounded-lg hover:bg-theme-hover transition-colors"
154
- >
155
- <RefreshCw className="w-3.5 h-3.5" />
156
- Try again
157
- </button>
156
+ <TimelineLoadError
157
+ className="flex-1"
158
+ error={loadError}
159
+ retrying={isFetching}
160
+ onRetry={() => refetch()}
161
+ onOpenDiagnostics={onOpenDiagnostics}
162
+ />
158
163
  </div>
159
164
  )
160
165
  }
@@ -164,17 +169,13 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
164
169
  // view-level banner already reports the shared failure, and a second note
165
170
  // would double up.
166
171
  const staleNote = isError && fetchedEvents && !selectionWindow ? (
167
- <div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
168
- <AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
169
- Live updates are failing — the list may be stale.
170
- <button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
171
- Retry now
172
- </button>
173
- </div>
172
+ <TimelineStaleNote error={error} subject="list" onRetry={() => refetch()} />
174
173
  ) : null
175
174
 
176
175
  return (
177
176
  <div className="flex h-full min-h-0 flex-1 flex-col">
177
+ {/* Under a scrubber TimelineView already shows the note above it. */}
178
+ {!selectionWindow && <TimelineRetentionBanner />}
178
179
  {staleNote}
179
180
  <div className="min-h-0 flex-1">
180
181
  <TimelineListUI
@@ -0,0 +1,62 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { ApiError } from '../../api/client'
4
+ import { TimelineLoadError, TimelineStaleNote } from './TimelineLoadError'
5
+
6
+ const driverError = 'open PostgreSQL timeline database: dial tcp 10.0.0.5:5432: connect: connection refused'
7
+
8
+ function databaseDown(): ApiError {
9
+ return new ApiError('Timeline history is unavailable because Radar can\'t connect to its PostgreSQL database.', 503, {
10
+ error_code: 'timeline_database_unreachable',
11
+ detail: driverError,
12
+ })
13
+ }
14
+
15
+ function render(error: Error, onOpenDiagnostics?: () => void): string {
16
+ return renderToStaticMarkup(
17
+ <TimelineLoadError error={error} onRetry={() => {}} onOpenDiagnostics={onOpenDiagnostics} />,
18
+ )
19
+ }
20
+
21
+ describe('TimelineLoadError', () => {
22
+ it('names the database and points to the logs when PostgreSQL is unreachable', () => {
23
+ const html = render(databaseDown(), () => {})
24
+ expect(html).toContain('Timeline history is unavailable')
25
+ expect(html).toContain('can&#x27;t connect to its PostgreSQL database')
26
+ expect(html).toContain('[timeline]')
27
+ // The driver's error names the database host; it belongs in the logs and
28
+ // diagnostics, not on the page.
29
+ expect(html).not.toContain(driverError)
30
+ expect(html).toContain('Open diagnostics')
31
+ expect(html).not.toContain('Failed to load timeline data')
32
+ })
33
+
34
+ it('omits the diagnostics link when the host cannot open diagnostics', () => {
35
+ expect(render(databaseDown())).not.toContain('Open diagnostics')
36
+ })
37
+
38
+ it('keeps the generic state, without a diagnostics link, for other failures', () => {
39
+ const html = render(new ApiError('upstream timed out', 502, { error: 'upstream timed out' }), () => {})
40
+ expect(html).toContain('Failed to load timeline data')
41
+ expect(html).toContain('upstream timed out')
42
+ expect(html).not.toContain('Open diagnostics')
43
+ })
44
+ })
45
+
46
+ describe('TimelineStaleNote', () => {
47
+ const note = (error: Error) =>
48
+ renderToStaticMarkup(<TimelineStaleNote error={error} subject="timeline" onRetry={() => {}} />)
49
+
50
+ it('names the database and points to the logs when PostgreSQL is the cause', () => {
51
+ const html = note(databaseDown())
52
+ expect(html).toContain('can&#x27;t connect to its PostgreSQL database, so the timeline may be stale')
53
+ expect(html).toContain('[timeline]')
54
+ expect(html).not.toContain(driverError)
55
+ })
56
+
57
+ it('keeps the generic note for other failures', () => {
58
+ const html = note(new ApiError('upstream timed out', 502, { error: 'upstream timed out' }))
59
+ expect(html).toContain('Live updates are failing, so the timeline may be stale')
60
+ expect(html).not.toContain('PostgreSQL')
61
+ })
62
+ })
@@ -0,0 +1,96 @@
1
+ import { clsx } from 'clsx'
2
+ import { AlertTriangle, Bug, RefreshCw } from 'lucide-react'
3
+ import { timelineDatabaseError } from '../../api/client'
4
+ import { SEVERITY_TEXT } from '@skyhook-io/k8s-ui'
5
+
6
+ const SECONDARY_BUTTON =
7
+ 'flex items-center gap-2 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border-light rounded-lg hover:bg-theme-hover transition-colors'
8
+
9
+ // TimelineLoadError is the full-screen state when the timeline has nothing to
10
+ // show. An unreachable PostgreSQL database gets its own copy: it says the
11
+ // database is the problem and where to look. The driver's error stays in
12
+ // Radar's logs and the diagnostics overlay.
13
+ export function TimelineLoadError({
14
+ error,
15
+ onRetry,
16
+ retrying = false,
17
+ onOpenDiagnostics,
18
+ className,
19
+ }: {
20
+ error?: Error | null
21
+ onRetry: () => void
22
+ /** A request is in flight: the error stays up, the button shows the attempt. */
23
+ retrying?: boolean
24
+ onOpenDiagnostics?: () => void
25
+ className?: string
26
+ }) {
27
+ const databaseError = timelineDatabaseError(error)
28
+ const detail = error?.message?.trim()
29
+
30
+ return (
31
+ <div className={clsx('flex flex-col items-center justify-center text-theme-text-tertiary gap-3 px-6', className)}>
32
+ <AlertTriangle className="w-10 h-10 text-amber-400/70" />
33
+ {databaseError !== null ? (
34
+ <>
35
+ <p className="text-base text-theme-text-secondary">Timeline history is unavailable</p>
36
+ <p className="max-w-md text-center text-sm">
37
+ Radar can't connect to its PostgreSQL database, so changes aren't being recorded. It keeps
38
+ retrying, and this page recovers on its own once the database is back.
39
+ </p>
40
+ <p className="max-w-md text-center text-xs">
41
+ Radar's logs have the details: search them for <code className="font-mono">[timeline]</code>.
42
+ </p>
43
+ </>
44
+ ) : (
45
+ <>
46
+ <p className="text-base">Failed to load timeline data</p>
47
+ {detail && <p className="max-w-md text-center text-sm">{detail}</p>}
48
+ </>
49
+ )}
50
+ <div className="flex items-center gap-2">
51
+ <button type="button" onClick={onRetry} disabled={retrying} className={clsx(SECONDARY_BUTTON, 'disabled:opacity-60 disabled:cursor-default')}>
52
+ <RefreshCw className={clsx('w-3.5 h-3.5', retrying && 'animate-spin')} />
53
+ Try again
54
+ </button>
55
+ {databaseError !== null && onOpenDiagnostics && (
56
+ <button type="button" onClick={onOpenDiagnostics} className={SECONDARY_BUTTON}>
57
+ <Bug className="w-3.5 h-3.5" />
58
+ Open diagnostics
59
+ </button>
60
+ )}
61
+ </div>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ // TimelineStaleNote is the banner over data that is still on screen while its
67
+ // refresh fails. When the database is the cause it says so and where to look,
68
+ // since the data underneath otherwise reads as current.
69
+ export function TimelineStaleNote({
70
+ error,
71
+ subject,
72
+ onRetry,
73
+ }: {
74
+ error?: Error | null
75
+ /** What may be stale: the swimlane view or the list. */
76
+ subject: 'timeline' | 'list'
77
+ onRetry: () => void
78
+ }) {
79
+ const databaseDown = timelineDatabaseError(error) !== null
80
+ return (
81
+ <div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
82
+ <AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
83
+ {databaseDown ? (
84
+ <span>
85
+ Radar can't connect to its PostgreSQL database, so the {subject} may be stale. Search Radar's logs
86
+ for <code className="font-mono">[timeline]</code>.
87
+ </span>
88
+ ) : (
89
+ <span>Live updates are failing, so the {subject} may be stale.</span>
90
+ )}
91
+ <button type="button" onClick={onRetry} className="underline hover:text-theme-text-primary">
92
+ Retry now
93
+ </button>
94
+ </div>
95
+ )
96
+ }