@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,164 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5
+ import { describe, expect, it, vi } from 'vitest'
6
+ import { useReviewedActionMutation } from './reviewedActionMutation'
7
+ import { ApiError } from './client'
8
+ import { actionErrorCode, actionCompleted } from './actions'
9
+ import { RadarFeatureUnsupportedError } from './radarFeatures'
10
+
11
+ ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
12
+
13
+ describe('reviewed action lifetime', () => {
14
+ it.each([
15
+ new TypeError('Connection lost'),
16
+ new ApiError('One write completed', 409, { code: 'partial', completed: ['Pod removed'] }),
17
+ new ApiError('upstream connect error or disconnect/reset before headers', 503),
18
+ ])('retains an ambiguous result through remount and prevents another request: %s', async (failure) => {
19
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
20
+ const host = document.body.appendChild(document.createElement('div'))
21
+ const root = createRoot(host)
22
+ const request = vi.fn(async () => { throw failure })
23
+ let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
24
+ function Dialog({ target }: { target: string }) {
25
+ current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>(target, 'ctx', { mutationFn: request })
26
+ return <div>{current.outcomeError?.message}</div>
27
+ }
28
+ const mount = (target?: string) => act(() => root.render(<QueryClientProvider client={client}>{target && <Dialog target={target} />}</QueryClientProvider>))
29
+ const attempt = () => act(async () => {
30
+ await current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {})
31
+ await new Promise(resolve => setTimeout(resolve, 0))
32
+ })
33
+ try {
34
+ mount('/api/cnpg/clusters/jobs/orders')
35
+ await attempt()
36
+ mount()
37
+ client.removeQueries()
38
+ client.clear()
39
+ mount('/api/cnpg/clusters/jobs/orders')
40
+ expect(current!.outcomeError?.message).toContain('reload this page')
41
+ expect(actionErrorCode(current!.outcomeError)).toBe(actionErrorCode(failure))
42
+ expect(actionCompleted(current!.outcomeError)).toEqual(actionCompleted(failure))
43
+ await attempt()
44
+ expect(request).toHaveBeenCalledTimes(1)
45
+ mount('/other-cluster/api/cnpg/clusters/jobs/orders')
46
+ expect(current!.outcomeError).toBeNull()
47
+ mount('/api/cnpg/scheduledbackups/jobs/orders')
48
+ expect(current!.outcomeError).toBeNull()
49
+ } finally {
50
+ act(() => root.unmount())
51
+ client.clear()
52
+ host.remove()
53
+ }
54
+ })
55
+ it.each([
56
+ { nextTarget: '/api/target', nextContext: 'other-context' },
57
+ { nextTarget: '/other-cluster/api/target', nextContext: 'ctx' },
58
+ ])('keeps a late result on the original cluster binding: %s', async ({ nextTarget, nextContext }) => {
59
+ const client = new QueryClient()
60
+ const host = document.body.appendChild(document.createElement('div'))
61
+ const root = createRoot(host)
62
+ let fail!: (error: Error) => void
63
+ let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
64
+ function Dialog({ target, context }: { target: string; context: string }) {
65
+ current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>(target, context, {
66
+ mutationFn: () => new Promise<null>((_, reject) => { fail = reject }),
67
+ })
68
+ return null
69
+ }
70
+ const mount = (target: string, context: string) => act(() => root.render(<QueryClientProvider client={client}><Dialog target={target} context={context} /></QueryClientProvider>))
71
+ try {
72
+ mount('/api/target', 'ctx')
73
+ let attempted!: Promise<unknown>
74
+ await act(async () => {
75
+ attempted = current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {})
76
+ await Promise.resolve()
77
+ })
78
+ mount(nextTarget, nextContext)
79
+ await act(async () => {
80
+ fail(new TypeError('Connection lost'))
81
+ await attempted
82
+ await new Promise(resolve => setTimeout(resolve, 0))
83
+ })
84
+ expect(current!.outcomeError).toBeNull()
85
+ mount('/api/target', 'ctx')
86
+ expect(current!.outcomeError?.message).toContain('Connection lost')
87
+ } finally {
88
+ act(() => root.unmount())
89
+ client.clear()
90
+ host.remove()
91
+ }
92
+ })
93
+ it.each([
94
+ new ApiError('Facts changed', 409, { code: 'changed' }),
95
+ new ApiError('The operator webhook did not answer', 503, { code: 'operator_webhook_unavailable' }),
96
+ new RadarFeatureUnsupportedError('cnpgWorkspace', {}),
97
+ ])('keeps a known refusal retryable after remount: %s', async (failure) => {
98
+ const client = new QueryClient()
99
+ const host = document.body.appendChild(document.createElement('div'))
100
+ const root = createRoot(host)
101
+ let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
102
+ const request = vi.fn(async () => { throw failure })
103
+ function Dialog() {
104
+ current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>('/api/target', 'ctx', { mutationFn: request })
105
+ return null
106
+ }
107
+ const render = (open: boolean) => act(() => root.render(<QueryClientProvider client={client}>{open && <Dialog />}</QueryClientProvider>))
108
+ try {
109
+ render(true)
110
+ await act(async () => { await current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {}) })
111
+ render(false)
112
+ render(true)
113
+ expect(current!.outcomeError).toBeNull()
114
+ await act(async () => { await current.mutateAsync({ request: { reviewedContext: 'ctx' } }).catch(() => {}) })
115
+ expect(request).toHaveBeenCalledTimes(2)
116
+ } finally {
117
+ act(() => root.unmount())
118
+ client.clear()
119
+ host.remove()
120
+ }
121
+ })
122
+
123
+ it('retains an in-flight request through remount and cache clearing without sending twice', async () => {
124
+ const client = new QueryClient()
125
+ const host = document.body.appendChild(document.createElement('div'))
126
+ const root = createRoot(host)
127
+ let complete!: (data: null) => void
128
+ const request = vi.fn(() => new Promise<null>(resolve => { complete = resolve }))
129
+ let current: ReturnType<typeof useReviewedActionMutation<null, { request: { reviewedContext: string } }>>
130
+ function Dialog() {
131
+ current = useReviewedActionMutation<null, { request: { reviewedContext: string } }>('/api/target', 'ctx', { mutationFn: request })
132
+ return null
133
+ }
134
+ const render = (open: boolean) => act(() => root.render(<QueryClientProvider client={client}>{open && <Dialog />}</QueryClientProvider>))
135
+ try {
136
+ render(true)
137
+ let attempted!: Promise<null>
138
+ await act(async () => {
139
+ attempted = current.mutateAsync({ request: { reviewedContext: 'ctx' } })
140
+ await Promise.resolve()
141
+ })
142
+ render(false)
143
+ client.removeQueries()
144
+ client.clear()
145
+ render(true)
146
+ expect(current!.isPending).toBe(true)
147
+ await act(async () => {
148
+ await expect(current.mutateAsync({ request: { reviewedContext: 'ctx' } })).rejects.toThrow('still in progress')
149
+ })
150
+ expect(request).toHaveBeenCalledTimes(1)
151
+ await act(async () => {
152
+ complete(null)
153
+ await attempted
154
+ await new Promise(resolve => setTimeout(resolve, 0))
155
+ })
156
+ expect(current!.isPending).toBe(false)
157
+ expect(current!.outcomeError).toBeNull()
158
+ } finally {
159
+ act(() => root.unmount())
160
+ client.clear()
161
+ host.remove()
162
+ }
163
+ })
164
+ })
@@ -0,0 +1,66 @@
1
+ import { useCallback, useSyncExternalStore } from 'react'
2
+ import { useMutation, useQueryClient, type MutationFunction, type QueryClient, type UseMutationOptions } from '@tanstack/react-query'
3
+ import { actionOutcomeLocked } from './actions'
4
+ import { ApiError } from './client'
5
+
6
+ interface Outcome {
7
+ pending: boolean
8
+ error: Error | null
9
+ }
10
+ const EMPTY: Outcome = { pending: false, error: null }
11
+ const stores = new WeakMap<QueryClient, { entries: Map<string, Outcome>; listeners: Set<() => void> }>()
12
+
13
+ /** Result memory belongs to the host, so dismissing a dialog cannot unlock a second write. */
14
+ export function useReviewedActionMutation<Data, Variables extends { request: { reviewedContext: string } }>(
15
+ target: string,
16
+ reviewedContext: string | undefined,
17
+ options: Omit<UseMutationOptions<Data, Error, Variables>, 'mutationFn' | 'mutationKey'> & { mutationFn: MutationFunction<Data, Variables> },
18
+ ) {
19
+ const client = useQueryClient()
20
+ let store = stores.get(client)
21
+ if (!store) {
22
+ store = { entries: new Map(), listeners: new Set() }
23
+ stores.set(client, store)
24
+ }
25
+ const outcomes = store
26
+ const keyFor = (context: string | undefined) => JSON.stringify([target, context])
27
+ const key = keyFor(reviewedContext)
28
+ const subscribe = useCallback((listener: () => void) => {
29
+ outcomes.listeners.add(listener)
30
+ return () => { outcomes.listeners.delete(listener) }
31
+ }, [outcomes])
32
+ const snapshot = useCallback(() => outcomes.entries.get(key) ?? EMPTY, [outcomes, key])
33
+ // Connection recovery purges query/mutation caches. It must not forget a
34
+ // request still in flight or a result that may already have changed the target.
35
+ const retained = useSyncExternalStore(subscribe, snapshot, () => EMPTY)
36
+ const publish = (key: string, value: Outcome) => {
37
+ if (value.pending || value.error) outcomes.entries.set(key, value)
38
+ else outcomes.entries.delete(key)
39
+ for (const listener of outcomes.listeners) listener()
40
+ }
41
+ const mutation = useMutation<Data, Error, Variables>({
42
+ ...options,
43
+ mutationKey: ['reviewed-action', target, reviewedContext],
44
+ mutationFn: async (variables, context) => {
45
+ const boundKey = keyFor(variables.request.reviewedContext)
46
+ const previous = outcomes.entries.get(boundKey)
47
+ if (previous?.error) throw previous.error
48
+ if (previous?.pending) throw new ApiError('A write to this target is still in progress. Wait for its result.', 409, { code: 'blocked' })
49
+ publish(boundKey, { pending: true, error: null })
50
+ let retainedError: Error | null = null
51
+ try {
52
+ return await options.mutationFn(variables, context)
53
+ } catch (error) {
54
+ if (actionOutcomeLocked(error)) {
55
+ const failure = error as Error
56
+ const message = `${failure.message} Inspect the current state, then reload this page and review the action again.`
57
+ retainedError = failure instanceof ApiError ? new ApiError(message, failure.status, failure.data) : new Error(message)
58
+ }
59
+ throw error
60
+ } finally {
61
+ publish(boundKey, { pending: false, error: retainedError })
62
+ }
63
+ },
64
+ })
65
+ return { ...mutation, isPending: mutation.isPending || retained.pending, outcomeError: retained.error }
66
+ }
@@ -19,6 +19,7 @@ import {
19
19
  applyClientFilters,
20
20
  localOverviewFromEvents,
21
21
  needsWindowFetch,
22
+ frozenWindowRefetchInterval,
22
23
  type RetainedRing,
23
24
  } from './timelineSource'
