@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
@@ -0,0 +1,119 @@
1
+ import { useMemo } from 'react'
2
+ import { useLocation } from 'react-router-dom'
3
+ import { PaneLoader, type WorkloadExtraTab } from '@skyhook-io/k8s-ui'
4
+ import { useRadarFeature } from '../api/client'
5
+ import type { RenderDiagnoseAction } from '../context/DiagnoseCustomization'
6
+ import { apiVersionToGroup } from '../utils/navigation'
7
+ import type { SelectedResource } from '../types'
8
+ import { cnpgResourceHost } from '../components/cnpg/host'
9
+ import {
10
+ composeDetailSlots, composeResourceHosts, hostsForResource, hostSupport, resourceHostForSlot,
11
+ type DetailSlots, type HostResource, type HostFeatures, type HostKind, type KindListProps,
12
+ type ResourceHost,
13
+ } from './resourceHost'
14
+ import { RayJobRenderer } from '../components/resources/renderers/RayJobRenderer'
15
+ import { JobRenderer, JobSetRenderer } from '../components/resources/renderers/JobAdmissionRenderers'
16
+ import { RayClusterRenderer } from '../components/resources/renderers/RayClusterRenderer'
17
+ import { RayServiceRenderer } from '../components/resources/renderers/RayServiceRenderer'
18
+ import { KueueWorkloadRenderer } from '../components/resources/renderers/KueueWorkloadRenderer'
19
+ import { PodRenderer } from '../components/resources/renderers/PodRenderer'
20
+ import { KarpenterNodePoolRenderer } from '../components/resources/renderers/KarpenterNodePoolRenderer'
21
+ import { NodeRenderer } from '../components/resources/renderers/NodeRenderer'
22
+ import { ServiceRenderer } from '../components/resources/renderers/ServiceRenderer'
23
+ import { WorkloadRenderer } from '../components/resources/renderers/WorkloadRenderer'
24
+ import { CompositeRenderer } from '../components/resources/CompositeRenderer'
25
+ import { ServiceAccountRenderer } from '../components/resources/renderers/ServiceAccountRenderer'
26
+ import { RoleRenderer } from '../components/resources/renderers/RoleRenderer'
27
+ import { RoleBindingRenderer } from '../components/resources/renderers/RoleBindingRenderer'
28
+ import { NamespaceRenderer } from '../components/resources/renderers/NamespaceRenderer'
29
+ import { CAPIClusterRenderer } from '../components/resources/renderers/CAPIClusterRenderer'
30
+ import { HPARenderer } from '../components/resources/renderers/HPARenderer'
31
+ import { PVCRenderer } from '../components/resources/renderers/PVCRenderer'
32
+ import { RolloutRenderer } from '../components/resources/renderers/RolloutRenderer'
33
+ import { KyvernoPolicyCoverage } from '../components/resources/renderers/KyvernoPolicyCoverage'
34
+ import { KyvernoPolicyQueued } from '../components/resources/renderers/KyvernoPolicyQueued'
35
+ import { VeleroBSLRenderer } from '../components/resources/renderers/VeleroBSLRenderer'
36
+ import { VeleroBackupRenderer } from '../components/resources/renderers/VeleroBackupRenderer'
37
+ import { VeleroRestoreRenderer } from '../components/resources/renderers/VeleroRestoreRenderer'
38
+
39
+ export const resourceHosts: readonly ResourceHost[] = [
40
+ { id: 'kubernetes', renderers: { PodRenderer, NodeRenderer, ServiceRenderer, WorkloadRenderer, HPARenderer, PVCRenderer } },
41
+ { id: 'rbac', renderers: { ServiceAccountRenderer, RoleRenderer, RoleBindingRenderer, NamespaceRenderer } },
42
+ { id: 'batch-and-admission', renderers: { JobRenderer, JobSetRenderer, KueueWorkloadRenderer } },
43
+ { id: 'ray', renderers: { RayJobRenderer, RayServiceRenderer, RayClusterRenderer } },
44
+ { id: 'capacity', renderers: { KarpenterNodePoolRenderer } },
45
+ { id: 'capi', renderers: { CAPIClusterRenderer } },
46
+ { id: 'crossplane', renderers: { CompositeRenderer } },
47
+ { id: 'rollouts', renderers: { RolloutRenderer } },
48
+ { id: 'kyverno', renderers: { KyvernoPolicyCoverage, KyvernoPolicyQueued } },
49
+ { id: 'velero', renderers: { VeleroBSLRenderer, VeleroBackupRenderer, VeleroRestoreRenderer } },
50
+ cnpgResourceHost,
51
+ ]
52
+
53
+ export const resourceRendererOverrides = composeResourceHosts(resourceHosts)
54
+
55
+ export function resourceExpandedPath(resource: HostResource, context?: string, tab?: 'yaml'): string | null {
56
+ return resourceHostForSlot(resourceHosts, resource, 'destination')?.expandedPath?.(resource, context, tab) ?? null
57
+ }
58
+
59
+ export function resourceDetailRedirect(resource: HostResource, search: URLSearchParams): string | null {
60
+ return resourceHostForSlot(resourceHosts, resource, 'destination')?.detailRedirect?.(resource, search) ?? null
61
+ }
62
+
63
+ export function resourceDetailSlots(resource: HostResource, inheritedTabs?: WorkloadExtraTab[]): DetailSlots {
64
+ const slots = hostsForResource(resourceHosts, resource)
65
+ .map(host => ({ id: host.id, slots: host.detailSlots?.(resource) }))
66
+ return composeDetailSlots(slots, inheritedTabs)
67
+ }
68
+
69
+ export function resourceLogs(resource: HostResource, data: { apiVersion?: string } | null | undefined) {
70
+ const observed = { ...resource, group: data?.apiVersion ? apiVersionToGroup(data.apiVersion) : resource.group }
71
+ const host = resourceHostForSlot(resourceHosts, observed, 'logs')
72
+ return host?.logs?.(observed, data) ?? null
73
+ }
74
+
75
+ export function ResourceDrawerNavigation({ resource }: { resource: SelectedResource }) {
76
+ const { pathname } = useLocation()
77
+ return <>{resourceHosts.map(host => {
78
+ const navigation = host.drawerNavigation
79
+ if (!navigation?.ownsPath(pathname)) return null
80
+ const Component = navigation.Component
81
+ return <Component key={host.id} resource={resource} />
82
+ })}</>
83
+ }
84
+
85
+ export function useResourceHostFeatures(): HostFeatures {
86
+ // Hook order is fixed here; selected adapters mount components that own their data hooks.
87
+ const { support } = useRadarFeature('cnpgWorkspace')
88
+ return useMemo(() => ({ cnpgWorkspace: support }), [support])
89
+ }
90
+
91
+
92
+ export function decorateResourceDiagnose(render: RenderDiagnoseAction | undefined): RenderDiagnoseAction | undefined {
93
+ return resourceHosts.reduce((current, host) => host.diagnoseAction?.(current) ?? current, render)
94
+ }
95
+
96
+
97
+ function kindListHost(kind: Pick<HostKind, 'name' | 'group'> | null) {
98
+ return kind ? resourceHosts.flatMap(host => (host.kindLists ?? []).map(list => ({ host, list })))
99
+ .find(({ list }) => list.kind.name === kind.name && list.kind.group === kind.group) : undefined
100
+ }
101
+
102
+ export function resourceKindListMode(kind: Pick<HostKind, 'name' | 'group'> | null, features: HostFeatures, pending: boolean) {
103
+ const contribution = kindListHost(kind)
104
+ return contribution?.list.mode(hostSupport(contribution.host, features), pending) ?? 'table'
105
+ }
106
+
107
+ export function resourceKindListTitle(kind: Pick<HostKind, 'name' | 'group'>): string | null {
108
+ return kindListHost(kind)?.list.title ?? null
109
+ }
110
+
111
+ export function renderResourceKindList(kind: Pick<HostKind, 'name' | 'group'> | null, features: HostFeatures, pending: boolean, props: KindListProps) {
112
+ const contribution = kindListHost(kind)
113
+ if (!contribution) return null
114
+ const mode = contribution.list.mode(hostSupport(contribution.host, features), pending)
115
+ if (mode === 'wait') return <PaneLoader label="Loading…" className="flex-1" />
116
+ if (mode === 'table') return null
117
+ const Component = contribution.list.Component
118
+ return <Component {...props} />
119
+ }
@@ -0,0 +1,20 @@
1
+ import type { SelectedResource } from '../types'
2
+
3
+ export interface WorkspaceScreenProps {
4
+ namespaces: string[]
5
+ selectedResource: SelectedResource | null
6
+ onOpenResource: (resource: SelectedResource) => void
7
+ onCloseResource: () => void
8
+ onClearNamespaces: () => void
9
+ }
10
+
11
+ export interface WorkspaceRoute {
12
+ id: 'cnpg' | 'capacity'
13
+ path: string
14
+ label: string
15
+ navView: 'resources' | 'capacity'
16
+ namespaceScope: 'view' | 'cluster'
17
+ namespaceFilterTooltip?: string
18
+ pageTitle: (pathname: string) => string
19
+ contextSwitchSearch?: (pathname: string, search: URLSearchParams) => URLSearchParams
20
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { workspaceContextSwitchSearch, workspaceForPath, workspaceForView } from './workspaceRoutes'
3
+
4
+ describe('workspace route ownership', () => {
5
+ it('preserves distinct placement, scope, route parsers and title/breadcrumb labels', () => {
6
+ const cnpg = workspaceForPath('/cnpg/backups/db/backup%2F1')!
7
+ expect(cnpg.id).toBe('cnpg')
8
+ expect(cnpg.navView).toBe('resources')
9
+ expect(cnpg.namespaceScope).toBe('view')
10
+ expect(cnpg.pageTitle('/cnpg/backups/db/backup%2F1')).toBe('backup/1')
11
+ expect(cnpg.pageTitle('/cnpg/protection')).toBe('CloudNativePG Backups')
12
+ expect(cnpg.pageTitle('/cnpg/unknown')).toBe('CloudNativePG Clusters')
13
+ const capacity = workspaceForView('capacity')!
14
+ expect(capacity.navView).toBe('capacity')
15
+ expect(capacity.namespaceScope).toBe('cluster')
16
+ expect(capacity.pageTitle('/capacity/pools/pool%2Fone/members')).toBe('pool/one')
17
+ expect(capacity.pageTitle('/capacity/demand')).toBe('Capacity Demand')
18
+ expect(capacity.pageTitle('/capacity/activity')).toBe('Capacity Activity')
19
+ expect(capacity.pageTitle('/capacity/unknown')).toBe('Capacity')
20
+ })
21
+
22
+ it('only owns whole route prefixes inside either OSS or an embedded router basename', () => {
23
+ expect(workspaceForPath('/cnpg')?.id).toBe('cnpg')
24
+ expect(workspaceForPath('/cnpg/')).toBeDefined()
25
+ expect(workspaceForPath('/cnpg-other')).toBeUndefined()
26
+ expect(workspaceForPath('/capacity-other')).toBeUndefined()
27
+ expect(workspaceForPath('/workload/clusters/db/pg')).toBeUndefined()
28
+ // React Router supplies basename-relative locations to the application.
29
+ expect(workspaceForPath('/c/demo/cnpg')).toBeUndefined()
30
+ })
31
+
32
+ it('pins only CNPG detail context on a switch, without adopting another workspace’s query policy', () => {
33
+ const search = new URLSearchParams('ctx=old-cluster&tab=storage&drawer=pod&namespaces=db&ai-run=run')
34
+ expect(workspaceContextSwitchSearch('/cnpg/clusters/db/pg', search).toString()).toBe('ctx=old-cluster')
35
+ expect(workspaceContextSwitchSearch('/cnpg', search).toString()).toBe('')
36
+ expect(workspaceContextSwitchSearch('/capacity/pools/default', search).toString()).toBe('')
37
+ expect(workspaceContextSwitchSearch('/resources/pods', search).toString()).toBe('')
38
+ expect(search.get('tab')).toBe('storage')
39
+ })
40
+ })
@@ -0,0 +1,22 @@
1
+ import { capacityWorkspaceRoute } from '../components/capacity/workspaceRoute'
2
+ import { cnpgWorkspaceRoute } from '../components/cnpg/workspaceRoute'
3
+ import type { WorkspaceRoute } from './workspaceRoute'
4
+
5
+ export const workspaceRoutes = [cnpgWorkspaceRoute, capacityWorkspaceRoute] as const
6
+
7
+ export const workspaceLabels = {
8
+ cnpg: cnpgWorkspaceRoute.label,
9
+ capacity: capacityWorkspaceRoute.label,
10
+ } satisfies Record<WorkspaceRoute['id'], string>
11
+
12
+ export function workspaceForPath(pathname: string) {
13
+ return workspaceRoutes.find(route => pathname === route.path || pathname.startsWith(`${route.path}/`))
14
+ }
15
+
16
+ export function workspaceForView(view: string) {
17
+ return workspaceRoutes.find(route => route.id === view)
18
+ }
19
+
20
+ export function workspaceContextSwitchSearch(pathname: string, search: URLSearchParams): URLSearchParams {
21
+ return workspaceForPath(pathname)?.contextSwitchSearch?.(pathname, search) ?? new URLSearchParams()
22
+ }
@@ -0,0 +1,14 @@
1
+ import type { ComponentType } from 'react'
2
+ import { CapacityView } from '../components/capacity/CapacityView'
3
+ import { CNPGView } from '../components/cnpg/CNPGView'
4
+ import type { WorkspaceRoute, WorkspaceScreenProps } from './workspaceRoute'
5
+
6
+ const screens: Record<WorkspaceRoute['id'], ComponentType<WorkspaceScreenProps>> = {
7
+ cnpg: CNPGView,
8
+ capacity: ({ onOpenResource }) => <CapacityView onOpenResource={onOpenResource} />,
9
+ }
10
+
11
+ export function WorkspaceScreen({ workspace, ...props }: WorkspaceScreenProps & { workspace: WorkspaceRoute }) {
12
+ const Component = screens[workspace.id]
13
+ return <Component {...props} />
14
+ }
@@ -0,0 +1,35 @@
1
+ import type { SelectedResource } from '../types'
2
+
3
+ // Drawer identity in the URL: kind:group:namespace:name, chained with "~" for
4
+ // the in-drawer trail (last entry is the one shown). Kubernetes names and API
5
+ // groups cannot contain ":" or "~", and the group is mandatory: CRD kinds
6
+ // collide across groups (CloudNativePG's Cluster and Backup with CAPI,
7
+ // KubeBlocks and Velero kinds).
8
+ export function encodeDrawerRef(r: SelectedResource): string {
9
+ return [r.kind, r.group ?? '', r.namespace ?? '', r.name].join(':')
10
+ }
11
+
12
+ export function decodeDrawerRef(s: string): SelectedResource | null {
13
+ const parts = s.split(':')
14
+ if (parts.length !== 4 || !parts[0] || !parts[3]) return null
15
+ return { kind: parts[0], group: parts[1], namespace: parts[2], name: parts[3] }
16
+ }
17
+
18
+ export function decodeDrawerTrail(param: string | null): SelectedResource[] {
19
+ if (!param) return []
20
+ return param.split('~').map(decodeDrawerRef).filter((r): r is SelectedResource => r !== null)
21
+ }
22
+
23
+ export function encodeDrawerTrail(trail: SelectedResource[]): string {
24
+ return trail.map(encodeDrawerRef).join('~')
25
+ }
26
+
27
+ export function sameSelectedResource(a: SelectedResource | null | undefined, b: SelectedResource | null | undefined): boolean {
28
+ if (!a || !b) return false
29
+ return (
30
+ a.kind.toLowerCase() === b.kind.toLowerCase() &&
31
+ (a.group ?? '') === (b.group ?? '') &&
32
+ (a.namespace ?? '') === (b.namespace ?? '') &&
33
+ a.name === b.name
34
+ )
35
+ }
@@ -0,0 +1,147 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { createKindFilterStore } from './kindFilterStore'
3
+
4
+ const storage = new Map<string, string>()
5
+ vi.stubGlobal('localStorage', {
6
+ getItem: (k: string) => storage.get(k) ?? null,
7
+ setItem: (k: string, v: string) => void storage.set(k, v),
8
+ removeItem: (k: string) => void storage.delete(k),
9
+ get length() { return storage.size },
10
+ key: (i: number) => [...storage.keys()][i] ?? null,
11
+ })
12
+ afterEach(() => {
13
+ storage.clear()
14
+ vi.stubGlobal('sessionStorage', undefined)
15
+ })
16
+
17
+ function fakeSessionStorage() {
18
+ const items = new Map<string, string>()
19
+ return {
20
+ getItem: (k: string) => items.get(k) ?? null,
21
+ setItem: (k: string, v: string) => void items.set(k, v),
22
+ get length() { return items.size },
23
+ key: (i: number) => [...items.keys()][i] ?? null,
24
+ }
25
+ }
26
+
27
+ describe('createKindFilterStore', () => {
28
+ it('keeps each cluster and each API group apart', () => {
29
+ const a = createKindFilterStore('/api|ctx-a')
30
+ const b = createKindFilterStore('/api|ctx-b')
31
+ a.set('services', '', 'filters=type%3AClusterIP')
32
+ a.set('services', 'serving.knative.dev', 'problems=failed')
33
+
34
+ expect(a.get('services', '')).toBe('filters=type%3AClusterIP')
35
+ expect(a.get('services', 'serving.knative.dev')).toBe('problems=failed')
36
+ expect(b.get('services', '')).toBeUndefined()
37
+ })
38
+
39
+ it('removes the entry when the filters are cleared', () => {
40
+ const store = createKindFilterStore('/api|ctx')
41
+ store.set('pods', '', 'problems=failed')
42
+ store.set('pods', '', '')
43
+
44
+ expect(store.get('pods', '')).toBeUndefined()
45
+ expect(storage.size).toBe(0)
46
+ })
47
+
48
+ it('remembers the last Resources list per cluster, with its API group only', () => {
49
+ const a = createKindFilterStore('/api|ctx-a')
50
+ a.recordLastKind('/resources/services', '?apiGroup=serving.knative.dev&filters=x&namespaces=n')
51
+ a.recordLastKind('/resources', '')
52
+ a.recordLastKind('/helm', '')
53
+
54
+ expect(a.lastKind()).toBe('/resources/services?apiGroup=serving.knative.dev')
55
+ expect(createKindFilterStore('/api|ctx-b').lastKind()).toBeUndefined()
56
+
57
+ a.recordLastKind('/resources/pods/', '?problems=failed')
58
+ expect(a.lastKind()).toBe('/resources/pods')
59
+ })
60
+
61
+ it('keeps a tab on its own filters while another tab saves different ones', () => {
62
+ const tabA = createKindFilterStore('/api|ctx')
63
+ const tabB = createKindFilterStore('/api|ctx')
64
+ tabA.set('pods', '', 'filters=namespace%3Aa')
65
+ tabB.set('pods', '', 'filters=namespace%3Ab')
66
+
67
+ expect(tabA.get('pods', '')).toBe('filters=namespace%3Aa')
68
+ expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Ab')
69
+
70
+ tabA.set('pods', '', '')
71
+ tabB.set('pods', '', 'filters=namespace%3Ac')
72
+ expect(tabA.get('pods', '')).toBeUndefined()
73
+ })
74
+
75
+ it('keeps what a tab restored when another tab saves later', () => {
76
+ createKindFilterStore('/api|ctx').set('pods', '', 'filters=namespace%3Aa')
77
+ const tabA = createKindFilterStore('/api|ctx')
78
+ expect(tabA.get('pods', '')).toBe('filters=namespace%3Aa')
79
+ expect(tabA.get('services', '')).toBeUndefined()
80
+
81
+ const tabB = createKindFilterStore('/api|ctx')
82
+ tabB.set('pods', '', 'filters=namespace%3Ab')
83
+ tabB.set('services', '', 'filters=type%3AClusterIP')
84
+
85
+ expect(tabA.get('pods', '')).toBe('filters=namespace%3Aa')
86
+ expect(tabA.get('services', '')).toBeUndefined()
87
+ })
88
+
89
+ it('clears one column on every kind of the cluster, in the tab and in storage', () => {
90
+ const tab = createKindFilterStore('/api|ctx')
91
+ tab.set('pods', '', 'filters=namespace%3Aa%7Cstatus%3ARunning&problems=failed')
92
+ tab.set('services', '', 'filters=namespace%3Aexclude%3Ab')
93
+ tab.set('deployments', '', 'problems=failed')
94
+ // Another tab saved this kind; this tab never read it.
95
+ createKindFilterStore('/api|ctx').set('configmaps', '', 'filters=namespace%3Ac')
96
+ const otherCluster = createKindFilterStore('/api|ctx:2')
97
+ otherCluster.set('pods', '', 'filters=namespace%3Aa')
98
+
99
+ tab.clearColumn('namespace')
100
+
101
+ expect(tab.get('pods', '')).toBe('filters=status%3ARunning&problems=failed')
102
+ expect(tab.get('services', '')).toBeUndefined()
103
+ expect(tab.get('deployments', '')).toBe('problems=failed')
104
+ expect(tab.get('configmaps', '')).toBeUndefined()
105
+ const fresh = createKindFilterStore('/api|ctx')
106
+ expect(fresh.get('pods', '')).toBe('filters=status%3ARunning&problems=failed')
107
+ expect(fresh.get('services', '')).toBeUndefined()
108
+ expect(fresh.get('configmaps', '')).toBeUndefined()
109
+ expect(createKindFilterStore('/api|ctx:2').get('pods', '')).toBe('filters=namespace%3Aa')
110
+ })
111
+
112
+ it("keeps the tab's own filters on reload and in a duplicated tab; a new tab starts from the last saved", () => {
113
+ const tabA = fakeSessionStorage()
114
+ vi.stubGlobal('sessionStorage', tabA)
115
+ createKindFilterStore('/api|ctx').set('pods', '', 'filters=namespace%3Aa')
116
+
117
+ // Tab B saves a newer choice.
118
+ vi.stubGlobal('sessionStorage', fakeSessionStorage())
119
+ createKindFilterStore('/api|ctx').set('pods', '', 'filters=namespace%3Ab')
120
+
121
+ vi.stubGlobal('sessionStorage', tabA)
122
+ expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Aa')
123
+
124
+ const duplicate = fakeSessionStorage()
125
+ for (let i = 0; i < tabA.length; i++) duplicate.setItem(tabA.key(i)!, tabA.getItem(tabA.key(i)!)!)
126
+ vi.stubGlobal('sessionStorage', duplicate)
127
+ expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Aa')
128
+
129
+ vi.stubGlobal('sessionStorage', fakeSessionStorage())
130
+ expect(createKindFilterStore('/api|ctx').get('pods', '')).toBe('filters=namespace%3Ab')
131
+ })
132
+
133
+ it('remembers filters in the tab only when storage is unavailable', () => {
134
+ vi.stubGlobal('localStorage', {
135
+ getItem: () => { throw new Error('denied') },
136
+ setItem: () => { throw new Error('denied') },
137
+ removeItem: () => { throw new Error('denied') },
138
+ })
139
+ const store = createKindFilterStore('/api|ctx')
140
+ store.set('pods', '', 'problems=failed')
141
+ store.recordLastKind('/resources/pods', '')
142
+
143
+ expect(store.get('pods', '')).toBe('problems=failed')
144
+ expect(createKindFilterStore('/api|ctx').get('pods', '')).toBeUndefined()
145
+ expect(store.lastKind()).toBeUndefined()
146
+ })
147
+ })
@@ -0,0 +1,91 @@
1
+ import { kindFiltersFromSearch, kindFiltersToSearch } from '../components/resources/resource-utils'
2
+ import { RESOURCE_LIST_PATH } from './navigation'
3
+ import { createTabStorage } from './tabStorage'
4
+
5
+ const FILTERS_PREFIX = 'radar-filters:'
6
+ const LAST_KIND_PREFIX = 'radar-last-kind:'
7
+
8
+ /**
9
+ * Per-cluster Resources filters (one query string per kind) and the last
10
+ * Resources kind, kept in localStorage so they survive reloads and new tabs.
11
+ * Storage can be unavailable (private mode, blocked site data): every access
12
+ * degrades to "nothing remembered".
13
+ *
14
+ * Filters are read from the tab's own copy first (sessionStorage: kept on
15
+ * reload, copied to a duplicated tab), so a tab keeps showing what happened
16
+ * in it while another tab saves different filters; localStorage only seeds a
17
+ * kind the first time the tab reads it.
18
+ */
19
+ export function createKindFilterStore(cluster: string) {
20
+ const filtersKey = (plural: string, group: string) => `${FILTERS_PREFIX}${cluster}:${plural}/${group}`
21
+ const lastKindKey = LAST_KIND_PREFIX + cluster
22
+ const tab = createTabStorage()
23
+
24
+ return {
25
+ get(plural: string, group: string): string | undefined {
26
+ const key = filtersKey(plural, group)
27
+ const own = tab.get(key)
28
+ if (own !== undefined) return own || undefined
29
+ let saved: string | undefined
30
+ try {
31
+ saved = localStorage.getItem(key) ?? undefined
32
+ } catch { /* unavailable: nothing saved */ }
33
+ // What the tab was given is now the tab's own, whatever other tabs save later.
34
+ tab.set(key, saved ?? '')
35
+ return saved
36
+ },
37
+ /** An empty search removes the entry. */
38
+ set(plural: string, group: string, search: string) {
39
+ tab.set(filtersKey(plural, group), search)
40
+ try {
41
+ if (search) localStorage.setItem(filtersKey(plural, group), search)
42
+ else localStorage.removeItem(filtersKey(plural, group))
43
+ } catch { /* ignore */ }
44
+ },
45
+ /** Removes one column's filter from every kind of this cluster, in the tab and in localStorage. */
46
+ clearColumn(column: string) {
47
+ const withoutColumn = (search: string) => {
48
+ const filters = kindFiltersFromSearch(search)
49
+ delete filters.columnFilters[column]
50
+ delete filters.columnFilterExcludes[column]
51
+ return kindFiltersToSearch(filters)
52
+ }
53
+ const prefix = `${FILTERS_PREFIX}${cluster}:`
54
+ // The rest is `<plural>/<group>`, never a colon: guards against a context named `<cluster>:…`.
55
+ const ofCluster = (key: string | null): key is string => !!key?.startsWith(prefix) && !key.slice(prefix.length).includes(':')
56
+ for (const key of tab.keys().filter(ofCluster)) tab.set(key, withoutColumn(tab.get(key) ?? ''))
57
+ try {
58
+ const keys: string[] = []
59
+ for (let i = 0; i < localStorage.length; i++) {
60
+ const key = localStorage.key(i)
61
+ if (ofCluster(key)) keys.push(key)
62
+ }
63
+ for (const key of keys) {
64
+ const next = withoutColumn(localStorage.getItem(key) ?? '')
65
+ if (next) localStorage.setItem(key, next)
66
+ else localStorage.removeItem(key)
67
+ }
68
+ } catch { /* ignore */ }
69
+ },
70
+ /** The last Resources list path (`/resources/<plural>[?apiGroup=…]`). */
71
+ lastKind(): string | undefined {
72
+ try {
73
+ return localStorage.getItem(lastKindKey) ?? undefined
74
+ } catch {
75
+ return undefined
76
+ }
77
+ },
78
+ /** Records a location; only a Resources list path counts. */
79
+ recordLastKind(pathname: string, search: string) {
80
+ const path = pathname.replace(/\/+$/, '')
81
+ if (!RESOURCE_LIST_PATH.test(path)) return
82
+ const group = new URLSearchParams(search).get('apiGroup')
83
+ const value = group ? `${path}?${new URLSearchParams({ apiGroup: group })}` : path
84
+ try {
85
+ localStorage.setItem(lastKindKey, value)
86
+ } catch { /* ignore */ }
87
+ },
88
+ }
89
+ }
90
+
91
+ export type KindFilterStore = ReturnType<typeof createKindFilterStore>
@@ -4,7 +4,7 @@ import {
4
4
  initNavigationMap,
5
5
  resetNavigationMap,
6
6
  } from '@skyhook-io/k8s-ui/utils/navigation'
7
- import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath, withCrossViewParams } from './navigation'
7
+ import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath, withCrossViewParams, writeViewParams } from './navigation'
8
8
 
