@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,262 @@
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import type { Grant } from '@skyhook-io/k8s-ui'
3
+ import { apiFetch, ApiError, fetchJSON, useRadarFeature } from './client'
4
+ import { actionErrorCode, type ActionCapability } from './actions'
5
+ import { useReviewedActionMutation } from './reviewedActionMutation'
6
+ import { apiUrl } from './config'
7
+ import { shouldRetryRadarQuery } from './radarFeatures'
8
+
9
+ export type CNPGReadState = 'ok' | 'denied' | 'notFound' | 'error' | 'skipped' | 'partial'
10
+
11
+ export interface CNPGReadCoverage {
12
+ state: CNPGReadState
13
+ grant?: Grant
14
+ reason?: string
15
+ }
16
+
17
+ export interface CNPGContainerState {
18
+ name: string
19
+ state: 'waiting' | 'running' | 'terminated' | 'unknown'
20
+ reason?: string
21
+ message?: string
22
+ exitCode?: number
23
+ restarts: number
24
+ ready: boolean
25
+ }
26
+
27
+ export interface CNPGRecoveryPod {
28
+ name: string
29
+ uid: string
30
+ kind: 'job' | 'instance'
31
+ job?: string
32
+ ownerVerified: boolean
33
+ phase: string
34
+ ready: boolean
35
+ startedAt?: string
36
+ initContainers: CNPGContainerState[]
37
+ containers: CNPGContainerState[]
38
+ }
39
+
40
+ export interface CNPGRecoveryJob {
41
+ name: string
42
+ /** The Job's cnpg.io/jobRole label. */
43
+ role?: string
44
+ active: number
45
+ succeeded: number
46
+ failed: number
47
+ complete: boolean
48
+ failedWith?: string
49
+ startedAt?: string
50
+ completedAt?: string
51
+ }
52
+
53
+ export interface CNPGRecoveryEvent {
54
+ type: string
55
+ reason: string
56
+ message: string
57
+ kind: string
58
+ name: string
59
+ count: number
60
+ lastSeen?: string
61
+ }
62
+
63
+ export interface CNPGRestoreValidationRef {
64
+ namespace: string
65
+ name: string
66
+ uid?: string
67
+ verified: boolean
68
+ }
69
+
70
+ export interface CNPGRestoreValidation {
71
+ version: number
72
+ recordedAt: string
73
+ recordedBy?: string
74
+ checked: string
75
+ targetTime?: string
76
+ source?: CNPGRestoreValidationRef
77
+ target: CNPGRestoreValidationRef
78
+ }
79
+
80
+ export interface CNPGRecoverySpec {
81
+ sourceKind: 'objectStore' | 'barmanObjectStore' | 'backup' | 'volumeSnapshots' | 'unknown'
82
+ source?: string
83
+ objectStore?: string
84
+ serverName?: string
85
+ backup?: string
86
+ target?: Record<string, unknown>
87
+ }
88
+
89
+ export interface CNPGRecoveryResponse {
90
+ cluster: {
91
+ uid: string
92
+ phase?: string
93
+ phaseReason?: string
94
+ instances: number | null
95
+ readyInstances: number | null
96
+ currentPrimary?: string
97
+ createdAt?: string
98
+ ready?: { status: string; reason?: string; message?: string }
99
+ }
100
+ recovery: CNPGRecoverySpec | null
101
+ pods: CNPGRecoveryPod[]
102
+ jobs: CNPGRecoveryJob[]
103
+ events: CNPGRecoveryEvent[]
104
+ coverage: Record<'pods' | 'jobs' | 'events', CNPGReadCoverage>
105
+ validation?: CNPGRestoreValidation
106
+ validationError?: string
107
+ capturedAt: string
108
+ }
109
+
110
+ const clusterPath = (namespace: string, name: string) => `/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`
111
+
112
+ // /api/cnpg/clusters/{ns}/{name}/recovery — the restored Cluster's progress:
113
+ // phase, the recovery Job's Pods and their init containers, the instances and
114
+ // Warning events about them. Polled every 5s until the Cluster reports healthy.
115
+ export function useCNPGRecovery(namespace: string, name: string, options?: { enabled?: boolean }) {
116
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
117
+ return useQuery<CNPGRecoveryResponse>({
118
+ queryKey: ['cnpg', 'recovery', namespace, name, ...gatedKey],
119
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGRecoveryResponse>(`${clusterPath(namespace, name)}/recovery`, signal)),
120
+ enabled: options?.enabled ?? true,
121
+ staleTime: 3_000,
122
+ refetchInterval: (query) => {
123
+ const d = query.state.data
124
+ return d?.recovery && d.cluster.phase !== 'Cluster in healthy state' ? 5_000 : 30_000
125
+ },
126
+ retry: (count, err) => shouldRetryRadarQuery(count, err) && !(err instanceof ApiError && (err.status === 403 || err.status === 404)) && count < 2,
127
+ })
128
+ }
129
+
130
+ export interface CNPGRestoreValidationRequest {
131
+ reviewedContext: string
132
+ uid: string
133
+ params: { checked: string; targetTime?: string; source?: { namespace: string; name: string } }
134
+ }
135
+
136
+ export function useRecordCNPGRestoreValidation(namespace: string, name: string, reviewedContext: string | undefined) {
137
+ const { guard } = useRadarFeature('cnpgWorkspace')
138
+ const queryClient = useQueryClient()
139
+ // The note does not change operational review facts, so its outcome lock is independent.
140
+ return useReviewedActionMutation<CNPGRestoreValidation, { request: CNPGRestoreValidationRequest }>(apiUrl(`${clusterPath(namespace, name)}/restore-validation`), reviewedContext, {
141
+ mutationFn: ({ request }) => guard(() =>
142
+ fetchJSON<CNPGRestoreValidation>(`${clusterPath(namespace, name)}/restore-validation`, {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify(request),
146
+ })),
147
+ onSuccess: () => {
148
+ queryClient.invalidateQueries({ queryKey: ['cnpg', 'recovery', namespace, name] })
149
+ queryClient.invalidateQueries({ queryKey: ['cnpg', 'workspace'] })
150
+ },
151
+ onError: (err) => {
152
+ if (actionErrorCode(err) !== undefined) {
153
+ queryClient.invalidateQueries({ queryKey: ['cnpg', 'recovery', namespace, name] })
154
+ queryClient.invalidateQueries({ queryKey: ['cnpg', 'workspace'] })
155
+ }
156
+ },
157
+ meta: { successMessage: 'Validation note recorded' },
158
+ })
159
+ }
160
+
161
+ export interface CNPGReportOptions {
162
+ logs: boolean
163
+ queryText: boolean
164
+ tailLines: number
165
+ }
166
+
167
+ /** Fetches the report zip as the caller; returns the blob and its file name. */
168
+ export async function downloadCNPGReport(namespace: string, name: string, opts: CNPGReportOptions): Promise<{ blob: Blob; filename: string }> {
169
+ const q = new URLSearchParams()
170
+ if (opts.logs) {
171
+ q.set('logs', 'true')
172
+ q.set('tailLines', String(opts.tailLines))
173
+ if (opts.queryText) q.set('queryText', 'true')
174
+ }
175
+ const qs = q.toString()
176
+ const res = await apiFetch(apiUrl(`${clusterPath(namespace, name)}/report${qs ? `?${qs}` : ''}`))
177
+ if (!res.ok) {
178
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
179
+ throw new ApiError(err.error || `HTTP ${res.status}`, res.status, err)
180
+ }
181
+ const disposition = res.headers.get('Content-Disposition') ?? ''
182
+ const match = /filename="([^"]+)"/.exec(disposition)
183
+ return { blob: await res.blob(), filename: match?.[1] ?? `report_cluster_${name}.zip` }
184
+ }
185
+
186
+ // Operator diagnosis — additive fields on /api/cnpg/operator.
187
+
188
+ export interface CNPGOperatorPod {
189
+ name: string
190
+ uid: string
191
+ phase: string
192
+ ready: boolean
193
+ startedAt?: string
194
+ restarts: number
195
+ leader: boolean
196
+ lastTermination?: { container: string; reason: string; exitCode: number; finishedAt?: string }
197
+ }
198
+
199
+ export interface CNPGOperatorLeader extends Omit<CNPGReadCoverage, 'state'> {
200
+ state: CNPGReadState | 'disabled'
201
+ lease?: string
202
+ holder?: string
203
+ holderPod?: string
204
+ holderIsCurrentPod: boolean
205
+ renewTime?: string
206
+ acquireTime?: string
207
+ leaseDurationSeconds?: number
208
+ transitions?: number
209
+ stale: boolean
210
+ }
211
+
212
+ export interface CNPGOperatorWebhookConfig extends CNPGReadCoverage {
213
+ kind: string
214
+ name: string
215
+ webhooks: { name: string; failurePolicy: string; caBundleSet: boolean; service?: string; url: boolean }[]
216
+ }
217
+
218
+ export interface CNPGOperatorWebhookService extends CNPGReadCoverage {
219
+ namespace: string
220
+ name: string
221
+ readyEndpoints: number | null
222
+ notReadyEndpoints: number | null
223
+ }
224
+
225
+ export interface CNPGOperatorReconcilePod {
226
+ pod: string
227
+ leader: boolean
228
+ startedAt?: string
229
+ state: 'ok' | 'partial' | 'denied' | 'unreachable' | 'error'
230
+ error?: string
231
+ reason?: string
232
+ capturedAt?: string
233
+ controllers: { controller: string; errors: number | null; total: number | null; results: Record<string, number> }[]
234
+ }
235
+
236
+ export interface CNPGOperatorDiagnosis {
237
+ namespace: string
238
+ deployment: string
239
+ pods: CNPGOperatorPod[]
240
+ podCoverage: CNPGReadCoverage
241
+ leader: CNPGOperatorLeader
242
+ watch: { all: boolean; namespaces: string[]; source: string; unresolved?: string }
243
+ webhooks: CNPGOperatorWebhookConfig[]
244
+ webhookServices: CNPGOperatorWebhookService[]
245
+ metricsPort: number
246
+ reconcile: CNPGOperatorReconcilePod[]
247
+ events: CNPGReadCoverage & { items: CNPGRecoveryEvent[] }
248
+ }
249
+
250
+ // /api/cnpg/restore/capability — whether the caller may create the restored
251
+ // Cluster in `namespace` (create clusters, and the operator's webhook admits
252
+ // writes), for every way into the restore dialog.
253
+ export function useCNPGRestoreCapability(namespace: string) {
254
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
255
+ return useQuery<ActionCapability>({
256
+ queryKey: ['cnpg', 'restore-capability', namespace, ...gatedKey],
257
+ queryFn: ({ signal }) => guard(() => fetchJSON<ActionCapability>(`/cnpg/restore/capability?namespace=${encodeURIComponent(namespace)}`, signal)),
258
+ enabled: !!namespace,
259
+ staleTime: 15_000,
260
+ retry: false,
261
+ })
262
+ }
@@ -0,0 +1,175 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import type { Grant } from '@skyhook-io/k8s-ui'
3
+ import { fetchJSON, useRadarFeature } from './client'
4
+ import type { CNPGClusterFacts, CNPGRuntimeSourceState } from './cnpg'
5
+ import type { ActionCapability } from './actions'
6
+
7
+ const enc = encodeURIComponent
8
+
9
+ export interface CNPGBackend {
10
+ pid: number
11
+ blockedBy: number[]
12
+ /** PostgreSQL's own backend_start; a cancel or terminate echoes it verbatim. */
13
+ backendStart: string
14
+ state?: string
15
+ waitEventType?: string
16
+ waitEvent?: string
17
+ user?: string
18
+ database?: string
19
+ application?: string
20
+ clientAddr?: string
21
+ backendType?: string
22
+ backendAgeSeconds?: number
23
+ xactAgeSeconds?: number
24
+ queryAgeSeconds?: number
25
+ stateAgeSeconds?: number
26
+ query?: string
27
+ queryTruncated?: boolean
28
+ }
29
+
30
+ export interface CNPGSessionInstance {
31
+ pod: string
32
+ role: 'primary' | 'replica' | 'unknown'
33
+ cpuRequest?: string
34
+ cpuLimit?: string
35
+ memoryRequest?: string
36
+ memoryLimit?: string
37
+ }
38
+
39
+ export interface CNPGSessionsResponse {
40
+ cluster: { namespace: string; name: string; uid: string }
41
+ pod: string
42
+ podUID?: string
43
+ role?: string
44
+ sampledAt: string
45
+ permission: { exec: 'allowed' | 'denied' | 'unknown'; grant?: Grant }
46
+ state: CNPGRuntimeSourceState | 'unavailable'
47
+ error?: string
48
+ reason?: string
49
+ capturedAt?: string
50
+ serverTime?: string
51
+ maxConnections?: number
52
+ superuserReservedConnections?: number
53
+ clientBackends?: number
54
+ involvedTotal?: number
55
+ truncated?: boolean
56
+ /** Only the backends in a blocking relation: blocking another, or waiting on one. */
57
+ sessions?: CNPGBackend[]
58
+ instances: CNPGSessionInstance[]
59
+ }
60
+
61
+ // /api/cnpg/clusters/{ns}/{name}/sessions — who blocks whom on one instance,
62
+ // read with fixed SQL over the caller's pods/exec. Denied is a state, not an error.
63
+ export function useCNPGSessions(namespace: string, name: string, pod?: string, enabled = true) {
64
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
65
+ return useQuery<CNPGSessionsResponse>({
66
+ queryKey: ['cnpg', 'sessions', namespace, name, pod ?? '', ...gatedKey],
67
+ queryFn: ({ signal }) => guard(() =>
68
+ fetchJSON<CNPGSessionsResponse>(`/cnpg/clusters/${enc(namespace)}/${enc(name)}/sessions${pod ? `?pod=${enc(pod)}` : ''}`, signal)),
69
+ enabled: enabled && !!name,
70
+ refetchInterval: (q) => (q.state.data?.state === 'denied' ? false : 10_000),
71
+ refetchIntervalInBackground: false,
72
+ staleTime: 5_000,
73
+ retry: false,
74
+ })
75
+ }
76
+
77
+ export interface CNPGDestroyPVC {
78
+ name: string
79
+ uid: string
80
+ role?: string
81
+ tablespace?: string
82
+ owned: boolean
83
+ detached: boolean
84
+ capacity?: string
85
+ storageClass?: string
86
+ }
87
+
88
+ export interface CNPGDestroyPlan {
89
+ uid: string
90
+ context: string
91
+ facts: CNPGClusterFacts
92
+ pod: string
93
+ podUID: string
94
+ role: string
95
+ pvcsReadable: boolean
96
+ pvcReason?: string
97
+ pvcs: CNPGDestroyPVC[]
98
+ jobsReadable: boolean
99
+ jobs: { name: string; uid: string }[]
100
+ actions: { delete: ActionCapability; keep: ActionCapability }
101
+ }
102
+
103
+ export function useCNPGDestroyPlan(namespace: string, name: string, pod: string, enabled = true) {
104
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
105
+ return useQuery<CNPGDestroyPlan>({
106
+ queryKey: ['cnpg', 'destroy-plan', namespace, name, pod, ...gatedKey],
107
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGDestroyPlan>(`/cnpg/clusters/${enc(namespace)}/${enc(name)}/instances/${enc(pod)}/destroy-plan`, signal)),
108
+ enabled: enabled && !!pod,
109
+ staleTime: 0,
110
+ retry: false,
111
+ })
112
+ }
113
+
114
+ export interface CNPGPoolerFacts {
115
+ generation: number
116
+ cluster: string
117
+ type: string
118
+ instances?: number
119
+ /** Desired state (spec.pgbouncer.paused); each PgBouncer applies it on its own. */
120
+ paused: boolean
121
+ poolMode?: string
122
+ parameters: Record<string, string>
123
+ terminating: boolean
124
+ deployment: {
125
+ name: string
126
+ state: 'ok' | 'missing' | 'unreadable' | 'foreign'
127
+ replicas?: number
128
+ readyReplicas?: number
129
+ updatedReplicas?: number
130
+ availableReplicas?: number
131
+ }
132
+ service: { name: string; state: 'ok' | 'missing' | 'unreadable' | 'foreign'; type?: string; port?: number }
133
+ }
134
+
135
+ export interface CNPGPoolerCapabilities {
136
+ uid: string
137
+ resourceVersion: string
138
+ context: string
139
+ facts: CNPGPoolerFacts
140
+ actions: { pause: ActionCapability; resume: ActionCapability; observeState: ActionCapability }
141
+ }
142
+
143
+ export function useCNPGPoolerCapabilities(namespace: string, name: string, enabled = true) {
144
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
145
+ return useQuery<CNPGPoolerCapabilities>({
146
+ queryKey: ['cnpg', 'capabilities', 'poolers', namespace, name, ...gatedKey],
147
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGPoolerCapabilities>(`/cnpg/poolers/${enc(namespace)}/${enc(name)}/capabilities`, signal)),
148
+ enabled: enabled && !!name,
149
+ staleTime: 5_000,
150
+ refetchInterval: 30_000,
151
+ retry: false,
152
+ })
153
+ }
154
+
155
+ export interface CNPGPgBouncerStateResponse {
156
+ pooler: { namespace: string; name: string; uid: string }
157
+ sampledAt: string
158
+ permission: { exec: 'allowed' | 'denied' | 'unknown'; grant?: Grant }
159
+ pods: { pod: string; state: CNPGRuntimeSourceState; error?: string; paused?: boolean; suspended?: boolean; active?: boolean }[]
160
+ }
161
+
162
+ // /api/cnpg/poolers/{ns}/{name}/pgbouncer-state — each PgBouncer's own SHOW
163
+ // STATE, the only place "paused" is observed rather than requested.
164
+ export function useCNPGPgBouncerState(namespace: string, name: string, enabled = true) {
165
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
166
+ return useQuery<CNPGPgBouncerStateResponse>({
167
+ queryKey: ['cnpg', 'pgbouncer-state', namespace, name, ...gatedKey],
168
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGPgBouncerStateResponse>(`/cnpg/poolers/${enc(namespace)}/${enc(name)}/pgbouncer-state`, signal)),
169
+ enabled: enabled && !!name,
170
+ refetchInterval: (q) => (q.state.data?.permission.exec === 'denied' ? false : 15_000),
171
+ refetchIntervalInBackground: false,
172
+ staleTime: 5_000,
173
+ retry: false,
174
+ })
175
+ }
@@ -0,0 +1,138 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import type { CNPGDiskReading, CNPGMetricIsolation, Grant } from '@skyhook-io/k8s-ui'
3
+ import { fetchJSON, useRadarFeature } from './client'
4
+
5
+ export type CNPGPVCRole = 'PG_DATA' | 'PG_WAL' | 'PG_TABLESPACE'
6
+
7
+ export interface CNPGStorageCoverage {
8
+ state: string
9
+ grant?: Grant
10
+ reason?: string
11
+ /** On Prometheus usage: how the series were tied to this cluster. */
12
+ isolation?: CNPGMetricIsolation
13
+ }
14
+
15
+ export interface CNPGStorageVolumeUsage {
16
+ /** ok | noSeries | invalid | noPrometheus | denied | error | notRead | ambiguous | scopeMismatch */
17
+ state: string
18
+ usedBytes?: number
19
+ capacityBytes?: number
20
+ ratio?: number
21
+ }
22
+
23
+ export interface CNPGStorageVolume {
24
+ claim: string
25
+ role: CNPGPVCRole | string
26
+ tablespace?: string
27
+ phase?: string
28
+ requested?: string
29
+ requestedBytes?: number
30
+ capacity?: string
31
+ capacityBytes?: number
32
+ /** allowVolumeExpansion is absent when the class could not be read. */
33
+ storageClass: { name?: string; allowVolumeExpansion?: boolean; reason?: string }
34
+ resize: { conditions?: { type: string; message?: string; since?: string }[]; allocatedStatus?: string; pending?: boolean }
35
+ /** Which Cluster status list names the claim: healthy | initializing | resizing | dangling | unusable. */
36
+ clusterState?: string
37
+ usage: CNPGStorageVolumeUsage
38
+ }
39
+
40
+ export interface CNPGStorageSource {
41
+ state: string
42
+ error?: string
43
+ reason?: string
44
+ capturedAt?: string
45
+ }
46
+
47
+ export interface CNPGStorageWAL {
48
+ slotInventoryTruncated?: boolean
49
+ status: CNPGStorageSource
50
+ metrics: CNPGStorageSource
51
+ volume?: string
52
+ sizeBytes?: number
53
+ segments?: number
54
+ readyToArchive?: number
55
+ lastArchivedAt?: string
56
+ lastFailedAt?: string
57
+ lastFailedWal?: string
58
+ archivingFailed?: boolean
59
+ slots?: { slot: string; bytes: number }[]
60
+ slotInventory?: { name: string; active?: boolean; retainedBytes?: number }[] | null
61
+ }
62
+
63
+ export interface CNPGStorageInstance {
64
+ name: string
65
+ /** primary | replica | noInstance (a claim whose instance the Cluster no longer lists) | unknown */
66
+ role: string
67
+ volumes: CNPGStorageVolume[]
68
+ wal?: CNPGStorageWAL
69
+ }
70
+
71
+ export interface CNPGStorageTarget {
72
+ role: CNPGPVCRole
73
+ tablespace?: string
74
+ field: string
75
+ declared?: string
76
+ storageClass?: string
77
+ }
78
+
79
+ export interface CNPGStorageFinding {
80
+ severity: 'warning' | 'critical'
81
+ instance: string
82
+ claim: string
83
+ role: string
84
+ tablespace?: string
85
+ ratio: number
86
+ message: string
87
+ }
88
+
89
+ export interface CNPGClusterStorageResponse {
90
+ cluster: { namespace: string; name: string; uid: string }
91
+ sampledAt: string
92
+ volumes: CNPGStorageCoverage
93
+ usage: CNPGStorageCoverage
94
+ usageSource: string
95
+ wal: CNPGStorageCoverage
96
+ expansion: { targets: CNPGStorageTarget[]; resizeInUseVolumes?: boolean }
97
+ instances: CNPGStorageInstance[]
98
+ findings: CNPGStorageFinding[]
99
+ excluded?: { claim: string; reason: string }[]
100
+ }
101
+
102
+ // /api/cnpg/clusters/{ns}/{name}/storage — the Cluster's claims, their
103
+ // measured use and each instance's WAL, every source with its own coverage.
104
+ export function useCNPGClusterStorage(namespace: string, name: string, enabled = true) {
105
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
106
+ return useQuery<CNPGClusterStorageResponse>({
107
+ queryKey: ['cnpg', 'storage', namespace, name, ...gatedKey],
108
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGClusterStorageResponse>(`/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/storage`, signal)),
109
+ enabled: enabled && !!name,
110
+ refetchInterval: 30_000,
111
+ refetchIntervalInBackground: false,
112
+ staleTime: 10_000,
113
+ retry: false,
114
+ placeholderData: (prev) => prev,
115
+ })
116
+ }
117
+
118
+ export interface CNPGFleetDiskResponse {
119
+ sampledAt: string
120
+ source: string
121
+ clusters: CNPGDiskReading[]
122
+ }
123
+
124
+ // /api/cnpg/disk — the fullest measured volume of each visible Cluster. Read
125
+ // with the same namespace set as the workspace so the two join row for row.
126
+ export function useCNPGFleetDisk(namespaces: string[], enabled = true) {
127
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
128
+ const ns = [...namespaces].sort().join(',')
129
+ return useQuery<CNPGFleetDiskResponse>({
130
+ queryKey: ['cnpg', 'disk', ns, ...gatedKey],
131
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGFleetDiskResponse>(`/cnpg/disk${ns ? `?namespaces=${encodeURIComponent(ns)}` : ''}`, signal)),
132
+ enabled,
133
+ staleTime: 20_000,
134
+ refetchInterval: 60_000,
135
+ retry: false,
136
+ placeholderData: (prev) => prev,
137
+ })
138
+ }
@@ -0,0 +1,43 @@
1
+ import { beforeEach, expect, it, vi } from 'vitest'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { fetchJSON } from './client'
4
+ import { useCNPGClusterActivity, useCNPGOperator, useCNPGWorkspace, useCNPGRuntime } from './cnpg'
5
+ import { guardRadarFeature, radarFeatureSupport } from './radarFeatures'
6
+
7
+ vi.mock('@tanstack/react-query', () => ({ useQuery: vi.fn(), useMutation: vi.fn(), useQueryClient: vi.fn() }))
8
+ vi.mock('./client', () => ({
9
+ fetchJSON: vi.fn(),
10
+ useRadarFeature: () => {
11
+ const support = radarFeatureSupport('cnpgWorkspace', { features: { resourceIssues: true } }, 'v1.16.0')
12
+ return { support, gatedKey: ['radar-feature-unsupported'], guard: (request: () => Promise<unknown>) => guardRadarFeature('cnpgWorkspace', support, {}, request) }
13
+ },
14
+ }))
15
+ beforeEach(() => vi.clearAllMocks())
16
+
17
+ it('still reads the shipped workspace, operator and activity on a server without the new flag', async () => {
18
+ const kinds = ['clusters', 'backups', 'scheduledBackups', 'poolers', 'databases', 'publications', 'subscriptions', 'imageCatalogs', 'clusterImageCatalogs', 'objectStores', 'pods']
19
+ const workspace = { installed: true, context: 'test', namespaces: ['db'], coverage: Object.fromEntries(kinds.map(k => [k, { state: 'full' }])), objects: Object.fromEntries(kinds.map(k => [k, []])), issues: [], audit: [], backupsOmitted: 0 }
20
+ vi.mocked(fetchJSON).mockResolvedValue(workspace)
21
+ useCNPGWorkspace(['db'])
22
+ useCNPGOperator()
23
+ useCNPGClusterActivity('db', 'pg')
24
+ const queries = vi.mocked(useQuery).mock.calls.map(([options]) => options)
25
+ for (const query of queries) {
26
+ const fn = query.queryFn
27
+ if (typeof fn !== 'function') throw new Error('Expected a query function')
28
+ await fn({ signal: new AbortController().signal } as any)
29
+ }
30
+ expect(vi.mocked(fetchJSON).mock.calls.map(([path]) => path)).toEqual([
31
+ '/cnpg/workspace?namespaces=db', '/cnpg/operator', expect.stringMatching(/^\/cnpg\/clusters\/db\/pg\/activity\?since=.*&limit=500$/),
32
+ ])
33
+ expect(queries[0].queryKey).toEqual(['cnpg', 'workspace', 'db'])
34
+ })
35
+
36
+ it('refuses the new live-runtime contract before fetching from the same old server', async () => {
37
+ useCNPGRuntime('db', 'pg')
38
+ const query = vi.mocked(useQuery).mock.calls[0][0]
39
+ const fn = query.queryFn
40
+ if (typeof fn !== 'function') throw new Error('Expected a query function')
41
+ await expect(fn({ signal: new AbortController().signal } as any)).rejects.toMatchObject({ feature: 'cnpgWorkspace' })
42
+ expect(fetchJSON).not.toHaveBeenCalled()
43
+ })
@@ -0,0 +1,15 @@
1
+ import { expect, it, vi } from 'vitest'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { useCNPGPoolerRuntime } from './cnpg'
4
+
5
+ vi.mock('@tanstack/react-query', () => ({ useQuery: vi.fn(), useMutation: vi.fn(), useQueryClient: vi.fn() }))
6
+ vi.mock('./client', () => ({ fetchJSON: vi.fn(), useRadarFeature: () => ({ guard: (f: () => unknown) => f(), gatedKey: [] }) }))
7
+
8
+ it('keeps the scheduling inventory fresh even when live proxy measurements are denied', () => {
9
+ useCNPGPoolerRuntime('prod', 'pooler')
10
+ const options = vi.mocked(useQuery).mock.calls[0][0]
11
+ const interval = options.refetchInterval
12
+ const deniedQuery = { state: { data: { permission: { proxy: 'denied' }, pods: [{ pod: 'pending', schedulingReason: 'Unschedulable' }] } } }
13
+ expect(typeof interval === 'function' ? interval(deniedQuery as any) : interval).toBe(30000)
14
+ expect(options.refetchIntervalInBackground).toBe(false)
15
+ })