@skyhook-io/radar-app 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +125 -87
  4. package/src/api/actions.errorCode.test.ts +40 -0
  5. package/src/api/actions.test.ts +48 -0
  6. package/src/api/actions.ts +79 -0
  7. package/src/api/apiResources.navigation.test.tsx +232 -0
  8. package/src/api/apiResources.ts +46 -3
  9. package/src/api/client.helmSource.test.ts +39 -0
  10. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  11. package/src/api/client.namespaceSwitch.test.ts +107 -0
  12. package/src/api/client.ts +266 -39
  13. package/src/api/cnpg-ha.live.test.ts +140 -0
  14. package/src/api/cnpg-ha.test.ts +78 -0
  15. package/src/api/cnpg-ha.ts +19 -0
  16. package/src/api/cnpg-history.ts +123 -0
  17. package/src/api/cnpg-inspect.ts +102 -0
  18. package/src/api/cnpg-protection.ts +112 -0
  19. package/src/api/cnpg-recovery.test.tsx +60 -0
  20. package/src/api/cnpg-recovery.ts +262 -0
  21. package/src/api/cnpg-sessions.ts +175 -0
  22. package/src/api/cnpg-storage.ts +138 -0
  23. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  24. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  25. package/src/api/cnpg.ts +428 -10
  26. package/src/api/radarFeatures.render.test.tsx +47 -3
  27. package/src/api/radarFeatures.ts +4 -1
  28. package/src/api/reviewedActionMutation.test.tsx +164 -0
  29. package/src/api/reviewedActionMutation.ts +66 -0
  30. package/src/api/timelineSource.test.ts +9 -0
  31. package/src/api/timelineSource.ts +8 -0
  32. package/src/components/audit/AuditView.tsx +9 -0
  33. package/src/components/capacity/CapacityActivity.tsx +3 -31
  34. package/src/components/capacity/CapacityDemand.tsx +4 -36
  35. package/src/components/capacity/CapacityOverview.tsx +217 -56
  36. package/src/components/capacity/CapacityPoolDetail.tsx +6 -55
  37. package/src/components/capacity/CapacityView.test.tsx +323 -0
  38. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  39. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  40. package/src/components/capacity/routes.ts +42 -0
  41. package/src/components/capacity/shared.tsx +11 -129
  42. package/src/components/capacity/workspaceRoute.ts +18 -0
  43. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  44. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  45. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  46. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  47. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  48. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  49. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  50. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  51. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  52. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  53. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  54. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  55. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  56. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  57. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  58. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  59. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  60. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  61. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  62. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  63. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  64. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  65. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  66. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  67. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  68. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  69. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  70. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  71. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  72. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  73. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  74. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  75. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  76. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  77. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  78. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  79. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  80. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  81. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  82. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  83. package/src/components/cnpg/CNPGProtection.tsx +228 -122
  84. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  85. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  86. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  87. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  88. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  89. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  90. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  91. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  92. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  93. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  94. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  95. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  96. package/src/components/cnpg/CNPGView.tsx +34 -21
  97. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  98. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  99. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  100. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  101. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  102. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  103. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  104. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  105. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  106. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  107. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  108. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  109. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  110. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  111. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  112. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  113. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  114. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  115. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  116. package/src/components/cnpg/actions/actionModel.ts +127 -0
  117. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  118. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  119. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  120. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  121. package/src/components/cnpg/archivingRepair.ts +145 -0
  122. package/src/components/cnpg/baseBackup.test.ts +36 -0
  123. package/src/components/cnpg/baseBackup.ts +36 -0
  124. package/src/components/cnpg/blocking.test.ts +59 -0
  125. package/src/components/cnpg/blocking.ts +131 -0
  126. package/src/components/cnpg/coverageText.test.ts +27 -0
  127. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  128. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  129. package/src/components/cnpg/creation/targetModel.ts +149 -0
  130. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  131. package/src/components/cnpg/fleetStatus.ts +43 -0
  132. package/src/components/cnpg/formFields.ts +2 -0
  133. package/src/components/cnpg/grantText.test.tsx +11 -0
  134. package/src/components/cnpg/host.test.tsx +24 -0
  135. package/src/components/cnpg/host.tsx +101 -0
  136. package/src/components/cnpg/inspectModel.test.ts +120 -0
  137. package/src/components/cnpg/inspectModel.ts +140 -0
  138. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  139. package/src/components/cnpg/logicalSlots.ts +29 -0
  140. package/src/components/cnpg/lsn.test.ts +40 -0
  141. package/src/components/cnpg/lsn.ts +43 -0
  142. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  143. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  144. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  145. package/src/components/cnpg/operations/model.test.ts +510 -0
  146. package/src/components/cnpg/operations/model.ts +499 -0
  147. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  148. package/src/components/cnpg/operations/presentation.ts +84 -0
  149. package/src/components/cnpg/operations/store.ts +98 -0
  150. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  151. package/src/components/cnpg/operatorStatus.ts +194 -0
  152. package/src/components/cnpg/paths.test.ts +54 -0
  153. package/src/components/cnpg/paths.ts +47 -4
  154. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  155. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  156. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  157. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  158. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  159. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  160. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  161. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  162. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  163. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  164. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  165. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  166. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  167. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  168. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  169. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  172. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  173. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  174. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  175. package/src/components/cnpg/refresh.test.ts +38 -0
  176. package/src/components/cnpg/refresh.ts +17 -0
  177. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  178. package/src/components/cnpg/routes.test.ts +34 -3
  179. package/src/components/cnpg/routes.ts +60 -48
  180. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  181. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  182. package/src/components/cnpg/runtimeModel.ts +196 -0
  183. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  184. package/src/components/cnpg/runtimeParts.tsx +79 -0
  185. package/src/components/cnpg/shared.test.tsx +59 -0
  186. package/src/components/cnpg/shared.tsx +100 -182
  187. package/src/components/cnpg/storageModel.test.ts +107 -0
  188. package/src/components/cnpg/storageModel.ts +101 -0
  189. package/src/components/cnpg/trendSamples.test.ts +148 -0
  190. package/src/components/cnpg/trendSamples.ts +222 -0
  191. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  192. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  193. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  194. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  195. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  196. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  197. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  198. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  199. package/src/components/cnpg/workspaceRoute.ts +23 -0
  200. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  201. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  202. package/src/components/cost/CostView.test.ts +2 -1
  203. package/src/components/cost/CostView.tsx +0 -2
  204. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  205. package/src/components/cost/errors.ts +3 -1
  206. package/src/components/cost/source.test.ts +10 -0
  207. package/src/components/cost/source.ts +5 -1
  208. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  209. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  210. package/src/components/dock/BottomDock.tsx +2 -0
  211. package/src/components/dock/TerminalTab.test.tsx +17 -0
  212. package/src/components/dock/TerminalTab.tsx +7 -3
  213. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  214. package/src/components/gitops/GitOpsView.tsx +16 -23
  215. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  216. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  217. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  218. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  219. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  220. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  221. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  222. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  223. package/src/components/helm/ValuesViewer.tsx +169 -78
  224. package/src/components/home/ActivitySummary.tsx +2 -1
  225. package/src/components/home/HomeView.tsx +20 -9
  226. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  227. package/src/components/home/TopologyPreview.tsx +2 -2
  228. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  229. package/src/components/issues/IssuesPane.test.ts +65 -1
  230. package/src/components/issues/IssuesPane.tsx +188 -25
  231. package/src/components/issues/uncheckedSources.ts +21 -0
  232. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  233. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  234. package/src/components/resource/PVCUsageBar.tsx +15 -6
  235. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  236. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  237. package/src/components/resources/CompositeRenderer.tsx +4 -4
  238. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  239. package/src/components/resources/ResourcesView.tsx +26 -12
  240. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  241. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  242. package/src/components/settings/SettingsDialog.tsx +28 -15
  243. package/src/components/timeline/TimelineList.tsx +22 -24
  244. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  245. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  246. package/src/components/timeline/TimelineView.tsx +18 -45
  247. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  248. package/src/components/timeline/useStickyError.test.tsx +47 -0
  249. package/src/components/timeline/useStickyError.ts +15 -0
  250. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  251. package/src/components/ui/Markdown.test.tsx +65 -0
  252. package/src/components/ui/Markdown.tsx +144 -127
  253. package/src/components/whats-new/releaseNotes.ts +63 -1
  254. package/src/components/workload/WorkloadView.test.ts +10 -1
  255. package/src/components/workload/WorkloadView.tsx +27 -239
  256. package/src/components/workspace/layout.test.tsx +29 -0
  257. package/src/components/workspace/layout.tsx +259 -0
  258. package/src/components/workspace/table.ts +7 -0
  259. package/src/contexts/CapabilitiesContext.tsx +1 -1
  260. package/src/integrations/resourceHost.test.tsx +55 -0
  261. package/src/integrations/resourceHost.ts +99 -0
  262. package/src/integrations/resourceHosts.test.tsx +141 -0
  263. package/src/integrations/resourceHosts.tsx +119 -0
  264. package/src/integrations/workspaceRoute.ts +20 -0
  265. package/src/integrations/workspaceRoutes.test.ts +40 -0
  266. package/src/integrations/workspaceRoutes.ts +22 -0
  267. package/src/integrations/workspaceScreens.tsx +14 -0
  268. package/src/utils/drawer-trail.ts +35 -0
  269. package/src/utils/kindFilterStore.test.ts +147 -0
  270. package/src/utils/kindFilterStore.ts +91 -0
  271. package/src/utils/navigation.test.ts +13 -1
  272. package/src/utils/navigation.ts +39 -2
  273. package/src/utils/page-links.ts +21 -0
  274. package/src/utils/tabStorage.ts +40 -0
  275. package/src/utils/viewMemory.test.ts +78 -0
  276. package/src/utils/viewMemory.ts +54 -0
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.17.0",
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.17.0",
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, refetchOnResourceEvents, resourceEventKey, 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) —
@@ -1205,6 +1234,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1205
1234
  onContextSwitchComplete: endSwitch,
