@skyhook-io/radar-app 1.8.6 → 1.8.7

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 (75) hide show
  1. package/README.md +7 -1
  2. package/package.json +11 -9
  3. package/src/App.tsx +221 -181
  4. package/src/RadarApp.tsx +91 -20
  5. package/src/api/client.delta.test.ts +89 -0
  6. package/src/api/client.deltaSync.test.ts +216 -0
  7. package/src/api/client.ts +262 -34
  8. package/src/api/diagnose.ts +288 -0
  9. package/src/api/timelineSource.test.ts +217 -0
  10. package/src/api/timelineSource.ts +580 -0
  11. package/src/components/ConnectionErrorView.tsx +174 -70
  12. package/src/components/ContextSwitcher.tsx +13 -5
  13. package/src/components/applications/ApplicationsView.tsx +327 -26
  14. package/src/components/audit/AuditSettingsDialog.tsx +2 -2
  15. package/src/components/curl/ServiceCurlButton.tsx +2 -2
  16. package/src/components/diagnose/AISettings.tsx +121 -0
  17. package/src/components/diagnose/DiagnoseContext.tsx +491 -0
  18. package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
  19. package/src/components/diagnose/Home.tsx +163 -0
  20. package/src/components/diagnose/InvestigationView.tsx +604 -0
  21. package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
  22. package/src/components/diagnose/launch.ts +65 -0
  23. package/src/components/diagnose/parts.tsx +1689 -0
  24. package/src/components/dock/BottomDock.tsx +2 -3
  25. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  26. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  27. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  28. package/src/components/execution/batch-run-actions.test.ts +48 -0
  29. package/src/components/execution/batch-run-actions.ts +24 -0
  30. package/src/components/execution/batch-timeline.test.ts +57 -0
  31. package/src/components/execution/batch-timeline.ts +46 -0
  32. package/src/components/execution/execution-definition.test.ts +208 -0
  33. package/src/components/execution/execution-definition.ts +245 -0
  34. package/src/components/helm/ChartBrowser.tsx +2 -3
  35. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  36. package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
  37. package/src/components/helm/HelmView.tsx +2 -3
  38. package/src/components/helm/InstallWizard.tsx +3 -5
  39. package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
  40. package/src/components/helm/ValuesDiffPreview.tsx +15 -4
  41. package/src/components/home/HomeView.tsx +17 -15
  42. package/src/components/home/MCPSetupDialog.tsx +1 -1
  43. package/src/components/home/mcpToolCatalog.ts +2 -2
  44. package/src/components/issues/IssuesPane.tsx +9 -1
  45. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  46. package/src/components/portforward/PortForwardButton.tsx +2 -2
  47. package/src/components/portforward/PortForwardManager.tsx +19 -14
  48. package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
  49. package/src/components/resource/RightsizingStrip.tsx +0 -3
  50. package/src/components/resources/ImageFilesystemModal.tsx +2 -2
  51. package/src/components/resources/PodFilesystemModal.tsx +2 -3
  52. package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
  53. package/src/components/resources/ResourcesView.tsx +13 -4
  54. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  55. package/src/components/resources/renderers/index.ts +1 -0
  56. package/src/components/settings/SettingsDialog.tsx +80 -23
  57. package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
  58. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  59. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  60. package/src/components/timeline/TimelineList.tsx +51 -10
  61. package/src/components/timeline/TimelineView.tsx +701 -15
  62. package/src/components/timeline/TimelineView.urlparams.test.ts +294 -0
  63. package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
  64. package/src/components/traffic/TrafficView.tsx +8 -4
  65. package/src/components/ui/CommandPalette.tsx +2 -2
  66. package/src/components/workload/WorkloadView.tsx +319 -35
  67. package/src/context/ConnectionContext.tsx +109 -11
  68. package/src/context/DiagnoseCustomization.tsx +42 -0
  69. package/src/context/TimelineSource.tsx +50 -0
  70. package/src/hooks/useClusterLoadState.ts +73 -0
  71. package/src/hooks/useEventSource.ts +6 -0
  72. package/src/index.css +157 -0
  73. package/src/index.ts +12 -0
  74. package/src/types/clusterLoadState.ts +33 -0
  75. package/src/utils/navigation.ts +10 -0
@@ -4,15 +4,17 @@ import { FetchResult, useDockReservedHeight, compareVersions } from '@skyhook-io
4
4
  import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
5
5
  import { TRANSITION_DRAWER } from '../../utils/animation'
6
6
  import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
7
- import { X, Copy, Check, RefreshCw, Package, Code, History, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock, GitCompare, ExternalLink } from 'lucide-react'
7
+ import { X, Copy, Check, RefreshCw, Package, Code, History, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock, GitCompare, ExternalLink, ChevronRight, SlidersHorizontal, Eye, Loader2 } from 'lucide-react'
8
+ import yaml from 'yaml'
8
9
  import { useNavigate } from 'react-router-dom'