24
25
  import type { TimelineEvent } from '../types'
@@ -650,3 +651,11 @@ describe('retained ring fetch (the OSS-identical accumulate model)', () => {
650
651
  // cursor is intact on the cached ring — nothing was committed
651
652
  })
652
653
  })
654
+
655
+ describe('frozenWindowRefetchInterval', () => {
656
+ it('polls a failed window until it loads, and leaves a loaded one alone', () => {
657
+ expect(frozenWindowRefetchInterval('error')).toBe(10_000)
658
+ expect(frozenWindowRefetchInterval('success')).toBe(false)
659
+ expect(frozenWindowRefetchInterval('pending')).toBe(false)
660
+ })
661
+ })
@@ -544,6 +544,13 @@ export function needsWindowFetch(
544
544
  // refresh.
545
545
  const FROZEN_WINDOW_STALE_MS = 10 * 60 * 1000
546
546
 
547
+ // A frozen window that failed to load does poll, at the ring's cadence, until
548
+ // it loads: the error page tells the user it recovers on its own once the
549
+ // timeline's database is back, and without a poll only "Try again" would.
550
+ export function frozenWindowRefetchInterval(status: 'pending' | 'error' | 'success'): number | false {
551
+ return status === 'error' ? RETAINED_POLL_MS : false
552
+ }
553
+
547
554
  // Ring keys whose next fetch must be a full reload (manual refresh). A side
548
555
  // flag, not a side cursor: consuming it only switches the PATH of the next
549
556
  // fetch — all sync state that must stay consistent with the data lives inside
@@ -750,6 +757,7 @@ function createRingEventsHook(opts: {
750
757
  },
751
758
  enabled: windowNeeded,
752
759
  staleTime: FROZEN_WINDOW_STALE_MS,
760
+ refetchInterval: (q) => frozenWindowRefetchInterval(q.state.status),
753
761
  })