1206
1235
  onContextSwitchProgress: updateProgress,
1207
1236
  onContextChanged: (context) => {
1237
+ resetNavigationMap()
1238
+
1208
1239
  // Clear all React Query caches when cluster context changes
1209
1240
  // This ensures helm releases, resources, etc. are refetched from the new cluster
1210
1241
  // removeQueries clears cached data, invalidateQueries triggers refetch
@@ -1248,10 +1279,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1248
1279
  const nextParams = new URLSearchParams()
1249
1280
  const diagnoseRun = new URLSearchParams(location.search).get('ai-run')
1250
1281
  if (diagnoseRun) nextParams.set('ai-run', diagnoseRun)
1251
- // A CNPG detail keeps the context it belongs to, so it can say it is
1252
- // not in the new one instead of loading a same-named object.
1253
- const pinnedCtx = new URLSearchParams(location.search).get('ctx')
1254
- 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)
1255
1283
  navigate(
1256
1284
  { pathname: location.pathname, search: nextParams.toString() },
1257
1285
  { replace: true, state: location.state },
@@ -1432,6 +1460,34 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1432
1460
  { onSuccess: () => setNamespaces([]) },
1433
1461
  )
1434
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])
1435
1491
  const initialBookmarkReconciledRef = useRef(false)
1436
1492
  const scopeActives = useMemo(() => namespaceScope?.actives ?? [], [namespaceScope?.actives])
