@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
package/src/api/client.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { canonicalResourceGroup } from '@skyhook-io/k8s-ui/utils/api-resources'
2
2
  import { knownKindForPluralWithGroup, pluralToKind } from '@skyhook-io/k8s-ui/utils/navigation'
3
- import { useEffect, useRef } from 'react'
3
+ import { useEffect, useRef, useCallback, useMemo } from 'react'
4
4
  import type { KueueAdmissionResponse } from '@skyhook-io/k8s-ui/types/scheduling'
5
+ import type { GitOpsWriteEvidence } from '@skyhook-io/k8s-ui/utils/gitops-write-guard'
5
6
  import type {
6
7
  AppHistory,
7
8
  AppRow,
@@ -20,7 +21,7 @@ import type {
20
21
  YamlDocumentIdentity,
21
22
  YamlSchemaLoadResult,
22
23
  } from '@skyhook-io/k8s-ui'
23
- import { useQuery, useMutation, useQueryClient, skipToken } from '@tanstack/react-query'
24
+ import { useQuery, useMutation, useQueryClient, skipToken, type QueryClient } from '@tanstack/react-query'
24
25
  import { showApiError, showApiSuccess } from '../components/ui/Toast'
25
26
  import { useIsAuthEnabled, useNamespacedCapabilities } from '../contexts/CapabilitiesContext'
26
27
  import type {
@@ -41,6 +42,8 @@ import type {
41
42
  ResourceDiff,
42
43
  UpgradeInfo,
43
44
  BatchUpgradeInfo,
45
+ ChartSourceCandidate,
46
+ ChartSourceStatus,
44
47
  ValuesDiff,
45
48
  ValuesPreviewResponse,
46
49
  HelmRepository,
@@ -228,6 +231,16 @@ export function isNotFoundError(error: unknown): boolean {
228
231
  return error instanceof ApiError && error.status === 404;
229
232
  }
230
233
 
234
+ /**
235
+ * The server's explanation when it refused a request because the browser
236
+ * reported it came from another site, or undefined for any other error.
237
+ */
238
+ export function crossOriginRefusalMessage(error: unknown): string | undefined {
239
+ return error instanceof ApiError && error.data?.error_code === "cross_origin_refused"
240
+ ? error.message
241
+ : undefined;
242
+ }
243
+
231
244
  export function isCapacityCursorInvalidError(error: unknown): boolean {
232
245
  return (
233
246
  error instanceof ApiError &&
@@ -286,6 +299,20 @@ export function isKindSyncFailed(error: unknown): boolean {
286
299
  );
287
300
  }
288
301
 
302
+ // timelineDatabaseError returns the driver's error when a timeline read failed
303
+ // because Radar can't connect to its PostgreSQL database, and null for any other
304
+ // failure. The server redacts the password before it sends the error.
305
+ export function timelineDatabaseError(error: unknown): string | null {
306
+ if (
307
+ !(error instanceof ApiError) ||
308
+ error.status !== 503 ||
309
+ error.data?.error_code !== "timeline_database_unreachable"
310
+ )
311
+ return null;
312
+ const detail = error.data.detail;
313
+ return typeof detail === "string" ? detail : "";
314
+ }
315
+
289
316
  // SyncKindState / SyncStatusSnapshot mirror k8score's SyncSnapshot — attached
290
317
  // to the connection-status payload while the initial informer sync is running.
291
318
  export interface SyncKindState {
@@ -439,7 +466,7 @@ export interface DashboardResourceCounts {
439
466
  ingresses: number;
440
467
  gateways?: number;
441
468
  routes?: number;
442
- nodes: { total: number; ready: number; notReady: number; cordoned: number };
469
+ nodes: { total: number; ready: number; notReady: number; cordoned: number; removing?: number; removingUnhealthy?: number };
443
470
  namespaces: number;
444
471
  jobs: { total: number; active: number; succeeded: number; failed: number };
445
472
  cronJobs: { total: number; active: number; suspended: number };
@@ -514,6 +541,10 @@ export type { AuditFinding, ResourceGroup, CheckMeta, Check };
514
541
 
515
542
  export interface AuditResponse {
516
543
  missingInputs?: string[];
544
+ // Absent on a Radar that predates the per-resource opt-out.
545
+ ignoreChecksAnnotation?: string;
546
+ // checkID → namespace → resources whose findings the annotation hid.
547
+ annotationIgnoredByNamespace?: Record<string, Record<string, number>>;
517
548
  summary: DashboardAudit;
518
549
  findings: AuditFinding[];
519
550
  groups: ResourceGroup[];
@@ -728,9 +759,13 @@ export interface IssuesResponse {
728
759
  // state 'degraded' means core workload reads are denied, so an empty list may
729
760
  // mean "can't see" rather than "nothing broken" — the UI must say so.
730
761
  visibility?: { state?: string; impact?: string };
762
+ // Issue sources Radar couldn't check, so their issues may be missing: an
763
+ // empty list is not an all-clear. Today only "helm"; a newer Radar may send
764
+ // others. Absent when every source was checked, and from older Radars.
765
+ unchecked_sources?: { source: string; reason: string }[];
731
766
  }
732
767
 
733
- export function useIssues(namespaces: string[] = []) {
768
+ export function useIssues(namespaces: string[] = [], enabled = true) {
734
769
  const params =
735
770
  namespaces.length > 0 ? `?namespaces=${namespaces.join(",")}` : "";
736
771
  return useQuery<IssuesResponse>({
@@ -738,6 +773,7 @@ export function useIssues(namespaces: string[] = []) {
738
773
  queryFn: () => fetchJSON(`/issues${params}`),
739
774
  staleTime: 30000,
740
775
  refetchInterval: ISSUES_REFRESH_INTERVAL_MS,
776
+ enabled,
741
777
  });
742
778
  }
743
779
 
@@ -789,6 +825,38 @@ export function useResourceIssues(
789
825
  });
790
826
  }
791
827
 
828
+ // The Issues page narrowed to one subject: the same per-resource lookup with
829
+ // ?coverage=1, which also says whether Radar reads the subject's kind and what
830
+ // the caller's RBAC withheld. Polls like the page list.
831
+ export interface SubjectIssuesResponse {
832
+ issues: Issue[];
833
+ coverage: "ok" | "syncing" | "notWatched" | "unknownKind";
834
+ withheld?: { issues: number };
835
+ visibility?: { state?: string; impact?: string };
836
+ }
837
+
838
+ export function useSubjectIssues(
839
+ subject: { kind: string; group: string; namespace: string; name: string } | null,
840
+ enabled: boolean,
841
+ ) {
842
+ const capabilities = useCapabilities();
843
+ const { support, gatedKey, guard } = useRadarFeature('resourceIssueCoverage');
844
+ const pathNs = subject?.namespace ? encodeURIComponent(subject.namespace) : "_";
845
+ const params = new URLSearchParams({ coverage: "1" });
846
+ if (subject?.group) params.set("group", subject.group);
847
+ return useQuery<SubjectIssuesResponse>({
848
+ queryKey: ["issues", "subject", subject?.kind ?? "", subject?.group ?? "", subject?.namespace ?? "", subject?.name ?? "", support, ...gatedKey],
849
+ queryFn: () => {
850
+ if (!capabilities.data && capabilities.error) throw capabilities.error;
851
+ return guard(() => fetchJSON<SubjectIssuesResponse>(`/issues/resource/${encodeURIComponent(subject!.kind)}/${pathNs}/${encodeURIComponent(subject!.name)}?${params}`));
852
+ },
853
+ staleTime: 30000,
854
+ refetchInterval: ISSUES_REFRESH_INTERVAL_MS,
855
+ // The existing route alone cannot prove support for this opt-in shape.
856
+ enabled: enabled && !!subject && !capabilities.isPending,
857
+ });
858
+ }
859
+
792
860
  import type { Trace as NetworkTrace, InClusterCapability, DrainPlan, DrainPlanPod } from '@skyhook-io/k8s-ui'
793
861
 
794
862
  // useTrace polls the static path-shaped diagnosis for one network entry
@@ -1780,13 +1848,14 @@ export function useRadarFeature(feature: RadarFeature) {
1780
1848
  const currentVersion = versionInfo?.currentVersion || radarVersion
1781
1849
  const latestVersion = versionInfo?.latestVersion
1782
1850
  const support = radarFeatureSupport(feature, capabilities, currentVersion)
1851
+ // Stable across renders, so a callback built on guard keeps its identity.
1852
+ const guard = useCallback(
1853
+ <T,>(request: () => Promise<T>): Promise<T> => guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
1854
+ [feature, support, currentVersion, latestVersion],
1855
+ )
1856
+ const gatedKey = useMemo(() => (support === 'unsupported' ? ['radar-feature-unsupported'] : []), [support])
1783
1857
 
1784
- return {
1785
- support,
1786
- gatedKey: support === 'unsupported' ? ['radar-feature-unsupported'] : [],
1787
- guard: <T,>(request: () => Promise<T>): Promise<T> =>
1788
- guardRadarFeature(feature, support, { currentVersion, latestVersion }, request),
1789
- }
1858
+ return { support, gatedKey, guard }
1790
1859
  }
1791
1860
 
1792
1861
  // ============================================================================
@@ -2447,6 +2516,11 @@ export function useApplicationHistory(
2447
2516
  });
2448
2517
  }
2449
2518
 
2519
+ // How often a page asks again while Radar is still connecting to argocd-server,
2520
+ // and while it retries a connect that failed.
2521
+ const ARGO_CONNECTING_POLL_MS = 2000;
2522
+ const ARGO_RETRYING_POLL_MS = 10_000;
2523
+
2450
2524
  export function useGitOpsTree(
2451
2525
  kind: string,
2452
2526
  namespace: string,
@@ -2469,6 +2543,14 @@ export function useGitOpsTree(
2469
2543
  ),
2470
2544
  enabled: Boolean(kind && name) && (options?.enabled ?? true),
2471
2545
  staleTime: 5000,
2546
+ // Radar answers at once while it connects to argocd-server; ask again
2547
+ // until Argo's per-resource health is in.
2548
+ refetchInterval: (query) =>
2549
+ query.state.data?.healthApiPending
2550
+ ? ARGO_CONNECTING_POLL_MS
2551
+ : query.state.data?.healthApiRetrying
2552
+ ? ARGO_RETRYING_POLL_MS
2553
+ : false,
2472
2554
  });
2473
2555
  }
2474
2556
 
@@ -2501,13 +2583,38 @@ export function useGitOpsInsights(
2501
2583
  staleTime: 5000,
2502
2584
  refetchInterval: (query) => {
2503
2585
  const phase = query.state.data?.summary?.operationPhase;
2504
- return phase === "Running" || phase === "Terminating"
2505
- ? INSIGHTS_RUNNING_POLL_MS
2586
+ if (phase === "Running" || phase === "Terminating") {
2587
+ return INSIGHTS_RUNNING_POLL_MS;
2588
+ }
2589
+ const summary = query.state.data?.summary;
2590
+ if (summary?.resourceHealthApiPending || query.state.data?.capabilities?.argoConnecting) {
2591
+ return ARGO_CONNECTING_POLL_MS;
2592
+ }
2593
+ return summary?.resourceHealthApiRetrying || query.state.data?.capabilities?.argoRetrying
2594
+ ? ARGO_RETRYING_POLL_MS
2506
2595
  : false;
2507
2596
  },
2508
2597
  });
2509
2598
  }
2510
2599
 
2600
+ // Field-level evidence behind the GitOps write guard: whether each path is in
2601
+ // the object's last client-side apply or owned by its GitOps controller, plus
2602
+ // the owner's sync policy. Read server-side because Radar's caches strip both.
2603
+ export function fetchGitOpsWriteEvidence(body: {
2604
+ kind: string;
2605
+ group: string;
2606
+ namespace: string;
2607
+ name: string;
2608
+ paths: string[];
2609
+ owner?: { kind: string; group: string; namespace: string; name: string };
2610
+ }): Promise<GitOpsWriteEvidence> {
2611
+ return fetchJSON<GitOpsWriteEvidence>("/gitops/write-evidence", {
2612
+ method: "POST",
2613
+ headers: { "Content-Type": "application/json" },
2614
+ body: JSON.stringify(body),
2615
+ });
2616
+ }
2617
+
2511
2618
  // Full Git-rendered desired-vs-live diff for one Argo CD managed resource.
2512
2619
  // ns/name identify the Application; the ref identifies the managed resource.
2513
2620
  // Fetched on demand — the caller mounts this only when the user opens "Full
@@ -2597,18 +2704,55 @@ export function fetchResourceWithRelationships<T>(
2597
2704
  );
2598
2705
  }