754
762
 
755
763
  const kindsKey = query.kinds?.join(',')
@@ -83,6 +83,9 @@ function BestPracticesView({ namespaces, onNavigateToResource }: AuditViewProps)
83
83
  )
84
84
  }
85
85
 
86
+ const annotationIgnored = Object.values(data.annotationIgnoredByNamespace ?? {})
87
+ .reduce((sum, byNS) => sum + Object.values(byNS).reduce((a, n) => a + n, 0), 0)
88
+
86
89
  const onResourceClick = (ref: CheckResourceRef) =>
87
90
  onNavigateToResource({ kind: ref.kind, namespace: ref.namespace, name: ref.name, group: ref.group })
88
91
 
@@ -108,6 +111,11 @@ function BestPracticesView({ namespaces, onNavigateToResource }: AuditViewProps)
108
111
  {ignoredCount > 0 && (
109
112
  <button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
110
113
  )}
114
+ {annotationIgnored > 0 && (
115
+ <Tooltip content={`Resources that opted out of a check with the ${data.ignoreChecksAnnotation} annotation`}>
116
+ <span className="text-xs text-theme-text-tertiary">{annotationIgnored} {annotationIgnored === 1 ? 'finding' : 'findings'} hidden by annotation</span>
117
+ </Tooltip>
118
+ )}
111
119
  <Tooltip content="Checks settings">