1437
1493
  const namespaceScopeKey = useMemo(() => namespaceScope ? [...scopeActives].sort().join(',') : null, [namespaceScope, scopeActives])
@@ -1510,32 +1566,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1510
1566
  if (skipUrlWriteAfterPopRef.current) return
1511
1567
  const currentSearch = window.location.search
1512
1568
  const params = new URLSearchParams(currentSearch)
1513
-
1514
- // Update namespaces param
1515
- if (namespaces.length > 0) {
1516
- params.set('namespaces', namespaces.join(','))
1517
- } else {
1518
- params.delete('namespaces')
1519
- }
1520
- // Remove legacy 'namespace' param if present
1521
- params.delete('namespace')
1522
-
1523
- // Topology-specific params: only set when on topology view, clean up otherwise
1524
- if (mainView === 'topology') {
1525
- if (topologyMode !== 'resources') {
1526
- params.set('mode', topologyMode)
1527
- } else {
1528
- params.delete('mode')
1529
- }
1530
- if (groupingMode !== 'none' && (namespaces.length === 0 || groupingMode !== 'namespace')) {
1531
- params.set('group', groupingMode)
1532
- } else {
1533
- params.delete('group')
1534
- }
1535
- } else {
1536
- params.delete('mode')
1537
- params.delete('group')
1538
- }
1569
+ writeViewParams(params, namespaces, mainView === 'topology' ? { mode: topologyMode, grouping: groupingMode } : null)
1539
1570
 