2599
2706
 
2707
+ /**
2708
+ * How often an open detail view re-checks a resource the server said is gone.
2709
+ * Watch events stop refetching it (see refetchOnResourceEvents), so this is
2710
+ * what finds one recreated under the same name when no event names it, such
2711
+ * as a ConfigMap: the server streams adds only for pods.
2712
+ */
2713
+ const GONE_RECHECK_MS = 60_000;
2714
+
2715
+ export function goneRecheckInterval(
2716
+ query: { state: { error: unknown } },
2717
+ otherwise: number | false | undefined,
2718
+ ): number | false | undefined {
2719
+ return isNotFoundError(query.state.error) ? GONE_RECHECK_MS : otherwise;
2720
+ }
2721
+
2722
+ export function resourceEventKey(kind: string, namespace: string, name: string): string {
2723
+ return `${kind}/${namespace}/${name}`;
2724
+ }
2725
+
2726
+ /**
2727
+ * Whether a batch of watch events for `kind` refetches this ['resource', kind,
2728
+ * namespace, name, group] query. One the server answered 404 for is refetched
2729
+ * only when an event in the batch names its object; otherwise every change to
2730
+ * the kind would ask again for something that is gone.
2731
+ */
2732
+ export function refetchOnResourceEvents(
2733
+ query: { queryKey: readonly unknown[]; state: { error: unknown } },
2734
+ kind: string,
2735
+ namedObjects: ReadonlySet<string>,
2736
+ ): boolean {
2737
+ if (!isNotFoundError(query.state.error)) return true;
2738
+ const [, , namespace, name] = query.queryKey;
2739
+ return namedObjects.has(resourceEventKey(kind, String(namespace ?? ""), String(name ?? "")));
2740
+ }
2741
+
2742
+ // `cacheScope` keeps a caller's copy apart from the shared one, for hosts
2743
+ // that point one QueryClient at several clusters (see useGitOpsWriteGuard).
2600
2744
  export function useResource<T>(
2601
2745
  kind: string,
2602
2746
  namespace: string,
2603
2747
  name: string,
2604
2748
  group?: string,
2605
- options?: { enabled?: boolean; refetchInterval?: number | false },
2749
+ options?: { enabled?: boolean; refetchInterval?: number | false; cacheScope?: string },
2606
2750
  ) {
2607
2751
  const query = useQuery<ResourceWithRelationships<T>>({
2608
- queryKey: ["resource", kind, namespace, name, group],
2752
+ queryKey: ["resource", kind, namespace, name, group, ...(options?.cacheScope ? [options.cacheScope] : [])],
2609
2753
  queryFn: () => fetchResourceWithRelationships<T>(kind, namespace, name, group),
2610
2754
  enabled: (options?.enabled ?? true) && Boolean(kind && name), // namespace can be empty for cluster-scoped resources
2611
- refetchInterval: options?.refetchInterval,
2755
+ refetchInterval: (query) => goneRecheckInterval(query, options?.refetchInterval),
2612
2756
  // Kind still completing its initial sync: stay in loading and poll until
2613
2757
  // it becomes readable instead of erroring out (deep links during startup).
2614
2758
  retry: (failureCount, error) => {
@@ -2636,11 +2780,13 @@ export function useResourceWithRelationships<T>(
2636
2780
  namespace: string,
2637
2781
  name: string,
2638
2782
  group?: string,
2783
+ options?: { cacheScope?: string },
2639
2784
  ) {
2640
2785
  return useQuery<ResourceWithRelationships<T>>({
2641
- queryKey: ["resource", kind, namespace, name, group],
2786
+ queryKey: ["resource", kind, namespace, name, group, ...(options?.cacheScope ? [options.cacheScope] : [])],
2642
2787
  queryFn: () => fetchResourceWithRelationships<T>(kind, namespace, name, group),
2643
2788
  enabled: Boolean(kind && name),
2789
+ refetchInterval: (query) => goneRecheckInterval(query, undefined),
2644
2790
  // Deep-linked detail views can mount while the kind's informer is still
2645
2791
  // completing its initial sync: keep polling instead of erroring out.
2646
2792
  retry: (failureCount, error) => {
@@ -2658,7 +2804,7 @@ export function useResources<T>(
2658
2804
  kind: string,
2659
2805
  namespace?: string,
2660
2806
  group?: string,
2661
- options?: { enabled?: boolean; refetchInterval?: number | false },
2807
+ options?: { enabled?: boolean; refetchInterval?: number | false; cacheScope?: string },
2662
2808
  ) {
2663
2809
  const params = new URLSearchParams();
2664
2810
  if (namespace) params.set("namespace", namespace);
@@ -2666,7 +2812,7 @@ export function useResources<T>(
2666
2812
  const queryString = params.toString();
2667
2813
 
2668
2814
  return useQuery<T[]>({
2669
- queryKey: ["resources", kind, group, namespace],
2815
+ queryKey: ["resources", kind, group, namespace, ...(options?.cacheScope ? [options.cacheScope] : [])],
2670
2816
  queryFn: () =>
2671
2817
  fetchJSON(`/resources/${kind}${queryString ? `?${queryString}` : ""}`),
2672
2818
  enabled: (options?.enabled ?? true) && Boolean(kind),
@@ -2981,6 +3127,38 @@ export function useWorkloadHistory(kind: string, namespace: string, name: string
2981
3127
  return query;
2982
3128
  }
2983
3129
 
3130
+ export interface ServiceEndpointSlicesResult {
3131
+ items: any[];
3132
+ /** More slices are labeled for the Service than the server returns in one page. */
3133
+ truncated: boolean;
3134
+ }
3135
+
3136
+ // The EndpointSlices published for one Service, selected server-side by the
3137
+ // kubernetes.io/service-name label. A Radar that predates the endpoint settles
3138
+ // as unsupported; ServiceRenderer then falls back to the namespace list.
3139
+ export function useServiceEndpointSlices(namespace: string, name: string, enabled = true) {
3140
+ const { guard, gatedKey, support } = useRadarFeature("serviceEndpointSlices");
3141
+ const query = useQuery<ServiceEndpointSlicesResult>({
3142
+ queryKey: ["service-endpointslices", namespace, name, ...gatedKey],
3143
+ queryFn: ({ signal }) =>
3144
+ guard(() =>
3145
+ fetchJSON<ServiceEndpointSlicesResult>(
3146
+ `/services/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/endpointslices`,
3147
+ signal,
3148
+ ),
3149
+ ),
3150
+ retry: shouldRetryRadarQuery,
3151
+ refetchInterval: (query) =>
3152
+ query.state.error instanceof RadarFeatureUnsupportedError ? false : 30000,
3153
+ enabled,
3154
+ });
3155
+ const { error, refetch } = query;
3156
+ useEffect(() => {
3157
+ if (support === "supported" && error instanceof RadarFeatureUnsupportedError) void refetch();
3158
+ }, [support, error, refetch]);
3159
+ return query;
3160
+ }
3161
+
2984
3162
  export function fetchWorkloadHistoryPage(kind: string, namespace: string, name: string, group: string | undefined, beforeSeq: number): Promise<WorkloadHistoryPage> {
2985
3163
  return fetchJSON(workloadHistoryPath(kind, namespace, name, group, beforeSeq));
2986
3164
  }
@@ -3114,23 +3292,31 @@ function retryMetricsQuery(failureCount: number, error: unknown): boolean {
3114
3292
  return !isMetricsUnavailableError(error) && failureCount < 1;
3115
3293
  }
3116
3294
 
3295
+ // The query behind usePodMetrics, for callers reading several Pods at once
3296
+ // (useQueries) that must share its cache and its null-when-unavailable rule.
3297
+ // null means no metrics for this Pod: the metrics API is missing, or the Pod
3298
+ // has not been scraped yet; one Pod's null cannot tell those apart.
3299
+ export function podMetricsQuery(namespace: string, podName: string, enabled = true) {
3300
+ return {
3301
+ queryKey: ["pod-metrics", namespace, podName] as const,
3302
+ queryFn: () =>
3303
+ fetchMetricsOrNull<PodMetrics>(`/metrics/pods/${namespace}/${podName}`),
3304
+ enabled: Boolean(namespace && podName) && enabled,
3305
+ staleTime: 15000,
3306
+ refetchInterval: 30000,
3307
+ refetchOnMount: "always" as const,
3308
+ refetchOnReconnect: "always" as const,
3309
+ retry: retryMetricsQuery,
3310
+ };
3311
+ }
3312
+
3117
3313
  // Fetch metrics for a specific pod
3118
3314
  export function usePodMetrics(
3119
3315
  namespace: string,
3120
3316
  podName: string,
3121
3317
  options?: { enabled?: boolean },
3122
3318
  ) {
3123
- return useQuery<PodMetrics | null>({
3124
- queryKey: ["pod-metrics", namespace, podName],
3125
- queryFn: () =>
3126
- fetchMetricsOrNull<PodMetrics>(`/metrics/pods/${namespace}/${podName}`),
3127
- enabled: Boolean(namespace && podName) && (options?.enabled ?? true),
3128
- staleTime: 15000,
3129
- refetchInterval: 30000,
3130
- refetchOnMount: "always",
3131
- refetchOnReconnect: "always",
3132
- retry: retryMetricsQuery,
3133
- });
3319
+ return useQuery<PodMetrics | null>(podMetricsQuery(namespace, podName, options?.enabled ?? true));
3134
3320
  }
3135
3321
 
3136
3322
  export function usePodEnvironment(namespace: string, podName: string, enabled = true) {
@@ -3197,6 +3383,7 @@ export interface ContainerMetricsHistory {
3197
3383
  }
3198
3384
 
3199
3385
  export interface PodMetricsHistory {
3386
+ metricsAPIReachable?: boolean;
3200
3387
  namespace: string;
3201
3388
  name: string;
3202
3389
  containers: ContainerMetricsHistory[];
@@ -3432,7 +3619,7 @@ export type PrometheusTimeRange =
3432
3619
  // PVC usage at a moment in time, derived from kubelet_volume_stats_*.
3433
3620
  export interface PrometheusPVCUsage {
3434
3621
  // Hub packages the frontend independently from per-cluster agent upgrades.
3435
- status?: "available" | "no_series" | "invalid_data" | "query_failed";
3622
+ status?: "available" | "no_series" | "invalid_data" | "query_failed" | "ambiguous_scope" | "scope_mismatch";
3436
3623
  namespace: string;
3437
3624
  name: string;
3438
3625
  used: number;
@@ -3580,6 +3767,8 @@ export interface ArgoStatus {
3580
3767
  // client is live. The two differ right after a restart (configured, reconnecting).
3581
3768
  configured: boolean;
3582
3769
  connected: boolean;
3770
+ // Not connected yet, and a connection is being made.
3771
+ connecting?: boolean;
3583
3772
  // Connected without a token: the install serves reads to everyone, so
3584
3773
  // there is nothing to add in Settings.
3585
3774
  anonymous?: boolean;
@@ -3592,8 +3781,12 @@ export function useArgoStatus(enabled = true) {
3592
3781
  queryKey: ["argocd-status"],
3593
3782
  queryFn: () => fetchJSON("/integrations/argocd/status"),
3594
3783
  enabled,
3595
- staleTime: 30000,
3596
- refetchInterval: 60000,
3784
+ // Cheap to ask (it never waits on Argo CD): reopening Settings asks
3785
+ // afresh, so "Connecting…" shows only while it's true.
3786
+ staleTime: 0,
3787
+ // While Radar connects, ask again soon so "Connecting…" doesn't linger.
3788
+ refetchInterval: (query) =>
3789
+ query.state.data?.connecting ? ARGO_CONNECTING_POLL_MS : 60000,
3597
3790
  });
3598
3791
  }
3599
3792
 
@@ -4102,7 +4295,7 @@ export function useUpdateResource() {
4102
4295
  });
4103
4296
  if (!response.ok) {
4104
4297
  const error = await readErrorBody(response);
4105
- const failure = new Error(error.error || `HTTP ${response.status}`);
4298
+ const failure = new ApiError(error.error || `HTTP ${response.status}`, response.status, error);
4106
4299
  // A reviewed apply's failure is shown in the review itself (the
4107
4300
  // changed-after-review notice, or the error), so it gets no toast.
4108
4301
  if (reviewedResourceVersion !== undefined || reviewedContext !== undefined) {
@@ -4157,7 +4350,7 @@ export function useUpdateResource() {
4157
4350
  });
4158
4351
  }
4159
4352
 
4160
- // Cascade delete preview — shows resources that will be garbage-collected
4353
+ // Cascade delete preview — visible related resources deletion may affect.
4161
4354
  export interface CascadeDeletePreview {
4162
4355
  root: { kind: string; namespace: string; name: string; group?: string };
4163
4356
  rootResolved: boolean;
@@ -4467,6 +4660,7 @@ export interface ApplyResourceResult {
4467
4660
  namespace: string;
4468
4661
  kind: string;
4469
4662
  apiVersion: string;
4663
+ uid?: string;
4470
4664
  created: boolean;
4471
4665
  }
4472
4666
 
@@ -5332,6 +5526,23 @@ export function useDrainPlan() {
5332
5526
  });
5333
5527
  }
5334
5528
 
5529
+ export function useNodeRemovalPlan(name: string | undefined, enabled: boolean) {
5530
+ const { guard, gatedKey } = useRadarFeature('drainPlan')
5531
+ return useQuery<DrainPlan>({
5532
+ queryKey: ['resources', 'node-removal-plan', name, ...gatedKey],
5533
+ queryFn: ({ signal }) => guard(() => fetchJSON<DrainPlan>(drainPlanPath(name!), {
5534
+ method: 'POST',
5535
+ headers: { 'Content-Type': 'application/json' },
5536
+ body: drainPlanBody({ force: true, deleteEmptyDirData: true }),
5537
+ signal,
5538
+ })),
5539
+ enabled: enabled && Boolean(name),
5540
+ staleTime: ISSUES_REFRESH_INTERVAL_MS,
5541
+ refetchInterval: (query) => query.state.error instanceof RadarFeatureUnsupportedError ? false : ISSUES_REFRESH_INTERVAL_MS,
5542
+ retry: shouldRetryRadarQuery,
5543
+ })
5544
+ }
5545
+
5335
5546
  export function useDrainNode() {
5336
5547
  const queryClient = useQueryClient();
5337
5548
 
@@ -6037,11 +6248,79 @@ export function useHelmOCISources() {
6037
6248
  });
6038
6249
  }
6039
6250
 
6251
+ export function useHelmSourceStatus(namespace: string, releaseName: string, enabled = true) {
6252
+ return useQuery<ChartSourceStatus>({
6253
+ queryKey: ["helm-source-status", namespace, releaseName],
6254
+ queryFn: () => fetchJSON(`/helm/releases/${encodeURIComponent(namespace)}/${encodeURIComponent(releaseName)}/source`),
6255
+ enabled: enabled && Boolean(namespace && releaseName),
6256
+ });
6257
+ }
6258
+
6259
+ async function mutateHelmSource(namespace: string, releaseName: string, source: ChartSourceCandidate): Promise<void> {
6260
+ const response = await apiFetch(apiUrl(`/helm/releases/${encodeURIComponent(namespace)}/${encodeURIComponent(releaseName)}/source`), {
6261
+ method: "PUT",
6262
+ headers: { "Content-Type": "application/json" },
6263
+ body: JSON.stringify(source),
6264
+ });
6265
+ if (!response.ok) {
6266
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
6267
+ throw new Error(error.error || `HTTP ${response.status}`);
6268
+ }
6269
+ }
6270
+
6271
+ export function useSetHelmSource(namespace: string, releaseName: string) {
6272
+ const queryClient = useQueryClient();
6273
+ return useMutation({
6274
+ mutationFn: (source: ChartSourceCandidate) => mutateHelmSource(namespace, releaseName, source),
6275
+ meta: { errorMessage: "Failed to associate chart source", successMessage: "Chart source associated" },
6276
+ onSuccess: () => {
6277
+ queryClient.invalidateQueries({ queryKey: ["helm-source-status", namespace, releaseName] });
6278
+ queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] });
6279
+ queryClient.invalidateQueries({ queryKey: ["helm-release-versions"] });
6280
+ queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] });
6281
+ },
6282
+ });
6283
+ }
6284
+
6285
+ interface AddHelmRepositoryRequest {
6286
+ name: string
6287
+ url: string
6288
+ namespace?: string
6289
+ releaseName?: string
6290
+ }
6291
+
6292
+ export function useAddHelmRepository() {
6293
+ const queryClient = useQueryClient();
6294
+ return useMutation({
6295
+ mutationFn: async (request: AddHelmRepositoryRequest): Promise<{ status: string; name: string }> => {
6296
+ const response = await apiFetch(apiUrl('/helm/repositories'), {
6297
+ method: "POST",
6298
+ headers: { "Content-Type": "application/json" },
6299
+ body: JSON.stringify(request),
6300
+ });
6301
+ if (!response.ok) {
6302
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
6303
+ throw new Error(error.error || `HTTP ${response.status}`);
6304
+ }
6305
+ return response.json();
6306
+ },
6307
+ meta: { errorMessage: "Failed to add Helm repository", successMessage: "Helm repository added" },
6308
+ // Also on error: the repository can be saved before associating it fails.
6309
+ onSettled: () => {
6310
+ queryClient.invalidateQueries({ queryKey: ["helm-repositories"] });
6311
+ queryClient.invalidateQueries({ queryKey: ["helm-source-status"] });
6312
+ queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] });
6313
+ queryClient.invalidateQueries({ queryKey: ["helm-release-versions"] });
6314
+ queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] });
6315
+ },
6316
+ });
6317
+ }
6318
+
6040
6319
  async function mutateOCISource(
6041
6320
  method: "POST" | "DELETE",
6042
6321
  source: string,
6043
6322
  ): Promise<string[]> {
6044
- const response = await apiFetch(`${getApiBase()}/helm/oci-sources`, {
6323
+ const response = await apiFetch(apiUrl('/helm/oci-sources'), {
6045
6324
  method,
6046
6325
  headers: { "Content-Type": "application/json" },
6047
6326
  body: JSON.stringify({ source }),
@@ -6053,14 +6332,18 @@ async function mutateOCISource(
6053
6332
  return response.json();
6054
6333
  }
6055
6334
 
6056
- // Invalidate the upgrade-info queries so a newly-registered source is probed
6057
- // immediately and "source not tracked" re-resolves.
6058
- function invalidateHelmAfterSourceChange(
6335
+ // Invalidate source inventory/status and upgrade info so an open source dialog
6336
+ // and upgrade checks immediately reflect source additions or removals.
6337
+ export function invalidateHelmAfterSourceChange(
6059
6338
  queryClient: ReturnType<typeof useQueryClient>,
6060
6339
  ) {
6061
- queryClient.invalidateQueries({ queryKey: ["helm-oci-sources"] });
6062
- queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] });
6063
- queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] });
6340
+ return Promise.all([
6341
+ queryClient.invalidateQueries({ queryKey: ["helm-oci-sources"] }),
6342
+ queryClient.invalidateQueries({ queryKey: ["helm-source-status"] }),
6343
+ queryClient.invalidateQueries({ queryKey: ["helm-upgrade-info"] }),
6344
+ queryClient.invalidateQueries({ queryKey: ["helm-release-versions"] }),
6345
+ queryClient.invalidateQueries({ queryKey: ["helm-batch-upgrade-info"] }),
6346
+ ])
6064
6347
  }
