@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/src/api/cnpg.ts CHANGED
@@ -1,6 +1,12 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import type { CNPGWorkspaceResponse, TimelineEvent } from '@skyhook-io/k8s-ui'
3
- import { fetchJSON } from './client'
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import type { CNPGKindCoverage, CNPGSchedulePreview, CNPGWorkspaceResponse, Grant, TimelineEvent } from '@skyhook-io/k8s-ui'
3
+ import { fetchJSON, useRadarFeature } from './client'
4
+ import { apiUrl } from './config'
5
+ import { useReviewedActionMutation } from './reviewedActionMutation'
6
+ import { shouldRetryRadarQuery } from './radarFeatures'
7
+ import type { CNPGOperatorDiagnosis } from './cnpg-recovery'
8
+ import type { CNPGPoolerFacts } from './cnpg-sessions'
9
+ import { actionErrorCode, type ActionCapability, type ActionErrorCode, type ActionRequest } from './actions'
4
10
 
5
11
  // /api/cnpg/workspace
6
12
  //
@@ -11,19 +17,18 @@ import { fetchJSON } from './client'
11
17
  export function useCNPGWorkspace(namespaces: string[], options?: { enabled?: boolean }) {
12
18
  const ns = [...namespaces].sort().join(',')
13
19
  return useQuery<CNPGWorkspaceResponse>({
14
- queryKey: ['cnpg', 'workspace', ns],
20
+ queryKey: ['cnpg', 'workspace', ns ],
15
21
  queryFn: ({ signal }) => fetchJSON<CNPGWorkspaceResponse>(`/cnpg/workspace${ns ? `?namespaces=${encodeURIComponent(ns)}` : ''}`, signal),
16
22
  enabled: options?.enabled ?? true,
17
23
  staleTime: 10_000,
18
24
  refetchInterval: 30_000,
19
25
  placeholderData: (prev) => prev,
26
+ retry: shouldRetryRadarQuery,
20
27
  })
21
28
  }
22
29
 
23
- export interface CNPGOperatorCoverage {
24
- state: 'full' | 'partial' | 'denied' | 'syncing' | 'error'
25
- deniedNamespaces?: string[]
26
- }
30
+ /** The operator's Deployments and Services are read with the same per-kind coverage as the workspace. */
31
+ export type CNPGOperatorCoverage = CNPGKindCoverage
27
32
 