9
9
  afterEach(resetNavigationMap)
10
10
 
@@ -76,3 +76,15 @@ describe('withCrossViewParams', () => {
76
76
  expect(withCrossViewParams('/helm', '')).toBe('/helm')
77
77
  })
78
78
  })
79
+
80
+ describe('writeViewParams', () => {
81
+ it('keeps topology mode and grouping only on topology', () => {
82
+ const topology = new URLSearchParams('namespace=old&apiGroup=example.com')
83
+ writeViewParams(topology, [], { mode: 'traffic', grouping: 'app' })
84
+ expect(topology.toString()).toBe('apiGroup=example.com&mode=traffic&group=app')
85
+
86
+ const issues = new URLSearchParams('mode=traffic&group=app&kind=Widget&apiGroup=example.com&resource=ns%2Fw')
87
+ writeViewParams(issues, ['ns'], null)
88
+ expect(issues.toString()).toBe('kind=Widget&apiGroup=example.com&resource=ns%2Fw&namespaces=ns')
89
+ })
90
+ })
@@ -1,7 +1,7 @@
1
1
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
2
2
  import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
3
3
  import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
4
- import type { SelectedResource, Topology } from '@skyhook-io/k8s-ui/types/core'
4
+ import type { GroupingMode, SelectedResource, Topology, TopologyMode } from '@skyhook-io/k8s-ui/types/core'
5
5
  import type { SearchHit } from '../api/client'
