@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
@@ -75,7 +75,8 @@ export function HelmCompareRoute() {
75
75
  const right = revisions.find((r) => r.revision === revision2)
76
76
 
77
77
  const manifestDiff = useHelmManifestDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
78
- const valuesDiff = useHelmValuesDiff(helmNamespace, releaseName, revision1, revision2, false, pairReady)
78
+ const overridesDiff = useHelmValuesDiff(helmNamespace, releaseName, revision1, revision2, false, pairReady)
79
+ const effectiveValuesDiff = useHelmValuesDiff(helmNamespace, releaseName, revision1, revision2, true, pairReady)
79
80
  const notesDiff = useHelmNotesDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
80
81
  const hooksDiff = useHelmHooksDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
81
82
  const resourceDiff = useHelmResourceDiff(helmNamespace, releaseName, revision1, revision2, pairReady)
@@ -219,7 +220,8 @@ export function HelmCompareRoute() {
219
220
  ['summary', 'Summary'],
220
221
  ['manifest', 'Manifest'],
221
222
  ['resources', 'Resources'],
222
- ['values', 'Values'],
223
+ ['effective-values', 'Effective Values'],
224
+ ['overrides', 'User Overrides'],
223
225
  ['hooks', 'Hooks'],
224
226
  ['notes', 'Notes'],
225
227
  ].map(([id, label]) => (
@@ -256,9 +258,12 @@ export function HelmCompareRoute() {
256
258
  manifestDiff={manifestDiff.data?.diff}
257
259
  manifestLoading={manifestDiff.isLoading}
258
260
  manifestError={manifestDiff.error}
259
- valuesDiff={valuesDiff.data?.diff}
260
- valuesLoading={valuesDiff.isLoading}
261
- valuesError={valuesDiff.error}
261
+ overridesDiff={overridesDiff.data?.diff}
262
+ overridesLoading={overridesDiff.isLoading}
263
+ overridesError={overridesDiff.error}
264
+ effectiveValuesDiff={effectiveValuesDiff.data?.diff}
265
+ effectiveValuesLoading={effectiveValuesDiff.isLoading}
266
+ effectiveValuesError={effectiveValuesDiff.error}
262
267
  notesDiff={notesDiff.data?.diff}
263
268
  notesLoading={notesDiff.isLoading}
264
269
  notesError={notesDiff.error}
@@ -293,14 +298,25 @@ export function HelmCompareRoute() {
293
298
  />
294
299
 
295
300
  <DiffSection
296
- id="values"
301
+ id="effective-values"
297
302
  icon={Settings}
298
- title="User-supplied values diff"
299
- description="Only values explicitly supplied to the release are compared here; computed chart defaults can still affect the rendered manifest."
300
- diff={valuesDiff.data?.diff || ''}
301
- isLoading={valuesDiff.isLoading}
302
- error={valuesDiff.error}
303
- emptyLabel="No user-supplied value changes found."
303
+ title="Effective values diff"
304
+ description="Computed values for each revision are compared here, including that revision's chart defaults and user overrides."
305
+ diff={effectiveValuesDiff.data?.diff || ''}
306
+ isLoading={effectiveValuesDiff.isLoading}
307
+ error={effectiveValuesDiff.error}
308
+ emptyLabel="No effective value changes found."
309
+ />
310
+
311
+ <DiffSection
312
+ id="overrides"
313
+ icon={Settings}
314
+ title="User overrides diff"
315
+ description="Only values explicitly supplied to each release revision are compared here."
316
+ diff={overridesDiff.data?.diff || ''}
317
+ isLoading={overridesDiff.isLoading}
318
+ error={overridesDiff.error}
319
+ emptyLabel="No user override changes found."
304
320
  />
305
321
 
306
322
  <HooksDiffSection diff={hooksDiff.data} isLoading={hooksDiff.isLoading} error={hooksDiff.error} />
@@ -485,7 +501,7 @@ function formatRevisionOption(revision: HelmRevision): string {
485
501
  return `rev ${revision.revision} - ${revision.status} - ${revision.chart}`
486
502
  }
487
503
 
488
- function CompareSummary({
504
+ export function CompareSummary({
489
505
  left,
490
506
  right,
491
507
  revision1,
@@ -493,9 +509,12 @@ function CompareSummary({
493
509
  manifestDiff,
494
510
  manifestLoading,
495
511
  manifestError,
496
- valuesDiff,
497
- valuesLoading,
498
- valuesError,
512
+ overridesDiff,
513
+ overridesLoading,
514
+ overridesError,
515
+ effectiveValuesDiff,
516
+ effectiveValuesLoading,
517
+ effectiveValuesError,
499
518
  notesDiff,
500
519
  notesLoading,
501
520
  notesError,
@@ -513,9 +532,12 @@ function CompareSummary({
513
532
  manifestDiff?: string
514
533
  manifestLoading: boolean
515
534
  manifestError: unknown
516
- valuesDiff?: string
517
- valuesLoading: boolean
518
- valuesError: unknown
535
+ overridesDiff?: string
536
+ overridesLoading: boolean
537
+ overridesError: unknown
538
+ effectiveValuesDiff?: string
539
+ effectiveValuesLoading: boolean
540
+ effectiveValuesError: unknown
519
541
  notesDiff?: string
520
542
  notesLoading: boolean
521
543
  notesError: unknown
@@ -527,7 +549,8 @@ function CompareSummary({
527
549
  resourceError: unknown
528
550
  }) {
529
551
  const manifestStats = diffStats(manifestDiff || '')
530
- const valuesStats = diffStats(valuesDiff || '')
552
+ const overridesStats = diffStats(overridesDiff || '')
553
+ const effectiveValuesStats = diffStats(effectiveValuesDiff || '')
531
554
  const notesStats = diffStats(notesDiff || '')
532
555
  const hookChanged = hooksDiff ? hooksDiff.added.length + hooksDiff.removed.length + hooksDiff.modified.length : 0
533
556
  const resourceChanged = resourceDiff ? resourceDiff.added.length + resourceDiff.removed.length + resourceDiff.modified.length : 0
@@ -566,12 +589,20 @@ function CompareSummary({
566
589
  sectionId="resources"
567
590
  />
568
591
  <SignalPill
569
- label="Values"
570
- loading={valuesLoading}
571
- error={valuesError}
572
- tone={valuesStats.changed ? 'info' : 'neutral'}
573
- value={valuesStats.changed ? `${valuesStats.additions} add / ${valuesStats.removals} remove` : 'same'}
574
- sectionId="values"
592
+ label="Effective values"
593
+ loading={effectiveValuesLoading}
594
+ error={effectiveValuesError}
595
+ tone={effectiveValuesStats.changed ? 'info' : 'neutral'}
596
+ value={effectiveValuesStats.changed ? `${effectiveValuesStats.additions} add / ${effectiveValuesStats.removals} remove` : 'same'}
597
+ sectionId="effective-values"
598
+ />
599
+ <SignalPill
600
+ label="Overrides"
601
+ loading={overridesLoading}
602
+ error={overridesError}
603
+ tone={overridesStats.changed ? 'info' : 'neutral'}
604
+ value={overridesStats.changed ? `${overridesStats.additions} add / ${overridesStats.removals} remove` : 'same'}
605
+ sectionId="overrides"
575
606
  />
576
607
  <SignalPill
577
608
  label="Hooks"
@@ -1,9 +1,17 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { isUpgradeSourceIssueActionable } from './HelmReleaseDrawer'
2
+ import { DEFAULT_SHOW_EFFECTIVE_VALUES, effectiveHelmStorageNamespace, isUpgradeSourceIssueActionable, upgradePickerVersions } from './HelmReleaseDrawer'
3
+
4
+ describe('Helm values view', () => {
5
+ it('opens on user overrides', () => {
6
+ expect(DEFAULT_SHOW_EFFECTIVE_VALUES).toBe(false)
7
+ })
8
+ })
3
9
 
4
10
  describe('isUpgradeSourceIssueActionable', () => {
5
- it('keeps classic repository ambiguity informational', () => {
6
- expect(isUpgradeSourceIssueActionable('ambiguous_repository')).toBe(false)
11
+ it('allows explicit association for ambiguous and unavailable sources', () => {
12
+ expect(isUpgradeSourceIssueActionable('ambiguous_repository')).toBe(true)
13
+ expect(isUpgradeSourceIssueActionable('ambiguous_source')).toBe(true)
14
+ expect(isUpgradeSourceIssueActionable('source_unavailable')).toBe(true)
7
15
  })
8
16
 
9
17
  it('lets OCI registration help untracked and repo-index states', () => {
@@ -15,3 +23,47 @@ describe('isUpgradeSourceIssueActionable', () => {
15
23
  expect(isUpgradeSourceIssueActionable(undefined)).toBe(false)
16
24
  })
17
25
  })
26
+
27
+ describe('effectiveHelmStorageNamespace', () => {
28
+ it('falls back to the release namespace when storageNamespace is empty', () => {
29
+ expect(effectiveHelmStorageNamespace({ namespace: 'production', name: 'release', storageNamespace: '' })).toBe('production')
30
+ })
31
+
32
+ it('uses an explicit storage namespace', () => {
33
+ expect(effectiveHelmStorageNamespace({ namespace: 'production', name: 'release', storageNamespace: 'helm-storage' })).toBe('helm-storage')
34
+ })
35
+ })
36
+
37
+ describe('upgradePickerVersions', () => {
38
+ it('lists the repository versions when the installed one is still published', () => {
39
+ expect(upgradePickerVersions(['1.5.0', '1.4.0'], '1.4.0')).toEqual([
40
+ { version: '1.5.0', selectable: true },
41
+ { version: '1.4.0', selectable: true },
42
+ ])
43
+ })
44
+
45
+ it('shows a dropped installed version in order but not selectable', () => {
46
+ expect(upgradePickerVersions(['1.6.0', '1.5.0', '1.3.0'], '1.4.0')).toEqual([
47
+ { version: '1.6.0', selectable: true },
48
+ { version: '1.5.0', selectable: true },
49
+ { version: '1.4.0', selectable: false },
50
+ { version: '1.3.0', selectable: true },
51
+ ])
52
+ })
53
+
54
+ it('offers a picker even when only one newer version is left', () => {
55
+ expect(upgradePickerVersions(['1.5.0'], '1.4.0')).toEqual([
56
+ { version: '1.5.0', selectable: true },
57
+ { version: '1.4.0', selectable: false },
58
+ ])
59
+ })
60
+
61
+ it('puts a dropped version older than every listed one at the end', () => {
62
+ expect(upgradePickerVersions(['2.1.0', '2.0.0'], '1.0.0')?.at(-1)).toEqual({ version: '1.0.0', selectable: false })
63
+ })
64
+
65
+ it('has nothing to pick with a single published version that is the installed one', () => {
66
+ expect(upgradePickerVersions(['1.5.0'], '1.5.0')).toBeNull()
67
+ expect(upgradePickerVersions(undefined, '1.5.0')).toBeNull()
68
+ })
69
+ })
@@ -37,6 +37,10 @@ interface HelmReleaseDrawerProps {
37
37
  rightInset?: number
38
38
  }
39
39
 
40
+ export function effectiveHelmStorageNamespace(release: SelectedHelmRelease): string {
41
+ return release.storageNamespace || release.namespace
42
+ }
43
+
40
44
  type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
41
45
 
42
46
  interface UpgradePreviewRequest {
@@ -51,7 +55,7 @@ interface ParsedUpgradeValues {
51
55
  }
52
56
 
53
57
  type UpgradeSourceIssue = NonNullable<UpgradeInfo['sourceIssue']>
54
- type ActionableUpgradeSourceIssue = Exclude<UpgradeSourceIssue, 'ambiguous_repository'>
58
+ type ActionableUpgradeSourceIssue = UpgradeSourceIssue
55
59
 
56
60
  function getUpgradeSourceIssue(upgradeInfo: UpgradeInfo): UpgradeSourceIssue | undefined {
57
61
  return upgradeInfo.sourceIssue
@@ -63,6 +67,10 @@ function getUpgradeSourceIssueLabel(issue: UpgradeSourceIssue) {
63
67
  return 'upgrade source blocked'
64
68
  case 'ambiguous_repository':
65
69
  return 'upgrade source ambiguous'
70
+ case 'ambiguous_source':
71
+ return 'upgrade source ambiguous'
72
+ case 'source_unavailable':
73
+ return 'recorded source unavailable'
66
74
  case 'untracked':
67
75
  return 'upgrade source not tracked'
68
76
  }
@@ -77,6 +85,10 @@ function getUpgradeSourceIssueTooltip(issue: UpgradeSourceIssue, error: string |
77
85
  return 'A configured Helm repo index failed. Fix or refresh that repo, or register an OCI prefix if this chart came from OCI.'
78
86
  case 'ambiguous_repository':
79
87
  return 'Multiple configured Helm repos match this chart. Fix the repo list or source metadata so Radar can identify one source.'
88
+ case 'ambiguous_source':
89
+ return 'Multiple configured sources match this chart. Select the original source explicitly.'
90
+ case 'source_unavailable':
91
+ return 'The recorded source is not configured or cannot verify the installed chart version on this Radar installation.'
80
92
  case 'untracked':
81
93
  return "Radar can't tell where this chart was installed from. Register an OCI chart source to track upgrades."
82
94
  }
@@ -96,12 +108,34 @@ function parseUpgradeValuesYaml(raw: string): ParsedUpgradeValues {
96
108
  }
97
109
 
98
110
  export function isUpgradeSourceIssueActionable(issue: UpgradeInfo['sourceIssue']): issue is ActionableUpgradeSourceIssue {
99
- return Boolean(issue && issue !== 'ambiguous_repository')
111
+ return Boolean(issue)
112
+ }
113
+
114
+ export interface UpgradePickerVersion {
115
+ version: string
116
+ // False for the installed version once the repository no longer publishes it:
117
+ // it is shown for orientation but can't be picked.
118
+ selectable: boolean
119
+ }
120
+
121
+ // Versions for the upgrade picker, newest first as the server returns them.
122
+ // When the repository dropped the installed version, it is slotted in by
123
+ // version order as a non-selectable entry so the user still sees where they are.
124
+ // Returns null when there is nothing to choose between.
125
+ export function upgradePickerVersions(available: string[] | undefined, current: string | undefined): UpgradePickerVersion[] | null {
126
+ if (!available) return null
127
+ const entries: UpgradePickerVersion[] = available.map(version => ({ version, selectable: true }))
128
+ if (current && !available.includes(current)) {
129
+ const olderIndex = available.findIndex(v => compareVersions(v, current) === -1)
130
+ entries.splice(olderIndex === -1 ? entries.length : olderIndex, 0, { version: current, selectable: false })
131
+ }
132
+ return entries.length > 1 ? entries : null
100
133
  }
101
134
 
102
135
  const MIN_WIDTH = 500
103
136
  const MAX_WIDTH_PERCENT = 0.8
104
137
  const DEFAULT_WIDTH = 1000
138
+ export const DEFAULT_SHOW_EFFECTIVE_VALUES = false
105
139
 
106
140
  export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOpen = true, rightInset = 0 }: HelmReleaseDrawerProps) {
107
141
  const navigate = useNavigate()
@@ -111,7 +145,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
111
145
  const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? DEFAULT_WIDTH : window.innerWidth))
112
146
  const [isResizing, setIsResizing] = useState(false)
113
147
  const [selectedRevision, setSelectedRevision] = useState<number | undefined>(undefined)
114
- const [showAllValues, setShowAllValues] = useState(false)
148
+ const [showEffectiveValues, setShowEffectiveValues] = useState(DEFAULT_SHOW_EFFECTIVE_VALUES)
115
149
  const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
116
150
  const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
117
151
  const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
@@ -128,7 +162,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
128
162
  const resizeStartWidth = useRef(DEFAULT_WIDTH)
129
163
  const targetVersionRef = useRef('')
130
164
  const editedUpgradeYamlRef = useRef('')
131
- const helmNamespace = release.storageNamespace || release.namespace
165
+ const helmNamespace = effectiveHelmStorageNamespace(release)
132
166
  const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct(helmNamespace)
133
167
 
134
168
  const { data: releaseDetail, isLoading, error: releaseError, refetch: refetchRelease } = useHelmRelease(
@@ -145,10 +179,10 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
145
179
  )
146
180
 
147
181
  // Fetch values
148
- const { data: values, isLoading: valuesLoading } = useHelmValues(
182
+ const { data: values, isLoading: valuesLoading, error: valuesError, refetch: refetchValues } = useHelmValues(
149
183
  helmNamespace,
150
184
  release.name,
151
- showAllValues,
185
+ true,
152
186
  true,
153
187
  selectedRevision,
154
188
  )
@@ -179,7 +213,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
179
213
  error: availableVersionsError,
180
214
  } = useHelmReleaseVersions(helmNamespace, release.name, showUpgradeConfirm)
181
215
  const targetVersion = selectedVersion ?? upgradeInfo?.latestVersion ?? ''
182
- const selectableVersions = availableVersions && availableVersions.length > 1 ? availableVersions : null
216
+ const selectableVersions = upgradePickerVersions(availableVersions, upgradeInfo?.currentVersion)
183
217
  // Semver compare, not list-position: the installed version may be older than
184
218
  // the newest-N versions the picker shows, so it isn't always in the list.
185
219
  const isDowngrade = Boolean(
@@ -735,8 +769,9 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
735
769
  <ValuesViewer
736
770
  values={values}
737
771
  isLoading={valuesLoading}
738
- showAllValues={showAllValues}
739
- onToggleAllValues={setShowAllValues}
772
+ error={valuesError}
773
+ showEffectiveValues={showEffectiveValues}
774
+ onToggleEffectiveValues={setShowEffectiveValues}
740
775
  onCopy={(text) => copyToClipboard(text, 'values')}
741
776
  copied={copied === 'values'}
742
777
  namespace={helmNamespace}
@@ -744,6 +779,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
744
779
  revision={selectedRevision}
745
780
  currentRevision={releaseDetail.revision}
746
781
  onApplySuccess={() => refetch()}
782
+ onRetry={() => void refetchValues()}
747
783
  />
748
784
  )}
749
785
  {activeTab === 'resources' && (
@@ -853,11 +889,11 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
853
889
  disabled={isUpgrading || isPreviewingUpgrade}
854
890
  className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-50"
855
891
  >
856
- {selectableVersions.map((v) => (
857
- <option key={v} value={v}>
892
+ {selectableVersions.map(({ version: v, selectable }) => (
893
+ <option key={v} value={v} disabled={!selectable}>
858
894
  {v}
859
895
  {v === upgradeInfo?.latestVersion ? ' (latest)' : ''}
860
- {v === upgradeInfo?.currentVersion ? ' (current)' : ''}
896
+ {v === upgradeInfo?.currentVersion ? (selectable ? ' (current)' : ' (current, no longer in the repository)') : ''}
861
897
  </option>
862
898
  ))}
863
899
  </select>
@@ -986,6 +1022,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
986
1022
  open={showTrackSource}
987
1023
  onClose={() => setShowTrackSource(false)}
988
1024
  chartName={releaseDetail?.chart}
1025
+ namespace={helmNamespace}
1026
+ releaseName={release.name}
989
1027
  sourceIssue={upgradeSourceIssue}
990
1028
  sourceError={upgradeInfo?.error}
991
1029
  />
@@ -0,0 +1,120 @@
1
+ // @vitest-environment jsdom
2
+ import { act, type InputHTMLAttributes, 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 { ChartSourceStatus, HelmRepository } from '../../types'
6
+ import { TrackChartSourceDialog } from './TrackChartSourceDialog'
7
+
8
+ const state = vi.hoisted(() => ({
9
+ status: undefined as ChartSourceStatus | undefined,
10
+ repositories: [] as HelmRepository[],
11
+ isOwner: true,
12
+ addRepository: vi.fn(),
13
+ }))
14
+
15
+ vi.mock('@skyhook-io/k8s-ui/components/ui/DialogPortal', () => ({
16
+ DialogPortal: ({ open, children }: { open: boolean; children: ReactNode }) => (open ? <div>{children}</div> : null),
17
+ }))
18
+ vi.mock('@skyhook-io/k8s-ui', () => ({
19
+ Input: (props: InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
20
+ }))
21
+ vi.mock('../settings/OperatorManagedNotice', () => ({ OperatorManagedNotice: () => null }))
22
+ vi.mock('../../api/client', () => {
23
+ const idle = { mutate: vi.fn(), mutateAsync: vi.fn(), isPending: false }
24
+ return {
25
+ useHelmSourceStatus: () => ({ data: state.status, error: null, refetch: vi.fn() }),
26
+ useHelmRepositories: () => ({ data: state.repositories, isLoading: false }),
27
+ useHelmOCISources: () => ({ data: [] }),
28
+ useClusterInfo: () => ({ data: { inCluster: false } }),
29
+ useCapabilities: () => ({ data: { configManagement: 'local' } }),
30
+ useCloudRole: () => ({ canAtLeast: () => state.isOwner }),
31
+ useAddHelmRepository: () => ({ mutate: state.addRepository, isPending: false }),
32
+ useAddOCISource: () => idle,
33
+ useRemoveOCISource: () => idle,
34
+ useSetHelmSource: () => idle,
35
+ }
36
+ })
37
+
38
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
39
+
40
+ const recordedURL = 'https://stefanprodan.github.io/podinfo'
41
+ const movedMachine: ChartSourceStatus = {
42
+ recorded: { type: 'repository', reference: 'podinfo', url: recordedURL },
43
+ configured: false,
44
+ available: false,
45
+ }
46
+
47
+ let root: Root
48
+
49
+ beforeEach(() => {
50
+ state.status = movedMachine
51
+ state.repositories = []
52
+ state.isOwner = true
53
+ state.addRepository.mockReset()
54
+ const element = document.createElement('div')
55
+ document.body.appendChild(element)
56
+ root = createRoot(element)
57
+ })
58
+
59
+ afterEach(async () => {
60
+ await act(async () => root.unmount())
61
+ document.body.replaceChildren()
62
+ })
63
+
64
+ async function renderDialog() {
65
+ await act(async () => root.render(<TrackChartSourceDialog open onClose={() => undefined} chartName="podinfo" namespace="web" releaseName="storefront" />))
66
+ }
67
+
68
+ function addRecordedButton() {
69
+ return Array.from(document.querySelectorAll('button')).find(button => button.textContent?.includes('and use it'))
70
+ }
71
+
72
+ describe('TrackChartSourceDialog one-click add of the recorded repository', () => {
73
+ it('adds the recorded URL under the recorded name when that name is free', async () => {
74
+ await renderDialog()
75
+ const button = addRecordedButton()
76
+ expect(button?.textContent).toBe('Add stefanprodan.github.io/podinfo and use it')
77
+ expect(button?.disabled).toBe(false)
78
+ expect(document.body.textContent).toContain('Saved in Helm as podinfo, the name it was recorded under.')
79
+
80
+ await act(async () => button?.click())
81
+ expect(state.addRepository).toHaveBeenCalledWith(
82
+ { name: 'podinfo', url: recordedURL, namespace: 'web', releaseName: 'storefront' },
83
+ expect.anything(),
84
+ )
85
+ })
86
+
87
+ it('never reuses a recorded name that points at a different repository here', async () => {
88
+ state.repositories = [
89
+ { name: 'podinfo', url: 'https://impostor.example.test/charts' },
90
+ { name: 'podinfo-2', url: 'https://other.example.test' },
91
+ ]
92
+ await renderDialog()
93
+ expect(document.body.textContent).toContain('Saved in Helm as podinfo-3. The name podinfo already points to a different repository here.')
94
+
95
+ await act(async () => addRecordedButton()?.click())
96
+ expect(state.addRepository).toHaveBeenCalledWith(
97
+ { name: 'podinfo-3', url: recordedURL, namespace: 'web', releaseName: 'storefront' },
98
+ expect.anything(),
99
+ )
100
+ })
101
+
102
+ it('is disabled for a non-owner, who is told who can add sources', async () => {
103
+ state.isOwner = false
104
+ await renderDialog()
105
+ expect(addRecordedButton()?.disabled).toBe(true)
106
+ expect(document.body.textContent).toContain('Only organization owners can add or remove chart sources.')
107
+ })
108
+
109
+ it('is not offered when the recorded source is configured, OCI, or not recorded', async () => {
110
+ for (const status of [
111
+ { ...movedMachine, configured: true },
112
+ { recorded: { type: 'oci' as const, reference: 'oci://ghcr.io/example/charts' }, configured: false, available: false },
113
+ { configured: false, available: false },
114
+ ]) {
115
+ state.status = status
116
+ await renderDialog()
117
+ expect(addRecordedButton()).toBeUndefined()
118
+ }
119
+ })
120
+ })
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getSourceIssueCopy, isValidHelmRepositoryURL, normalizedRepositoryName, recordedRepositoryName, repositoryDisplayURL, sourceStateLabel, sourceStatusLine } from './TrackChartSourceDialog'
3
+
4
+ describe('chart source recovery presentation', () => {
5
+ it('keeps recorded, configured, and available states distinct', () => {
6
+ expect(sourceStateLabel(false, false, false)).toBe('Not recorded')
7
+ expect(sourceStateLabel(true, false, false)).toBe('Recorded · not configured here')
8
+ expect(sourceStateLabel(true, true, false)).toBe('Recorded and configured · unavailable or version missing')
9
+ expect(sourceStateLabel(true, true, true)).toBe('Recorded · configured · available')
10
+ })
11
+
12
+ it('claims nothing about the recorded source until it has been read', () => {
13
+ expect(sourceStatusLine(undefined, undefined)).toBe('Checking the recorded source...')
14
+ expect(sourceStatusLine(undefined, new Error('failed to load repo file'))).toBe("Could not read this release's source: failed to load repo file")
15
+ expect(sourceStatusLine({ configured: false, available: false }, new Error('refetch failed'))).toBe('Not recorded')
16
+ expect(sourceStatusLine({ recorded: { type: 'repository', reference: 'repo', url: 'https://charts.example.test' }, configured: false, available: false }, undefined)).toBe('Recorded · not configured here')
17
+ })
18
+
19
+ it('makes ambiguity explicit rather than selecting a source', () => {
20
+ expect(getSourceIssueCopy('ambiguous_source')?.body).toContain('will not guess')
21
+ })
22
+
23
+ it('accepts only HTTP repository URLs', () => {
24
+ expect(isValidHelmRepositoryURL('https://charts.example.test')).toBe(true)
25
+ expect(isValidHelmRepositoryURL('oci://registry.example.test/charts')).toBe(false)
26
+ })
27
+
28
+ it('picks a free local name for the recorded repository', () => {
29
+ expect(recordedRepositoryName('podinfo', [])).toBe('podinfo')
30
+ expect(recordedRepositoryName('podinfo', [{ name: 'Podinfo', url: 'https://impostor.example.test' }])).toBe('podinfo-2')
31
+ expect(recordedRepositoryName('podinfo', [{ name: 'podinfo', url: 'a' }, { name: 'podinfo-2', url: 'b' }])).toBe('podinfo-3')
32
+ })
33
+
34
+ it('checks names the way the server stores them', () => {
35
+ expect(normalizedRepositoryName(' My Charts! ')).toBe('my-charts')
36
+ const alias40 = 'a'.repeat(40)
37
+ expect(recordedRepositoryName(alias40, [{ name: alias40, url: 'a' }])).toBe(`${'a'.repeat(38)}-2`)
38
+ // A longer alias is stored cut to 40 characters, so that is the name that collides.
39
+ const alias45 = `${'b'.repeat(40)}extra`
40
+ expect(recordedRepositoryName(alias45, [])).toBe('b'.repeat(40))
41
+ expect(recordedRepositoryName(alias45, [{ name: 'b'.repeat(40), url: 'a' }])).toBe(`${'b'.repeat(38)}-2`)
42
+ expect(recordedRepositoryName('My_Repo', [{ name: 'my_repo', url: 'a' }])).toBe('my_repo-2')
43
+ expect(recordedRepositoryName('___', [])).toBe('')
44
+ })
45
+
46
+ it('shortens repository URLs for labels', () => {
47
+ expect(repositoryDisplayURL('https://stefanprodan.github.io/podinfo/')).toBe('stefanprodan.github.io/podinfo')
48
+ expect(repositoryDisplayURL('https://charts.example.test')).toBe('charts.example.test')
49
+ })
50
+ })