9
10
  import { clsx } from 'clsx'
10
- import { useHelmRelease, useHelmManifest, useHelmValues, useHelmUpgradeInfo, useHelmReleaseVersions, useHelmUninstall, upgradeWithProgress, rollbackWithProgress } from '../../api/client'
11
+ import { useHelmRelease, useHelmManifest, useHelmValues, useHelmUpgradeInfo, useHelmReleaseVersions, useHelmUninstall, upgradeWithProgress, rollbackWithProgress, useHelmPreviewValues } from '../../api/client'
12
+ import { ValuesDiffPreview } from './ValuesDiffPreview'
11
13
  import { useQueryClient } from '@tanstack/react-query'
12
14
  import { ConfirmDialog } from '../ui/ConfirmDialog'
13
15
  import { Tooltip } from '../ui/Tooltip'
14
16
  import { Markdown } from '../ui/Markdown'
15
- import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation, HelmOperationInsight, HelmOwnedResource, HookDiagnostic, HookLogEvidence } from '../../types'
17
+ import type { SelectedHelmRelease, HelmHook, ChartDependency, HelmOperation, HelmOperationInsight, HelmOwnedResource, HookDiagnostic, HookLogEvidence, UpgradeInfo, ValuesPreviewResponse } from '../../types'
16
18
  import { apiVersionToGroup, kindToPlural, type NavigateToResource } from '../../utils/navigation'
17
19
  import { formatDate } from './helm-utils'
18
20
  import { getHelmStatusColor, getKindBadgeColor, getResourceStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
@@ -30,15 +32,77 @@ interface HelmReleaseDrawerProps {
30
32
  onNavigateToResource?: NavigateToResource
31
33
  /** Controls slide-in/out animation (driven by useAnimatedUnmount) */
32
34
  isOpen?: boolean
35
+ /** Right inset in px so the drawer sits beside a docked side panel (AI), not under it. */
36
+ rightInset?: number
33
37
  }
34
38
 
35
39
  type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks'
36
40
 
41
+ interface UpgradePreviewRequest {
42
+ targetVersion: string
43
+ values: Record<string, unknown>
44
+ repositoryName?: string
45
+ }
46
+
47
+ interface ParsedUpgradeValues {
48
+ values: Record<string, unknown> | null
49
+ error: string | null
50
+ }
51
+
52
+ type UpgradeSourceIssue = NonNullable<UpgradeInfo['sourceIssue']>
53
+ type ActionableUpgradeSourceIssue = Exclude<UpgradeSourceIssue, 'ambiguous_repository'>
54
+
55
+ function getUpgradeSourceIssue(upgradeInfo: UpgradeInfo): UpgradeSourceIssue | undefined {
56
+ return upgradeInfo.sourceIssue
57
+ }
58
+
59
+ function getUpgradeSourceIssueLabel(issue: UpgradeSourceIssue) {
60
+ switch (issue) {
61
+ case 'repo_index_error':
62
+ return 'upgrade source blocked'
63
+ case 'ambiguous_repository':
64
+ return 'upgrade source ambiguous'
65
+ case 'untracked':
66
+ return 'upgrade source not tracked'
67
+ }
68
+ }
69
+
70
+ function getUpgradeSourceIssueTooltip(issue: UpgradeSourceIssue, error: string | undefined, canHelmWrite: boolean, disabledReason: string | undefined) {
71
+ if (!canHelmWrite) {
72
+ return disabledReason ?? error ?? 'Helm actions are disabled.'
73
+ }
74
+ switch (issue) {
75
+ case 'repo_index_error':
76
+ return 'A configured Helm repo index failed. Fix or refresh that repo, or register an OCI prefix if this chart came from OCI.'
77
+ case 'ambiguous_repository':
78
+ return 'Multiple configured Helm repos match this chart. Fix the repo list or source metadata so Radar can identify one source.'
79
+ case 'untracked':
80
+ return "Radar can't tell where this chart was installed from. Register an OCI chart source to track upgrades."
81
+ }
82
+ }
83
+
84
+ function parseUpgradeValuesYaml(raw: string): ParsedUpgradeValues {
85
+ try {
86
+ const parsed = yaml.parse(raw)
87
+ if (parsed === null || parsed === undefined) return { values: {}, error: null }
88
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
89
+ return { values: null, error: 'Values must be a YAML mapping (key: value), not a list or single value' }
90
+ }
91
+ return { values: parsed as Record<string, unknown>, error: null }
92
+ } catch (err) {
93
+ return { values: null, error: err instanceof Error ? err.message : 'Invalid YAML' }
94
+ }
95
+ }
96
+
97
+ export function isUpgradeSourceIssueActionable(issue: UpgradeInfo['sourceIssue']): issue is ActionableUpgradeSourceIssue {
98
+ return Boolean(issue && issue !== 'ambiguous_repository')
99
+ }
100
+
37
101
  const MIN_WIDTH = 500