6
6
 
7
7
  /**
@@ -137,7 +137,44 @@ export function openExternal(url: string): void {
137
137
  // setMainView's cross-view params. A navigation whose search omits
138
138
  // ?namespaces= reads as an empty pick to App's URL sync, which clears the
139
139
  // user's namespace scope.
140
- const CROSS_VIEW_PARAMS = ['namespaces', 'ai-run'] as const
140
+ export const CROSS_VIEW_PARAMS = ['namespaces', 'ai-run'] as const
141
+
142
+ /** A Resources list route (`/resources/<plural>`), as opposed to bare `/resources` or a detail page. */
143
+ export const RESOURCE_LIST_PATH = /^\/resources\/[^/]+$/
144
+
145
+ /**
146
+ * Writes App's view state into the query string. Topology's `mode` and
147
+ * `group` are dropped on every other view, so no other view can use those
148
+ * names for params of its own.
149
+ */
150
+ export function writeViewParams(
151
+ params: URLSearchParams,
152
+ namespaces: string[],
153
+ topology: { mode: TopologyMode; grouping: GroupingMode } | null,
154
+ ): void {
155
+ if (namespaces.length > 0) {
156
+ params.set('namespaces', namespaces.join(','))
157
+ } else {
158
+ params.delete('namespaces')
159
+ }
160
+ // Legacy single-namespace param
161
+ params.delete('namespace')
162
+ if (!topology) {
163
+ params.delete('mode')
164
+ params.delete('group')
165
+ return
166
+ }
167
+ if (topology.mode !== 'resources') {
168
+ params.set('mode', topology.mode)
169
+ } else {
170
+ params.delete('mode')
171
+ }
172
+ if (topology.grouping !== 'none' && (namespaces.length === 0 || topology.grouping !== 'namespace')) {
173
+ params.set('group', topology.grouping)
174
+ } else {
175
+ params.delete('group')
176
+ }
177
+ }
141
178
 