112
120
  <button
113
121
  onClick={() => setShowSettings(true)}
@@ -129,6 +137,7 @@ function BestPracticesView({ namespaces, onNavigateToResource }: AuditViewProps)
129
137
  onResourceClick={onResourceClick}
130
138
  onHideCheck={canEdit ? hideCheck : undefined}
131
139
  onHideCategory={canEdit ? hideCategory : undefined}
140
+ ignoreChecksAnnotation={data.ignoreChecksAnnotation}
132
141
  />
133
142
 
134
143
  {showSettings && <AuditSettingsDialog namespaces={namespaces} onClose={() => setShowSettings(false)} />}
@@ -17,37 +17,9 @@ import {
17
17
  useCapacityActivity,
18
18
  } from "../../api/client";
19
19
  import type { SelectedResource } from "../../types";
20
- import {
21
- ActivityStateBadge,
22
- CapacityFreshness,
23
- InlineEmpty,
24
- LinkButton,
25
- Notice,
26
- PageControls,
27
- PoolSelector,
28
- ROW_HOVER,
29
- ScopeBadges,
30
- ScrollableContent,
31
- TABLE_HEAD,
32
- TABLE_WRAP,
33
- TBODY,
34
- TD,
35
- TH,
36
- activityTypeLabel,
37
- activityWindowPreset,
38
- coverageHasObservations,
39
- coverageIsLowerBound,
40
- coverageMessage,
41
- errorMessage,
42
- formatTimestamp,
43
- identityKey,
44
- identityToSelectedResource,
45
- integrationBlock,
46
- relativeTime,
47
- retentionLabel,
48
- useCapacityCursorRecovery,
49
- useCapacityPagination,
50
- } from "./shared";
20
+ import { ActivityStateBadge, CapacityFreshness, InlineEmpty, LinkButton, PageControls, PoolSelector, ScopeBadges, ScrollableContent, activityTypeLabel, activityWindowPreset, coverageHasObservations, coverageIsLowerBound, coverageMessage, errorMessage, formatTimestamp, identityKey, identityToSelectedResource, integrationBlock, relativeTime, retentionLabel, useCapacityCursorRecovery, useCapacityPagination } from "./shared";
21
+ import { Notice } from "../workspace/layout";
22
+ import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from "../workspace/table";
51
23
 
