@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
@@ -1,204 +1,181 @@
1
- import { useState, useId } from 'react'
1
+ import { useId, useState, type FormEvent } from 'react'
2
2
  import { DialogPortal } from '@skyhook-io/k8s-ui/components/ui/DialogPortal'
3
- import { X, Plus, Trash2, Link2, AlertTriangle } from 'lucide-react'
4
- import { clsx } from 'clsx'
5
- import { useHelmOCISources, useAddOCISource, useRemoveOCISource, useClusterInfo, useCapabilities, useCloudRole } from '../../api/client'
6
- import { OperatorManagedNotice } from '../settings/OperatorManagedNotice'
3
+ import { AlertTriangle, Link2, Plus, Trash2, X } from 'lucide-react'
7
4
  import { Input } from '@skyhook-io/k8s-ui'
8
- import type { UpgradeInfo } from '../../types'
5
+ import { useAddHelmRepository, useAddOCISource, useCapabilities, useCloudRole, useClusterInfo, useHelmOCISources, useHelmRepositories, useHelmSourceStatus, useRemoveOCISource, useSetHelmSource } from '../../api/client'
6
+ import { OperatorManagedNotice } from '../settings/OperatorManagedNotice'
7
+ import type { ChartSourceStatus, HelmRepository, UpgradeInfo } from '../../types'
9
8
 
10
9
  interface TrackChartSourceDialogProps {
11
10
  open: boolean
12
11
  onClose: () => void
13
- /** Chart name of the release this was opened from, for the example prompt. */
14
12
  chartName?: string
13
+ namespace: string
14
+ releaseName: string
15
15
  sourceIssue?: UpgradeInfo['sourceIssue']
16
16
  sourceError?: string
17
17
  }
18
18
 