38
102
  const MAX_WIDTH_PERCENT = 0.8
39
103
  const DEFAULT_WIDTH = 1000
40
104
 
41
- export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOpen = true }: HelmReleaseDrawerProps) {
105
+ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOpen = true, rightInset = 0 }: HelmReleaseDrawerProps) {
42
106
  const navigate = useNavigate()
43
107
  const [activeTab, setActiveTab] = useState<TabId>('overview')
44
108
  const [copied, setCopied] = useState<string | null>(null)
@@ -52,8 +116,18 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
52
116
  const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
53
117
  const [showTrackSource, setShowTrackSource] = useState(false)
54
118
  const [selectedVersion, setSelectedVersion] = useState<string | null>(null)
119
+ const [adjustValues, setAdjustValues] = useState(false)
120
+ const [renderAdjustValues, setRenderAdjustValues] = useState(false)
121
+ const [editedUpgradeYaml, setEditedUpgradeYaml] = useState('')
122
+ const [upgradeYamlError, setUpgradeYamlError] = useState<string | null>(null)
123
+ const [upgradeValuesSeeded, setUpgradeValuesSeeded] = useState(false)
124
+ const [upgradePreview, setUpgradePreview] = useState<ValuesPreviewResponse | null>(null)
125
+ const [upgradePreviewRequest, setUpgradePreviewRequest] = useState<UpgradePreviewRequest | null>(null)
126
+ const [showUpgradePreview, setShowUpgradePreview] = useState(false)
55
127
  const resizeStartX = useRef(0)
56
128
  const resizeStartWidth = useRef(DEFAULT_WIDTH)
129
+ const targetVersionRef = useRef('')
130
+ const editedUpgradeYamlRef = useRef('')
57
131
  const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
58
132
  // Cloud viewers can't view release manifests / values / diffs
59
133
  // (backend gate at requireCloudRole('member')). Skip the queries
@@ -85,6 +159,16 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
85
159
  canViewSensitive,
86
160
  selectedRevision,
87
161
  )
162
+ const {
163
+ data: upgradeValues,
164
+ isLoading: upgradeValuesLoading,
165
+ error: upgradeValuesError,
166
+ } = useHelmValues(
167
+ helmNamespace,
168
+ release.name,
169
+ false,
170
+ canViewSensitive && showUpgradeConfirm && adjustValues,
171
+ )
88
172
 
89
173
  // Lazy check for upgrade availability
90
174
  const { data: upgradeInfo, isLoading: upgradeLoading, error: upgradeError } = useHelmUpgradeInfo(
@@ -92,11 +176,17 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
92
176
  release.name
93
177
  )
94
178
  const upgradeErrorMessage = upgradeError instanceof Error ? upgradeError.message : 'Upgrade check failed'
179
+ const upgradeSourceIssue = upgradeInfo ? getUpgradeSourceIssue(upgradeInfo) : undefined
95
180
 
96
181
  // Available versions for the upgrade dialog's picker — only fetched while the
97
182
  // confirm dialog is open. Default the selection to latest when it opens.
98
- const { data: availableVersions } = useHelmReleaseVersions(helmNamespace, release.name, showUpgradeConfirm)
183
+ const {
184
+ data: availableVersions,
185
+ isLoading: availableVersionsLoading,
186
+ error: availableVersionsError,
187
+ } = useHelmReleaseVersions(helmNamespace, release.name, showUpgradeConfirm)
99
188
  const targetVersion = selectedVersion ?? upgradeInfo?.latestVersion ?? ''
189
+ const selectableVersions = availableVersions && availableVersions.length > 1 ? availableVersions : null
100
190
  // Semver compare, not list-position: the installed version may be older than
101
191
  // the newest-N versions the picker shows, so it isn't always in the list.