6065
6348
 
6066
6349
  export function useAddOCISource() {
@@ -6413,13 +6696,16 @@ export const useFluxResume = createGitOpsMutation<FluxResourceVars>({
6413
6696
  export const useFluxSyncWithSource = createGitOpsMutation<FluxResourceVars>({
6414
6697
  getPath: (v) => `/flux/${v.kind}/${v.namespace}/${v.name}/sync-with-source`,
6415
6698
  errorMessage: "Failed to sync with source",
6416
- successMessage: "Sync with source triggered",
6699
+ // The server names what it reconciled, and says when a source was skipped.
6700
+ getSuccessMessage: (data) => data.message,
6417
6701
  getInvalidateKeys: (v) => [
6418
6702
  ...fluxInvalidateKeys(v),
6419
6703
  // Also invalidate source resources as they were reconciled too
6420
6704
  ["resources", "gitrepositories"],
6421
6705
  ["resources", "ocirepositories"],
6422
6706
  ["resources", "helmrepositories"],
6707
+ ["resources", "helmcharts"],
6708
+ ["resources", "buckets"],
6423
6709
  ],
6424
6710
  });
6425
6711
 
@@ -6675,8 +6961,35 @@ export function useNamespaceScope() {
6675
6961
  });
6676
6962
  }
6677
6963
 
6678
- const NAMESPACE_SWITCH_TIMEOUT = 5000;
6964
+ const NAMESPACE_SWITCH_TIMEOUT = 15000;
6679
6965
  const NAMESPACE_RESCOPE_TIMEOUT = 120000;
6966
+ const NAMESPACE_RECONCILE_TIMEOUT = 5000;
6967
+
6968
+ // After a switch times out on the client the server may still have applied
6969
+ // it. Returns the server's scope when it holds exactly the requested pick, so
6970
+ // a slow success is not reported as a failure.
6971
+ export async function reconcileNamespaceSwitch(
6972
+ namespaces: string[],
6973
+ ): Promise<NamespaceScope | null> {
6974
+ const controller = new AbortController();
6975
+ const timeoutId = setTimeout(
6976
+ () => controller.abort(),
6977
+ NAMESPACE_RECONCILE_TIMEOUT,
6978
+ );
6979
+ try {
6980
+ const scope = await fetchJSON<NamespaceScope>(
6981
+ "/cluster/namespace-scope",
6982
+ controller.signal,
6983
+ );
6984
+ const want = [...new Set(namespaces)].sort().join(",");
6985
+ const got = [...new Set(scope.actives)].sort().join(",");
6986
+ return want === got ? scope : null;
6987
+ } catch {
6988
+ return null;
6989
+ } finally {
6990
+ clearTimeout(timeoutId);
6991
+ }
6992
+ }
6680
6993
 
6681
6994
  export function debugNamespaceLog(
6682
6995
  label: string,
@@ -6692,6 +7005,20 @@ export function debugNamespaceLog(
6692
7005
  });
6693
7006
  }
