@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,301 @@
1
+ // @vitest-environment jsdom
2
+ import { act, useState, type ReactNode } from 'react'
3
+ import { createRoot, type Root } from 'react-dom/client'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import type { HelmValues } from '../../types'
6
+ import { ValuesViewer } from './ValuesViewer'
7
+
8
+ const mutations = vi.hoisted(() => ({
9
+ apply: vi.fn(async () => undefined),
10
+ preview: vi.fn(async () => undefined),
11
+ }))
12
+
13
+ const overridesQuery = vi.hoisted(() => ({
14
+ current: {} as { data?: { userSupplied: Record<string, unknown> | null }; error: unknown; isFetching: boolean; refetch?: () => void },
15
+ refetch: vi.fn(),
16
+ }))
17
+
18
+ vi.mock('@skyhook-io/k8s-ui', () => ({
19
+ PaneLoader: ({ label }: { label: string }) => <div>{label}</div>,
20
+ }))
21
+ vi.mock('../../api/client', () => ({
22
+ useCanHelmAct: () => ({ allowed: true }),
23
+ useHelmValues: () => ({ refetch: overridesQuery.refetch, ...overridesQuery.current }),
24
+ useHelmApplyValues: () => ({
25
+ mutateAsync: mutations.apply,
26
+ isPending: false,
27
+ error: null,
28
+ }),
29
+ useHelmPreviewValues: () => ({
30
+ mutateAsync: mutations.preview,
31
+ isPending: false,
32
+ error: null,
33
+ }),
34
+ }))
35
+ vi.mock('../ui/CodeViewer', () => ({
36
+ CodeViewer: ({ code }: { code: string }) => (
37
+ <pre data-testid="values-code">{code}</pre>
38
+ ),
39
+ }))
40
+ vi.mock('../ui/YamlEditor', () => ({
41
+ YamlEditor: ({ value }: { value: string }) => (
42
+ <textarea aria-label="User Overrides YAML" value={value} readOnly />
43
+ ),
44
+ }))
45
+ vi.mock('./ValuesDiffPreview', () => ({ ValuesDiffPreview: () => null }))
46
+ vi.mock('../ui/Tooltip', () => ({
47
+ Tooltip: ({ children }: { children: ReactNode }) => children,
48
+ }))
49
+
50
+ const values: HelmValues = {
51
+ userSupplied: { image: { tag: 'pinned' } },
52
+ computed: {
53
+ image: { repository: 'example/app', tag: 'pinned' },
54
+ replicaCount: 2,
55
+ },
56
+ }
57
+
58
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
59
+
60
+ let root: Root
61
+
62
+ beforeEach(() => {
63
+ overridesQuery.current = {
64
+ data: { userSupplied: { image: { tag: 'pinned' } } },
65
+ error: null,
66
+ isFetching: false,
67
+ }
68
+ mutations.apply.mockClear()
69
+ mutations.preview.mockClear()
70
+ const element = document.createElement('div')
71
+ document.body.appendChild(element)
72
+ root = createRoot(element)
73
+ })
74
+
75
+ afterEach(async () => {
76
+ await act(async () => root.unmount())
77
+ document.body.replaceChildren()
78
+ })
79
+
80
+ function Harness({
81
+ helmValues = values,
82
+ error,
83
+ onRetry,
84
+ }: {
85
+ helmValues?: HelmValues | null
86
+ error?: unknown
87
+ onRetry?: () => void
88
+ }) {
89
+ const [showEffectiveValues, setShowEffectiveValues] = useState(true)
90
+ return (
91
+ <ValuesViewer
92
+ values={helmValues ?? undefined}
93
+ isLoading={false}
94
+ error={error}
95
+ showEffectiveValues={showEffectiveValues}
96
+ onToggleEffectiveValues={setShowEffectiveValues}
97
+ onCopy={() => undefined}
98
+ copied={false}
99
+ namespace="demo"
100
+ name="example"
101
+ onRetry={onRetry}
102
+ />
103
+ )
104
+ }
105
+
106
+ async function renderViewer(node: ReactNode = <Harness />) {
107
+ await act(async () => root.render(node))
108
+ }
109
+
110
+ function button(label: string) {
111
+ return Array.from(document.querySelectorAll('button')).find(
112
+ (element) => element.textContent?.trim() === label,
113
+ )
114
+ }
115
+
116
+ describe('ValuesViewer', () => {
117
+ it('shows read-only effective values by default and switches to user overrides', async () => {
118
+ await renderViewer()
119
+
120
+ expect(document.body.textContent).toContain('Effective Values')
121
+ expect(
122
+ document.querySelector('[data-testid="values-code"]')?.textContent,
123
+ ).toContain('replicaCount: 2')
124
+ expect(document.querySelector('textarea')).toBeNull()
125
+
126
+ await act(async () => button('User Overrides')!.click())
127
+
128
+ expect(document.body.textContent).toContain('User Overrides')
129
+ expect(
130
+ document.querySelector('[data-testid="values-code"]')?.textContent,
131
+ ).toContain('tag: pinned')
132
+ expect(
133
+ document.querySelector('[data-testid="values-code"]')?.textContent,
134
+ ).not.toContain('replicaCount')
135
+ })
136
+
137
+ it('labels editing explicitly and applies only user overrides', async () => {
138
+ await renderViewer()
139
+
140
+ await act(async () => button('Edit Overrides')!.click())
141
+
142
+ expect(document.body.textContent).toContain('Editing User Overrides')
143
+ expect(
144
+ (document.querySelector('textarea') as HTMLTextAreaElement).value,
145
+ ).toContain('tag: pinned')
146
+ expect(
147
+ (document.querySelector('textarea') as HTMLTextAreaElement).value,
148
+ ).not.toContain('replicaCount')
149
+
150
+ await act(async () => button('Apply')!.click())
151
+
152
+ expect(mutations.apply).toHaveBeenCalledWith({
153
+ namespace: 'demo',
154
+ name: 'example',
155
+ values: { image: { tag: 'pinned' } },
156
+ })
157
+ })
158
+
159
+ it('keeps user overrides and editing usable when only effective values fail to load', async () => {
160
+ const onRetry = vi.fn()
161
+ await renderViewer(<Harness helmValues={null} error={new Error('forbidden')} onRetry={onRetry} />)
162
+
163
+ expect(document.body.textContent).toContain("Couldn't load effective values.")
164
+ expect(document.querySelector('[data-testid="values-code"]')).toBeNull()
165
+ await act(async () => button('Try again')!.click())
166
+ expect(onRetry).toHaveBeenCalledTimes(1)
167
+
168
+ await act(async () => button('User Overrides')!.click())
169
+ expect(document.body.textContent).not.toContain("Couldn't load effective values")
170
+ expect(document.querySelector('[data-testid="values-code"]')?.textContent).toContain('tag: pinned')
171
+
172
+ await act(async () => button('Edit Overrides')!.click())
173
+ expect((document.querySelector('textarea') as HTMLTextAreaElement).value).toContain('tag: pinned')
174
+ await act(async () => button('Apply')!.click())
175
+ expect(mutations.apply).toHaveBeenCalledWith({ namespace: 'demo', name: 'example', values: { image: { tag: 'pinned' } } })
176
+ })
177
+
178
+ it('blocks editing when both reads fail, and says so in each view', async () => {
179
+ overridesQuery.current = { data: undefined, error: new Error('forbidden'), isFetching: false }
180
+ await renderViewer(<Harness helmValues={null} error={new Error('forbidden')} />)
181
+
182
+ expect(document.body.textContent).toContain("Couldn't load effective values.")
183
+ expect(document.body.textContent).toContain("Couldn't load your overrides, so editing is off.")
184
+ expect((button('Edit Overrides') as HTMLButtonElement).disabled).toBe(true)
185
+
186
+ await act(async () => button('User Overrides')!.click())
187
+ expect(document.body.textContent).toContain("Couldn't load your overrides. Editing is off until they load.")
188
+ expect(document.body.textContent).not.toContain('Showing the last ones that loaded')
189
+ expect((button('Edit Overrides') as HTMLButtonElement).disabled).toBe(true)
190
+ expect(document.querySelector('textarea')).toBeNull()
191
+ expect(mutations.apply).not.toHaveBeenCalled()
192
+ })
193
+
194
+ it('keeps showing loaded values when a later refetch fails, but blocks editing', async () => {
195
+ overridesQuery.current = { ...overridesQuery.current, error: new Error('refetch failed') }
196
+ await renderViewer(<Harness error={new Error('refetch failed')} />)
197
+
198
+ expect(document.body.textContent).toContain("Couldn't refresh effective values. Showing the last ones that loaded.")
199
+ expect(document.body.textContent).toContain("Couldn't refresh your overrides, so editing is off.")
200
+ expect(
201
+ document.querySelector('[data-testid="values-code"]')?.textContent,
202
+ ).toContain('replicaCount: 2')
203
+ expect((button('Edit Overrides') as HTMLButtonElement).disabled).toBe(true)
204
+
205
+ await act(async () => button('User Overrides')!.click())
206
+ expect(document.body.textContent).toContain("Couldn't refresh your overrides. Showing the last ones that loaded. Editing is off until they load again.")
207
+ expect(document.querySelector('[data-testid="values-code"]')?.textContent).toContain('tag: pinned')
208
+ })
209
+
210
+ it('seeds the editor from the overrides-only read, not the all-values response', async () => {
211
+ // An older Radar can answer all=true with empty overrides when its second read fails.
212
+ await renderViewer(<Harness helmValues={{ ...values, userSupplied: {} }} />)
213
+
214
+ await act(async () => button('Edit Overrides')!.click())
215
+
216
+ expect((document.querySelector('textarea') as HTMLTextAreaElement).value).toContain('tag: pinned')
217
+ })
218
+
219
+ it('shows overrides from the overrides-only read', async () => {
220
+ await renderViewer(<Harness helmValues={{ ...values, userSupplied: {} }} />)
221
+
222
+ await act(async () => button('User Overrides')!.click())
223
+
224
+ expect(document.querySelector('[data-testid="values-code"]')?.textContent).toContain('tag: pinned')
225
+ })
226
+
227
+ it('lets an older Radar edit a release with no overrides (null userSupplied)', async () => {
228
+ overridesQuery.current = { ...overridesQuery.current, data: { userSupplied: null } }
229
+ await renderViewer()
230
+
231
+ await act(async () => button('Edit Overrides')!.click())
232
+
233
+ expect((document.querySelector('textarea') as HTMLTextAreaElement).value).toBe('')
234
+ })
235
+
236
+ it('explains a failed refresh in the empty state', async () => {
237
+ overridesQuery.current = { ...overridesQuery.current, data: { userSupplied: {} }, error: new Error('refetch failed') }
238
+ await renderViewer(<Harness helmValues={{ ...values, userSupplied: {} }} />)
239
+
240
+ await act(async () => button('User Overrides')!.click())
241
+
242
+ expect(document.body.textContent).toContain('No user overrides')
243
+ expect(document.body.textContent).toContain("Couldn't refresh your overrides. Showing the last ones that loaded.")
244
+ })
245
+
246
+ it('never shows the all-values copy of overrides when the overrides-only read failed', async () => {
247
+ overridesQuery.current = { data: undefined, error: new Error('forbidden'), isFetching: false }
248
+ await renderViewer(<Harness helmValues={{ ...values, userSupplied: {} }} />)
249
+
250
+ await act(async () => button('User Overrides')!.click())
251
+
252
+ expect(document.body.textContent).toContain("Couldn't load your overrides. Editing is off until they load.")
253
+ expect(document.body.textContent).not.toContain('No user overrides')
254
+ expect(document.body.textContent).not.toContain('Showing the last ones that loaded')
255
+ expect((button('Edit Overrides') as HTMLButtonElement).disabled).toBe(true)
256
+
257
+ overridesQuery.refetch.mockClear()
258
+ await act(async () => button('Try again')!.click())
259
+ expect(overridesQuery.refetch).toHaveBeenCalledTimes(1)
260
+ })
261
+
262
+ it('keeps successfully loaded empty overrides editable', async () => {
263
+ overridesQuery.current = { ...overridesQuery.current, data: { userSupplied: {} } }
264
+ await renderViewer(
265
+ <Harness helmValues={{ ...values, userSupplied: {} }} />,
266
+ )
267
+
268
+ await act(async () => button('Edit Overrides')!.click())
269
+
270
+ expect((document.querySelector('textarea') as HTMLTextAreaElement).value).toBe('')
271
+ await act(async () => button('Apply')!.click())
272
+ expect(mutations.apply).toHaveBeenCalledWith({
273
+ namespace: 'demo',
274
+ name: 'example',
275
+ values: {},
276
+ })
277
+ })
278
+
279
+ it('keeps the edit and blocks apply while overrides reload or fail to reload', async () => {
280
+ await renderViewer()
281
+ await act(async () => button('Edit Overrides')!.click())
282
+
283
+ overridesQuery.current = { ...overridesQuery.current, isFetching: true }
284
+ await renderViewer()
285
+ expect(document.body.textContent).toContain('Editing User Overrides')
286
+ expect(document.body.textContent).toContain('Reloading user overrides. Your edits are kept.')
287
+ expect(document.querySelector('textarea')).not.toBeNull()
288
+ expect((button('Apply') as HTMLButtonElement).disabled).toBe(true)
289
+
290
+ overridesQuery.current = { ...overridesQuery.current, isFetching: false, error: new Error('refetch failed') }
291
+ await renderViewer()
292
+ expect(document.body.textContent).toContain("Couldn't reload user overrides, so Apply is off.")
293
+ expect(document.querySelector('textarea')).not.toBeNull()
294
+ expect((button('Apply') as HTMLButtonElement).disabled).toBe(true)
295
+
296
+ overridesQuery.refetch.mockClear()
297
+ await act(async () => button('Try again')!.click())
298
+ expect(overridesQuery.refetch).toHaveBeenCalledTimes(1)
299
+ expect(document.querySelector('textarea')).not.toBeNull()
300
+ })
301
+ })