@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/README.md CHANGED
@@ -10,7 +10,7 @@ This package is source-only — it ships TypeScript + TSX files under `src/`. Co
10
10
  npm install @skyhook-io/radar-app
11
11
  ```
12
12
 
13
- Peer deps: `react >=19`, `react-dom >=19`, `react-router-dom >=7`, `@tanstack/react-query >=5`, `@skyhook-io/k8s-ui >=1.5.0`, plus `clsx`, `tailwind-merge`, `lucide-react`, `@xyflow/react`, `elkjs`.
13
+ Peer deps: `react >=19`, `react-dom >=19`, `react-router-dom >=7`, `@tanstack/react-query >=5`, `@skyhook-io/k8s-ui >=1.17.1`, plus `clsx`, `tailwind-merge`, `lucide-react`, `@xyflow/react`, `elkjs`.
14
14
 
15
15
  ## Use
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.16.3",
3
+ "version": "1.17.1",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "@fontsource/dm-mono": "^5.3.0",
35
35
  "@monaco-editor/react": "^4.7.0",
36
36
  "diff": "^9.0.0",
37
- "monaco-editor": "0.55.1",
37
+ "monaco-editor": "0.57.0",
38
38
  "react-markdown": "^10.1.0",
39
39
  "react-virtuoso": "^4.18.16",
40
40
  "remark-gfm": "^4.0.1",
@@ -42,7 +42,7 @@
42
42
  "yaml": "^2.9.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@skyhook-io/k8s-ui": ">=1.16.3",
45
+ "@skyhook-io/k8s-ui": ">=1.17.1",
46
46
  "@tanstack/react-query": ">=5",
47
47
  "@xyflow/react": ">=12.0.0",
48
48
  "clsx": ">=2",
package/src/App.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
1
+ import { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } from 'react'
2
2
  import { flushSync } from 'react-dom'
3
3
  import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
4
4
  import { englishPlural } from '@skyhook-io/k8s-ui/utils/pluralize'
@@ -10,7 +10,7 @@ import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
10
10
  import { investigationWorkspaceSearch, isInvestigationWorkspacePath, useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
11
11
  import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
12
12
  import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
13
- import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
13
+ import { resetNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
14
14
  import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
15
15
  import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
16
16
  import { TimelineView } from './components/timeline/TimelineView'
@@ -23,7 +23,6 @@ import { HelmView } from './components/helm/HelmView'
23
23
  import { HelmCompareRoute } from './components/helm/HelmCompareRoute'
24
24
  import { TrafficView } from './components/traffic/TrafficView'
25
25
  import { CostView } from './components/cost/CostView'
26
- import { CapacityView } from './components/capacity/CapacityView'
27
26
  import { AuditView } from './components/audit/AuditView'
28
27
  import { IssuesPane } from './components/issues/IssuesPane'
29
28
  import { GitOpsView } from './components/gitops/GitOpsView'
@@ -38,9 +37,10 @@ import { CloudFunnelButton } from './components/CloudFunnelButton'
38
37
  import { useNavCustomization } from './context/NavCustomization'
39
38
  import type { FleetTakeoverTarget } from './context/NavCustomization'
40
39
  import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
41
- import { CNPGView } from './components/cnpg/CNPGView'
42
- import { CNPG_SCREENS, cnpgDetailKindFor, cnpgDetailPath, parseCNPGRoute } from './components/cnpg/routes'
43
- import { currentPageLabel } from './components/cnpg/paths'
40
+ import { resourceExpandedPath, resourceKindListTitle } from './integrations/resourceHosts'
41
+ import { workspaceContextSwitchSearch, workspaceForPath, workspaceForView, workspaceLabels } from './integrations/workspaceRoutes'
42
+ import { WorkspaceScreen } from './integrations/workspaceScreens'
43
+ import { currentPageLabel } from './utils/page-links'
44
44
  import { navigateFromPrimaryRail } from './components/nav/navigation'
45
45
  import { useNavRailPinned } from './hooks/useNavRailPinned'
46
46
  import { useMediaQuery } from './hooks/useMediaQuery'
@@ -58,12 +58,12 @@ import { openWhatsNew, useWhatsNewStatus, WhatsNew } from './components/whats-ne
58
58
  import { useUsageData, useUsageRecording } from './api/usage-data'
59
59
  import { UsageDataPrompt } from './components/usage-data/UsageDataPrompt'
60
60
  import { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay'
61
- import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
61
+ import { DiagnosticsOverlay, type DiagnosticsFocus } from './components/ui/DiagnosticsOverlay'
62
62
  import { useEventSource } from './hooks/useEventSource'
63
- import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
63
+ import { debugNamespaceLog, refetchOnResourceEvents, resourceEventKey, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit, useClusterInfo } from './api/client'
64
64
  import { buildAuditSeverityMap } from './utils/auditBadges'
65
65
  import { isInNamespaceScope, scopeNodesToNamespaces } from './utils/topology-namespace'
66
- import { routePath, apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
66
+ import { routePath, apiUrl, getApiBase, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
67
67
  import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts, useSuppressBaseShortcuts } from './hooks/useKeyboardShortcuts'
68
68
  import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
69
69
  import { useDocumentTitle } from './hooks/useDocumentTitle'
@@ -75,7 +75,9 @@ import { Tooltip } from './components/ui/Tooltip'
75
75
  import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
76
76
  import { SettingsDialog, type SettingsSectionId } from './components/settings/SettingsDialog'
77
77
  import type { APIResource, TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
78
- import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource, withCrossViewParams } from './utils/navigation'
78
+ import { createViewMemory } from './utils/viewMemory'
79
+ import { createKindFilterStore } from './utils/kindFilterStore'
80
+ import { kindToPluralWithGroup, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource, withCrossViewParams, writeViewParams, CROSS_VIEW_PARAMS } from './utils/navigation'
79
81
  import { findSelectedTopologyNode } from './utils/topology-selection'
80
82
  import { type OmnibarHandle } from './components/ui/Omnibar'
81
83
  import { RadarOmnibar } from './components/ui/RadarOmnibar'
@@ -83,7 +85,7 @@ import type { ContextSwitcherHandle } from './components/ContextSwitcher'
83
85
 
84
86
  // All possible node kinds (core + GitOps)
85
87
  const ALL_NODE_KINDS: NodeKind[] = [
86
- 'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
88
+ 'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute', 'UDPRoute',
87
89
  'Service', 'Deployment', 'Rollout', 'DaemonSet', 'StatefulSet',
88
90
  'ReplicaSet', 'Pod', 'PodGroup', 'ConfigMap', 'Secret', 'ServiceAccount', 'SealedSecret', 'ServiceMonitor', 'PodMonitor', 'HorizontalPodAutoscaler', 'Job', 'CronJob', 'PersistentVolumeClaim', 'Namespace',
89
91
  'Application', 'Kustomization', 'HelmRelease', 'GitRepository',
@@ -141,8 +143,8 @@ function getViewFromPath(pathname: string): ExtendedMainView {
141
143
  if (path === 'helm') return 'helm'
142
144
  if (path === 'traffic') return 'traffic'
143
145
  if (path === 'cost') return 'cost'
144
- if (path === 'capacity') return 'capacity'
145
- if (path === 'cnpg') return 'cnpg'
146
+ const workspace = workspaceForPath(pathname)
147
+ if (workspace) return workspace.id
146
148
  if (path === 'workload') return 'workload'
147
149
  if (path === 'checks' || path === 'audit') return 'checks' // /audit = legacy → checks
148
150
  if (path === 'gitops') return 'gitops'
@@ -167,13 +169,14 @@ function usageView(pathname: string, view: ExtendedMainView, upgrade: boolean):
167
169
 
168
170
  // The screen a crash is counted under. Fixed names, because release builds
169
171
  // shorten component names and the component stack can't say which screen.
170
- const CRASH_LABELS: Record<ExtendedMainView, string> = {
172
+ const CRASH_LABELS = {
171
173
  home: 'Home', topology: 'Topology', resources: 'Resources', timeline: 'Timeline',
172
174
  issues: 'Issues', helm: 'Helm', helmCompare: 'HelmCompare', traffic: 'Traffic',
173
- cost: 'Cost', capacity: 'Capacity', cnpg: 'CloudNativePG', checks: 'Checks', gitops: 'GitOps',
175
+ cost: 'Cost', checks: 'Checks', gitops: 'GitOps',
174
176
  applications: 'Applications', workload: 'Workload', compare: 'Compare',
175
177
  investigations: 'Investigations',
176
- }
178
+ ...workspaceLabels,
179
+ } satisfies Record<ExtendedMainView, string>
177
180
 
178
181
  // The namespace scope filter is meaningful only on namespaced surfaces. On
179
182
  // cluster-scoped views it does nothing, so we disable it with an explanation
@@ -203,12 +206,8 @@ function namespaceFilterDisabled(
203
206
  tooltip: 'Cost is reported per namespace across the whole cluster — the namespace filter doesn’t apply here.',
204
207
  }
205
208
  }
206
- if (view === 'capacity') {
207
- return {
208
- disabled: true,
209
- tooltip: 'Capacity is reported across the cluster — the namespace filter doesn’t apply here.',
210
- }
211
- }
209
+ const workspace = workspaceForView(view)
210
+ if (workspace?.namespaceScope === 'cluster') return { disabled: true, tooltip: workspace.namespaceFilterTooltip }
212
211
  if (view === 'checks' && pathname.startsWith('/checks/upgrade')) {
213
212
  return {
214
213
  disabled: true,
@@ -263,6 +262,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: APIResourc
263
262
  const resourceName = decode(pathSegments[1] ?? '')
264
263
  if (!resourceName) return 'Resources'
265
264
  const group = new URLSearchParams(search).get('apiGroup') || ''
265
+ const hostTitle = resourceKindListTitle({ name: resourceName, group })
266
+ if (hostTitle) return hostTitle
266
267
  const match = findAPIResourceForRoute(apiResources, resourceName, group)
267
268
  return pluralKindTitle(match?.kind ?? pluralToKind(resourceName), resourceName)
268
269
  }
@@ -281,18 +282,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: APIResourc
281
282
  if (view === 'checks' && pathSegments[1] === 'upgrade') return 'Upgrade impact'
282
283
 
283
284
  // The landing view reads "Overview" rather than "Home" in the tab.
284
- if (view === 'capacity') {
285
- if (pathSegments[1] === 'pools') return decode(pathSegments[2] ?? '') || 'Capacity'
286
- if (pathSegments[1] === 'demand') return 'Capacity Demand'
287
- if (pathSegments[1] === 'activity') return 'Capacity Activity'
288
- }
289
-
290
- if (view === 'cnpg') {
291
- const route = parseCNPGRoute(pathname)
292
- if (route.detail) return route.detail.name
293
- const screen = CNPG_SCREENS.find((s) => s.id === route.screen)
294
- return `CloudNativePG ${screen?.label ?? 'Overview'}`
295
- }
285
+ const workspace = workspaceForView(view)
286
+ if (workspace) return workspace.pageTitle(pathname)
296
287
  if (view === 'home') return 'Overview'
297
288
  // Every other view's label is its id capitalized — getViewFromPath has already
298
289
  // normalized aliases (e.g. /audit → 'checks'), so no lookup table is needed.
@@ -351,6 +342,7 @@ interface AppProps {
351
342
  }
352
343
 
353
344
  function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterLoadStateChange }: AppProps) {
345
+ useLayoutEffect(() => () => resetNavigationMap(), [])
354
346
  const navigate = useNavigate()
355
347
  const location = useLocation()
356
348
  const navigationType = useNavigationType()
@@ -452,6 +444,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
452
444
 
453
445
  // Get mainView from URL path
454
446
  const mainView = getViewFromPath(location.pathname)
447
+ const activeWorkspace = workspaceForView(mainView)
455
448
  const upgradeReadinessRoute = location.pathname.startsWith('/checks/upgrade')
456
449
 
457
450
  // Opt-in usage data. Embedded hosts own their own consent, so Radar never asks there.
@@ -459,12 +452,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
459
452
  // A status cached by another screen must not start recording inside a host.
460
453
  useUsageRecording(usageView(location.pathname, mainView, upgradeReadinessRoute), navCustomization.embedded ? undefined : usageData.data)
461
454
 
462
- // Initialize the kind→plural discovery map app-wide (not just on ResourcesView
463
- // mount) so the omnibar can open a CRD hit with an irregular plural from any
464
- // view — kindToPlural would otherwise English-guess the route before a
465
- // resources view has run initNavigationMap().
455
+ // The omnibar needs discovered plurals even before ResourcesView mounts.
466
456
  const { data: navApiResources } = useAPIResources()
467
- useEffect(() => { if (navApiResources) initNavigationMap(navApiResources) }, [navApiResources])
468
457
 
469
458
  // View-aware namespace scope: disabled on cluster-scoped surfaces so the
470
459
  // chip isn't a dead control next to the cluster switcher.
@@ -482,15 +471,45 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
482
471
  return slug || 'pods'
483
472
  }, [location.pathname])
484
473
 
485
- // Canonical URL — `/resources` is not stable for bookmarks/sharing; normalize to `/resources/pods`.
474
+ // Section memory and Resources filters are kept per cluster; inert until the context is known.
475
+ const { data: clusterInfoData, isLoading: clusterInfoLoading } = useClusterInfo()
476
+ const clusterContext = clusterInfoData?.context
477
+ const viewMemory = useMemo(
478
+ () => (clusterContext ? createViewMemory(`${getApiBase()}|${clusterContext}`) : undefined),
479
+ [clusterContext],
480
+ )
481
+ const kindFilterStore = useMemo(
482
+ () => (clusterContext ? createKindFilterStore(`${getApiBase()}|${clusterContext}`) : undefined),
483
+ [clusterContext],
484
+ )
485
+ useEffect(() => {
486
+ viewMemory?.record(mainView, location.pathname, location.search)
487
+ if (mainView === 'resources') kindFilterStore?.recordLastKind(location.pathname, location.search)
488
+ }, [viewMemory, kindFilterStore, mainView, location.pathname, location.search])
489
+ const rememberedKindSearch = useMemo(
490
+ () => kindFilterStore && ((kind: { name: string; group: string }) => kindFilterStore.get(kind.name, kind.group)),
491
+ [kindFilterStore],
492
+ )
493
+ const saveKindFilters = useMemo(
494
+ () => kindFilterStore && ((kind: { name: string; group: string }, search: string) => kindFilterStore.set(kind.name, kind.group, search)),
495
+ [kindFilterStore],
496
+ )
497
+
498
+ // Canonical URL — `/resources` is not stable for bookmarks/sharing; normalize to
499
+ // the last kind (hosts such as Radar Hub link to bare `/resources`), else Pods.
486
500
  useEffect(() => {
487
501
  const path = location.pathname.replace(/\/+$/, '') || '/'
488
- if (path !== '/resources') return
502
+ // Wait for the cluster so its last kind can be read; landing on Pods first would overwrite it.
503
+ if (path !== '/resources' || clusterInfoLoading) return
504
+ const current = new URLSearchParams(location.search)
505
+ const onlyCrossView = [...current.keys()].every(key => (CROSS_VIEW_PARAMS as readonly string[]).includes(key))
506
+ const destination = new URL((onlyCrossView && kindFilterStore?.lastKind()) || '/resources/pods', 'http://radar.invalid')
507
+ current.forEach((value, key) => destination.searchParams.set(key, value))
489
508
  navigate(
490
- { pathname: '/resources/pods', search: location.search, hash: location.hash },
509
+ { pathname: destination.pathname, search: destination.search, hash: location.hash },
491
510
  { replace: true },
492
511
  )
493
- }, [location.pathname, location.search, location.hash, navigate])
512
+ }, [location.pathname, location.search, location.hash, navigate, kindFilterStore, clusterInfoLoading])
494
513
 
495
514
  // Set mainView by navigating to the path
496
515
  const setMainView = useCallback((view: ExtendedMainView, params?: Record<string, string>) => {
@@ -508,6 +527,15 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
508
527
  }
509
528
  }
510
529
 
530
+ // Explicit params are a link with its own state; otherwise go back to where the user left this section.
531
+ const remembered = params
532
+ ? undefined
533
+ : viewMemory?.sectionPath(view) ?? (view === 'resources' ? kindFilterStore?.lastKind() : undefined)
534
+ if (remembered) {
535
+ navigate(withCrossViewParams(remembered, location.search))
536
+ return
537
+ }
538
+
511
539
  const path = view === 'home' ? '/' : `/${view}`
512
540
 
513
541
  // Start fresh — keep only cross-view params, discard view-specific ones.
@@ -532,7 +560,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
532
560
  }
533
561
 
534
562
  navigate({ pathname: path, search: newParams.toString() })
535
- }, [location.search, navigate, takeover, goHost])
563
+ }, [location.search, navigate, takeover, goHost, viewMemory, kindFilterStore])
536
564
 
537
565
  const whatsNewStatus = useWhatsNewStatus()
538
566
 
@@ -637,6 +665,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
637
665
 
638
666
  // Diagnostics overlay state
639
667
  const [showDiagnostics, setShowDiagnostics] = useState(false)
668
+ const [diagnosticsFocus, setDiagnosticsFocus] = useState<DiagnosticsFocus>()
640
669
 
641
670
  // The peek drawer "expanded" into a fullscreen overlay = ?full=1 with a selected
642
671
  // resource, on ANY view (resources list, topology graph, GitOps, Applications…) —
@@ -953,7 +982,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
953
982
  category: 'General' as const,
954
983
  scope: 'global' as const,
955
984
  allowInInputs: true,
956
- handler: () => setShowDiagnostics(prev => !prev),
985
+ handler: () => { setDiagnosticsFocus(undefined); setShowDiagnostics(prev => !prev) },
957
986
  },
958
987
  // Settings exposes local-binary controls that don't apply to embedded hosts.
959
988
  // Register the shortcut only when standalone (matching the gear button) —
@@ -1069,12 +1098,13 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1069
1098
  // update that arrived *after* the fast structural flush already ran.
1070
1099
  const fastInvalidationRef = useRef<{
1071
1100
  changedKinds: Set<string> // every changed kind (any op) → detail drawer
1101
+ changedObjects: Set<string> // resourceEventKey of every event → detail views that got a 404
1072
1102
  structuralKinds: Set<string> // add/delete kinds → list membership + counts + dashboard
1073
1103
  environmentNamespaces: Set<string>
1074
1104
  environmentPods: Map<string, Set<string>>
1075
1105
  secretsChanged: boolean
1076
1106
  timer: number | null
1077
- }>({ changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null })
1107
+ }>({ changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null })
1078
1108
  const cnpgInvalidationPendingRef = useRef(false)
1079
1109
  const slowInvalidationRef = useRef<{
1080
1110
  updatedKinds: Set<string> // update-only churn → throttled list + dashboard
@@ -1111,6 +1141,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1111
1141
 
1112
1142
  const fast = fastInvalidationRef.current
1113
1143
  fast.changedKinds.add(kind)
1144
+ fast.changedObjects.add(resourceEventKey(kind, event.namespace ?? '', event.name))
1114
1145
  if (structural) fast.structuralKinds.add(kind)
1115
1146
  if (kind === 'secrets') fast.secretsChanged = true
1116
1147
  if ((kind === 'configmaps' || kind === 'secrets') && event.namespace) fast.environmentNamespaces.add(event.namespace)
@@ -1128,7 +1159,12 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1128
1159
  fast.timer = window.setTimeout(() => {
1129
1160
  const f = fastInvalidationRef.current
1130
1161
  for (const k of f.changedKinds) {
1131
- queryClient.invalidateQueries({ queryKey: ['resource', k] }) // open detail drawer stays live
1162
+ // Open detail drawers stay live; a deleted one is left alone unless
1163
+ // an event names it.
1164
+ queryClient.invalidateQueries({
1165
+ queryKey: ['resource', k],
1166
+ predicate: (q) => refetchOnResourceEvents(q, k, f.changedObjects),
1167
+ })
1132
1168
  }
1133
1169
  for (const k of f.structuralKinds) {
1134
1170
  queryClient.invalidateQueries({ queryKey: ['resources', k] }) // list membership changed
@@ -1156,7 +1192,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1156
1192
  cnpgInvalidationPendingRef.current = false
1157
1193
  queryClient.invalidateQueries({ queryKey: ['cnpg', 'workspace'] })
1158
1194
  }
1159
- fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
1195
+ fastInvalidationRef.current = { changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
1160
1196
  }, 3000)
1161
1197
  }
1162
1198
 
@@ -1185,7 +1221,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1185
1221
  if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
1186
1222
  if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
1187
1223
  if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
1188
- fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
1224
+ fastInvalidationRef.current = { changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
1189
1225
  slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
1190
1226
  timelineInvalidationRef.current = { timer: null }
1191
1227
  }, [])
@@ -1198,6 +1234,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1198
1234
  onContextSwitchComplete: endSwitch,
1199
1235
  onContextSwitchProgress: updateProgress,
1200
1236
  onContextChanged: (context) => {
1237
+ resetNavigationMap()
1238
+
1201
1239
  // Clear all React Query caches when cluster context changes
1202
1240
  // This ensures helm releases, resources, etc. are refetched from the new cluster
1203
1241
  // removeQueries clears cached data, invalidateQueries triggers refetch
@@ -1208,7 +1246,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1208
1246
  if (fastInvalidationRef.current.timer !== null) clearTimeout(fastInvalidationRef.current.timer)
1209
1247
  if (slowInvalidationRef.current.timer !== null) clearTimeout(slowInvalidationRef.current.timer)
1210
1248
  if (timelineInvalidationRef.current.timer !== null) clearTimeout(timelineInvalidationRef.current.timer)
1211
- fastInvalidationRef.current = { changedKinds: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
1249
+ fastInvalidationRef.current = { changedKinds: new Set(), changedObjects: new Set(), structuralKinds: new Set(), environmentNamespaces: new Set(), environmentPods: new Map(), secretsChanged: false, timer: null }
1212
1250
  slowInvalidationRef.current = { updatedKinds: new Set(), timer: null }
1213
1251
  timelineInvalidationRef.current = { timer: null }
1214
1252
 
@@ -1241,10 +1279,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1241
1279
  const nextParams = new URLSearchParams()
1242
1280
  const diagnoseRun = new URLSearchParams(location.search).get('ai-run')
1243
1281
  if (diagnoseRun) nextParams.set('ai-run', diagnoseRun)
1244
- // A CNPG detail keeps the context it belongs to, so it can say it is
1245
- // not in the new one instead of loading a same-named object.
1246
- const pinnedCtx = new URLSearchParams(location.search).get('ctx')
1247
- if (pinnedCtx && location.pathname.startsWith('/cnpg/')) nextParams.set('ctx', pinnedCtx)
1282
+ for (const [key, value] of workspaceContextSwitchSearch(location.pathname, new URLSearchParams(location.search))) nextParams.set(key, value)
1248
1283
  navigate(
1249
1284
  { pathname: location.pathname, search: nextParams.toString() },
1250
1285
  { replace: true, state: location.state },
@@ -1425,6 +1460,34 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1425
1460
  { onSuccess: () => setNamespaces([]) },
1426
1461
  )
1427
1462
  }, [namespaceScope?.cacheScoped, namespaces.length, setActiveNamespace])
1463
+ // Same write as the header picker, which then mirrors it into `namespaces`.
1464
+ // The flash shows where that scope lives from now on. The picker owns the
1465
+ // namespace scope from then on, so no kind keeps a Namespace column filter.
1466
+ const [namespacePickerFlash, setNamespacePickerFlash] = useState(0)
1467
+ const applyNamespacesToAllSections = useCallback(
1468
+ (picked: string[]) => setActiveNamespace.mutateAsync({ namespaces: picked }).then(() => {
1469
+ kindFilterStore?.clearColumn('namespace')
1470
+ setNamespacePickerFlash(n => n + 1)
1471
+ }),
1472
+ [setActiveNamespace, kindFilterStore],
1473
+ )
1474
+ const issuesShowNamespace = useMemo(() => {
1475
+ if (namespaceScope?.cacheScoped) {
1476
+ if (!namespaceScope.namespaceRescope) return undefined
1477
+ return {
1478
+ mode: 'switch' as const,
1479
+ show: (ns: string) => { setNamespaces([ns]); setActiveNamespace.mutate({ namespaces: [ns] }) },
1480
+ }
1481
+ }
1482
+ return {
1483
+ mode: 'add' as const,
1484
+ show: (ns: string) => {
1485
+ const next = namespaces.includes(ns) ? namespaces : [...namespaces, ns]
1486
+ setNamespaces(next)
1487
+ setActiveNamespace.mutate({ namespaces: next })
1488
+ },
1489
+ }
1490
+ }, [namespaceScope?.cacheScoped, namespaceScope?.namespaceRescope, namespaces, setActiveNamespace])
1428
1491
  const initialBookmarkReconciledRef = useRef(false)
1429
1492
  const scopeActives = useMemo(() => namespaceScope?.actives ?? [], [namespaceScope?.actives])
1430
1493
  const namespaceScopeKey = useMemo(() => namespaceScope ? [...scopeActives].sort().join(',') : null, [namespaceScope, scopeActives])
@@ -1503,32 +1566,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1503
1566
  if (skipUrlWriteAfterPopRef.current) return
1504
1567
  const currentSearch = window.location.search
1505
1568
  const params = new URLSearchParams(currentSearch)
1506
-
1507
- // Update namespaces param
1508
- if (namespaces.length > 0) {
1509
- params.set('namespaces', namespaces.join(','))
1510
- } else {
1511
- params.delete('namespaces')
1512
- }
1513
- // Remove legacy 'namespace' param if present
1514
- params.delete('namespace')
1515
-
1516
- // Topology-specific params: only set when on topology view, clean up otherwise
1517
- if (mainView === 'topology') {
1518
- if (topologyMode !== 'resources') {
1519
- params.set('mode', topologyMode)
1520
- } else {
1521
- params.delete('mode')
1522
- }
1523
- if (groupingMode !== 'none' && (namespaces.length === 0 || groupingMode !== 'namespace')) {
1524
- params.set('group', groupingMode)
1525
- } else {
1526
- params.delete('group')
1527
- }
1528
- } else {
1529
- params.delete('mode')
1530
- params.delete('group')
1531
- }
1569
+ writeViewParams(params, namespaces, mainView === 'topology' ? { mode: topologyMode, grouping: groupingMode } : null)
1532
1570
 
1533
1571
  // Only update if params actually changed vs current URL
1534
1572
  if (params.toString() !== new URLSearchParams(currentSearch).toString()) {
@@ -1771,7 +1809,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1771
1809
  setVisibleKinds(new Set())
1772
1810
  }, [])
1773
1811
 
1774
- const navActiveView = mainView === 'helmCompare' ? 'helm' : mainView === 'cnpg' ? 'resources' : mainView
1812
+ const navActiveView = mainView === 'helmCompare' ? 'helm' : activeWorkspace?.navView ?? mainView
1775
1813
 
1776
1814
  return (
1777
1815
  <PortForwardProvider>
@@ -1814,7 +1852,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1814
1852
  <ScopePill>
1815
1853
  <ContextSwitcher ref={contextSwitcherRef} variant="segment" />
1816
1854
  <NamespaceSwitcher
1855
+ key={namespacePickerFlash}
1817
1856
  ref={namespaceSwitcherRef}
1857
+ className={namespacePickerFlash ? 'reach-flash' : undefined}
1818
1858
  variant="segment"
1819
1859
  disabled={namespaceFilter.disabled}
1820
1860
  disabledTooltip={namespaceFilter.tooltip}
@@ -2230,7 +2270,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2230
2270
  )}
2231
2271
 
2232
2272
  {/* Resources view */}
2233
- {mainView === 'resources' && (
2273
+ {/* Bare /resources waits for App's redirect to the last kind; mounted, the view would redirect to Pods first. */}
2274
+ {mainView === 'resources' && (location.pathname.replace(/\/+$/, '') || '/') !== '/resources' && (
2234
2275
  <ResourcesView
2235
2276
  namespaces={namespaces}
2236
2277
  selectedResource={routeSelectedResource}
@@ -2238,6 +2279,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2238
2279
  onResourceClickYaml={(res) => navigateToResource(res, 'yaml')}
2239
2280
  onKindChange={() => setSelectedResource(null)}
2240
2281
  onClearNamespaces={clearAllNamespaces}
2282
+ rememberedKindSearch={rememberedKindSearch}
2283
+ onKindFiltersChange={saveKindFilters}
2284
+ // Embedded hosts own their namespace picker, and a cache-scoped
2285
+ // Radar can't take an arbitrary set.
2286
+ onApplyNamespacesToAllSections={namespaceScope?.cacheScoped || !kindFilterStore ? undefined : applyNamespacesToAllSections}
2241
2287
  />
2242
2288
  )}
2243
2289
 
@@ -2257,6 +2303,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2257
2303
  setNamespaces([ns])
2258
2304
  setActiveNamespace.mutate({ namespaces: [ns] })
2259
2305
  }}
2306
+ onOpenDiagnostics={() => { setDiagnosticsFocus('timeline'); setShowDiagnostics(true) }}
2260
2307
  />
2261
2308
  )}
2262
2309
 
@@ -2313,7 +2360,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2313
2360
 
2314
2361
  {/* Traffic view */}
2315
2362
  {!viewsSyncGated && mainView === 'traffic' && (
2316
- <TrafficView namespaces={namespaces} />
2363
+ <TrafficView namespaces={namespaces} onSetNamespaces={(ns) => { setNamespaces(ns); setActiveNamespace.mutate({ namespaces: ns }) }} />
2317
2364
  )}
2318
2365
 
2319
2366
  {/* Cost detail view */}
@@ -2321,12 +2368,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2321
2368
  <CostView namespaces={namespaces} onBack={() => setMainView('home')} onOpenResource={navigateToResource} />
2322
2369
  )}
2323
2370
 
2324
- {!viewsSyncGated && mainView === 'capacity' && (
2325
- <CapacityView onOpenResource={navigateToResource} />
2326
- )}
2327
-
2328
- {!viewsSyncGated && mainView === 'cnpg' && (
2329
- <CNPGView
2371
+ {!viewsSyncGated && activeWorkspace && (
2372
+ <WorkspaceScreen
2373
+ workspace={activeWorkspace}
2330
2374
  namespaces={namespaces}
2331
2375
  selectedResource={routeSelectedResource}
2332
2376
  onOpenResource={navigateToResource}
@@ -2365,6 +2409,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2365
2409
  <IssuesPane
2366
2410
  namespaces={namespaces}
2367
2411
  onNavigateToResource={navigateFromIssue}
2412
+ showNamespace={issuesShowNamespace}
2368
2413
  />
2369
2414
  )}
2370
2415
 
@@ -2403,10 +2448,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2403
2448
  onNavigate={(res) => navigateToResource(res)}
2404
2449
  canCollapseToDrawer={!isMobile}
2405
2450
  onExpand={(res, opts) => {
2406
- const cnpgPlural = cnpgDetailKindFor(res.kind, res.group)
2407
- if (cnpgPlural) {
2451
+ const detailPath = resourceExpandedPath(res, connection.context || undefined, opts?.yaml ? 'yaml' : undefined)
2452
+ if (detailPath) {
2408
2453
  navigate(
2409
- cnpgDetailPath({ plural: cnpgPlural, namespace: res.namespace, name: res.name }, connection.context || undefined, opts?.yaml ? 'yaml' : undefined),
2454
+ detailPath,
2410
2455
  { state: { returnLabel: currentPageLabel(), returnCtx: connection.context } },
2411
2456
  )
2412
2457
  return
@@ -2534,7 +2579,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2534
2579
  {helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
2535
2580
 
2536
2581
  {/* Diagnostics overlay */}
2537
- {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
2582
+ {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} focus={diagnosticsFocus} onClose={() => { setShowDiagnostics(false); setDiagnosticsFocus(undefined) }} />}
2538
2583
 
2539
2584
  {/* Settings dialog — My permissions is rendered inline in its own section */}
2540
2585
  <SettingsDialog
package/src/RadarApp.tsx CHANGED
@@ -46,6 +46,7 @@ import type {
46
46
  import type { ClusterLoadState } from "./types/clusterLoadState";
47
47
  import { TimelineSourceProvider } from "./context/TimelineSource";
48
48
  import type { TimelineSourceConfig } from "./api/timelineSource";
49
+ import { TimelineRetentionNoteProvider } from "./context/TimelineCustomization";
49
50
  import { DiagnoseCustomizationProvider } from "./context/DiagnoseCustomization";
50
51
  import type {
51
52
  RenderDiagnoseAction,
@@ -180,6 +181,12 @@ export interface RadarAppProps {
180
181
  * React hook-order violation). Set it once at mount when possible.
181
182
  */
182
183
  timelineSource?: TimelineSourceConfig;
184
+ /**
185
+ * A note shown above the timeline and the Applications history, saying how
186
+ * far back history goes. Omitted → no note. Set it only when the timeline server
187
+ * actually limits history, or the note misstates what is shown.
188
+ */
189
+ timelineRetentionNote?: React.ReactNode;
183
190
  }
184
191
 
185
192
  // Default QueryClient with the same shape Radar's standalone binary uses.
@@ -236,6 +243,7 @@ export function RadarApp({
236
243
  onClusterLoadStateChange,
237
244
  onInvestigationFocus,
238
245
  timelineSource,
246
+ timelineRetentionNote,
239
247
  }: RadarAppProps): React.ReactElement {
240
248
  // Apply runtime config during render so module-level singletons are set
241
249
  // before children construct URLs. getApiBase() / getAuthHeaders() /
@@ -266,24 +274,26 @@ export function RadarApp({
266
274
  <RadarUpgradeHostProvider value={upgradeHost}>
267
275
  <FilterLocationBridge>
268
276
  <TimelineSourceProvider config={timelineSource}>
269
- <DiagnoseCustomizationProvider
270
- value={renderDiagnoseAction ?? defaultDiagnoseAction}
271
- consentCopy={diagnoseConsent}
272
- renderRunActions={renderInvestigationRunActions}
273
- renderRefusalAction={renderInvestigationRefusalAction}
274
- >
275
- <DiagnoseProvider
276
- browserURLState={router !== "memory"}
277
- forceRouterURLState={router === "memory"}
278
- onFocusedRun={onInvestigationFocus}
277
+ <TimelineRetentionNoteProvider value={timelineRetentionNote}>
278
+ <DiagnoseCustomizationProvider
279
+ value={renderDiagnoseAction ?? defaultDiagnoseAction}
280
+ consentCopy={diagnoseConsent}
281
+ renderRunActions={renderInvestigationRunActions}
282
+ renderRefusalAction={renderInvestigationRefusalAction}
279
283
  >
280
- <App
281
- manageDocumentTitle={manageDocumentTitle}
282
- documentTitleSuffix={documentTitleSuffix}
283
- onClusterLoadStateChange={onClusterLoadStateChange}
284
- />
285
- </DiagnoseProvider>
286
- </DiagnoseCustomizationProvider>
284
+ <DiagnoseProvider
285
+ browserURLState={router !== "memory"}
286
+ forceRouterURLState={router === "memory"}
287
+ onFocusedRun={onInvestigationFocus}
288
+ >
289
+ <App
290
+ manageDocumentTitle={manageDocumentTitle}
291
+ documentTitleSuffix={documentTitleSuffix}
292
+ onClusterLoadStateChange={onClusterLoadStateChange}
293
+ />
294
+ </DiagnoseProvider>
295
+ </DiagnoseCustomizationProvider>
296
+ </TimelineRetentionNoteProvider>
287
297
  </TimelineSourceProvider>
288
298
  </FilterLocationBridge>
289
299
  </RadarUpgradeHostProvider>