1540
1571
  // Only update if params actually changed vs current URL
1541
1572
  if (params.toString() !== new URLSearchParams(currentSearch).toString()) {
@@ -1778,7 +1809,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1778
1809
  setVisibleKinds(new Set())
1779
1810
  }, [])
1780
1811
 
1781
- const navActiveView = mainView === 'helmCompare' ? 'helm' : mainView === 'cnpg' ? 'resources' : mainView
1812
+ const navActiveView = mainView === 'helmCompare' ? 'helm' : activeWorkspace?.navView ?? mainView
1782
1813
 
1783
1814
  return (
1784
1815
  <PortForwardProvider>
@@ -1821,7 +1852,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1821
1852
  <ScopePill>
1822
1853
  <ContextSwitcher ref={contextSwitcherRef} variant="segment" />
1823
1854
  <NamespaceSwitcher
1855
+ key={namespacePickerFlash}
1824
1856
  ref={namespaceSwitcherRef}
1857
+ className={namespacePickerFlash ? 'reach-flash' : undefined}
1825
1858
  variant="segment"
1826
1859
  disabled={namespaceFilter.disabled}
1827
1860
  disabledTooltip={namespaceFilter.tooltip}
@@ -2237,7 +2270,8 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2237
2270
  )}
2238
2271
 
2239
2272
  {/* Resources view */}
2240
- {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' && (
2241
2275
  <ResourcesView
2242
2276
  namespaces={namespaces}
2243
2277
  selectedResource={routeSelectedResource}
@@ -2245,6 +2279,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2245
2279
  onResourceClickYaml={(res) => navigateToResource(res, 'yaml')}
2246
2280
  onKindChange={() => setSelectedResource(null)}
2247
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}
2248
2287
  />
2249
2288
  )}
2250
2289
 
@@ -2264,6 +2303,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2264
2303
  setNamespaces([ns])
2265
2304
  setActiveNamespace.mutate({ namespaces: [ns] })
2266
2305
  }}
2306
+ onOpenDiagnostics={() => { setDiagnosticsFocus('timeline'); setShowDiagnostics(true) }}
2267
2307
  />
2268
2308
  )}
2269
2309
 
@@ -2328,12 +2368,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2328
2368
  <CostView namespaces={namespaces} onBack={() => setMainView('home')} onOpenResource={navigateToResource} />
2329
2369
  )}
2330
2370
 