102
192
  const isDowngrade = Boolean(
@@ -106,12 +196,18 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
106
196
 
107
197
  // Mutations for actions
108
198
  const uninstallMutation = useHelmUninstall()
199
+ const upgradePreviewMutation = useHelmPreviewValues()
200
+ const isPreviewingUpgrade = upgradePreviewMutation.isPending
109
201
  const queryClient = useQueryClient()
110
202
  const [upgradeProgress, setUpgradeProgress] = useState<{ phase: string; message: string }[]>([])
111
203
  const [isUpgrading, setIsUpgrading] = useState(false)
112
204
  const [rollbackProgress, setRollbackProgress] = useState<{ phase: string; message: string }[]>([])
113
205
  const [isRollingBack, setIsRollingBack] = useState(false)
114
206
 
207
+ useEffect(() => {
208
+ targetVersionRef.current = targetVersion
209
+ }, [targetVersion])
210
+
115
211
  // ESC key handler
116
212
  useEffect(() => {
117
213
  const handleKeyDown = (event: KeyboardEvent) => {
@@ -257,8 +353,90 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
257
353
  )
258
354
  }
259
355
 
260
- const handleUpgradeConfirm = async () => {
261
- if (!targetVersion) return
356
+ const resetUpgradeDialog = () => {
357
+ setUpgradeProgress([])
358
+ setSelectedVersion(null)
359
+ setAdjustValues(false)
360
+ setRenderAdjustValues(false)
361
+ setEditedUpgradeYaml('')
362
+ editedUpgradeYamlRef.current = ''
363
+ setUpgradeYamlError(null)
364
+ setUpgradeValuesSeeded(false)
365
+ setUpgradePreview(null)
366
+ setUpgradePreviewRequest(null)
367
+ setShowUpgradePreview(false)
368
+ upgradePreviewMutation.reset()
369
+ }
370
+
371
+ useEffect(() => {
372
+ if (!adjustValues || !upgradeValues || upgradeValuesSeeded) return
373
+ const nextYaml = userSuppliedToYaml(upgradeValues.userSupplied)
374
+ editedUpgradeYamlRef.current = nextYaml
375
+ setEditedUpgradeYaml(nextYaml)
376
+ setUpgradeYamlError(null)
377
+ setUpgradeValuesSeeded(true)
378
+ }, [adjustValues, upgradeValues, upgradeValuesSeeded])
379
+
380
+ const handleToggleAdjustValues = () => {
381
+ if (adjustValues) {
382
+ setAdjustValues(false)
383
+ return
384
+ }
385
+
386
+ setRenderAdjustValues(true)
387
+ setEditedUpgradeYaml('')
388
+ editedUpgradeYamlRef.current = ''
389
+ setUpgradeYamlError(null)
390
+ setUpgradeValuesSeeded(false)
391
+ upgradePreviewMutation.reset()
392
+ setAdjustValues(true)
393
+ }
394
+
395
+ const getEditedUpgradeValues = () => {
396
+ if (!adjustValues) return {}
397
+ const parsed = parseUpgradeValuesYaml(editedUpgradeYamlRef.current)
398
+ setUpgradeYamlError(parsed.error)
399
+ return parsed.values
400
+ }
401
+
402
+ const handleUpgradePreview = async () => {
403
+ if (!targetVersion || upgradeValuesLoading || upgradeValuesError || !upgradeValuesSeeded) return
404
+ const editedValues = getEditedUpgradeValues()
405
+ if (!editedValues) return
406
+ const previewYaml = editedUpgradeYamlRef.current
407
+ const previewRequest: UpgradePreviewRequest = {
408
+ targetVersion,
409
+ values: editedValues,
410
+ repositoryName: upgradeInfo?.repositoryName,
411
+ }
412
+ try {
413
+ const result = await upgradePreviewMutation.mutateAsync({
414
+ namespace: helmNamespace,
415
+ name: release.name,
416
+ values: previewRequest.values,
417
+ version: previewRequest.targetVersion,
418
+ repository: previewRequest.repositoryName,
419
+ })
420
+ if (targetVersionRef.current !== previewRequest.targetVersion || editedUpgradeYamlRef.current !== previewYaml) return
421
+ setUpgradePreview(result)
422
+ setUpgradePreviewRequest(previewRequest)
423
+ setShowUpgradePreview(true)
424
+ } catch {
425
+ return
426
+ }
427
+ }
428
+
429
+ const runUpgrade = async (versionToApply: string, editedValuesOverride: Record<string, unknown> | undefined, repositoryName: string | undefined) => {
430
+ if (!versionToApply) return
431
+
432
+ let editedValues = editedValuesOverride
433
+ if (editedValues === undefined && adjustValues) {
434
+ if (upgradeValuesLoading || upgradeValuesError || !upgradeValuesSeeded) return
435
+ editedValues = getEditedUpgradeValues() ?? undefined
436
+ if (!editedValues) return
437
+ }
438
+
439
+ setShowUpgradePreview(false)
262
440
  setIsUpgrading(true)
263
441
  setUpgradeProgress([])
264
442
 
@@ -266,8 +444,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
266
444
  await upgradeWithProgress(
267
445
  helmNamespace,
268
446
  release.name,
269
- targetVersion,
270
- upgradeInfo?.repositoryName,
447
+ versionToApply,
448
+ repositoryName,
271
449
  (event) => {
272
450
  if (event.type === 'progress' && event.message) {
273
451
  setUpgradeProgress(prev => [...prev, {
@@ -275,24 +453,25 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
275
453
  message: event.message || '',
276
454
  }])
277
455
  }
278
- }
456
+ },
457
+ editedValues
279
458
  )
280
459
 
281
460
  setUpgradeProgress(prev => [...prev, {
282
461
  phase: 'complete',
283
- message: `Successfully upgraded to ${targetVersion}`,
462
+ message: `Successfully upgraded to ${versionToApply}`,
284
463
  }])
285
464
 
286
465
  // Invalidate queries
287
466
  queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
288
467
  queryClient.invalidateQueries({ queryKey: ['helm-release', helmNamespace, release.name] })
289
468
  queryClient.invalidateQueries({ queryKey: ['helm-upgrade-info', helmNamespace, release.name] })
469
+ queryClient.invalidateQueries({ queryKey: ['helm-values', helmNamespace, release.name] })
290
470
  queryClient.invalidateQueries({ queryKey: ['helm-batch-upgrade-info'] })
291
471
 
292
472
  setTimeout(() => {
293
473
  setShowUpgradeConfirm(false)
294
- setUpgradeProgress([])
295
- setSelectedVersion(null)
474
+ resetUpgradeDialog()
296
475
  refetch()
297
476
  switchTab('resources')
298
477
  }, 1500)
@@ -306,6 +485,15 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
306
485
  }
307
486
  }
308
487
 
488
+ const handleUpgradeConfirm = () => {
489
+ void runUpgrade(targetVersion, undefined, upgradeInfo?.repositoryName)
490
+ }
491
+
492
+ const handleApplyUpgradePreview = () => {
493
+ if (!upgradePreviewRequest) return
494
+ void runUpgrade(upgradePreviewRequest.targetVersion, upgradePreviewRequest.values, upgradePreviewRequest.repositoryName)
495
+ }
496
+
309
497
  const headerHeight = 49
310
498
  const dockInset = useDockReservedHeight()
311
499
 
@@ -325,7 +513,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
325
513
  TRANSITION_DRAWER,
326
514
  isOpen ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
327
515
  )}
