@skyhook-io/radar-app 0.1.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 (251) hide show
  1. package/README.md +67 -0
  2. package/package.json +80 -0
  3. package/src/App.tsx +1538 -0
  4. package/src/RadarApp.tsx +145 -0
  5. package/src/api/apiResources.ts +28 -0
  6. package/src/api/client.ts +2583 -0
  7. package/src/api/config.ts +116 -0
  8. package/src/api/traffic.ts +139 -0
  9. package/src/components/ConnectionErrorView.tsx +272 -0
  10. package/src/components/ContextSwitcher.tsx +481 -0
  11. package/src/components/DebugOverlay.tsx +94 -0
  12. package/src/components/UserMenu.tsx +87 -0
  13. package/src/components/audit/AuditSettingsDialog.tsx +162 -0
  14. package/src/components/audit/AuditView.tsx +123 -0
  15. package/src/components/cost/CostTrendChart.tsx +388 -0
  16. package/src/components/cost/CostView.tsx +545 -0
  17. package/src/components/dock/BottomDock.tsx +96 -0
  18. package/src/components/dock/DockContext.tsx +11 -0
  19. package/src/components/dock/LocalTerminalTab.tsx +22 -0
  20. package/src/components/dock/LogsTab.tsx +26 -0
  21. package/src/components/dock/NodeTerminalTab.tsx +50 -0
  22. package/src/components/dock/TerminalTab.tsx +42 -0
  23. package/src/components/dock/TrafficFlowListTab.tsx +18 -0
  24. package/src/components/dock/WorkloadLogsTab.tsx +23 -0
  25. package/src/components/dock/index.ts +2 -0
  26. package/src/components/gitops/GitOpsActions.tsx +1 -0
  27. package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
  28. package/src/components/gitops/ManagedResourcesList.tsx +1 -0
  29. package/src/components/gitops/SyncCountdown.tsx +1 -0
  30. package/src/components/gitops/index.ts +4 -0
  31. package/src/components/helm/ChartBrowser.tsx +580 -0
  32. package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
  33. package/src/components/helm/HelmView.tsx +475 -0
  34. package/src/components/helm/InstallWizard.tsx +1060 -0
  35. package/src/components/helm/ManifestDiffViewer.tsx +91 -0
  36. package/src/components/helm/ManifestViewer.tsx +61 -0
  37. package/src/components/helm/OwnedResources.tsx +465 -0
  38. package/src/components/helm/RevisionHistory.tsx +167 -0
  39. package/src/components/helm/ValuesDiffPreview.tsx +190 -0
  40. package/src/components/helm/ValuesViewer.tsx +365 -0
  41. package/src/components/helm/helm-utils.ts +37 -0
  42. package/src/components/home/ActivitySummary.tsx +262 -0
  43. package/src/components/home/CertificateHealthCard.tsx +105 -0
  44. package/src/components/home/ClusterHealthCard.tsx +483 -0
  45. package/src/components/home/CostCard.tsx +112 -0
  46. package/src/components/home/HealthRing.tsx +1 -0
  47. package/src/components/home/HelmSummary.tsx +129 -0
  48. package/src/components/home/HomeView.tsx +224 -0
  49. package/src/components/home/MCPSetupDialog.tsx +417 -0
  50. package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
  51. package/src/components/home/TopologyPreview.tsx +219 -0
  52. package/src/components/home/TrafficSummary.tsx +154 -0
  53. package/src/components/logs/JsonLogLine.tsx +1 -0
  54. package/src/components/logs/LogCore.tsx +2 -0
  55. package/src/components/logs/LogsViewer.tsx +44 -0
  56. package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
  57. package/src/components/logs/useLogBuffer.ts +2 -0
  58. package/src/components/logs/useLogSearch.ts +1 -0
  59. package/src/components/portforward/PortForwardButton.tsx +375 -0
  60. package/src/components/portforward/PortForwardManager.tsx +871 -0
  61. package/src/components/resource/PrometheusCharts.tsx +687 -0
  62. package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
  63. package/src/components/resources/ImageFilesystemModal.tsx +745 -0
  64. package/src/components/resources/PodFilesystemModal.tsx +407 -0
  65. package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
  66. package/src/components/resources/ResourcesView.tsx +190 -0
  67. package/src/components/resources/drawer-components.tsx +1 -0
  68. package/src/components/resources/file-browser-utils.ts +35 -0
  69. package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
  70. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
  71. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
  72. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
  73. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
  74. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
  75. package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
  76. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
  77. package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
  78. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
  79. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
  80. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
  81. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
  82. package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
  83. package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
  84. package/src/components/resources/renderers/EventRenderer.tsx +1 -0
  85. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
  86. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
  87. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
  88. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
  89. package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
  90. package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
  91. package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
  92. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
  93. package/src/components/resources/renderers/HPARenderer.tsx +1 -0
  94. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
  95. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
  96. package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
  97. package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
  98. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
  99. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
  100. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
  101. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
  102. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
  103. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
  104. package/src/components/resources/renderers/JobRenderer.tsx +1 -0
  105. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
  106. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
  107. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
  108. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
  109. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
  110. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
  111. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
  112. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
  113. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
  114. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
  116. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
  117. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
  118. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
  119. package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
  120. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
  121. package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
  122. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
  123. package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
  124. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
  125. package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
  126. package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
  127. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
  128. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
  129. package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
  130. package/src/components/resources/renderers/PodRenderer.tsx +94 -0
  131. package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
  132. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
  133. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
  134. package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
  135. package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
  136. package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
  137. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
  138. package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
  139. package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
  140. package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
  141. package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
  142. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
  143. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
  144. package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
  145. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
  146. package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
  147. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
  148. package/src/components/resources/renderers/VPARenderer.tsx +1 -0
  149. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
  150. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
  151. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
  152. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
  153. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
  154. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
  155. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
  156. package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
  157. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
  158. package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
  159. package/src/components/resources/renderers/argo-cells.tsx +1 -0
  160. package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
  161. package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
  162. package/src/components/resources/renderers/eso-cells.tsx +1 -0
  163. package/src/components/resources/renderers/flux-cells.tsx +1 -0
  164. package/src/components/resources/renderers/index.ts +91 -0
  165. package/src/components/resources/renderers/istio-cells.tsx +1 -0
  166. package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
  167. package/src/components/resources/renderers/keda-cells.tsx +1 -0
  168. package/src/components/resources/renderers/knative-cells.tsx +1 -0
  169. package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
  170. package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
  171. package/src/components/resources/renderers/traefik-cells.tsx +1 -0
  172. package/src/components/resources/renderers/trivy-cells.tsx +1 -0
  173. package/src/components/resources/renderers/trivy-shared.tsx +1 -0
  174. package/src/components/resources/renderers/velero-cells.tsx +1 -0
  175. package/src/components/resources/resource-utils-argo.ts +2 -0
  176. package/src/components/resources/resource-utils-certmanager.ts +2 -0
  177. package/src/components/resources/resource-utils-cnpg.ts +2 -0
  178. package/src/components/resources/resource-utils-eso.ts +2 -0
  179. package/src/components/resources/resource-utils-flux.ts +2 -0
  180. package/src/components/resources/resource-utils-istio.ts +2 -0
  181. package/src/components/resources/resource-utils-karpenter.ts +2 -0
  182. package/src/components/resources/resource-utils-keda.ts +2 -0
  183. package/src/components/resources/resource-utils-knative.ts +2 -0
  184. package/src/components/resources/resource-utils-kyverno.ts +2 -0
  185. package/src/components/resources/resource-utils-prometheus.ts +2 -0
  186. package/src/components/resources/resource-utils-traefik.ts +1 -0
  187. package/src/components/resources/resource-utils-trivy.ts +2 -0
  188. package/src/components/resources/resource-utils-velero.ts +2 -0
  189. package/src/components/resources/resource-utils.ts +5 -0
  190. package/src/components/settings/SettingsDialog.tsx +537 -0
  191. package/src/components/shared/CreateResourceDialog.tsx +17 -0
  192. package/src/components/shared/EditableYamlView.tsx +24 -0
  193. package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
  194. package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
  195. package/src/components/timeline/DiffViewer.tsx +1 -0
  196. package/src/components/timeline/TimelineList.tsx +69 -0
  197. package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
  198. package/src/components/timeline/TimelineView.tsx +157 -0
  199. package/src/components/timeline/shared.tsx +1 -0
  200. package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
  201. package/src/components/traffic/TrafficFlowList.tsx +415 -0
  202. package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
  203. package/src/components/traffic/TrafficGraph.tsx +1546 -0
  204. package/src/components/traffic/TrafficView.tsx +1213 -0
  205. package/src/components/traffic/TrafficWizard.tsx +386 -0
  206. package/src/components/traffic/index.ts +3 -0
  207. package/src/components/ui/CodeViewer.tsx +8 -0
  208. package/src/components/ui/CommandPalette.tsx +460 -0
  209. package/src/components/ui/ConfirmDialog.tsx +1 -0
  210. package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
  211. package/src/components/ui/ErrorBoundary.tsx +46 -0
  212. package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
  213. package/src/components/ui/Markdown.tsx +108 -0
  214. package/src/components/ui/MetricsChart.tsx +1 -0
  215. package/src/components/ui/NamespaceSelector.tsx +436 -0
  216. package/src/components/ui/ResourceBar.tsx +1 -0
  217. package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
  218. package/src/components/ui/Toast.tsx +1 -0
  219. package/src/components/ui/Tooltip.tsx +1 -0
  220. package/src/components/ui/UpdateNotification.tsx +299 -0
  221. package/src/components/ui/YamlEditor.tsx +1 -0
  222. package/src/components/workload/WorkloadView.tsx +532 -0
  223. package/src/context/ConnectionContext.tsx +173 -0
  224. package/src/context/ContextSwitchContext.tsx +56 -0
  225. package/src/context/NavCustomization.tsx +62 -0
  226. package/src/context/ThemeContext.tsx +97 -0
  227. package/src/contexts/CapabilitiesContext.tsx +130 -0
  228. package/src/hooks/useAnimatedUnmount.ts +1 -0
  229. package/src/hooks/useDesktopDownload.ts +41 -0
  230. package/src/hooks/useEventSource.ts +262 -0
  231. package/src/hooks/useFavorites.ts +69 -0
  232. package/src/hooks/useKeyboardShortcuts.tsx +7 -0
  233. package/src/hooks/useRefreshAnimation.ts +1 -0
  234. package/src/index.css +243 -0
  235. package/src/index.ts +17 -0
  236. package/src/main.tsx +158 -0
  237. package/src/types/gitops.ts +2 -0
  238. package/src/types.ts +3 -0
  239. package/src/utils/animation.ts +2 -0
  240. package/src/utils/badge-colors.ts +2 -0
  241. package/src/utils/context-name.ts +2 -0
  242. package/src/utils/desktop-download.ts +66 -0
  243. package/src/utils/desktop-open-folder.ts +21 -0
  244. package/src/utils/format.ts +2 -0
  245. package/src/utils/log-format.ts +12 -0
  246. package/src/utils/navigation.ts +23 -0
  247. package/src/utils/resource-hierarchy.ts +2 -0
  248. package/src/utils/resource-icons.ts +2 -0
  249. package/src/utils/skeleton-yaml.ts +2 -0
  250. package/src/utils/traffic-colors.ts +54 -0
  251. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,1060 @@