2331
- {!viewsSyncGated && mainView === 'capacity' && (
2332
- <CapacityView onOpenResource={navigateToResource} />
2333
- )}
2334
-
2335
- {!viewsSyncGated && mainView === 'cnpg' && (
2336
- <CNPGView
2371
+ {!viewsSyncGated && activeWorkspace && (
2372
+ <WorkspaceScreen
2373
+ workspace={activeWorkspace}
2337
2374
  namespaces={namespaces}
2338
2375
  selectedResource={routeSelectedResource}
2339
2376
  onOpenResource={navigateToResource}
@@ -2372,6 +2409,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2372
2409
  <IssuesPane
2373
2410
  namespaces={namespaces}
2374
2411
  onNavigateToResource={navigateFromIssue}
2412
+ showNamespace={issuesShowNamespace}
2375
2413
  />
2376
2414
  )}
2377
2415
 
@@ -2410,10 +2448,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2410
2448
  onNavigate={(res) => navigateToResource(res)}
2411
2449
  canCollapseToDrawer={!isMobile}
2412
2450
  onExpand={(res, opts) => {
2413
- const cnpgPlural = cnpgDetailKindFor(res.kind, res.group)
2414
- if (cnpgPlural) {
2451
+ const detailPath = resourceExpandedPath(res, connection.context || undefined, opts?.yaml ? 'yaml' : undefined)
2452
+ if (detailPath) {
2415
2453
  navigate(
2416
- cnpgDetailPath({ plural: cnpgPlural, namespace: res.namespace, name: res.name }, connection.context || undefined, opts?.yaml ? 'yaml' : undefined),
2454
+ detailPath,
2417
2455
  { state: { returnLabel: currentPageLabel(), returnCtx: connection.context } },
2418
2456
  )
2419
2457
  return
@@ -2541,7 +2579,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
2541
2579
  {helpOverlay.shouldRender && <ShortcutHelpOverlay isOpen={helpOverlay.isOpen} onClose={() => setShowHelp(false)} currentView={mainView} />}
2542
2580
 
2543
2581
  {/* Diagnostics overlay */}
2544
- {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} onClose={() => setShowDiagnostics(false)} />}
2582
+ {diagnosticsOverlay.shouldRender && <DiagnosticsOverlay isOpen={diagnosticsOverlay.isOpen} focus={diagnosticsFocus} onClose={() => { setShowDiagnostics(false); setDiagnosticsFocus(undefined) }} />}
2545
2583
 
2546
2584
  {/* Settings dialog — My permissions is rendered inline in its own section */}
2547
2585
  <SettingsDialog
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ApiError } from './client'
3
+ import { cnpgActionErrorCode } from './cnpg'
4
+ import { actionCompleted, actionOutcomeLocked } from './actions'
5
+
6
+ describe('cnpgActionErrorCode', () => {
7
+ it('keeps the server code when there is one', () => {
8
+ expect(cnpgActionErrorCode(new ApiError('changed', 409, { code: 'changed' }))).toBe('changed')
9
+ })
10
+ it('treats a lost connection or an apiserver timeout as an unknown outcome', () => {
11
+ expect(cnpgActionErrorCode(new TypeError('Failed to fetch'))).toBe('outcome_unknown')
12
+ expect(cnpgActionErrorCode(new ApiError('timeout', 504))).toBe('outcome_unknown')
13
+ expect(cnpgActionErrorCode(new ApiError('internal', 500))).toBe('outcome_unknown')
14
+ })
15
+ it('treats an uncoded 503 as unknown: a proxy may have forwarded the write before the upstream reset', () => {
16
+ const reset = new ApiError('upstream connect error or disconnect/reset before headers', 503)
17
+ expect(cnpgActionErrorCode(reset)).toBe('outcome_unknown')
18
+ expect(actionOutcomeLocked(reset)).toBe(true)
19
+ })
20
+ it('treats refusals before any write as known failures', () => {
21
+ expect(cnpgActionErrorCode(new ApiError('webhook down', 503, { code: 'operator_webhook_unavailable' }))).toBe('operator_webhook_unavailable')
22
+ expect(actionOutcomeLocked(new ApiError('webhook down', 503, { code: 'operator_webhook_unavailable' }))).toBe(false)
23
+ expect(actionOutcomeLocked(new ApiError('Not connected to cluster', 503, { error: 'Not connected to cluster', code: 'unavailable' }))).toBe(false)
24
+ expect(cnpgActionErrorCode(new ApiError('forbidden', 403))).toBeUndefined()
25
+ expect(cnpgActionErrorCode(null)).toBeUndefined()
26
+ })
27
+ })
28
+
29
+ describe('partial outcomes', () => {
30
+ const partial = new ApiError('stopped', 403, { code: 'partial', completed: ['deleted PVC pg-2', 'deleted PVC pg-2-wal'] })
31
+ it('locks confirm and lists what already happened, whatever the status', () => {
32
+ expect(cnpgActionErrorCode(partial)).toBe('partial')
33
+ expect(actionOutcomeLocked(partial)).toBe(true)
34
+ expect(actionCompleted(partial)).toEqual(['deleted PVC pg-2', 'deleted PVC pg-2-wal'])
35
+ })
36
+ it('leaves known refusals unlocked', () => {
37
+ expect(actionOutcomeLocked(new ApiError('changed', 409, { code: 'changed' }))).toBe(false)
38
+ expect(actionCompleted(new ApiError('changed', 409, { code: 'changed' }))).toEqual([])
39
+ })
40
+ })
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { capabilityReason, actionErrorCode, actionOutcomeLocked, actionDialogResult } from './actions'
3
+ import { ApiError } from './client'
4
+ import { RadarFeatureUnsupportedError } from './radarFeatures'
5
+
6
+ describe('capabilityReason', () => {
7
+ it('says nothing for an allowed action, and the server reason when there is one', () => {
8
+ expect(capabilityReason({ allowed: true, permission: 'allowed' })).toBeUndefined()
9
+ expect(capabilityReason({ allowed: false, permission: 'allowed', reason: 'The Cluster is hibernated' })).toBe('The Cluster is hibernated')
10
+ })
11
+ it('names the missing grant when the reason is absent', () => {
12
+ expect(capabilityReason({ allowed: false, permission: 'denied', grant: { verb: 'create', group: 'postgresql.cnpg.io', resource: 'backups', namespace: 'db' } })).toBe(
13
+ 'Your account may not do this (create backups (postgresql.cnpg.io) in namespace db)',
14
+ )
15
+ expect(capabilityReason({ allowed: false, permission: 'unknown' })).toBe('Not available')
16
+ })
17
+ })
18
+
19
+ it('does not call a pre-request feature refusal an unknown write outcome', () => {
20
+ const error = new RadarFeatureUnsupportedError('cnpgWorkspace', {})
21
+ expect(actionErrorCode(error)).toBeUndefined()
22
+ expect(actionOutcomeLocked(error)).toBe(false)
23
+ expect(actionDialogResult(error)?.state).toBe('error')
24
+ })
25
+
26
+ describe('actionDialogResult', () => {
27
+ it.each([
28
+ [new ApiError('Review changed', 409, { code: 'changed' }), 'error'],
29
+ [new ApiError('upstream connect error or disconnect/reset before headers', 503), 'unknown'],
30
+ [new ApiError('The operator webhook did not answer', 503, { code: 'operator_webhook_unavailable' }), 'error'],
31
+ [new ApiError('Gateway timeout', 504), 'unknown'],
32
+ [new Error('Connection lost'), 'unknown'],
33
+ [new ApiError('Write result missing', 500, { code: 'outcome_unknown' }), 'unknown'],
34
+ [new ApiError('One volume removed', 409, { code: 'partial', completed: ['delete volume'] }), 'partial'],
35
+ ] as const)('keeps the result classification for %s', (error, state) => {
36
+ expect(actionDialogResult(error)?.state).toBe(state)
37
+ expect(actionDialogResult(error)?.message).toBe(error.message)
38
+ })
39
+
40
+ it('selects the heading belonging to the result and retains no empty result', () => {
41
+ const titles = { error: 'Review could not be loaded', unknown: 'The Cluster may have changed', partial: 'Earlier writes partially completed' }
42
+ expect(actionDialogResult(new ApiError('Refused', 409), titles)?.title).toBe(titles.error)
43
+ expect(actionDialogResult(new Error('Connection lost'), titles)?.title).toBe(titles.unknown)
44
+ expect(actionDialogResult(new ApiError('Partial', 409, { code: 'partial' }), titles)?.title).toBe(titles.partial)
45
+ expect(actionDialogResult(new ApiError('Partial', 409, { code: 'partial' }), { unknown: 'May have changed' })?.title).toBeUndefined()
46
+ expect(actionDialogResult(null)).toBeUndefined()
47
+ })
48
+ })