328
- style={{ width: renderedDrawerWidth, top: headerHeight, height: `calc(100vh - ${headerHeight}px - ${dockInset}px)` }}
516
+ style={{ width: renderedDrawerWidth, top: headerHeight, right: rightInset, height: `calc(100vh - ${headerHeight}px - ${dockInset}px)` }}
329
517
  >
330
518
  {/* Resize handle */}
331
519
  <div
@@ -401,10 +589,15 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
401
589
  source via GitOps
402
590
  </span>
403
591
  </Tooltip>
404
- ) : upgradeInfo.untracked ? (
405
- // Helm doesn't record the install source. Offer to register one so
406
- // Radar can track upgrades for the user's own (e.g. OCI) charts.
407
- <Tooltip content={canHelmWrite ? "Radar can't tell where this chart was installed from. Register your chart source to track upgrades." : upgradeInfo.error}>
592
+ ) : upgradeSourceIssue && !isUpgradeSourceIssueActionable(upgradeSourceIssue) ? (
593
+ <Tooltip content={getUpgradeSourceIssueTooltip(upgradeSourceIssue, upgradeInfo.error, canHelmWrite, helmActReason)}>
594
+ <span className="badge bg-theme-hover/50 text-theme-text-secondary">
595
+ <Link2 className="w-3 h-3" />
596
+ {getUpgradeSourceIssueLabel(upgradeSourceIssue)}
597
+ </span>
598
+ </Tooltip>
599
+ ) : isUpgradeSourceIssueActionable(upgradeSourceIssue) ? (
600
+ <Tooltip content={getUpgradeSourceIssueTooltip(upgradeSourceIssue, upgradeInfo.error, canHelmWrite, helmActReason)}>
408
601
  <button
409
602
  onClick={() => canHelmWrite && setShowTrackSource(true)}
410
603
  disabled={!canHelmWrite}
@@ -414,12 +607,10 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
414
607
  )}
415
608
  >
416
609
  <Link2 className="w-3 h-3" />
417
- upgrade source not tracked
610
+ {getUpgradeSourceIssueLabel(upgradeSourceIssue)}
418
611
  </button>
419
612
  </Tooltip>