19
- function getSourceIssueCopy(sourceIssue: UpgradeInfo['sourceIssue'], sourceError?: string, operatorManaged = false) {
19
+ export function isValidHelmRepositoryURL(value: string) {
20
+ try {
21
+ const parsed = new URL(value.trim())
22
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:'
23
+ } catch {
24
+ return false
25
+ }
26
+ }
27
+
28
+ export function sourceStateLabel(recorded: boolean, configured: boolean, available: boolean) {
29
+ if (!recorded) return 'Not recorded'
30
+ if (!configured) return 'Recorded · not configured here'
31
+ if (!available) return 'Recorded and configured · unavailable or version missing'
32
+ return 'Recorded · configured · available'
33
+ }
34
+
35
+ // What the dialog says about the release's recorded source. "Not recorded" is
36
+ // a claim about the release, so it shows only once the read has succeeded.
37
+ export function sourceStatusLine(status: ChartSourceStatus | undefined, error: unknown) {
38
+ if (status) return sourceStateLabel(Boolean(status.recorded), status.configured, status.available)
39
+ if (error) return `Could not read this release's source: ${error instanceof Error ? error.message : String(error)}`
40
+ return 'Checking the recorded source...'
41
+ }
42
+
43
+ // Short form of a repository URL for a button label: host and path, no scheme.
44
+ export function repositoryDisplayURL(url: string) {
45
+ try {
46
+ const parsed = new URL(url)
47
+ return `${parsed.host}${parsed.pathname}`.replace(/\/+$/, '')
48
+ } catch {
49
+ return url
50
+ }
51
+ }
52
+
53
+ // Mirrors normalizedRepositoryName in internal/helm/chart_sources.go. The
54
+ // server stores names in this form, so collisions are checked on it.
55
+ const MAX_REPOSITORY_NAME_LENGTH = 40
56
+ export function normalizedRepositoryName(name: string) {
57
+ const base = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-._]+|[-._]+$/g, '')
58
+ return base.length > MAX_REPOSITORY_NAME_LENGTH ? base.slice(0, MAX_REPOSITORY_NAME_LENGTH).replace(/[-._]+$/, '') : base
59
+ }
60
+
61
+ // Name for re-adding the recorded repository on this machine. Reuses the
62
+ // recorded alias when it is free here; otherwise the first free "<alias>-N",
63
+ // so an alias that points at a different repository is never reused. The
64
+ // server refuses a taken name, which covers a repository added meanwhile.
65
+ // An empty result lets the server pick a name from the URL.
66
+ export function recordedRepositoryName(alias: string, repositories: HelmRepository[]) {
67
+ const base = normalizedRepositoryName(alias)
68
+ if (!base) return ''
69
+ const taken = new Set(repositories.map(repository => normalizedRepositoryName(repository.name)))
70
+ if (!taken.has(base)) return base
71
+ for (let n = 2; ; n++) {
72
+ const suffix = `-${n}`
73
+ const name = base.slice(0, MAX_REPOSITORY_NAME_LENGTH - suffix.length).replace(/[-._]+$/, '') + suffix
74
+ if (!taken.has(name)) return name
75
+ }
76
+ }
77
+
78
+ export function getSourceIssueCopy(sourceIssue: UpgradeInfo['sourceIssue'], sourceError?: string) {
20
79
  switch (sourceIssue) {
21
- case 'repo_index_error':
22
- return {
23
- title: 'A Helm repo index failed',
24
- body: operatorManaged
25
- ? 'Ask the operator to fix the Helm repo index, or add the chart’s OCI registry prefix to helm.ociSources if it came from OCI.'
26
- : 'Fix, refresh, or remove the broken Helm repo index. If this release came from OCI, add its registry prefix and Radar will check OCI before reporting that repo error.',
27
- }
80
+ case 'repo_index_error': return { title: 'A Helm repo index failed', body: 'Fix or refresh the repository before associating it.' }
28
81
  case 'ambiguous_repository':
29
- return {
30
- title: 'Multiple Helm repos match',
31
- body: 'Radar will not guess between classic repos. Adding an OCI prefix will not enable direct upgrades while those repos remain ambiguous.',
32
- }
33
- case 'untracked':
34
- return {
35
- title: 'Source not tracked',
36
- body: operatorManaged
37
- ? 'Helm does not record the install source. Ask the operator to add the OCI prefix that contains this chart to helm.ociSources.'
38
- : 'Helm does not record the install source. Add the OCI prefix that contains this chart.',
39
- }
40
- default:
41
- return sourceError
42
- ? { title: 'Source unresolved', body: sourceError }
43
- : undefined
82
+ case 'ambiguous_source': return { title: 'Multiple chart sources match', body: 'Radar will not guess. Select the source that originally supplied this release.' }
83
+ case 'source_unavailable': return { title: 'Recorded source unavailable', body: 'The release provenance is preserved, but that source cannot currently be verified here.' }
84
+ case 'untracked': return { title: 'Source not recorded', body: 'Select an exact configured match, or add the original classic repository or OCI prefix.' }
85
+ default: return sourceError ? { title: 'Source unresolved', body: sourceError } : undefined
44
86
  }
45
87
  }
46
88
 