142
179
  /** Carries the current cross-view params onto an in-app path that doesn't set them itself. */
143
180
  export function withCrossViewParams(path: string, currentSearch: string): string {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The label for "← back" on the page a push lands on: the title of the page
3
+ * being left, which Radar keeps in the document title.
4
+ */
5
+ export function currentPageLabel(): string {
6
+ return document.title.replace(/\s*·\s*Radar$/, '') || 'previous page'
7
+ }
8
+
9
+ /**
10
+ * Radar's Issues page narrowed to one subject (it has no link to a single issue).
11
+ * The subject travels as `resource=ns/name`, never `namespace=`: App reads a bare
12
+ * `namespace` as the view filter, and a URL without `namespaces` clears it.
13
+ */
14
+ export function issuesPathForSubject(subject: { kind: string; group?: string; namespace: string; name: string }, viewNamespaces?: string | null): string {
15
+ const params = new URLSearchParams()
16
+ if (viewNamespaces) params.set('namespaces', viewNamespaces)
17
+ params.set('kind', subject.kind)
18
+ if (subject.group) params.set('apiGroup', subject.group)
19
+ params.set('resource', subject.namespace ? `${subject.namespace}/${subject.name}` : subject.name)
20
+ return `/issues?${params}`
21
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Per-tab strings in sessionStorage: they survive a reload and are copied
3
+ * when the tab is duplicated, while a tab opened from scratch starts empty.
4
+ * Without sessionStorage (private mode, blocked site data) they live in
5
+ * memory, so a reload forgets them.
6
+ */
7
+ export function createTabStorage() {
8
+ let session: Storage | undefined
9
+ try {
10
+ session = sessionStorage
11
+ session.getItem('')
12
+ } catch {
13
+ session = undefined
14
+ }
15
+ const memory = new Map<string, string>()
16
+
17
+ return {
18
+ get(key: string): string | undefined {
19
+ return session ? session.getItem(key) ?? undefined : memory.get(key)
20
+ },
21
+ set(key: string, value: string) {
22
+ if (!session) {
23
+ memory.set(key, value)
24
+ return
25
+ }
26
+ try {
27
+ session.setItem(key, value)
28
+ } catch { /* over quota: keep the previous value */ }
29
+ },
30
+ keys(): string[] {
31
+ if (!session) return [...memory.keys()]
32
+ const keys: string[] = []
33
+ for (let i = 0; i < session.length; i++) {
34
+ const key = session.key(i)
35
+ if (key !== null) keys.push(key)
36
+ }
37
+ return keys
38
+ },
39
+ }
40
+ }