52
24
  const WINDOW_PILLS: [number | undefined, string][] = [
53
25
  [undefined, "Retained"],
@@ -21,41 +21,9 @@ import {
21
21
  } from "../../api/client";
22
22
  import type { SelectedResource } from "../../types";
23
23
  import { refToSelectedResource } from "../../utils/navigation";
24
- import {
25
- CapacityFreshness,
26
- CapacityIssueEvidence,
27
- DemandStateBadge,
28
- EmptyState,
29
- InlineEmpty,
30
- LinkButton,
31
- Notice,
32
- PageControls,
33
- PoolEvaluationBadge,
34
- PoolSelector,
35
- QuantityInline,
36
- ResourceLink,
37
- ROW_HOVER,
38
- ScopeBadges,
39
- ScrollableContent,
40
- TABLE_HEAD,
41
- TABLE_WRAP,
42
- TBODY,
43
- TD,
44
- TH,
45
- coverageHasObservations,
46
- coverageIsLowerBound,
47
- coverageMessage,
48
- demandStateLabel,
49
- errorMessage,
50
- formatTimestamp,
51
- humanizeCode,
52
- identityKey,
53
- integrationBlock,
54
- namespaceCoverageDescription,
55
- quantityText,
56
- useCapacityCursorRecovery,
57
- useCapacityPagination,
58
- } from "./shared";
24
+ import { CapacityFreshness, CapacityIssueEvidence, DemandStateBadge, InlineEmpty, LinkButton, PageControls, PoolEvaluationBadge, PoolSelector, QuantityInline, ResourceLink, ScopeBadges, ScrollableContent, coverageHasObservations, coverageIsLowerBound, coverageMessage, demandStateLabel, errorMessage, formatTimestamp, humanizeCode, identityKey, integrationBlock, namespaceCoverageDescription, quantityText, useCapacityCursorRecovery, useCapacityPagination } from "./shared";
25
+ import { Notice, ScreenEmptyState } from "../workspace/layout";
26
+ import { ROW_HOVER, TABLE_HEAD, TABLE_WRAP, TBODY, TD, TH } from "../workspace/table";
59
27
 
60
28
  const STATE_PILLS: [CapacityDemandState | undefined, string][] = [
61
29
  [undefined, "All states"],
@@ -184,7 +152,7 @@ export function CapacityDemand({
184
152
  const clearPodFilter = () => updateSearchParam("pod", undefined);
185
153
  if (poolFilter && !responseData && isNotFoundError(query.error)) {
186
154
  return (
187
- <EmptyState
155
+ <ScreenEmptyState
188
156
  icon={AlertTriangle}
189
157
  title="NodePool not found"
190
158
  detail={`This link evaluates demand against NodePool “${poolFilter}”, which no longer exists. It may have been removed or belongs to another cluster context.`}