47
- // TrackChartSourceDialog lets the user register an OCI chart-source prefix — the
48
- // OCI analog of `helm repo add`. Helm doesn't persist the ref a release was
49
- // installed from, so for charts published to an OCI registry (and not managed by
50
- // GitOps) Radar can only track upgrades once the user declares where they live.
51
- // Registering a registry/org prefix lets Radar probe "<prefix>/<chartName>".
52
- export function TrackChartSourceDialog({ open, onClose, chartName, sourceIssue, sourceError }: TrackChartSourceDialogProps) {
53
- const titleId = useId()
54
- const [value, setValue] = useState('')
55
- const { data: sources } = useHelmOCISources()
89
+ export function TrackChartSourceDialog({ open, onClose, chartName, namespace, releaseName, sourceIssue, sourceError }: TrackChartSourceDialogProps) {
90
+ const [ociPrefix, setOCIPrefix] = useState('')
91
+ const [repositoryName, setRepositoryName] = useState('')
92
+ const [repositoryURL, setRepositoryURL] = useState('')
93
+ const { data: status, error: statusError, refetch: refetchStatus } = useHelmSourceStatus(namespace, releaseName, open)
94
+ const { data: ociSources } = useHelmOCISources()
56
95
  const { data: clusterInfo } = useClusterInfo()
57
- const addSource = useAddOCISource()
58
- const removeSource = useRemoveOCISource()
59
96
  const { data: capabilities } = useCapabilities()
60
- const operatorManaged = capabilities?.configManagement === 'operator'
97
+ const addOCI = useAddOCISource()
98
+ const removeOCI = useRemoveOCISource()
99
+ const addRepository = useAddHelmRepository()
100
+ const setSource = useSetHelmSource(namespace, releaseName)
101
+ const issueCopy = getSourceIssueCopy(sourceIssue, sourceError)
102
+ const recorded = status?.recorded
103
+ const configManaged = capabilities?.configManagement === 'operator'
61
104
  // Chart sources are shared configuration that every user's upgrades resolve
62
105
  // against, so in Radar Cloud only owners change them (the server enforces it).
63
106
  const ownerOnly = !useCloudRole().canAtLeast('owner')
64
- const canEdit = capabilities != null && !operatorManaged && !ownerOnly
65
-
66
- // In-cluster Radar has no `helm registry login` store (the pod's HELM_CONFIG_HOME
67
- // points at an empty /tmp), so private registries can't authenticate — only
68
- // public charts can be tracked. Be honest about it rather than silently failing.
69
- const inCluster = clusterInfo?.inCluster ?? false
70
-
71
- const trimmed = value.trim()
72
- const invalid = trimmed !== '' && !trimmed.startsWith('oci://')
73
- const normalizedInput = trimmed.replace(/\/+$/, '')
74
- const normalizedChartName = chartName?.trim().replace(/^\/+|\/+$/g, '') ?? ''
75
- const probedRef = normalizedInput && normalizedChartName ? `${normalizedInput}/${normalizedChartName}` : ''
76
- const looksLikeFullChartRef = Boolean(normalizedInput && normalizedChartName && normalizedInput.endsWith(`/${normalizedChartName}`))
77
- const sourceIssueCopy = getSourceIssueCopy(sourceIssue, sourceError, operatorManaged)
107
+ const canEditSources = capabilities != null && !configManaged && !ownerOnly
108
+ const titleId = useId()
109
+ // The release says which repository it came from, but this machine has no
110
+ // repository with that URL: offer to add exactly that one.
111
+ const missingRecordedRepository = status && !status.configured && recorded?.type === 'repository' && recorded.url ? recorded : undefined
112
+ const { data: repositories, isLoading: repositoriesLoading } = useHelmRepositories()
113
+ const recordedName = missingRecordedRepository ? recordedRepositoryName(missingRecordedRepository.reference, repositories ?? []) : undefined
78
114
 
79
- const handleAdd = () => {
80
- if (!trimmed || invalid) return
81
- addSource.mutate(trimmed, { onSuccess: () => setValue('') })
115
+ const addRecordedRepository = () => {
116
+ if (!missingRecordedRepository || recordedName === undefined) return
117
+ addRepository.mutate({ name: recordedName, url: missingRecordedRepository.url, namespace, releaseName }, { onSuccess: onClose })
82
118
  }
83
119
 
84
- return (
85
- <DialogPortal ariaLabelledBy={titleId} open={open} onClose={onClose} className="max-w-lg w-full">
86
- <div className="flex items-start gap-3 p-4 border-b border-theme-border">
87
- <div className="flex items-center justify-center w-10 h-10 rounded-full shrink-0 bg-theme-hover">
88
- <Link2 className="w-5 h-5 text-theme-text-secondary" />
89
- </div>
90
- <div className="flex-1 min-w-0">
91
- <h3 id={titleId} className="text-lg font-semibold text-theme-text-primary">Track chart source</h3>
92
- <p className="text-sm text-theme-text-secondary mt-1">
93
- Helm doesn&apos;t record where a chart was installed from. Radar checks registered OCI
94
- prefixes for newer versions of your charts.
95
- </p>
96
- </div>
97
- <button
98
- onClick={onClose}
99
- aria-label="Close"
100
- className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
101
- >
102
- <X className="w-5 h-5" />
103
- </button>
104
- </div>
105
-
106
- <div className="p-4 space-y-4">
107
- {operatorManaged && <OperatorManagedNotice helmValue="helm.ociSources" />}
108
- {!operatorManaged && ownerOnly && (
109
- <p className="text-sm text-theme-text-secondary">
110
- Only organization owners can add or remove chart sources.
111
- </p>
112
- )}
113
- {sourceIssueCopy && (
114
- <div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-sm">
115
- <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-theme-text-secondary" />
116
- <div>
117
- <p className="font-medium text-theme-text-primary">{sourceIssueCopy.title}</p>
118
- <p className="text-theme-text-secondary">{sourceIssueCopy.body}</p>
119
- </div>
120
- </div>
121
- )}
122
-
123
- {canEdit && <div>
124
- <label className="block text-sm font-medium text-theme-text-secondary mb-2">
125
- OCI registry prefix
126
- </label>
127
- <div className="flex gap-2">
128
- <Input
129
- value={value}
130
- onChange={(e) => setValue(e.target.value)}
131
- onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
132
- placeholder="oci://ghcr.io/myorg/charts"
133
- aria-invalid={invalid ? true : undefined}
134
- className={clsx(
135
- 'flex-1 px-3 py-2 bg-theme-elevated border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2',
136
- invalid ? 'border-red-500/60 focus:ring-red-500' : 'border-theme-border-light focus:ring-accent',
137
- )}
138
- />
139
- <button
140
- onClick={handleAdd}
141
- disabled={!trimmed || invalid || addSource.isPending}
142
- className="btn-brand px-3 py-2 text-sm inline-flex items-center gap-1 disabled:opacity-50 disabled:pointer-events-none"
143
- >
144
- <Plus className="w-4 h-4" />
145
- Add
146
- </button>
147
- </div>
148
- <p className="mt-1 text-xs text-theme-text-tertiary">
149
- {invalid
150
- ? 'Must be an oci:// reference.'
151
- : looksLikeFullChartRef
152
- ? `This looks like a full chart ref. Radar would probe "${probedRef}"; remove the final "/${normalizedChartName}" if "${normalizedChartName}" is the chart name.`
153
- : chartName
154
- ? `Radar will probe "${probedRef || `<prefix>/${normalizedChartName}`}".`
155
- : 'Radar probes <prefix>/<chartName> for each untracked release.'}
156
- </p>
157
- </div>}
120
+ const addRepositoryAndAssociate = (event: FormEvent) => {
121
+ event.preventDefault()
122
+ const name = repositoryName.trim()
123
+ const url = repositoryURL.trim()
124
+ if (!name || !isValidHelmRepositoryURL(url)) return
125
+ addRepository.mutate({ name, url, namespace, releaseName }, { onSuccess: onClose })
126
+ }
158
127
 
159
- {sources && sources.length > 0 && (
160
- <div>
161
- <p className="text-xs font-medium text-theme-text-tertiary uppercase tracking-wide mb-2">
162
- Registered sources
163
- </p>
164
- <ul className="space-y-1">
165
- {sources.map((src) => (
166
- <li
167
- key={src}
168
- className="flex items-center justify-between gap-2 px-3 py-2 bg-theme-elevated rounded-lg"
169
- >
170
- <span className="text-sm text-theme-text-primary font-mono truncate">{src}</span>
171
- {canEdit && <button
172
- onClick={() => removeSource.mutate(src)}
173
- disabled={removeSource.isPending}
174
- className="p-1 text-theme-text-secondary hover:text-red-400 hover:bg-red-500/10 rounded disabled:opacity-50"
175
- aria-label={`Remove ${src}`}
176
- >
177
- <Trash2 className="w-4 h-4" />
178
- </button>}
179
- </li>
180
- ))}
181
- </ul>
182
- </div>
183
- )}
128
+ const addOCIPrefix = async (event: FormEvent) => {
129
+ event.preventDefault()
130
+ if (!ociPrefix.trim().startsWith('oci://')) return
131
+ await addOCI.mutateAsync(ociPrefix.trim())
132
+ setOCIPrefix('')
133
+ await refetchStatus()
134
+ }
184
135
 
185
- {inCluster ? (
186
- <div className="flex items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400">
187
- <AlertTriangle className="w-4 h-4 shrink-0 mt-px" />
188
- <span>
189
- Radar is running in-cluster, where it has no{' '}
190
- <span className="font-mono">helm registry login</span> credentials — only{' '}
191
- <strong>public</strong> charts can be tracked. Private-registry support for in-cluster
192
- Radar isn&apos;t available yet.
193
- </span>
194
- </div>
195
- ) : (
196
- <p className="text-xs text-theme-text-tertiary">
197
- Credentials are reused from your <span className="font-mono">helm registry login</span>.
198
- Radar stores no registry secrets.
199
- </p>
200
- )}
136
+ return <DialogPortal ariaLabelledBy={titleId} open={open} onClose={onClose} className="max-w-2xl w-full">
137
+ <div className="flex items-start gap-3 p-4 border-b border-theme-border">
138
+ <div className="flex items-center justify-center w-10 h-10 rounded-full bg-theme-hover"><Link2 className="w-5 h-5" /></div>
139
+ <div className="flex-1 min-w-0"><h3 id={titleId} className="text-lg font-semibold">Track chart source</h3><p className="text-sm text-theme-text-secondary mt-1">Tell Radar which repository or OCI source this release was installed from. Upgrades then come only from that source.</p></div>
140
+ <button onClick={onClose} aria-label="Close" className="p-1 rounded hover:bg-theme-elevated"><X className="w-5 h-5" /></button>
141
+ </div>
142
+ <div className="p-4 space-y-4">
143
+ {configManaged && <OperatorManagedNotice helmValue="helm.ociSources" />}
144
+ {!configManaged && ownerOnly && <p className="text-sm text-theme-text-secondary">Only organization owners can add or remove chart sources.</p>}
145
+ {issueCopy && <div className="flex gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-sm"><AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" /><div><p className="font-medium">{issueCopy.title}</p><p className="text-theme-text-secondary">{issueCopy.body}</p></div></div>}
146
+ <div className="rounded-lg border border-theme-border px-3 py-2">
147
+ <p className="text-xs uppercase tracking-wide text-theme-text-tertiary">Release source state</p>
148
+ <p className="text-sm font-medium mt-1 break-all">{sourceStatusLine(status, statusError)}</p>
149
+ {!status && statusError && <button type="button" onClick={() => refetchStatus()} className="mt-2 px-3 py-1.5 text-sm text-theme-text-primary border border-theme-border rounded-lg hover:bg-theme-elevated transition-colors">Retry</button>}
150
+ {recorded && <p className="font-mono text-xs text-theme-text-secondary mt-1 break-all">{recorded.url ?? recorded.reference}</p>}
201
151
  </div>
202
- </DialogPortal>
203
- )
152
+ {missingRecordedRepository && recordedName !== undefined && <div className="space-y-1">
153
+ <button type="button" onClick={addRecordedRepository} disabled={!canEditSources || repositoriesLoading || addRepository.isPending} className="btn-brand inline-flex items-center gap-1 px-3 py-2 max-w-full"><Plus className="w-4 h-4 shrink-0" /><span className="truncate">Add {repositoryDisplayURL(missingRecordedRepository.url)} and use it</span></button>
154
+ <p className="text-xs text-theme-text-tertiary">{!recordedName
155
+ ? 'Radar picks a name for it in Helm.'
156
+ : recordedName === missingRecordedRepository.reference
157
+ ? <>Saved in Helm as <span className="font-mono">{recordedName}</span>, the name it was recorded under.</>
158
+ : recordedName === normalizedRepositoryName(missingRecordedRepository.reference)
159
+ ? <>Saved in Helm as <span className="font-mono">{recordedName}</span>.</>
160
+ : <>Saved in Helm as <span className="font-mono">{recordedName}</span>. The name <span className="font-mono">{missingRecordedRepository.reference}</span> already points to a different repository here.</>}</p>
161
+ </div>}
162
+ {status?.candidates && status.candidates.length > 0 && <div>
163
+ <p className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-2">Configured sources that publish {chartName || 'this chart'} at the installed version</p>
164
+ <div className="space-y-2">{status.candidates.map(candidate => <button key={`${candidate.type}:${candidate.url ?? candidate.reference}`} onClick={() => setSource.mutate(candidate, { onSuccess: onClose })} disabled={setSource.isPending} className="w-full text-left rounded-lg border border-theme-border px-3 py-2 hover:border-accent disabled:opacity-50"><span className="text-xs uppercase text-theme-text-tertiary">{candidate.type}</span><span className="block font-mono text-sm break-all">{candidate.url ?? candidate.reference}</span></button>)}</div>
165
+ {status.candidates.length > 1 && <p className="text-xs text-amber-600 mt-2">Multiple sources publish this exact chart and version. Choose explicitly; Radar will not auto-associate one.</p>}
166
+ </div>}
167
+ <form onSubmit={addRepositoryAndAssociate} className="space-y-2">
168
+ <p className="text-xs uppercase tracking-wide text-theme-text-tertiary">Add classic repository and associate</p>
169
+ <div className="grid grid-cols-1 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.8fr)_auto] gap-2"><Input value={repositoryName} onChange={event => setRepositoryName(event.target.value)} placeholder="Repository name" disabled={!canEditSources} /><Input value={repositoryURL} onChange={event => setRepositoryURL(event.target.value)} placeholder="https://charts.example.com" disabled={!canEditSources} /><button type="submit" disabled={!canEditSources || !repositoryName.trim() || !isValidHelmRepositoryURL(repositoryURL) || addRepository.isPending} className="btn-brand inline-flex items-center justify-center gap-1 px-3 py-2"><Plus className="w-4 h-4" /> Add</button></div>
170
+ <p className="text-xs text-theme-text-tertiary">Association succeeds only if the repository publishes {chartName || 'the installed chart'} at the exact installed version.</p>
171
+ </form>
172
+ <form onSubmit={addOCIPrefix} className="space-y-2">
173
+ <p className="text-xs uppercase tracking-wide text-theme-text-tertiary">Register OCI prefix</p>
174
+ <div className="flex gap-2"><Input value={ociPrefix} onChange={event => setOCIPrefix(event.target.value)} placeholder="oci://registry.example.com/charts" disabled={!canEditSources} /><button type="submit" disabled={!canEditSources || !ociPrefix.trim().startsWith('oci://') || addOCI.isPending} className="btn-brand px-3 py-2">Add</button></div>
175
+ <p className="text-xs text-theme-text-tertiary">After registration, select it in the list above. Credentials are reused from Helm and are never recorded in release provenance.</p>
176
+ </form>
177
+ {ociSources && ociSources.length > 0 && <ul className="space-y-1">{ociSources.map(source => <li key={source} className="flex items-center justify-between gap-2 rounded-lg bg-theme-elevated px-3 py-2"><span className="font-mono text-sm truncate">{source}</span>{canEditSources && <button onClick={() => removeOCI.mutate(source)} disabled={removeOCI.isPending} aria-label={`Remove ${source}`} className="p-1 hover:text-red-400 disabled:opacity-50"><Trash2 className="w-4 h-4" /></button>}</li>)}</ul>}
178
+ {clusterInfo?.inCluster && <p className="text-xs text-amber-600 flex items-start gap-2"><AlertTriangle className="w-4 h-4 shrink-0" />Private OCI sources require credentials available to the in-cluster Radar process.</p>}
179
+ </div>
180
+ </DialogPortal>
204
181
  }