6694
7007
 
7008
+ /**
7009
+ * A cache-scoped switch rebuilds the watched inventory. View-filter switches
7010
+ * use namespace-specific query keys and do not invalidate the whole client.
7011
+ */
7012
+ export async function refreshAfterNamespaceSwitch(
7013
+ queryClient: QueryClient,
7014
+ scope: Pick<NamespaceScope, "cacheScoped" | "actives">,
7015
+ ): Promise<void> {
7016
+ if (!scope.cacheScoped) return;
7017
+ const notScope = (query: { queryKey: readonly unknown[] }) =>
7018
+ query.queryKey[0] !== "namespace-scope";
7019
+ await queryClient.invalidateQueries({ predicate: notScope }, { cancelRefetch: false });
7020
+ }
7021
+
6695
7022
  export function useSetActiveNamespace() {
6696
7023
  const queryClient = useQueryClient();
6697
7024
  return useMutation<NamespaceScope, Error, { namespaces: string[] }>({
@@ -6713,7 +7040,7 @@ export function useSetActiveNamespace() {
6713
7040
  // flag). If the scope query is missing/stale we can't yet tell a cheap
6714
7041
  // view-filter change from a cache-rebuilding rescope, so bias to the long
6715
7042
  // timeout — only a confirmed non-scoped session gets the fast switch timeout.
6716
- // Aborting a real rebuild at 5s surfaces a spurious failure while the server
7043
+ // Aborting a real rebuild at the switch timeout surfaces a spurious failure while the server
6717
7044
  // keeps going.
6718
7045
  const isRescope = currentScope?.cacheScoped !== false;
6719
7046
  const timeoutMs = isRescope
@@ -6747,6 +7074,13 @@ export function useSetActiveNamespace() {
6747
7074
  error: error instanceof Error ? error.message : String(error),
6748
7075
  });
6749
7076
  if (error instanceof Error && error.name === "AbortError") {
7077
+ // A rescope publishes its target before the rebuild finishes and can
7078
+ // still roll back, so only a view-filter pick is proven by the scope.
7079
+ const applied = isRescope ? null : await reconcileNamespaceSwitch(namespaces);
7080
+ if (applied) {
7081
+ debugNamespaceLog("mutation:timeout-but-applied", { namespaces });
7082
+ return applied;
7083
+ }
6750
7084
  throw new Error(
6751
7085
  isRescope
6752
7086
  ? "Namespace rescope timed out. The cluster may still be loading."
@@ -6769,9 +7103,7 @@ export function useSetActiveNamespace() {
6769
7103
  });
6770
7104
  }
6771
7105
  queryClient.setQueryData<NamespaceScope>(["namespace-scope"], scope);
6772
- if (scope.cacheScoped) {
6773
- queryClient.invalidateQueries();
6774
- }
7106
+ void refreshAfterNamespaceSwitch(queryClient, scope);
6775
7107
  debugNamespaceLog("mutation:success-after-scope-cache-write");
6776
7108
  },
6777
7109
  onError: () => {
@@ -7215,6 +7547,8 @@ export interface DiagnosticsSnapshot {
7215
7547
  };
7216
7548
  timeline?: {
7217
7549
  storageType: string;
7550
+ degraded?: boolean;
7551
+ degradedReason?: string;
7218
7552
  totalEvents: number;
7219
7553
  oldestEvent?: string;
7220
7554
  newestEvent?: string;