28
33
  export interface CNPGOperatorComponent {
29
34
  role: 'operator' | 'plugin'
@@ -34,6 +39,26 @@ export interface CNPGOperatorComponent {
34
39
  version?: string
35
40
  readyReplicas: number | null
36
41
  replicas: number | null
42
+ /** The Deployment's Pods; null when they could not be read (see podCoverage). */
43
+ pods?: CNPGOperatorComponentPod[] | null
44
+ podCoverage?: { state: string; grant?: Grant; reason?: string }
45
+ }
46
+
47
+ export interface CNPGContainerTermination {
48
+ container: string
49
+ reason: string
50
+ exitCode: number
51
+ finishedAt?: string
52
+ }
53
+
54
+ export interface CNPGOperatorComponentPod {
55
+ name: string
56
+ ready: boolean
57
+ /** Restarts across the Pod's containers since the Pod was created. */
58
+ restarts: number
59
+ /** When the current container started. */
60
+ startedAt?: string
61
+ lastTermination?: CNPGContainerTermination
37
62
  }
38
63
 
39
64
  export interface CNPGOperatorConfig {
@@ -51,6 +76,7 @@ export interface CNPGOperatorResponse {
51
76
  coverage: { deployments: CNPGOperatorCoverage; services: CNPGOperatorCoverage }
52
77
  components: CNPGOperatorComponent[]
53
78
  config: CNPGOperatorConfig[]
79
+ diagnosis?: CNPGOperatorDiagnosis[]
54
80
  }
55
81
 
56
82
  // /api/cnpg/operator
@@ -60,11 +86,43 @@ export interface CNPGOperatorResponse {
60
86
  // its own namespace, which users rarely have selected.
61
87
  export function useCNPGOperator(options?: { enabled?: boolean }) {
62
88
  return useQuery<CNPGOperatorResponse>({
63
- queryKey: ['cnpg', 'operator'],
89
+ queryKey: ['cnpg', 'operator' ],
64
90
  queryFn: ({ signal }) => fetchJSON<CNPGOperatorResponse>('/cnpg/operator', signal),
65
91
  enabled: options?.enabled ?? true,
66
92
  staleTime: 30_000,
67
93
  refetchInterval: 60_000,
94
+ retry: shouldRetryRadarQuery,
95
+ })
96
+ }
97
+
98
+ /**
99
+ * Whether the operator watching a namespace is acting on it. While it is not,
100
+ * CNPG status there is whatever the operator last wrote. webhookRejects is
101
+ * null when the webhook state could not be read.
102
+ */
103
+ export interface CNPGOperatorVerdict {
104
+ state: 'reconciling' | 'notReconciling' | 'unknown' | 'notWatched'
105
+ reasons?: string[]
106
+ unknown?: string
107
+ webhookRejects: boolean | null
108
+ webhookReason?: string
109
+ operator?: string
110
+ }
111
+
112
+ // /api/cnpg/operator/status?namespaces=
113
+ //
114
+ // The cheap operator verdict for the fleet and cluster pages, keyed by namespace.
115
+ export function useCNPGOperatorStatus(namespaces: string[], options?: { enabled?: boolean }) {
116
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
117
+ const ns = [...new Set(namespaces)].sort().join(',')
118
+ return useQuery<{ namespaces: Record<string, CNPGOperatorVerdict> }>({
119
+ queryKey: ['cnpg', 'operator-status', ns, ...gatedKey],
120
+ queryFn: ({ signal }) => guard(() => fetchJSON<{ namespaces: Record<string, CNPGOperatorVerdict> }>(`/cnpg/operator/status?namespaces=${encodeURIComponent(ns)}`, signal)),
121
+ enabled: (options?.enabled ?? true) && ns !== '',
122
+ staleTime: 10_000,
123
+ refetchInterval: 30_000,
124
+ placeholderData: (prev) => prev,
125
+ retry: shouldRetryRadarQuery,
68
126
  })
69
127
  }
70
128
 
@@ -81,7 +139,7 @@ export interface CNPGClusterActivityResponse {
81
139
  // attributed to it, including ones since deleted.
82
140
  export function useCNPGClusterActivity(namespace: string, name: string, sinceHours = 24) {
83
141
  return useQuery<CNPGClusterActivityResponse>({
84
- queryKey: ['cnpg', 'activity', namespace, name, sinceHours],
142
+ queryKey: ['cnpg', 'activity', namespace, name, sinceHours ],
85
143
  queryFn: ({ signal }) => {
86
144
  const since = new Date(Date.now() - sinceHours * 3600_000).toISOString()
87
145
  return fetchJSON<CNPGClusterActivityResponse>(
@@ -91,5 +149,365 @@ export function useCNPGClusterActivity(namespace: string, name: string, sinceHou
91
149
  },
92
150
  staleTime: 10_000,
93
151
  refetchInterval: 30_000,
152
+ retry: shouldRetryRadarQuery,
153
+ })
154
+ }
155
+
156
+ export interface CNPGInstanceFact {
157
+ pod: string
158
+ podUID: string
159
+ role: 'primary' | 'standby'
160
+ ready: boolean
161
+ healthy: boolean
162
+ fenced: boolean
163
+ podReadable: boolean
164
+ podExists: boolean
165
+ }
166
+
167
+ export interface CNPGBackupMethod {
168
+ method: 'plugin' | 'volumeSnapshot' | 'barmanObjectStore'
169
+ pluginName?: string
170
+ capability: 'backup' | 'unknown' | 'none'
171
+ reason?: string
172
+ deprecated?: boolean
173
+ }
174
+
175
+ export interface CNPGClusterFacts {
176
+ generation: number
177
+ currentPrimary?: string
178
+ targetPrimary?: string
179
+ phase?: string
180
+ phaseReason?: string
181
+ hibernation: string
182
+ hibernated: boolean
183
+ fencedInstances: { raw: string; all: boolean; instances: string[]; malformed?: boolean }
184
+ instances: CNPGInstanceFact[]
185
+ backupMethods: CNPGBackupMethod[]
186
+ backupTarget?: string
187
+ /** The ContinuousArchiving condition is False. */
188
+ archivingFailing: boolean
189
+ isReplicaCluster: boolean
190
+ maintenance: { declared: boolean; inProgress: boolean; reusePVC: boolean }
191
+ terminating: boolean
192
+ /** A running offline volume-snapshot Backup: the operator fenced its instance and lifts the fence itself. */
193
+ coldSnapshotBackup?: string
194
+ }
195
+
196
+ export type CNPGClusterActionName =
197
+ | 'backup'
198
+ | 'switchover'
199
+ | 'restart'
200
+ | 'restartInstance'
201
+ | 'reload'
202
+ | 'fence'
203
+ | 'unfence'
204
+ | 'hibernate'
205
+ | 'rehydrate'
206
+ | 'setMaintenance'
207
+ | 'unsetMaintenance'
208
+
209
+ export interface CNPGClusterCapabilities {
210
+ uid: string
211
+ resourceVersion: string
212
+ context: string
213
+ facts: CNPGClusterFacts
214
+ /** `restore` is creating a new Cluster in this namespace from this one's backups (`create clusters`). */
215
+ actions: Record<CNPGClusterActionName, ActionCapability> & { psql: ActionCapability; destroyInstance: ActionCapability; restore: ActionCapability }
216
+ instanceActions: Record<string, { restart: ActionCapability; switchoverTarget: ActionCapability; fence: ActionCapability; unfence: ActionCapability; psql: ActionCapability; destroy: ActionCapability }>
217
+ restartPlan?: {
218
+ primaryUpdateStrategy?: string
219
+ primaryUpdateMethod?: string
220
+ steps: { instance: string; role: string; effect: 'recreate' | 'skipped_fenced' | 'switchover' | 'restart' | 'wait_for_user' | 'restart_only_instance' }[]
221
+ }
222
+ hibernateEffects?: {
223
+ poolers: CNPGEffectList
224
+ unsuspendedScheduledBackups: CNPGEffectList
225
+ databases: CNPGEffectList
226
+ publications: CNPGEffectList
227
+ subscriptions: CNPGEffectList
228
+ volumes: { available: boolean; reason?: string; items: { name: string; instance: string; role: string; capacity?: string; requested?: string }[] }
229
+ }
230
+ operator?: CNPGOperatorVerdict
231
+ }
232
+
233
+ export interface CNPGEffectList {
234
+ available: boolean
235
+ reason?: string
236
+ names: string[]
237
+ }
238
+
239
+ export type CNPGScheduleActionName = 'suspend' | 'resume' | 'run' | 'setSchedule'
240
+
241
+ export interface CNPGScheduleCapabilities {
242
+ uid: string
243
+ resourceVersion: string
244
+ context: string
245
+ facts: {
246
+ generation: number
247
+ cluster: string
248
+ clusterUID: string
249
+ clusterGeneration: number
250
+ suspended: boolean
251
+ nextScheduleTime?: string
252
+ method?: string
253
+ pluginName?: string
254
+ target?: string
255
+ clusterState: 'ok' | 'missing' | 'hibernated' | 'unreadable'
256
+ backupBlockedReason?: string
257
+ terminating: boolean
258
+ catchUp: boolean
259
+ /** spec.schedule verbatim; setSchedule binds it. */
260
+ schedule: string
261
+ preview: CNPGSchedulePreview
262
+ }
263
+ actions: Record<CNPGScheduleActionName, ActionCapability>
264
+ operator?: CNPGOperatorVerdict
265
+ }
266
+
267
+ export function cnpgPath(kind: 'clusters' | 'scheduledbackups' | 'poolers', namespace: string, name: string) {
268
+ return `/cnpg/${kind}/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`
269
+ }
270
+
271
+ export function useCNPGClusterCapabilities(namespace: string, name: string, enabled = true) {
272
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
273
+ return useQuery<CNPGClusterCapabilities>({
274
+ queryKey: ['cnpg', 'capabilities', 'clusters', namespace, name, ...gatedKey],
275
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGClusterCapabilities>(`${cnpgPath('clusters', namespace, name)}/capabilities`, signal)),
276
+ enabled: enabled && !!name,
277
+ staleTime: 5_000,
278
+ retry: false,
279
+ })
280
+ }
281
+
282
+ // Parsed server-side with the operator's cron semantics; runs counted from the
283
+ // schedule's own lastCheckTime. Pure computation: writes nothing.
284
+ export function useCNPGSchedulePreview(namespace: string, name: string, schedule: string, enabled = true) {
285
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
286
+ return useQuery<CNPGSchedulePreview>({
287
+ queryKey: ['cnpg', 'schedule-preview', namespace, name, schedule, ...gatedKey],
288
+ queryFn: ({ signal }) => guard(() =>
289
+ fetchJSON<CNPGSchedulePreview>(`${cnpgPath('scheduledbackups', namespace, name)}/schedule-preview?schedule=${encodeURIComponent(schedule)}`, signal)),
290
+ enabled: enabled && !!name,
291
+ staleTime: 30_000,
292
+ retry: false,
293
+ placeholderData: (prev) => prev,
294
+ })
295
+ }
296
+
297
+ export function useCNPGScheduleCapabilities(namespace: string, name: string, enabled = true) {
298
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
299
+ return useQuery<CNPGScheduleCapabilities>({
300
+ queryKey: ['cnpg', 'capabilities', 'scheduledbackups', namespace, name, ...gatedKey],
301
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGScheduleCapabilities>(`${cnpgPath('scheduledbackups', namespace, name)}/capabilities`, signal)),
302
+ enabled: enabled && !!name,
303
+ staleTime: 5_000,
304
+ retry: false,
305
+ })
306
+ }
307
+
308
+ export interface CNPGActionResult {
309
+ action: string
310
+ message: string
311
+ backup?: string
312
+ resolvedAfterTimeout?: boolean
313
+ catchUp?: boolean
314
+ /** What the action acted on, for following its outcome. */
315
+ target?: { pod?: string; podUID?: string; pid?: number; backendStart?: string; keepPVC?: boolean; pvcs?: { name: string; uid: string }[]; jobs?: string[]; paused?: boolean; generation?: number }
316
+ }
317
+
318
+ // No mutation meta: errors stay with the dialog (shown inline so the user can
319
+ // adjust and retry), and the caller toasts success worded from the result.
320
+ type CNPGActionMutation = {
321
+ clusters:
322
+ | { action: CNPGClusterActionName | 'destroyInstance'; request: ActionRequest<CNPGClusterFacts> }
323
+ | { action: 'cancelBackend' | 'terminateBackend'; request: ActionRequest<Record<string, never>, { pod: string; podUID?: string; pid: number; backendStart: string }> }
324
+ scheduledbackups: { action: CNPGScheduleActionName; request: ActionRequest<CNPGScheduleCapabilities['facts']> }
325
+ poolers: { action: 'pause' | 'resume'; request: ActionRequest<Pick<CNPGPoolerFacts, 'paused' | 'generation'>> }
326
+ }
327
+
328
+ export function useCNPGAction<Kind extends keyof CNPGActionMutation>(kind: Kind, namespace: string, name: string, reviewedContext: string | undefined) {
329
+ const { guard } = useRadarFeature('cnpgWorkspace')
330
+ const queryClient = useQueryClient()
331
+ return useReviewedActionMutation<CNPGActionResult, CNPGActionMutation[Kind]>(apiUrl(cnpgPath(kind, namespace, name)), reviewedContext, {
332
+ mutationFn: ({ action, request }) => guard(() =>
333
+ fetchJSON<CNPGActionResult>(`${cnpgPath(kind, namespace, name)}/actions/${action}`, {
334
+ method: 'POST',
335
+ headers: { 'Content-Type': 'application/json' },
336
+ body: JSON.stringify(request),
337
+ })),
338
+ onSuccess: () => {
339
+ queryClient.invalidateQueries({ queryKey: ['cnpg'] })
340
+ queryClient.invalidateQueries({ queryKey: ['resource'] })
341
+ },
342
+ // A refusal over changed facts re-reads them, so the dialog shows what
343
+ // the user would now be confirming.
344
+ onError: (err) => {
345
+ if (cnpgActionErrorCode(err) !== undefined) queryClient.invalidateQueries({ queryKey: ['cnpg'] })
346
+ },
347
+ })
348
+ }
349
+
350
+ /** The action refusals the CloudNativePG handlers add to the shared ones. */
351
+ export type CNPGActionErrorCode = ActionErrorCode | 'all_fenced' | 'operator_webhook_unavailable' | 'invalid_schedule'
352
+
353
+ export function cnpgActionErrorCode(err: unknown): CNPGActionErrorCode | undefined {
354
+ return actionErrorCode<CNPGActionErrorCode>(err)
355
+ }
356
+
357
+ export type CNPGRuntimeSourceState = 'ok' | 'denied' | 'unreachable' | 'error' | 'partial'
358
+
359
+ export interface CNPGRuntimeReplication {
360
+ applicationName: string
361
+ state?: string
362
+ syncState?: string
363
+ syncPriority?: number
364
+ writeLag?: number
365
+ flushLag?: number
366
+ replayLag?: number
367
+ sentLsn?: string
368
+ writeLsn?: string
369
+ flushLsn?: string
370
+ replayLsn?: string
371
+ }
372
+
373
+ export interface CNPGRuntimeBaseBackup {
374
+ applicationName: string
375
+ instance?: string
376
+ phase: string
377
+ startedAt?: string
378
+ /** Absent while PostgreSQL has no estimate (waiting for a checkpoint, or estimation disabled). */
379
+ totalBytes?: number
380
+ streamedBytes: number
381
+ tablespacesTotal: number
382
+ tablespacesStreamed: number
383
+ }
384
+
385
+ export interface CNPGRuntimeInstance {
386
+ pod: string
387
+ podUID?: string
388
+ role: 'primary' | 'replica' | 'unknown'
389
+ status: {
390
+ state: CNPGRuntimeSourceState
391
+ error?: string
392
+ reason?: string
393
+ capturedAt?: string
394
+ isPrimary?: boolean
395
+ currentLsn?: string
396
+ receivedLsn?: string
397
+ replayLsn?: string
398
+ timeline?: number
399
+ replayPaused?: boolean
400
+ pendingRestart?: boolean
401
+ pendingRestartForDecrease?: boolean
402
+ isPgRewindRunning?: boolean
403
+ instanceManagerVersion?: string
404
+ roleDetail?: 'primary' | 'pgRewind' | 'replayPaused' | 'streaming' | 'fileBased'
405
+ isWalReceiverActive?: boolean
406
+ archiving?: { lastArchivedWal?: string; lastArchivedAt?: string; lastFailedWal?: string; lastFailedAt?: string; readyWalFiles?: number }
407
+ replication?: CNPGRuntimeReplication[] | null
408
+ slots?: null | { name: string; type?: string; plugin?: string; active?: boolean; database?: string; restartLsn?: string; walStatus?: string; retainedBytes?: number }[]
409
+ slotsTruncated?: boolean
410
+ /** pg_basebackup streams to joining instances; [] when the report says none run. */
411
+ baseBackups?: CNPGRuntimeBaseBackup[] | null
412
+ /**
413
+ * The instance manager answered without finishing its reads (a masked
414
+ * error, or pg_rewind running): replication, slots, baseBackups and
415
+ * archiving are null when unread, and pendingRestart is not established.
416
+ */
417
+ incomplete?: boolean
418
+ maskedError?: string
419
+ }
420
+ metrics: {
421
+ state: CNPGRuntimeSourceState
422
+ error?: string
423
+ reason?: string
424
+ capturedAt?: string
425
+ missing?: string[]
426
+ maxConnections?: number
427
+ sessions?: { state: string; database: string; user: string; application: string; count: number }[]
428
+ sessionsTotal?: number
429
+ waitingBackends?: number
430
+ oldestXactSeconds?: number
431
+ xidAge?: { database: string; age: number }[]
432
+ mxidAge?: { database: string; age: number }[]
433
+ /** null when cnpg_pg_extensions_update_available was not exported (see missing); [] when all are current. */
434
+ extensionUpdates?: { database: string; extension: string; installedVersion: string; defaultVersion: string }[] | null
435
+ databaseSizes?: { database: string; bytes: number }[]
436
+ archiver?: { archivedCount?: number; failedCount?: number; secondsSinceLastArchival?: number; secondsSinceLastFailure?: number }
437
+ xactCommitTotal?: number
438
+ xactRollbackTotal?: number
439
+ blksHit?: number
440
+ blksRead?: number
441
+ deadlocksTotal?: number
442
+ tempBytesTotal?: number
443
+ walBytes?: number
444
+ /** cnpg_last_update_timestamp: when the exporter last ran its queries (epoch seconds). */
445
+ lastUpdateTimestamp?: number
446
+ /** Client sessions per state over every group (absent when there are none). */
447
+ sessionsByState?: Record<string, number>
448
+ walSegments?: number
449
+ postmasterStartTime?: number
450
+ /** pg_stat_database counters per database, cumulative since the last stats reset. */
451
+ databases?: { database: string; xactCommit?: number; xactRollback?: number; tempFiles?: number; tempBytes?: number; deadlocks?: number; blksHit?: number; blksRead?: number }[]
452
+ /** Absent when neither pg_stat_checkpointer (17+) nor pg_stat_bgwriter was exported. */
453
+ checkpoints?: {
454
+ source: 'pg_stat_checkpointer' | 'pg_stat_bgwriter'
455
+ timed?: number
456
+ requested?: number
457
+ restartpointsTimed?: number
458
+ restartpointsRequested?: number
459
+ restartpointsDone?: number
460
+ buffersWritten?: number
461
+ }
462
+ }
463
+ }
464
+
465
+ export interface CNPGRuntimeResponse {
466
+ cluster: { namespace: string; name: string; uid: string }
467
+ sampledAt: string
468
+ permission: { proxy: 'allowed' | 'denied'; grant?: Grant }
469
+ instances: CNPGRuntimeInstance[]
470
+ }
471
+
472
+ // /api/cnpg/clusters/{ns}/{name}/runtime — live instance-manager status and
473
+ // exporter metrics read through pods/proxy. Polled only while visible.
474
+ export function useCNPGRuntime(namespace: string, name: string, enabled = true) {
475
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
476
+ return useQuery<CNPGRuntimeResponse>({
477
+ queryKey: ['cnpg', 'runtime', namespace, name, ...gatedKey],
478
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGRuntimeResponse>(`/cnpg/clusters/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/runtime`, signal)),
479
+ enabled: enabled && !!name,
480
+ refetchInterval: (q) => (q.state.data?.permission.proxy === 'denied' ? false : 5_000),
481
+ refetchIntervalInBackground: false,
482
+ staleTime: 4_000,
483
+ retry: false,
484
+ })
485
+ }
486
+
487
+ export interface CNPGPoolerRuntimeResponse {
488
+ pooler: { namespace: string; name: string; uid: string }
489
+ sampledAt: string
490
+ permission: { proxy: 'allowed' | 'denied'; grant?: Grant }
491
+ pods: {
492
+ pod: string
493
+ schedulingReason?: string
494
+ state: CNPGRuntimeSourceState
495
+ error?: string
496
+ reason?: string
497
+ missing?: string[]
498
+ pools?: { database: string; user: string; clActive?: number; clWaiting?: number; svActive?: number; svIdle?: number; svUsed?: number; maxwaitSeconds?: number; poolMode?: string }[]
499
+ }[]
500
+ }
501
+
502
+ export function useCNPGPoolerRuntime(namespace: string, name: string, enabled = true) {
503
+ const { guard, gatedKey } = useRadarFeature('cnpgWorkspace')
504
+ return useQuery<CNPGPoolerRuntimeResponse>({
505
+ queryKey: ['cnpg', 'pooler-runtime', namespace, name, ...gatedKey],
506
+ queryFn: ({ signal }) => guard(() => fetchJSON<CNPGPoolerRuntimeResponse>(`/cnpg/poolers/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/runtime`, signal)),
507
+ enabled: enabled && !!name,
508
+ refetchInterval: 30_000,
509
+ refetchIntervalInBackground: false,
510
+ staleTime: 25_000,
511
+ retry: false,
94
512
  })
95
513
  }
@@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client'
4
4
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5
5
  import { afterEach, beforeEach, expect, it, vi } from 'vitest'
6
6
  import { getRadarUpgradeRequirement } from '@skyhook-io/k8s-ui'
7
- import { ApiError, isNotFoundError, useApplications, useCapacityPoolDetail, useDrainPlan, useNodeRemovalPlan, usePodEnvironment, useWorkloadHistory } from './client'
7
+ import { ApiError, isNotFoundError, useApplications, useCapacityPoolDetail, useDrainPlan, useNodeRemovalPlan, usePodEnvironment, useSubjectIssues, useWorkloadHistory } from './client'
8
8
  import { getApiBase } from './config'
9
9
  import { usePolicyResource } from './policy'
10
10
  import { isRadarFeatureUnsupported } from './radarFeatures'
@@ -35,6 +35,7 @@ beforeEach(() => {
35
35
  if (url.includes('/capacity/pools/')) return Promise.resolve(Response.json({ pool: { name: 'default' } }))
36
36
  if (url.endsWith('/environment')) return Promise.resolve(Response.json({ containers: [], coverage: {} }))
37
37
  if (url.includes('/policy/resource/')) return Promise.resolve(Response.json({ evaluated: true, status: 'ready' }))
38
+ if (url.includes('/issues/resource/')) return Promise.resolve(Response.json({ issues: [], coverage: 'ok' }))
38
39
  return new Promise<Response>(() => {})
39
40
  }))
40
41
  element = document.createElement('div')
@@ -73,6 +74,49 @@ function Policy() {
73
74
  return <span>{data?.status ?? (getRadarUpgradeRequirement(error) ? 'upgrade' : 'pending')}</span>
74
75
  }
75
76
 
77
+ function SubjectIssues() {
78
+ const { data, error } = useSubjectIssues({ kind: 'Pod', group: '', namespace: 'shop', name: 'web' }, true)
79
+ return <span>{data ? 'data' : getRadarUpgradeRequirement(error) ? 'upgrade' : error ? 'unconfirmed' : 'pending'}</span>
80
+ }
81
+
82
+ it('waits for coverage capabilities instead of probing an older array-shaped issues route', async () => {
83
+ await render(<SubjectIssues />)
84
+ expect(element.textContent).toBe('pending')
85
+ expect(asked('/issues/resource/')).toBe(false)
86
+
87
+ await act(async () => { client.setQueryData(['capabilities'], { features: { resourceIssues: true } }) })
88
+ await settle()
89
+ await settle()
90
+ expect(element.textContent).toBe('upgrade')
91
+ expect(asked('/issues/resource/')).toBe(false)
92
+
93
+ await act(async () => { client.setQueryData(['capabilities'], { features: { resourceIssueCoverage: true } }) })
94
+ await settle()
95
+ await settle()
96
+ expect(element.textContent).toBe('data')
97
+ expect(asked('/issues/resource/')).toBe(true)
98
+ })
99
+
100
+ it('leaves issue coverage unconfirmed after a failed capability check and recovers when it succeeds', async () => {
101
+ const answer = vi.mocked(fetch).getMockImplementation()!
102
+ vi.mocked(fetch).mockImplementation((input, init) =>
103
+ String(input).endsWith('/capabilities')
104
+ ? Promise.resolve(Response.json({ error: 'capabilities unavailable' }, { status: 503 }))
105
+ : answer(input, init),
106
+ )
107
+ await render(<SubjectIssues />)
108
+ await settle()
109
+ await settle()
110
+ expect(element.textContent).toBe('unconfirmed')
111
+ expect(asked('/issues/resource/')).toBe(false)
112
+
113
+ await act(async () => { client.setQueryData(['capabilities'], { features: { resourceIssueCoverage: true } }) })
114
+ await settle()
115
+ await settle()
116
+ expect(element.textContent).toBe('data')
117
+ expect(asked('/issues/resource/')).toBe(true)
118
+ })
119
+
76
120
  it('fetches for real once the agent turns out to advertise the feature', async () => {
77
121
  await render(<Environment />)
78
122
  expect(element.textContent).toBe('upgrade')
@@ -90,14 +134,14 @@ it('turns an unsupported policy answer into not installed once discovery shows n
90
134
  // No upgrade note yet: discovery may still withdraw it.
91
135
  expect(element.textContent).toBe('loading')
92
136
 
93
- await act(async () => { client.setQueryData(['api-resources'], [{ group: '', name: 'pods' }]) })
137
+ await act(async () => { client.setQueryData(['api-resources'], [{ group: '', version: 'v1', kind: 'Pod', name: 'pods', namespaced: true, isCrd: false, verbs: ['get', 'list', 'watch'] }]) })
94
138
  await settle()
95
139
  expect(element.textContent).toBe('not_installed')
96
140
  expect(asked('/policy/resource/')).toBe(false)
97
141
  })
98
142
 
99
143
  it('keeps the upgrade note when discovery shows an OpenReports policy engine', async () => {
100
- client.setQueryData(['api-resources'], [{ group: 'openreports.io', name: 'reports' }])
144
+ client.setQueryData(['api-resources'], [{ group: 'openreports.io', version: 'v1alpha1', kind: 'Report', name: 'reports', namespaced: true, isCrd: true, verbs: ['get', 'list', 'watch'] }])
101
145
  await render(<Policy />)
102
146
  expect(element.textContent).toBe('upgrade')
103
147
  })
@@ -28,6 +28,7 @@ export interface RadarFeatureSpec {
28
28
 
29
29
  export const RADAR_FEATURES = {
30
30
  resourceIssues: { label: 'Operational issues', minimumVersion: 'v1.8.0', flag: 'resourceIssues' },
31
+ resourceIssueCoverage: { label: 'Resource-specific issue views', flag: 'resourceIssueCoverage', flagShippedWithEndpoint: true },
31
32
  podEnvironment: { label: 'Values from ConfigMaps and Secrets', minimumVersion: 'v1.9.0', flag: 'podEnvironment' },
32
33
  policyResource: { label: 'Policy results', minimumVersion: 'v1.10.0', flag: 'policyResource' },
33
34
  capacity: { label: 'Capacity views', minimumVersion: 'v1.9.0' },
@@ -35,9 +36,11 @@ export const RADAR_FEATURES = {
35
36
  drainPlan: { label: 'Drain plans', minimumVersion: 'v1.14.0' },
36
37
  applications: { label: 'Applications', minimumVersion: 'v1.7.7' },
37
38
  workloadHistory: { label: 'Workload history', flag: 'workloadHistory', flagShippedWithEndpoint: true },
39
+ cnpgWorkspace: { label: 'CloudNativePG views', flag: 'cnpgWorkspace', flagShippedWithEndpoint: true },
40
+ cnpgProtectionSetup: { label: 'CloudNativePG backup setup', flag: 'cnpgProtectionSetup', flagShippedWithEndpoint: true },
41
+ gitopsWriteEvidence: { label: 'GitOps revert warnings', flag: 'gitopsWriteEvidence', flagShippedWithEndpoint: true },
38
42
  serviceEndpointSlices: { label: 'Service EndpointSlice inventories', flag: 'serviceEndpointSlices', flagShippedWithEndpoint: true },
39
43
  trafficRecords: { label: 'Flow records for a graph selection', flag: 'trafficRecords', flagShippedWithEndpoint: true },
40
- gitopsWriteEvidence: { label: 'GitOps revert warnings', flag: 'gitopsWriteEvidence', flagShippedWithEndpoint: true },
41
44
  } as const satisfies Record<string, RadarFeatureSpec>
42
45
 
43
46
  export type RadarFeature = keyof typeof RADAR_FEATURES