420
613
  ) : (
421
- // Repo-side error (stale/broken index, classic ambiguity) — not an
422
- // OCI tracking issue, so surface it without steering to registration.
423
614
  <Tooltip content={upgradeInfo.error}>
424
615
  <span className="badge bg-theme-hover/50 text-theme-text-secondary">
425
616
  upgrade source unresolved
@@ -626,8 +817,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
626
817
  open={showUpgradeConfirm}
627
818
  onClose={() => {
628
819
  setShowUpgradeConfirm(false)
629
- setUpgradeProgress([])
630
- setSelectedVersion(null)
820
+ resetUpgradeDialog()
631
821
  if (isUpgrading) {
632
822
  // Upgrade continues server-side — switch to resources tab to monitor
633
823
  setIsUpgrading(false)
@@ -638,58 +828,201 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
638
828
  title="Upgrade Release"
639
829
  message={`Upgrade "${release.name}" to version ${targetVersion}?`}
640
830
  details={upgradeProgress.length === 0
641
- ? `The chart will move from version ${upgradeInfo?.currentVersion} to ${targetVersion}. Your existing values will be preserved. The change is applied to your cluster immediately.`
831
+ ? `The chart will move from version ${upgradeInfo?.currentVersion} to ${targetVersion}. ${adjustValues ? 'Your edited values will be applied.' : 'Your existing values will be preserved.'} The change is applied to your cluster immediately.`
642
832
  : undefined
643
833
  }
644
834
  confirmLabel={isDowngrade ? 'Downgrade' : 'Upgrade'}
645
835
  variant="warning"
646
836
  isLoading={isUpgrading}
837
+ confirmDisabled={isPreviewingUpgrade || (adjustValues && (upgradeValuesLoading || !!upgradeValuesError || !upgradeValuesSeeded || !!upgradeYamlError))}
647
838
  isClosable
839
+ className="max-w-2xl"
648
840
  >
649
- {upgradeProgress.length === 0 && availableVersions && availableVersions.length > 1 && (
841
+ {upgradeProgress.length === 0 && (
650
842
  <div className="mb-1">
651
- <label htmlFor="upgrade-version" className="block text-sm font-medium text-theme-text-secondary mb-1.5">
843
+ <label
844
+ id="upgrade-version-label"
845
+ htmlFor={selectableVersions ? 'upgrade-version' : undefined}
846
+ className="block text-sm font-medium text-theme-text-secondary mb-1.5"
847
+ >
652
848
  Target version
653
849
  </label>
654
- <select
655
- id="upgrade-version"
656
- value={targetVersion}
657
- onChange={(e) => setSelectedVersion(e.target.value)}
658
- disabled={isUpgrading}
659
- 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"
660
- >
661
- {availableVersions.map((v) => (
662
- <option key={v} value={v}>
663
- {v}
664
- {v === upgradeInfo?.latestVersion ? ' (latest)' : ''}
665
- {v === upgradeInfo?.currentVersion ? ' (current)' : ''}
666
- </option>
667
- ))}
668
- </select>
850
+ {availableVersionsLoading ? (
851
+ <div
852
+ aria-labelledby="upgrade-version-label"
853
+ aria-live="polite"
854
+ className="flex h-10 items-center gap-2 rounded-lg border border-theme-border-light bg-theme-elevated px-3 text-sm text-theme-text-secondary"
855
+ >
856
+ <Loader2 className="h-4 w-4 animate-spin" />
857
+ Loading available versions...
858
+ </div>
859
+ ) : selectableVersions ? (
860
+ <select
861
+ id="upgrade-version"
862
+ value={targetVersion}
863
+ onChange={(e) => {
864
+ targetVersionRef.current = e.target.value
865
+ setSelectedVersion(e.target.value)
866
+ }}
867
+ disabled={isUpgrading || isPreviewingUpgrade}
868
+ 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"
869
+ >
870
+ {selectableVersions.map((v) => (
871
+ <option key={v} value={v}>
872
+ {v}
873
+ {v === upgradeInfo?.latestVersion ? ' (latest)' : ''}
874
+ {v === upgradeInfo?.currentVersion ? ' (current)' : ''}
875
+ </option>
876
+ ))}
877
+ </select>
878
+ ) : (
879
+ <div
880
+ aria-labelledby="upgrade-version-label"
881
+ className="flex h-10 items-center rounded-lg border border-theme-border-light bg-theme-elevated px-3 text-sm text-theme-text-primary"
882
+ >
883
+ {targetVersion}
884
+ </div>
885
+ )}
669
886
  {isDowngrade && (
670
887
  <p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
671
888
  This is a downgrade from {upgradeInfo?.currentVersion}.
672
889
  </p>
673
890
  )}
674
- {availableVersions.length >= 50 && (
891
+ {availableVersionsError && (
675
892
  <p className="mt-1 text-xs text-theme-text-tertiary">
676
- Showing the 50 newest versions. Type to filter.
893
+ Version list unavailable; using the detected target version.
677
894
  </p>
678
895
  )}
896
+ {availableVersions && availableVersions.length >= 50 && (
897
+ <p className="mt-1 text-xs text-theme-text-tertiary">
898
+ Showing the 50 newest versions.
899
+ </p>
900
+ )}
901
+ </div>
902
+ )}
903
+ {upgradeProgress.length === 0 && canHelmWrite && canViewSensitive && (
904
+ <div className="mt-3 border-t border-theme-border pt-3">
905
+ <Tooltip content="Edit the user-supplied Helm values that Radar will pass to this upgrade.">
906
+ <button
907
+ type="button"
908
+ onClick={handleToggleAdjustValues}
909
+ disabled={isUpgrading || isPreviewingUpgrade}
910
+ aria-expanded={adjustValues}
911
+ aria-controls="helm-upgrade-values-panel"
912
+ className="flex items-center gap-1.5 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50"
913
+ >
914
+ <ChevronRight className={clsx('w-4 h-4 transition-transform duration-200', adjustValues && 'rotate-90')} />
915
+ <SlidersHorizontal className="w-3.5 h-3.5" />
916
+ Adjust your values (optional)
917
+ </button>
918
+ </Tooltip>
919
+ <div
920
+ id="helm-upgrade-values-panel"
921
+ className={`issue-details-motion ${adjustValues ? 'issue-details-motion-open' : ''}`}
922
+ onTransitionEnd={(event) => {
923
+ if (event.target !== event.currentTarget) return
924
+ if (event.propertyName !== 'grid-template-rows') return
925
+ if (!adjustValues) {
926
+ setRenderAdjustValues(false)
927
+ }
928
+ }}
929
+ >
930
+ <div className="overflow-hidden">
931
+ {renderAdjustValues && (
932
+ <div className="mt-2">
933
+ <p className="mb-2 text-xs text-theme-text-tertiary">
934
+ These are your current settings — edit them to carry into {targetVersion}. What you see here is exactly what gets applied.
935
+ </p>
936
+ {upgradeValuesError && (
937
+ <div className="mt-2 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
938
+ Couldn’t load current values: {upgradeValuesError instanceof Error ? upgradeValuesError.message : 'Unknown error'}
939
+ </div>
940
+ )}
941
+ {(upgradeValuesLoading || !upgradeValuesSeeded) && (
942
+ <div className="flex min-h-[180px] items-center gap-2 rounded border border-theme-border bg-theme-elevated px-3 text-sm text-theme-text-secondary">
943
+ <Loader2 className="w-4 h-4 animate-spin" />
944
+ Loading current values...
945
+ </div>
946
+ )}
947
+ {!upgradeValuesLoading && !upgradeValuesError && upgradeValuesSeeded && (
948
+ <textarea
949
+ defaultValue={editedUpgradeYaml}
950
+ onChange={(event) => {
951
+ editedUpgradeYamlRef.current = event.target.value
952
+ if (upgradeYamlError) setUpgradeYamlError(null)
953
+ }}
954
+ onBlur={() => setUpgradeYamlError(parseUpgradeValuesYaml(editedUpgradeYamlRef.current).error)}
955
+ aria-label="Edited Helm values YAML"
956
+ readOnly={isPreviewingUpgrade || isUpgrading}
957
+ spellCheck={false}
958
+ autoCorrect="off"
959
+ autoCapitalize="off"
960
+ rows={9}
961
+ className="max-h-[360px] min-h-[180px] w-full resize-y rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 font-mono text-xs leading-5 text-theme-text-primary placeholder:text-theme-text-tertiary focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 read-only:opacity-70"
962
+ />
963
+ )}
964
+ {upgradeYamlError && (
965
+ <div className="mt-2 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
966
+ {upgradeYamlError}
967
+ </div>
968
+ )}
969
+ {upgradePreviewMutation.error && (
970
+ <div className="mt-2 px-3 py-2 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
971
+ Couldn’t preview against {targetVersion}: {upgradePreviewMutation.error.message}. You can still upgrade.
972
+ </div>
973
+ )}
974
+ <div className="mt-2 flex justify-end">
975
+ <Tooltip
976
+ content="Render this release with the selected chart version and edited values, then show the manifest diff. Nothing is applied until you confirm from the preview."
977
+ wrapperClassName="shrink-0"
978
+ >
979
+ <button
980
+ type="button"
981
+ onClick={handleUpgradePreview}
982
+ disabled={upgradeValuesLoading || !!upgradeValuesError || !upgradeValuesSeeded || !!upgradeYamlError || isPreviewingUpgrade || isUpgrading}
983
+ className="flex items-center gap-1 px-2.5 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded border border-theme-border disabled:opacity-50 disabled:cursor-not-allowed"
984
+ >
985
+ {upgradePreviewMutation.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Eye className="w-3.5 h-3.5" />}
986
+ Preview what changes
987
+ </button>
988
+ </Tooltip>
989
+ </div>
990
+ </div>
991
+ )}
992
+ </div>
993
+ </div>
679
994
  </div>
680
995
  )}
681
996
  {upgradeProgress.length > 0 && <ProgressLog entries={upgradeProgress} />}
682
997
  </ConfirmDialog>
683
998
 
999
+ {showUpgradePreview && upgradePreview && upgradePreviewRequest && (
1000
+ <ValuesDiffPreview
1001
+ previewData={upgradePreview}
1002
+ onClose={() => setShowUpgradePreview(false)}
1003
+ onApply={handleApplyUpgradePreview}
1004
+ isApplying={isUpgrading}
1005
+ title={`Preview upgrade to ${upgradePreviewRequest.targetVersion}`}
1006
+ applyLabel={upgradeInfo?.currentVersion && compareVersions(upgradePreviewRequest.targetVersion, upgradeInfo.currentVersion) === -1 ? 'Downgrade' : 'Upgrade'}
1007
+ />
1008
+ )}
1009
+
684
1010
  <TrackChartSourceDialog
685
1011
  open={showTrackSource}
686
1012
  onClose={() => setShowTrackSource(false)}
687
1013
  chartName={releaseDetail?.chart}
1014
+ sourceIssue={upgradeSourceIssue}
1015
+ sourceError={upgradeInfo?.error}
688
1016
  />
689
1017
  </div>
690
1018
  )
691
1019
  }
692
1020
 
1021
+ function userSuppliedToYaml(userSupplied: Record<string, unknown> | undefined): string {
1022
+ if (!userSupplied || Object.keys(userSupplied).length === 0) return ''
1023
+ return yaml.stringify(userSupplied, { lineWidth: 0 })
1024
+ }
1025
+
693
1026
  // Shared progress log for streaming Helm operations
694
1027
  function ProgressLog({ entries }: { entries: { phase: string; message: string }[] }) {
695
1028
  return (
@@ -1,7 +1,7 @@
1
1
  import { useState, useMemo, useRef, useEffect, useCallback, forwardRef } from 'react'
2
2
  import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
3
3
  import { Package, Search, ArrowUpCircle, LayoutGrid, List, Shield, GitBranch, ChevronRight, RotateCcw, Clock } from 'lucide-react'
4
- import { PaneLoader, PageHeader, SortableTh, FreshnessControl, type SortDir } from '@skyhook-io/k8s-ui'
4
+ import { PaneLoader, PageHeader, SortableTh, FreshnessControl, type SortDir, Input } from '@skyhook-io/k8s-ui'
5
5
  import { useConnection } from '../../context/ConnectionContext'
6
6
  import { clsx } from 'clsx'
7
7
  import { useHelmReleases, useHelmBatchUpgradeInfo, isForbiddenError } from '../../api/client'
@@ -240,9 +240,8 @@ export function HelmView({ namespaces, selectedRelease, onReleaseClick }: HelmVi
240
240
  <div className="flex items-center gap-4 px-4 py-3 border-b border-theme-border bg-theme-surface/50 shrink-0">
241
241
  <div className="flex-1 relative">
242
242
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
243
- <input
243
+ <Input
244
244
  ref={searchInputRef}
245
- type="text"
246
245
  placeholder="Search releases..."
247
246
  value={searchTerm}
248
247
  onChange={(e) => setSearchTerm(e.target.value)}
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
2
2
  import { X, Package, ChevronRight, ChevronLeft, Play, Loader2, AlertTriangle, CheckCircle, User, BookOpen, Link as LinkIcon, Star, BadgeCheck, Shield, Globe, Building2, Plus, Minus, Terminal } from 'lucide-react'
3
- import { PaneLoader } from '@skyhook-io/k8s-ui'
3
+ import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
4
4
  import { clsx } from 'clsx'
5
5
  import yaml from 'yaml'
6
6
  import { createPatch } from 'diff'
@@ -561,8 +561,7 @@ function InfoStep({
561
561
  <label className="block text-sm font-medium text-theme-text-secondary mb-2">
562
562
  Release Name
563
563
  </label>
564
- <input
565
- type="text"
564
+ <Input
566
565
  value={releaseName}
567
566
  onChange={(e) => setReleaseName(e.target.value)}
568
567
  placeholder="my-release"
@@ -591,8 +590,7 @@ function InfoStep({
591
590
  <label className="block text-sm font-medium text-theme-text-secondary mb-2">
592
591
  Namespace
593
592
  </label>
594
- <input
595
- type="text"
593
+ <Input
596
594
  list="namespace-suggestions"
597
595
  value={namespace}
598
596
  onChange={(e) => setNamespace(e.target.value)}