1
+ import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
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 { clsx } from 'clsx'
4
+ import yaml from 'yaml'
5
+ import { createPatch } from 'diff'
6
+ import { useQueryClient } from '@tanstack/react-query'
7
+ import { useChartDetail, useNamespaces, useArtifactHubChart, installChartWithProgress, type InstallProgressEvent } from '../../api/client'
8
+ import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
9
+ import type { ChartSource, ChartDetail, ArtifactHubChartDetail } from '../../types'
10
+ import { YamlEditor } from '../ui/YamlEditor'
11
+ import { Tooltip } from '../ui/Tooltip'
12
+ import { Markdown } from '../ui/Markdown'
13
+ import { SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
14
+
15
+ // Deep merge two objects — values from `overrides` take priority
16
+ function deepMerge(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {
17
+ const result = { ...base }
18
+ for (const key of Object.keys(overrides)) {
19
+ const baseVal = base[key]
20
+ const overVal = overrides[key]
21
+ if (baseVal && overVal && typeof baseVal === 'object' && typeof overVal === 'object' && !Array.isArray(baseVal) && !Array.isArray(overVal)) {
22
+ result[key] = deepMerge(baseVal as Record<string, unknown>, overVal as Record<string, unknown>)
23
+ } else {
24
+ result[key] = overVal
25
+ }
26
+ }
27
+ return result
28
+ }
29
+
30
+ interface InstallWizardProps {
31
+ repo: string
32
+ chartName: string
33
+ version: string
34
+ source: ChartSource
35
+ repoUrl?: string // Direct repo URL (overrides ArtifactHub lookup)
36
+ defaultValues?: Record<string, unknown> // Pre-populated values (e.g., resource limits from traffic wizard)
37
+ onClose: () => void
38
+ onSuccess: (namespace: string, releaseName: string) => void
39
+ }
40
+
41
+ type WizardStep = 'info' | 'values' | 'review' | 'installing'
42
+
43
+ // Progress log entry
44
+ interface ProgressEntry {
45
+ phase: string
46
+ message: string
47
+ detail?: string
48
+ timestamp: Date
49
+ }
50
+
51
+ export function InstallWizard({ repo, chartName, version, source, repoUrl, defaultValues, onClose, onSuccess }: InstallWizardProps) {
52
+ const [step, setStep] = useState<WizardStep>('info')
53
+ const [releaseName, setReleaseName] = useState(chartName)
54
+ const [namespace, setNamespace] = useState(chartName)
55
+ const [createNamespace, setCreateNamespace] = useState(true)
56
+ const [valuesYaml, setValuesYaml] = useState('')
57
+ const [yamlError, setYamlError] = useState<string | null>(null)
58
+ const [showReadme, setShowReadme] = useState(false)
59
+
60
+ // Install progress state
61
+ const [progressLogs, setProgressLogs] = useState<ProgressEntry[]>([])
62
+ const [installError, setInstallError] = useState<string | null>(null)
63
+ const [isInstalling, setIsInstalling] = useState(false)
64
+ const progressEndRef = useRef<HTMLDivElement>(null)
65
+
66
+ const queryClient = useQueryClient()
67
+ const canHelmWrite = useCanHelmWrite()
68
+
69
+ // Choose the right data based on source
70
+ const isLocal = source === 'local'
71
+
72
+ // Use appropriate hook based on source - only enable the relevant one
73
+ const { data: localChartDetail, isLoading: localChartLoading } = useChartDetail(
74
+ repo, chartName, version, isLocal
75
+ )
76
+ const { data: artifactHubDetail, isLoading: artifactHubLoading } = useArtifactHubChart(
77
+ repo, chartName, version, !isLocal
78
+ )
79
+
80
+ const chartLoading = isLocal ? localChartLoading : artifactHubLoading
81
+ const chartDetail = isLocal ? localChartDetail : artifactHubDetail
82
+
83
+ const { data: namespaces } = useNamespaces()
84
+
85
+ // Auto-scroll progress logs
86
+ useEffect(() => {
87
+ if (progressEndRef.current) {
88
+ progressEndRef.current.scrollIntoView({ behavior: 'smooth' })
89
+ }
90
+ }, [progressLogs])
91
+
92
+ // Initialize values from chart defaults, merging any pre-populated defaultValues on top
93
+ useEffect(() => {
94
+ let baseValues: Record<string, unknown> | undefined
95
+ if (isLocal && localChartDetail?.values) {
96
+ baseValues = localChartDetail.values as Record<string, unknown>
97
+ } else if (!isLocal && artifactHubDetail?.values) {
98
+ try {
99
+ baseValues = yaml.parse(artifactHubDetail.values) as Record<string, unknown>
100
+ } catch {
101
+ // If parsing fails, use the raw string as-is
102
+ setValuesYaml(artifactHubDetail.values)
103
+ return
104
+ }
105
+ }
106
+ if (baseValues && defaultValues) {
107
+ setValuesYaml(yaml.stringify(deepMerge(baseValues, defaultValues)))
108
+ } else if (defaultValues && !baseValues) {
109
+ setValuesYaml(yaml.stringify(defaultValues))
110
+ } else if (baseValues) {
111
+ setValuesYaml(yaml.stringify(baseValues))
112
+ }
113
+ }, [localChartDetail?.values, artifactHubDetail?.values, isLocal, defaultValues])
114
+
115
+ const handleInstall = useCallback(async () => {
116
+ let values: Record<string, unknown> | undefined
117
+ if (valuesYaml.trim()) {
118
+ try {
119
+ values = yaml.parse(valuesYaml)
120
+ } catch (err) {
121
+ setYamlError(err instanceof Error ? err.message : 'Invalid YAML')
122
+ return
123
+ }
124
+ }
125
+
126
+ // For ArtifactHub charts, we need to use the repository URL
127
+ // If repoUrl is provided directly, use it (for non-ArtifactHub external repos)
128
+ const repository = repoUrl || (isLocal ? repo : (artifactHubDetail?.repository.url || repo))
129
+
130
+ // Switch to installing step
131
+ setStep('installing')
132
+ setIsInstalling(true)
133
+ setInstallError(null)
134
+ setProgressLogs([])
135
+
136
+ try {
137
+ const release = await installChartWithProgress(
138
+ {
139
+ releaseName,
140
+ namespace,
141
+ chartName,
142
+ version,
143
+ repository,
144
+ values,
145
+ createNamespace,
146
+ },
147
+ (event: InstallProgressEvent) => {
148
+ if (event.type === 'progress' && event.message) {
149
+ setProgressLogs(prev => [...prev, {
150
+ phase: event.phase || 'progress',
151
+ message: event.message || '',
152
+ detail: event.detail,
153
+ timestamp: new Date(),
154
+ }])
155
+ }
156
+ }
157
+ )
158
+
159
+ // Success - add final log entry
160
+ setProgressLogs(prev => [...prev, {
161
+ phase: 'complete',
162
+ message: `Successfully installed ${release.name}`,
163
+ timestamp: new Date(),
164
+ }])
165
+
166
+ // Invalidate queries
167
+ queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
168
+
169
+ // Wait a moment to show success, then close
170
+ setTimeout(() => {
171
+ onSuccess(namespace, releaseName)
172
+ }, 1500)
173
+ } catch (err) {
174
+ setInstallError(err instanceof Error ? err.message : 'Install failed')
175
+ setProgressLogs(prev => [...prev, {
176
+ phase: 'error',
177
+ message: err instanceof Error ? err.message : 'Install failed',
178
+ timestamp: new Date(),
179
+ }])
180
+ } finally {
181
+ setIsInstalling(false)
182
+ }
183
+ }, [releaseName, namespace, chartName, version, repo, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
184
+
185
+ const canProceedFromInfo = releaseName.trim() !== '' && namespace.trim() !== ''
186
+ const canInstall = canProceedFromInfo && !yamlError
187
+
188
+ const steps: { id: WizardStep; label: string }[] = [
189
+ { id: 'info', label: 'Details' },
190
+ { id: 'values', label: 'Values' },
191
+ { id: 'review', label: 'Review' },
192
+ ]
193
+
194
+ return (
195
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
196
+ {/* Backdrop */}
197
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
198
+
199
+ {/* Dialog */}
200
+ <div className="relative dialog max-w-3xl w-full mx-4 max-h-[90vh] flex flex-col">
201
+ {/* Header */}
202
+ <div className="flex items-center justify-between px-4 py-3 border-b border-theme-border shrink-0">
203
+ <div className="flex items-center gap-3">
204
+ {(isLocal ? (chartDetail as ChartDetail)?.icon : (chartDetail as ArtifactHubChartDetail)?.logoUrl) ? (
205
+ <img
206
+ src={isLocal ? (chartDetail as ChartDetail)?.icon : (chartDetail as ArtifactHubChartDetail)?.logoUrl}
207
+ alt=""
208
+ className="w-8 h-8 rounded object-contain bg-white/10 p-1"
209
+ />
210
+ ) : (
211
+ <Package className="w-8 h-8 text-purple-400" />
212
+ )}
213
+ <div>
214
+ <div className="flex items-center gap-2">
215
+ <h2 className="text-lg font-semibold text-theme-text-primary">Install {chartName}</h2>
216
+ {!isLocal && (
217
+ <Tooltip content="From ArtifactHub">
218
+ <Globe className="w-4 h-4 text-blue-400" />
219
+ </Tooltip>
220
+ )}
221
+ </div>
222
+ <div className="flex items-center gap-2 text-sm text-theme-text-tertiary">
223
+ <span>{repo} / {version}</span>
224
+ {!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.official && (
225
+ <Tooltip content="Official">
226
+ <BadgeCheck className="w-3.5 h-3.5 text-blue-400" />
227
+ </Tooltip>
228
+ )}
229
+ {!isLocal && (chartDetail as ArtifactHubChartDetail)?.repository?.verifiedPublisher && (
230
+ <Tooltip content="Verified Publisher">
231
+ <Shield className="w-3.5 h-3.5 text-green-400" />
232
+ </Tooltip>
233
+ )}
234
+ </div>
235
+ </div>
236
+ </div>
237
+ <button
238
+ onClick={onClose}
239
+ className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
240
+ >
241
+ <X className="w-5 h-5" />
242
+ </button>
243
+ </div>
244
+
245
+ {/* Step indicator - hidden during install */}
246
+ {step !== 'installing' && (
247
+ <div className="flex items-center justify-center gap-2 px-4 py-3 border-b border-theme-border bg-theme-surface/50">
248
+ {steps.map((s, i) => (
249
+ <div key={s.id} className="flex items-center">
250
+ {i > 0 && <ChevronRight className="w-4 h-4 text-theme-text-disabled mx-2" />}
251
+ <button
252
+ onClick={() => {
253
+ if (s.id === 'info' || (s.id === 'values' && canProceedFromInfo) || (s.id === 'review' && canInstall)) {
254
+ setStep(s.id)
255
+ }
256
+ }}
257
+ className={clsx(
258
+ 'flex items-center gap-2 px-3 py-1.5 rounded-full text-sm transition-colors',
259
+ step === s.id
260
+ ? SEVERITY_BADGE.info
261
+ : 'text-theme-text-secondary hover:text-theme-text-primary'
262
+ )}
263
+ >
264
+ <span className={clsx(
265
+ 'w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium',
266
+ step === s.id ? 'bg-blue-500 text-white' : 'bg-theme-elevated text-theme-text-secondary'
267
+ )}>
268
+ {i + 1}
269
+ </span>
270
+ {s.label}
271
+ </button>
272
+ </div>
273
+ ))}
274
+ </div>
275
+ )}
276
+
277
+ {/* Content */}
278
+ <div className="flex-1 overflow-auto p-4">
279
+ {chartLoading ? (
280
+ <div className="flex items-center justify-center h-32 text-theme-text-tertiary">
281
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
282
+ Loading chart details...
283
+ </div>
284
+ ) : (
285
+ <>
286
+ {step === 'info' && (
287
+ <InfoStep
288
+ chartDetail={chartDetail}
289
+ source={source}
290
+ releaseName={releaseName}
291
+ setReleaseName={setReleaseName}
292
+ namespace={namespace}
293
+ setNamespace={setNamespace}
294
+ namespaces={namespaces || []}
295
+ createNamespace={createNamespace}
296
+ setCreateNamespace={setCreateNamespace}
297
+ showReadme={showReadme}
298
+ setShowReadme={setShowReadme}
299
+ />
300
+ )}
301
+
302
+ {step === 'values' && (
303
+ <ValuesStep
304
+ valuesYaml={valuesYaml}
305
+ setValuesYaml={setValuesYaml}
306
+ yamlError={yamlError}
307
+ setYamlError={setYamlError}
308
+ chartDetail={chartDetail}
309
+ source={source}
310
+ />
311
+ )}
312
+
313
+ {step === 'review' && (
314
+ <ReviewStep
315
+ releaseName={releaseName}
316
+ namespace={namespace}
317
+ chartName={chartName}
318
+ version={version}
319
+ repo={repo}
320
+ source={source}
321
+ artifactHubRepoUrl={!isLocal ? (chartDetail as ArtifactHubChartDetail)?.repository?.url : undefined}
322
+ createNamespace={createNamespace}
323
+ valuesYaml={valuesYaml}
324
+ defaultValuesYaml={
325
+ isLocal
326
+ ? (localChartDetail?.values ? yaml.stringify(localChartDetail.values) : '')
327
+ : (artifactHubDetail?.values || '')
328
+ }
329
+ />
330
+ )}
331
+
332
+ {step === 'installing' && (
333
+ <InstallingStep
334
+ releaseName={releaseName}
335
+ namespace={namespace}
336
+ chartName={chartName}
337
+ progressLogs={progressLogs}
338
+ isInstalling={isInstalling}
339
+ installError={installError}
340
+ progressEndRef={progressEndRef}
341
+ />
342
+ )}
343
+ </>
344
+ )}
345
+ </div>
346
+
347
+ {/* Footer */}
348
+ <div className="flex items-center justify-between px-4 py-3 border-t border-theme-border shrink-0">
349
+ <div>
350
+ {step !== 'info' && step !== 'installing' && (
351
+ <button
352
+ onClick={() => setStep(step === 'review' ? 'values' : 'info')}
353
+ className="flex items-center gap-1 px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
354
+ >
355
+ <ChevronLeft className="w-4 h-4" />
356
+ Back
357
+ </button>
358
+ )}
359
+ </div>
360
+ <div className="flex items-center gap-3">
361
+ {step === 'installing' ? (
362
+ // Installing step - only show close if done or error
363
+ <>
364
+ {!isInstalling && installError && (
365
+ <button
366
+ onClick={() => setStep('review')}
367
+ className="flex items-center gap-1 px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
368
+ >
369
+ <ChevronLeft className="w-4 h-4" />
370
+ Back to Review
371
+ </button>
372
+ )}
373
+ {isInstalling && (
374
+ <span className="text-sm text-theme-text-tertiary">
375
+ Installing...
376
+ </span>
377
+ )}
378
+ </>
379
+ ) : (
380
+ <>
381
+ <button
382
+ onClick={onClose}
383
+ className="px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
384
+ >
385
+ Cancel
386
+ </button>
387
+ {step === 'review' ? (
388
+ <button
389
+ onClick={handleInstall}
390
+ disabled={!canInstall || isInstalling || !canHelmWrite}
391
+ className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
392
+ title={!canHelmWrite ? 'Helm write permissions required (rbac.helm=true)' : undefined}
393
+ >
394
+ {isInstalling ? (
395
+ <Loader2 className="w-4 h-4 animate-spin" />
396
+ ) : (
397
+ <Play className="w-4 h-4" />
398
+ )}
399
+ Install
400
+ </button>
401
+ ) : (
402
+ <button
403
+ onClick={() => setStep(step === 'info' ? 'values' : 'review')}
404
+ disabled={step === 'info' && !canProceedFromInfo}
405
+ className="flex items-center gap-1 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
406
+ >
407
+ Next
408
+ <ChevronRight className="w-4 h-4" />
409
+ </button>
410
+ )}
411
+ </>
412
+ )}
413
+ </div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ )
418
+ }
419
+
420
+ interface InfoStepProps {
421
+ chartDetail: ChartDetail | ArtifactHubChartDetail | undefined
422
+ source: ChartSource
423
+ releaseName: string
424
+ setReleaseName: (name: string) => void
425
+ namespace: string
426
+ setNamespace: (ns: string) => void
427
+ namespaces: { name: string }[]
428
+ createNamespace: boolean
429
+ setCreateNamespace: (create: boolean) => void
430
+ showReadme: boolean
431
+ setShowReadme: (show: boolean) => void
432
+ }
433
+
434
+ function InfoStep({
435
+ chartDetail,
436
+ source,
437
+ releaseName,
438
+ setReleaseName,
439
+ namespace,
440
+ setNamespace,
441
+ namespaces,
442
+ createNamespace,
443
+ setCreateNamespace,
444
+ showReadme,
445
+ setShowReadme,
446
+ }: InfoStepProps) {
447
+ const isLocal = source === 'local'
448
+ const localDetail = chartDetail as ChartDetail | undefined
449
+ const ahDetail = chartDetail as ArtifactHubChartDetail | undefined
450
+
451
+ const description = isLocal ? localDetail?.description : ahDetail?.description
452
+ const home = isLocal ? localDetail?.home : ahDetail?.homeUrl
453
+ const maintainers = isLocal ? localDetail?.maintainers : ahDetail?.maintainers
454
+ const readme = isLocal ? localDetail?.readme : ahDetail?.readme
455
+
456
+ return (
457
+ <div className="space-y-6">
458
+ {/* ArtifactHub specific metadata */}
459
+ {!isLocal && ahDetail && (
460
+ <div className="flex items-center gap-4 p-3 bg-theme-elevated/30 rounded-lg">
461
+ {ahDetail.stars > 0 && (
462
+ <Tooltip content={`${ahDetail.stars} stars`}>
463
+ <span className="flex items-center gap-1 text-sm text-amber-400">
464
+ <Star className="w-4 h-4 fill-current" />
465
+ {ahDetail.stars > 999 ? `${(ahDetail.stars / 1000).toFixed(1)}k` : ahDetail.stars}
466
+ </span>
467
+ </Tooltip>
468
+ )}
469
+ {ahDetail.productionOrgsCount && ahDetail.productionOrgsCount > 0 && (
470
+ <Tooltip content={`Used in ${ahDetail.productionOrgsCount} production environments`}>
471
+ <span className="flex items-center gap-1 text-sm text-theme-text-secondary">
472
+ <Building2 className="w-4 h-4" />
473
+ {ahDetail.productionOrgsCount} orgs
474
+ </span>
475
+ </Tooltip>
476
+ )}
477
+ {ahDetail.security && (ahDetail.security.critical || ahDetail.security.high) && (
478
+ <Tooltip content={`Security: ${ahDetail.security.critical || 0} critical, ${ahDetail.security.high || 0} high`}>
479
+ <span className={clsx(
480
+ 'flex items-center gap-1 text-sm',
481
+ ahDetail.security.critical ? 'text-red-400' : 'text-orange-400'
482
+ )}>
483
+ <Shield className="w-4 h-4" />
484
+ {(ahDetail.security.critical || 0) + (ahDetail.security.high || 0)} issues
485
+ </span>
486
+ </Tooltip>
487
+ )}
488
+ {ahDetail.signed && (
489
+ <span className="flex items-center gap-1 text-sm text-green-400">
490
+ <BadgeCheck className="w-4 h-4" />
491
+ Signed
492
+ </span>
493
+ )}
494
+ {ahDetail.license && (
495
+ <span className="text-sm text-theme-text-tertiary">{ahDetail.license}</span>
496
+ )}
497
+ </div>
498
+ )}
499
+
500
+ {/* Chart description */}
501
+ {description && (
502
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
503
+ <p className="text-sm text-theme-text-secondary">{description}</p>
504
+ {(home || maintainers?.length) && (
505
+ <div className="flex flex-wrap gap-4 mt-3 pt-3 border-t border-theme-border">
506
+ {home && (
507
+ <a
508
+ href={home}
509
+ target="_blank"
510
+ rel="noopener noreferrer"
511
+ className="flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300"
512
+ >
513
+ <LinkIcon className="w-3.5 h-3.5" />
514
+ Homepage
515
+ </a>
516
+ )}
517
+ {maintainers && maintainers.length > 0 && (
518
+ <span className="flex items-center gap-1 text-xs text-theme-text-tertiary">
519
+ <User className="w-3.5 h-3.5" />
520
+ {maintainers.map((m) => m.name).join(', ')}
521
+ </span>
522
+ )}
523
+ </div>
524
+ )}
525
+ </div>
526
+ )}
527
+
528
+ {/* Release name input */}
529
+ <div>
530
+ <label className="block text-sm font-medium text-theme-text-secondary mb-2">
531
+ Release Name
532
+ </label>
533
+ <input
534
+ type="text"
535
+ value={releaseName}
536
+ onChange={(e) => setReleaseName(e.target.value)}
537
+ placeholder="my-release"
538
+ className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
539
+ />
540
+ <p className="mt-1 text-xs text-theme-text-tertiary">
541
+ A unique name for this release in the namespace
542
+ </p>
543
+ </div>
544
+
545
+ {/* Namespace selection */}
546
+ <div>
547
+ <label className="block text-sm font-medium text-theme-text-secondary mb-2">
548
+ Namespace
549
+ </label>
550
+ <input
551
+ type="text"
552
+ list="namespace-suggestions"
553
+ value={namespace}
554
+ onChange={(e) => setNamespace(e.target.value)}
555
+ placeholder="Enter namespace name"
556
+ className="w-full px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
557
+ />
558
+ <datalist id="namespace-suggestions">
559
+ {namespaces.map(ns => (
560
+ <option key={ns.name} value={ns.name} />
561
+ ))}
562
+ </datalist>
563
+ <label className="flex items-center gap-2 mt-2 text-sm text-theme-text-secondary">
564
+ <input
565
+ type="checkbox"
566
+ checked={createNamespace}
567
+ onChange={(e) => setCreateNamespace(e.target.checked)}
568
+ className="rounded border-theme-border text-blue-500 focus:ring-blue-500"
569
+ />
570
+ Create namespace if it doesn't exist
571
+ </label>
572
+ </div>
573
+
574
+ {/* README toggle */}
575
+ {readme && (
576
+ <div>
577
+ <button
578
+ onClick={() => setShowReadme(!showReadme)}
579
+ className="flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300"
580
+ >
581
+ <BookOpen className="w-4 h-4" />
582
+ {showReadme ? 'Hide' : 'Show'} Chart README
583
+ </button>
584
+ {showReadme && (
585
+ <div className="mt-3 p-4 bg-theme-base/50 rounded-lg text-xs overflow-auto max-h-96">
586
+ <Markdown>{readme}</Markdown>
587
+ </div>
588
+ )}
589
+ </div>
590
+ )}
591
+ </div>
592
+ )
593
+ }
594
+
595
+ interface ValuesStepProps {
596
+ valuesYaml: string
597
+ setValuesYaml: (yaml: string) => void
598
+ yamlError: string | null
599
+ setYamlError: (error: string | null) => void
600
+ chartDetail: ChartDetail | ArtifactHubChartDetail | undefined
601
+ source: ChartSource
602
+ }
603
+
604
+ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartDetail, source }: ValuesStepProps) {
605
+ const [showEditor, setShowEditor] = useState(false)
606
+
607
+ const isLocal = source === 'local'
608
+ const localDetail = chartDetail as ChartDetail | undefined
609
+ const ahDetail = chartDetail as ArtifactHubChartDetail | undefined
610
+
611
+ const defaultValues = isLocal
612
+ ? (localDetail?.values ? yaml.stringify(localDetail.values) : '')
613
+ : (ahDetail?.values || '')
614
+
615
+ const hasDefaults = Boolean(defaultValues)
616
+ const hasValues = Boolean(valuesYaml)
617
+
618
+ // Get README/docs link
619
+ const homeUrl = isLocal ? localDetail?.home : ahDetail?.homeUrl
620
+
621
+ return (
622
+ <div className="space-y-4">
623
+ {/* Info banner */}
624
+ <div className="flex items-start gap-3 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
625
+ <CheckCircle className="w-5 h-5 text-blue-400 shrink-0 mt-0.5" />
626
+ <div>
627
+ <p className="text-sm font-medium text-theme-text-primary">Ready to install with defaults</p>
628
+ <p className="text-xs text-theme-text-secondary mt-1">
629
+ {hasValues
630
+ ? 'Default values are shown below. Edit only the values you want to change, then proceed to install.'
631
+ : 'Most charts work out of the box. You can skip this step or customize values below.'}
632
+ </p>
633
+ {homeUrl && (
634
+ <a
635
+ href={homeUrl}
636
+ target="_blank"
637
+ rel="noopener noreferrer"
638
+ className="inline-flex items-center gap-1 text-xs text-blue-400 hover:text-blue-300 mt-2"
639
+ >
640
+ <LinkIcon className="w-3 h-3" />
641
+ View chart documentation
642
+ </a>
643
+ )}
644
+ </div>
645
+ </div>
646
+
647
+ {/* Collapsible editor section */}
648
+ <div className="border border-theme-border rounded-lg overflow-hidden">
649
+ <button
650
+ onClick={() => setShowEditor(!showEditor)}
651
+ className="w-full flex items-center justify-between px-4 py-3 bg-theme-elevated/50 hover:bg-theme-elevated transition-colors"
652
+ >
653
+ <div className="flex items-center gap-2">
654
+ <ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform', showEditor && 'rotate-90')} />
655
+ <span className="text-sm font-medium text-theme-text-primary">
656
+ {hasValues ? (showEditor ? 'Hide' : 'Show') : 'Add'} configuration values
657
+ </span>
658
+ <span className="text-xs text-theme-text-tertiary">(optional)</span>
659
+ {hasValues && !showEditor && (
660
+ <span className="text-xs text-green-400 ml-2">Default values loaded</span>
661
+ )}
662
+ </div>
663
+ </button>
664
+
665
+ {showEditor && (
666
+ <div className="p-4 border-t border-theme-border">
667
+ {/* Action buttons */}
668
+ <div className="flex items-center gap-3 mb-4">
669
+ {hasDefaults && !hasValues && (
670
+ <button
671
+ onClick={() => setValuesYaml(defaultValues)}
672
+ className={clsx('text-xs px-3 py-1.5 rounded hover:bg-sky-500/30 transition-colors', SEVERITY_BADGE.info)}
673
+ >
674
+ Load default values
675
+ </button>
676
+ )}
677
+ {hasValues && hasDefaults && (
678
+ <button
679
+ onClick={() => setValuesYaml(defaultValues)}
680
+ className="text-xs px-3 py-1.5 bg-theme-elevated border border-theme-border-light rounded hover:bg-theme-hover transition-colors text-theme-text-secondary"
681
+ >
682
+ Reset to defaults
683
+ </button>
684
+ )}
685
+ {hasValues && (
686
+ <button
687
+ onClick={() => setValuesYaml('')}
688
+ className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary"
689
+ >
690
+ Clear all
691
+ </button>
692
+ )}
693
+ </div>
694
+
695
+ {yamlError && (
696
+ <div className="flex items-center gap-2 px-3 py-2 mb-4 text-xs text-red-400 bg-red-500/10 border border-red-500/30 rounded">
697
+ <AlertTriangle className="w-4 h-4 shrink-0" />
698
+ {yamlError}
699
+ </div>
700
+ )}
701
+
702
+ {hasValues ? (
703
+ <YamlEditor
704
+ value={valuesYaml}
705
+ onChange={setValuesYaml}
706
+ height="300px"
707
+ onValidate={(isValid, errors) => {
708
+ setYamlError(isValid ? null : errors[0] || 'Invalid YAML')
709
+ }}
710
+ />
711
+ ) : (
712
+ <div className="flex flex-col items-center justify-center py-8 text-theme-text-tertiary bg-theme-base/30 rounded-lg">
713
+ <p className="text-sm">No default values available for this chart</p>
714
+ <p className="text-xs mt-1 text-theme-text-disabled">
715
+ {homeUrl ? 'Check the documentation for available options' : 'You can add custom values manually'}
716
+ </p>
717
+ <button
718
+ onClick={() => setValuesYaml('# Custom values\n')}
719
+ className="mt-3 text-xs px-3 py-1.5 bg-theme-elevated border border-theme-border-light rounded hover:bg-theme-hover transition-colors text-theme-text-secondary"
720
+ >
721
+ Add custom values
722
+ </button>
723
+ </div>
724
+ )}
725
+ </div>
726
+ )}
727
+ </div>
728
+ </div>
729
+ )
730
+ }
731
+
732
+ interface ReviewStepProps {
733
+ releaseName: string
734
+ namespace: string
735
+ chartName: string
736
+ version: string
737
+ repo: string
738
+ source: ChartSource
739
+ artifactHubRepoUrl?: string
740
+ createNamespace: boolean
741
+ valuesYaml: string
742
+ defaultValuesYaml: string
743
+ }
744
+
745
+ function ReviewStep({
746
+ releaseName,
747
+ namespace,
748
+ chartName,
749
+ version,
750
+ repo,
751
+ source,
752
+ artifactHubRepoUrl,
753
+ createNamespace,
754
+ valuesYaml,
755
+ defaultValuesYaml,
756
+ }: ReviewStepProps) {
757
+ const hasCustomValues = valuesYaml.trim() !== ''
758
+ const hasDefaults = defaultValuesYaml.trim() !== ''
759
+
760
+ // Compute the diff between defaults and current values
761
+ const diffLines = useMemo(() => {
762
+ if (!hasCustomValues) return null
763
+
764
+ // If no defaults, show all custom values as additions
765
+ if (!hasDefaults) {
766
+ return valuesYaml.split('\n').map((line, i) => ({
767
+ type: 'add' as const,
768
+ content: line,
769
+ lineNumber: i + 1,
770
+ }))
771
+ }
772
+
773
+ // If values are the same as defaults, no diff
774
+ if (valuesYaml.trim() === defaultValuesYaml.trim()) return null
775
+
776
+ // Generate unified diff
777
+ const patch = createPatch('values.yaml', defaultValuesYaml, valuesYaml, '', '', { context: 2 })
778
+ const lines = patch.split('\n')
779
+
780
+ // Parse the diff, skipping the header lines
781
+ const result: { type: 'context' | 'add' | 'remove' | 'header'; content: string; lineNumber?: number }[] = []
782
+ let newLineNum = 0
783
+ let seenHeader = false
784
+
785
+ for (const line of lines) {
786
+ // Skip diff metadata lines
787
+ if (line.startsWith('Index:') || line.startsWith('===') ||
788
+ line.startsWith('---') || line.startsWith('+++')) {
789
+ continue
790
+ }
791
+
792
+ // Hunk header
793
+ if (line.startsWith('@@')) {
794
+ seenHeader = true
795
+ // Parse line numbers from @@ -1,3 +1,4 @@
796
+ const match = line.match(/@@ -\d+,?\d* \+(\d+),?\d* @@/)
797
+ if (match) {
798
+ newLineNum = parseInt(match[1], 10)
799
+ }
800
+ result.push({ type: 'header', content: line })
801
+ continue
802
+ }
803
+
804
+ if (!seenHeader) continue
805
+
806
+ if (line.startsWith('+')) {
807
+ result.push({ type: 'add', content: line.slice(1), lineNumber: newLineNum })
808
+ newLineNum++
809
+ } else if (line.startsWith('-')) {
810
+ result.push({ type: 'remove', content: line.slice(1) })
811
+ } else if (line.startsWith(' ')) {
812
+ result.push({ type: 'context', content: line.slice(1), lineNumber: newLineNum })
813
+ newLineNum++
814
+ }
815
+ }
816
+
817
+ return result.length > 0 ? result : null
818
+ }, [valuesYaml, defaultValuesYaml, hasCustomValues, hasDefaults])
819
+
820
+ const hasChanges = diffLines && diffLines.some(l => l.type === 'add' || l.type === 'remove')
821
+
822
+ return (
823
+ <div className="space-y-6">
824
+ <div className="flex items-center gap-3 p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
825
+ <CheckCircle className="w-6 h-6 text-blue-400 shrink-0" />
826
+ <div>
827
+ <p className="text-sm font-medium text-theme-text-primary">Ready to install</p>
828
+ <p className="text-xs text-theme-text-secondary mt-0.5">
829
+ Review the configuration below and click Install to proceed
830
+ </p>
831
+ </div>
832
+ </div>
833
+
834
+ {/* ArtifactHub notice */}
835
+ {source === 'artifacthub' && (
836
+ <div className="flex items-start gap-3 p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
837
+ <Globe className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
838
+ <div>
839
+ <p className="text-sm font-medium text-amber-400">Installing from ArtifactHub</p>
840
+ <p className="text-xs text-theme-text-secondary mt-1">
841
+ This chart will be installed from: <code className="bg-theme-elevated px-1 rounded">{artifactHubRepoUrl || repo}</code>
842
+ </p>
843
+ </div>
844
+ </div>
845
+ )}
846
+
847
+ <div className="bg-theme-elevated/30 rounded-lg p-4 space-y-3">
848
+ <h3 className="text-sm font-medium text-theme-text-secondary">Installation Summary</h3>
849
+ <div className="grid grid-cols-2 gap-3 text-sm">
850
+ <div>
851
+ <dt className="text-theme-text-tertiary">Release Name</dt>
852
+ <dd className="text-theme-text-primary font-medium">{releaseName}</dd>
853
+ </div>
854
+ <div>
855
+ <dt className="text-theme-text-tertiary">Namespace</dt>
856
+ <dd className="text-theme-text-primary font-medium">
857
+ {namespace}
858
+ {createNamespace && (
859
+ <span className="ml-2 text-xs text-amber-400">(will be created)</span>
860
+ )}
861
+ </dd>
862
+ </div>
863
+ <div>
864
+ <dt className="text-theme-text-tertiary">Chart</dt>
865
+ <dd className="text-theme-text-primary">{chartName}</dd>
866
+ </div>
867
+ <div>
868
+ <dt className="text-theme-text-tertiary">Version</dt>
869
+ <dd className="text-theme-text-primary">{version}</dd>
870
+ </div>
871
+ <div>
872
+ <dt className="text-theme-text-tertiary">Source</dt>
873
+ <dd className="text-theme-text-primary flex items-center gap-1">
874
+ {source === 'local' ? (
875
+ <>Local: {repo}</>
876
+ ) : (
877
+ <>
878
+ <Globe className="w-3.5 h-3.5 text-blue-400" />
879
+ ArtifactHub: {repo}
880
+ </>
881
+ )}
882
+ </dd>
883
+ </div>
884
+ <div>
885
+ <dt className="text-theme-text-tertiary">Custom Values</dt>
886
+ <dd className="text-theme-text-primary">
887
+ {hasChanges ? 'Modified' : hasCustomValues ? 'Using defaults' : 'No (using defaults)'}
888
+ </dd>
889
+ </div>
890
+ </div>
891
+ </div>
892
+
893
+ {/* Values diff view */}
894
+ {hasChanges && diffLines && (
895
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
896
+ <h3 className="text-sm font-medium text-theme-text-secondary mb-3">Values Changes</h3>
897
+ <div className="bg-theme-base/50 rounded overflow-auto max-h-64 font-mono text-xs">
898
+ {diffLines.map((line, i) => {
899
+ if (line.type === 'header') {
900
+ return (
901
+ <div key={i} className="px-3 py-1 bg-blue-500/10 text-blue-400 border-y border-theme-border">
902
+ {line.content}
903
+ </div>
904
+ )
905
+ }
906
+
907
+ return (
908
+ <div
909
+ key={i}
910
+ className={clsx(
911
+ 'flex items-start px-3 py-0.5',
912
+ line.type === 'add' && 'bg-green-500/10',
913
+ line.type === 'remove' && 'bg-red-500/10'
914
+ )}
915
+ >
916
+ <span className={clsx(
917
+ 'w-4 shrink-0',
918
+ line.type === 'add' && 'text-green-400',
919
+ line.type === 'remove' && 'text-red-400',
920
+ line.type === 'context' && 'text-theme-text-disabled'
921
+ )}>
922
+ {line.type === 'add' && <Plus className="w-3 h-3 inline" />}
923
+ {line.type === 'remove' && <Minus className="w-3 h-3 inline" />}
924
+ </span>
925
+ <span className={clsx(
926
+ 'flex-1 whitespace-pre',
927
+ line.type === 'add' && 'text-green-400',
928
+ line.type === 'remove' && 'text-red-400',
929
+ line.type === 'context' && 'text-theme-text-secondary'
930
+ )}>
931
+ {line.content || ' '}
932
+ </span>
933
+ </div>
934
+ )
935
+ })}
936
+ </div>
937
+ </div>
938
+ )}
939
+ </div>
940
+ )
941
+ }
942
+
943
+ // Installing step - shows progress logs during installation
944
+ interface InstallingStepProps {
945
+ releaseName: string
946
+ namespace: string
947
+ chartName: string
948
+ progressLogs: ProgressEntry[]
949
+ isInstalling: boolean
950
+ installError: string | null
951
+ progressEndRef: React.RefObject<HTMLDivElement | null>
952
+ }
953
+
954
+ function InstallingStep({
955
+ releaseName,
956
+ namespace,
957
+ chartName,
958
+ progressLogs,
959
+ isInstalling,
960
+ installError,
961
+ progressEndRef,
962
+ }: InstallingStepProps) {
963
+ const isComplete = progressLogs.some(l => l.phase === 'complete')
964
+
965
+ return (
966
+ <div className="space-y-6">
967
+ {/* Header */}
968
+ <div className={clsx(
969
+ 'flex items-center gap-3 p-4 rounded-lg border',
970
+ isComplete ? 'bg-green-500/10 border-green-500/30' :
971
+ installError ? 'bg-red-500/10 border-red-500/30' :
972
+ 'bg-blue-500/10 border-blue-500/30'
973
+ )}>
974
+ {isComplete ? (
975
+ <CheckCircle className="w-6 h-6 text-green-400 shrink-0" />
976
+ ) : installError ? (
977
+ <AlertTriangle className="w-6 h-6 text-red-400 shrink-0" />
978
+ ) : (
979
+ <Loader2 className="w-6 h-6 text-blue-400 shrink-0 animate-spin" />
980
+ )}
981
+ <div>
982
+ <p className={clsx(
983
+ 'text-sm font-medium',
984
+ isComplete ? 'text-green-400' :
985
+ installError ? 'text-red-400' :
986
+ 'text-theme-text-primary'
987
+ )}>
988
+ {isComplete ? 'Installation Complete' :
989
+ installError ? 'Installation Failed' :
990
+ `Installing ${chartName}...`}
991
+ </p>
992
+ <p className="text-xs text-theme-text-secondary mt-0.5">
993
+ {isComplete ? `${releaseName} is now running in ${namespace}` :
994
+ installError ? 'See logs below for details' :
995
+ `Deploying to namespace ${namespace}`}
996
+ </p>
997
+ </div>
998
+ </div>
999
+
1000
+ {/* Progress logs */}
1001
+ <div className="bg-theme-base rounded-lg border border-theme-border overflow-hidden">
1002
+ <div className="flex items-center gap-2 px-4 py-2 border-b border-theme-border bg-theme-elevated/50">
1003
+ <Terminal className="w-4 h-4 text-theme-text-tertiary" />
1004
+ <span className="text-xs font-medium text-theme-text-secondary">Install Progress</span>
1005
+ </div>
1006
+ <div className="p-3 max-h-64 overflow-auto font-mono text-xs">
1007
+ {progressLogs.length === 0 && isInstalling && (
1008
+ <div className="text-theme-text-tertiary">Starting installation...</div>
1009
+ )}
1010
+ {progressLogs.map((log, i) => (
1011
+ <div
1012
+ key={i}
1013
+ className={clsx(
1014
+ 'flex items-start gap-2 py-1',
1015
+ log.phase === 'error' && 'text-red-400',
1016
+ log.phase === 'complete' && 'text-green-400'
1017
+ )}
1018
+ >
1019
+ <span className="text-theme-text-disabled shrink-0 w-16">
1020
+ {log.timestamp.toLocaleTimeString()}
1021
+ </span>
1022
+ <span className={clsx(
1023
+ 'shrink-0 badge-sm uppercase',
1024
+ log.phase === 'error' ? SEVERITY_BADGE.error :
1025
+ log.phase === 'complete' ? SEVERITY_BADGE.success :
1026
+ 'bg-theme-elevated text-theme-text-tertiary'
1027
+ )}>
1028
+ {log.phase}
1029
+ </span>
1030
+ <span className={clsx(
1031
+ log.phase === 'error' ? SEVERITY_TEXT.error :
1032
+ log.phase === 'complete' ? SEVERITY_TEXT.success :
1033
+ 'text-theme-text-secondary'
1034
+ )}>
1035
+ {log.message}
1036
+ </span>
1037
+ </div>
1038
+ ))}
1039
+ {isInstalling && (
1040
+ <div className="flex items-center gap-2 py-1 text-theme-text-tertiary">
1041
+ <Loader2 className="w-3 h-3 animate-spin" />
1042
+ <span>Waiting for resources...</span>
1043
+ </div>
1044
+ )}
1045
+ <div ref={progressEndRef} />
1046
+ </div>
1047
+ </div>
1048
+
1049
+ {/* Error details if failed */}
1050
+ {installError && (
1051
+ <div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4">
1052
+ <p className="text-sm font-medium text-red-400 mb-2">Error Details</p>
1053
+ <pre className="text-xs text-red-300 whitespace-pre-wrap font-mono">
1054
+ {installError}
1055
+ </pre>
1056
+ </div>
1057
+ )}
1058
+ </div>
1059
+ )
1060
+ }