@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,774 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react'
2
+ import { flushSync } from 'react-dom'
3
+ import { TRANSITION_DRAWER } from '../../utils/animation'
4
+ import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
5
+ import { X, Copy, Check, RefreshCw, Package, Code, History, FileText, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2 } from 'lucide-react'
6
+ import { clsx } from 'clsx'
7
+ import { useHelmRelease, useHelmManifest, useHelmValues, useHelmManifestDiff, useHelmUpgradeInfo, useHelmUninstall, upgradeWithProgress, rollbackWithProgress } from '../../api/client'
8
+ import { useQueryClient } from '@tanstack/react-query'
9
+ import { ConfirmDialog } from '../ui/ConfirmDialog'
10
+ import { Markdown } from '../ui/Markdown'
11
+ import type { SelectedHelmRelease, HelmHook, ChartDependency } from '../../types'
12
+ import type { NavigateToResource } from '../../utils/navigation'
13
+ import { formatDate } from './helm-utils'
14
+ import { getHelmStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
15
+ import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
16
+ import { RevisionHistory } from './RevisionHistory'
17
+ import { ManifestViewer } from './ManifestViewer'
18
+ import { ValuesViewer } from './ValuesViewer'
19
+ import { OwnedResources } from './OwnedResources'
20
+ import { ManifestDiffViewer } from './ManifestDiffViewer'
21
+
22
+ interface HelmReleaseDrawerProps {
23
+ release: SelectedHelmRelease
24
+ onClose: () => void
25
+ onNavigateToResource?: NavigateToResource
26
+ /** Controls slide-in/out animation (driven by useAnimatedUnmount) */
27
+ isOpen?: boolean
28
+ }
29
+
30
+ type TabId = 'overview' | 'history' | 'manifest' | 'values' | 'resources' | 'hooks' | 'diff'
31
+
32
+ const MIN_WIDTH = 500
33
+ const MAX_WIDTH_PERCENT = 0.8
34
+ const DEFAULT_WIDTH = 1000
35
+
36
+ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOpen = true }: HelmReleaseDrawerProps) {
37
+ const [activeTab, setActiveTab] = useState<TabId>('overview')
38
+ const [copied, setCopied] = useState<string | null>(null)
39
+ const [drawerWidth, setDrawerWidth] = useState(DEFAULT_WIDTH)
40
+ const [isResizing, setIsResizing] = useState(false)
41
+ const [selectedRevision, setSelectedRevision] = useState<number | undefined>(undefined)
42
+ const [showAllValues, setShowAllValues] = useState(false)
43
+ const [diffRevisions, setDiffRevisions] = useState<{ rev1: number; rev2: number } | null>(null)
44
+ const [rollbackRevision, setRollbackRevision] = useState<number | null>(null)
45
+ const [showUninstallConfirm, setShowUninstallConfirm] = useState(false)
46
+ const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
47
+ const resizeStartX = useRef(0)
48
+ const resizeStartWidth = useRef(DEFAULT_WIDTH)
49
+ const canHelmWrite = useCanHelmWrite()
50
+
51
+ const { data: releaseDetail, isLoading, refetch: refetchRelease } = useHelmRelease(
52
+ release.namespace,
53
+ release.name
54
+ )
55
+ const [refetch, isRefreshAnimating] = useRefreshAnimation(refetchRelease)
56
+
57
+ // Fetch manifest for selected revision (or latest)
58
+ const { data: manifest, isLoading: manifestLoading } = useHelmManifest(
59
+ release.namespace,
60
+ release.name,
61
+ selectedRevision
62
+ )
63
+
64
+ // Fetch values
65
+ const { data: values, isLoading: valuesLoading } = useHelmValues(
66
+ release.namespace,
67
+ release.name,
68
+ showAllValues
69
+ )
70
+
71
+ // Fetch diff if comparing revisions
72
+ const { data: diffData, isLoading: diffLoading } = useHelmManifestDiff(
73
+ release.namespace,
74
+ release.name,
75
+ diffRevisions?.rev1 || 0,
76
+ diffRevisions?.rev2 || 0
77
+ )
78
+
79
+ // Lazy check for upgrade availability
80
+ const { data: upgradeInfo, isLoading: upgradeLoading } = useHelmUpgradeInfo(
81
+ release.namespace,
82
+ release.name
83
+ )
84
+
85
+ // Mutations for actions
86
+ const uninstallMutation = useHelmUninstall()
87
+ const queryClient = useQueryClient()
88
+ const [upgradeProgress, setUpgradeProgress] = useState<{ phase: string; message: string }[]>([])
89
+ const [isUpgrading, setIsUpgrading] = useState(false)
90
+ const [rollbackProgress, setRollbackProgress] = useState<{ phase: string; message: string }[]>([])
91
+ const [isRollingBack, setIsRollingBack] = useState(false)
92
+
93
+ // ESC key handler
94
+ useEffect(() => {
95
+ const handleKeyDown = (event: KeyboardEvent) => {
96
+ if (event.key === 'Escape') onClose()
97
+ }
98
+ document.addEventListener('keydown', handleKeyDown)
99
+ return () => document.removeEventListener('keydown', handleKeyDown)
100
+ }, [onClose])
101
+
102
+ // Resize handlers
103
+ const handleResizeStart = useCallback((e: React.MouseEvent) => {
104
+ e.preventDefault()
105
+ setIsResizing(true)
106
+ resizeStartX.current = e.clientX
107
+ resizeStartWidth.current = drawerWidth
108
+ }, [drawerWidth])
109
+
110
+ useEffect(() => {
111
+ if (!isResizing) return
112
+
113
+ document.body.style.cursor = 'ew-resize'
114
+ document.body.style.userSelect = 'none'
115
+
116
+ const maxWidth = window.innerWidth * MAX_WIDTH_PERCENT
117
+ const handleMouseMove = (e: MouseEvent) => {
118
+ const deltaX = resizeStartX.current - e.clientX
119
+ const newWidth = resizeStartWidth.current + deltaX
120
+ setDrawerWidth(Math.max(MIN_WIDTH, Math.min(newWidth, maxWidth)))
121
+ }
122
+ const handleMouseUp = () => setIsResizing(false)
123
+ document.addEventListener('mousemove', handleMouseMove)
124
+ document.addEventListener('mouseup', handleMouseUp)
125
+ return () => {
126
+ document.body.style.cursor = ''
127
+ document.body.style.userSelect = ''
128
+ document.removeEventListener('mousemove', handleMouseMove)
129
+ document.removeEventListener('mouseup', handleMouseUp)
130
+ }
131
+ }, [isResizing])
132
+
133
+ const copyToClipboard = useCallback((text: string, key: string) => {
134
+ navigator.clipboard.writeText(text)
135
+ setCopied(key)
136
+ setTimeout(() => setCopied(null), 2000)
137
+ }, [])
138
+
139
+ const switchTab = useCallback((tab: TabId) => {
140
+ const update = () => flushSync(() => setActiveTab(tab))
141
+ if (document.startViewTransition) {
142
+ document.startViewTransition(update)
143
+ } else {
144
+ setActiveTab(tab)
145
+ }
146
+ }, [])
147
+
148
+ const handleCompareRevisions = (rev1: number, rev2: number) => {
149
+ setDiffRevisions({ rev1, rev2 })
150
+ switchTab('diff')
151
+ }
152
+
153
+ const handleViewRevision = (revision: number) => {
154
+ setSelectedRevision(revision)
155
+ switchTab('manifest')
156
+ }
157
+
158
+ const handleRollbackRequest = (revision: number) => {
159
+ setRollbackRevision(revision)
160
+ }
161
+
162
+ const handleRollbackConfirm = async () => {
163
+ if (rollbackRevision === null) return
164
+ setIsRollingBack(true)
165
+ setRollbackProgress([])
166
+
167
+ try {
168
+ await rollbackWithProgress(
169
+ release.namespace,
170
+ release.name,
171
+ rollbackRevision,
172
+ (event) => {
173
+ if (event.type === 'progress' && event.message) {
174
+ setRollbackProgress(prev => [...prev, {
175
+ phase: event.phase || 'progress',
176
+ message: event.message || '',
177
+ }])
178
+ }
179
+ }
180
+ )
181
+
182
+ setRollbackProgress(prev => [...prev, {
183
+ phase: 'complete',
184
+ message: `Successfully rolled back to revision ${rollbackRevision}`,
185
+ }])
186
+
187
+ queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
188
+ queryClient.invalidateQueries({ queryKey: ['helm-release', release.namespace, release.name] })
189
+
190
+ setTimeout(() => {
191
+ setRollbackRevision(null)
192
+ setRollbackProgress([])
193
+ refetch()
194
+ switchTab('resources')
195
+ }, 1500)
196
+ } catch (err) {
197
+ setRollbackProgress(prev => [...prev, {
198
+ phase: 'error',
199
+ message: err instanceof Error ? err.message : 'Rollback failed',
200
+ }])
201
+ } finally {
202
+ setIsRollingBack(false)
203
+ }
204
+ }
205
+
206
+ const handleUninstallConfirm = () => {
207
+ uninstallMutation.mutate(
208
+ { namespace: release.namespace, name: release.name },
209
+ {
210
+ onSuccess: () => {
211
+ setShowUninstallConfirm(false)
212
+ onClose()
213
+ },
214
+ onError: () => {
215
+ // Keep dialog open on error so user can see the error state
216
+ },
217
+ }
218
+ )
219
+ }
220
+
221
+ const handleUpgradeConfirm = async () => {
222
+ if (!upgradeInfo?.latestVersion) return
223
+ setIsUpgrading(true)
224
+ setUpgradeProgress([])
225
+
226
+ try {
227
+ await upgradeWithProgress(
228
+ release.namespace,
229
+ release.name,
230
+ upgradeInfo.latestVersion,
231
+ (event) => {
232
+ if (event.type === 'progress' && event.message) {
233
+ setUpgradeProgress(prev => [...prev, {
234
+ phase: event.phase || 'progress',
235
+ message: event.message || '',
236
+ }])
237
+ }
238
+ }
239
+ )
240
+
241
+ setUpgradeProgress(prev => [...prev, {
242
+ phase: 'complete',
243
+ message: `Successfully upgraded to ${upgradeInfo.latestVersion}`,
244
+ }])
245
+
246
+ // Invalidate queries
247
+ queryClient.invalidateQueries({ queryKey: ['helm-releases'] })
248
+ queryClient.invalidateQueries({ queryKey: ['helm-release', release.namespace, release.name] })
249
+ queryClient.invalidateQueries({ queryKey: ['helm-upgrade-info', release.namespace, release.name] })
250
+ queryClient.invalidateQueries({ queryKey: ['helm-batch-upgrade-info'] })
251
+
252
+ setTimeout(() => {
253
+ setShowUpgradeConfirm(false)
254
+ setUpgradeProgress([])
255
+ refetch()
256
+ switchTab('resources')
257
+ }, 1500)
258
+ } catch (err) {
259
+ setUpgradeProgress(prev => [...prev, {
260
+ phase: 'error',
261
+ message: err instanceof Error ? err.message : 'Upgrade failed',
262
+ }])
263
+ } finally {
264
+ setIsUpgrading(false)
265
+ }
266
+ }
267
+
268
+ const headerHeight = 49
269
+
270
+ const tabs: { id: TabId; label: string; icon: typeof Package }[] = [
271
+ { id: 'overview', label: 'Overview', icon: Package },
272
+ { id: 'history', label: 'History', icon: History },
273
+ { id: 'manifest', label: 'Manifest', icon: Code },
274
+ { id: 'values', label: 'Values', icon: Settings },
275
+ { id: 'resources', label: 'Resources', icon: Link2 },
276
+ { id: 'hooks', label: 'Hooks', icon: Anchor },
277
+ ]
278
+
279
+ // Add diff tab only when comparing
280
+ if (diffRevisions) {
281
+ tabs.push({ id: 'diff', label: 'Diff', icon: FileText })
282
+ }
283
+
284
+ return (
285
+ <div
286
+ className={clsx(
287
+ 'fixed right-0 bg-theme-surface border-l border-theme-border flex flex-col shadow-drawer z-40',
288
+ TRANSITION_DRAWER,
289
+ isOpen ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
290
+ )}
291
+ style={{ width: drawerWidth, top: headerHeight, height: `calc(100vh - ${headerHeight}px)` }}
292
+ >
293
+ {/* Resize handle */}
294
+ <div
295
+ onMouseDown={handleResizeStart}
296
+ className={clsx(
297
+ 'absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize z-10 hover:bg-blue-500/50 transition-colors',
298
+ 'hidden sm:block',
299
+ isResizing && 'bg-blue-500/50'
300
+ )}
301
+ />
302
+
303
+ {/* Header */}
304
+ <div className="border-b border-theme-border shrink-0">
305
+ <div className="flex items-center justify-between px-4 pt-3 pb-2">
306
+ <div className="flex items-center gap-2 flex-wrap">
307
+ <span className={clsx('badge', SEVERITY_BADGE.info)}>
308
+ Helm Release
309
+ </span>
310
+ {releaseDetail && (
311
+ <span className={clsx('badge', getHelmStatusColor(releaseDetail.status))}>
312
+ {releaseDetail.status}
313
+ </span>
314
+ )}
315
+ {/* Upgrade indicator */}
316
+ {upgradeLoading ? (
317
+ <span className="badge bg-theme-hover/50 text-theme-text-secondary animate-pulse">
318
+ checking...
319
+ </span>
320
+ ) : upgradeInfo?.updateAvailable ? (
321
+ <button
322
+ onClick={() => setShowUpgradeConfirm(true)}
323
+ disabled={!canHelmWrite}
324
+ className={clsx(
325
+ 'badge transition-colors', SEVERITY_BADGE.warning,
326
+ canHelmWrite ? 'hover:bg-amber-500/30 cursor-pointer' : 'opacity-50 cursor-not-allowed'
327
+ )}
328
+ title={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` : 'Helm write permissions required (rbac.helm=true)'}
329
+ >
330
+ <ArrowUpCircle className="w-3 h-3" />
331
+ {upgradeInfo.latestVersion}
332
+ </button>
333
+ ) : upgradeInfo && !upgradeInfo.error ? (
334
+ <span className={clsx('badge', SEVERITY_BADGE.success)} title="Chart is up to date">
335
+ latest
336
+ </span>
337
+ ) : null}
338
+ </div>
339
+ <div className="flex items-center gap-1">
340
+ <button
341
+ onClick={refetch}
342
+ disabled={isRefreshAnimating}
343
+ className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
344
+ title="Refresh"
345
+ >
346
+ <RefreshCw className={clsx('w-4 h-4', isRefreshAnimating && 'animate-spin')} />
347
+ </button>
348
+ <button
349
+ onClick={() => setShowUninstallConfirm(true)}
350
+ disabled={!canHelmWrite}
351
+ className={clsx(
352
+ 'p-1.5 rounded',
353
+ canHelmWrite
354
+ ? 'text-theme-text-secondary hover:text-red-400 hover:bg-red-500/10'
355
+ : 'text-theme-text-disabled cursor-not-allowed'
356
+ )}
357
+ title={canHelmWrite ? 'Uninstall release' : 'Helm write permissions required (rbac.helm=true)'}
358
+ >
359
+ <Trash2 className="w-4 h-4" />
360
+ </button>
361
+ <button onClick={onClose} className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded" title="Close (Esc)">
362
+ <X className="w-4 h-4" />
363
+ </button>
364
+ </div>
365
+ </div>
366
+
367
+ {/* Name and namespace */}
368
+ <div className="px-4 pb-3">
369
+ <div className="flex items-center gap-2">
370
+ <Package className="w-5 h-5 text-purple-400" />
371
+ <h2 className="text-lg font-semibold text-theme-text-primary truncate">{release.name}</h2>
372
+ <button
373
+ onClick={() => copyToClipboard(release.name, 'name')}
374
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
375
+ title="Copy name"
376
+ >
377
+ {copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
378
+ </button>
379
+ </div>
380
+ <p className="text-sm text-theme-text-tertiary">{release.namespace}</p>
381
+ </div>
382
+
383
+ {/* Tabs */}
384
+ <div className="flex items-center gap-1 px-4 pb-2 overflow-x-auto">
385
+ {tabs.map((tab) => (
386
+ <button
387
+ key={tab.id}
388
+ onClick={() => switchTab(tab.id)}
389
+ className={clsx(
390
+ 'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors whitespace-nowrap',
391
+ activeTab === tab.id
392
+ ? 'bg-theme-elevated text-theme-text-primary'
393
+ : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/50'
394
+ )}
395
+ >
396
+ <tab.icon className="w-3.5 h-3.5" />
397
+ {tab.label}
398
+ </button>
399
+ ))}
400
+ </div>
401
+ </div>
402
+
403
+ {/* Content */}
404
+ <div className="flex-1 overflow-y-auto" style={{ viewTransitionName: 'helm-drawer-content' }}>
405
+ {isLoading ? (
406
+ <div className="flex items-center justify-center h-32 text-theme-text-tertiary">Loading...</div>
407
+ ) : !releaseDetail ? (
408
+ <div className="flex items-center justify-center h-32 text-theme-text-tertiary">Release not found</div>
409
+ ) : (
410
+ <>
411
+ {activeTab === 'overview' && (
412
+ <OverviewTab release={releaseDetail} onCopy={copyToClipboard} copied={copied} />
413
+ )}
414
+ {activeTab === 'history' && (
415
+ <RevisionHistory
416
+ history={releaseDetail.history}
417
+ currentRevision={releaseDetail.revision}
418
+ onViewRevision={handleViewRevision}
419
+ onCompare={handleCompareRevisions}
420
+ onRollback={canHelmWrite ? handleRollbackRequest : undefined}
421
+ />
422
+ )}
423
+ {activeTab === 'manifest' && (
424
+ <ManifestViewer
425
+ manifest={manifest || ''}
426
+ isLoading={manifestLoading}
427
+ revision={selectedRevision}
428
+ onCopy={(text) => copyToClipboard(text, 'manifest')}
429
+ copied={copied === 'manifest'}
430
+ />
431
+ )}
432
+ {activeTab === 'values' && (
433
+ <ValuesViewer
434
+ values={values}
435
+ isLoading={valuesLoading}
436
+ showAllValues={showAllValues}
437
+ onToggleAllValues={setShowAllValues}
438
+ onCopy={(text) => copyToClipboard(text, 'values')}
439
+ copied={copied === 'values'}
440
+ namespace={release.namespace}
441
+ name={release.name}
442
+ onApplySuccess={() => refetch()}
443
+ />
444
+ )}
445
+ {activeTab === 'resources' && (
446
+ <OwnedResources
447
+ resources={releaseDetail.resources}
448
+ onNavigate={onNavigateToResource}
449
+ />
450
+ )}
451
+ {activeTab === 'hooks' && (
452
+ <HooksTab hooks={releaseDetail.hooks || []} />
453
+ )}
454
+ {activeTab === 'diff' && diffRevisions && (
455
+ <ManifestDiffViewer
456
+ diff={diffData?.diff || ''}
457
+ isLoading={diffLoading}
458
+ revision1={diffRevisions.rev1}
459
+ revision2={diffRevisions.rev2}
460
+ onClose={() => {
461
+ setDiffRevisions(null)
462
+ setActiveTab('history')
463
+ }}
464
+ />
465
+ )}
466
+ </>
467
+ )}
468
+ </div>
469
+
470
+ {/* Rollback confirmation dialog */}
471
+ <ConfirmDialog
472
+ open={rollbackRevision !== null}
473
+ onClose={() => {
474
+ setRollbackRevision(null)
475
+ setRollbackProgress([])
476
+ if (isRollingBack) {
477
+ setIsRollingBack(false)
478
+ switchTab('resources')
479
+ }
480
+ }}
481
+ onConfirm={handleRollbackConfirm}
482
+ title="Rollback Release"
483
+ message={`Rollback "${release.name}" to revision ${rollbackRevision}?`}
484
+ details={rollbackProgress.length === 0
485
+ ? `This will create a new revision that reverts the release to the state it was in at revision ${rollbackRevision}. The rollback will be applied to your cluster immediately.`
486
+ : undefined
487
+ }
488
+ confirmLabel="Rollback"
489
+ variant="warning"
490
+ isLoading={isRollingBack}
491
+ isClosable
492
+ >
493
+ {rollbackProgress.length > 0 && <ProgressLog entries={rollbackProgress} />}
494
+ </ConfirmDialog>
495
+
496
+ {/* Uninstall confirmation dialog */}
497
+ <ConfirmDialog
498
+ open={showUninstallConfirm}
499
+ onClose={() => setShowUninstallConfirm(false)}
500
+ onConfirm={handleUninstallConfirm}
501
+ title="Uninstall Release"
502
+ message={`Are you sure you want to uninstall "${release.name}"?`}
503
+ details={`This will remove the Helm release and all associated Kubernetes resources from the "${release.namespace}" namespace. This action cannot be undone.`}
504
+ confirmLabel="Uninstall"
505
+ variant="danger"
506
+ isLoading={uninstallMutation.isPending}
507
+ />
508
+
509
+ {/* Upgrade confirmation dialog */}
510
+ <ConfirmDialog
511
+ open={showUpgradeConfirm}
512
+ onClose={() => {
513
+ setShowUpgradeConfirm(false)
514
+ setUpgradeProgress([])
515
+ if (isUpgrading) {
516
+ // Upgrade continues server-side — switch to resources tab to monitor
517
+ setIsUpgrading(false)
518
+ switchTab('resources')
519
+ }
520
+ }}
521
+ onConfirm={handleUpgradeConfirm}
522
+ title="Upgrade Release"
523
+ message={`Upgrade "${release.name}" to version ${upgradeInfo?.latestVersion}?`}
524
+ details={upgradeProgress.length === 0
525
+ ? `This will upgrade the chart from version ${upgradeInfo?.currentVersion} to ${upgradeInfo?.latestVersion}. Your existing values will be preserved. The upgrade will be applied to your cluster immediately.`
526
+ : undefined
527
+ }
528
+ confirmLabel="Upgrade"
529
+ variant="warning"
530
+ isLoading={isUpgrading}
531
+ isClosable
532
+ >
533
+ {upgradeProgress.length > 0 && <ProgressLog entries={upgradeProgress} />}
534
+ </ConfirmDialog>
535
+ </div>
536
+ )
537
+ }
538
+
539
+ // Shared progress log for streaming Helm operations
540
+ function ProgressLog({ entries }: { entries: { phase: string; message: string }[] }) {
541
+ return (
542
+ <div className="space-y-1.5 max-h-48 overflow-auto">
543
+ {entries.map((log, i) => (
544
+ <div key={i} className="flex items-start gap-2 text-xs">
545
+ <span className={clsx(
546
+ 'px-1.5 py-0.5 rounded font-medium shrink-0',
547
+ log.phase === 'error' ? SEVERITY_BADGE.error :
548
+ log.phase === 'complete' ? SEVERITY_BADGE.success :
549
+ SEVERITY_BADGE.info
550
+ )}>
551
+ {log.phase}
552
+ </span>
553
+ <span className={clsx(
554
+ log.phase === 'error' ? SEVERITY_TEXT.error :
555
+ log.phase === 'complete' ? SEVERITY_TEXT.success :
556
+ 'text-theme-text-secondary'
557
+ )}>
558
+ {log.message}
559
+ </span>
560
+ </div>
561
+ ))}
562
+ </div>
563
+ )
564
+ }
565
+
566
+ // Overview tab content
567
+ interface OverviewTabProps {
568
+ release: {
569
+ chart: string
570
+ chartVersion: string
571
+ appVersion: string
572
+ revision: number
573
+ updated: string
574
+ description: string
575
+ notes: string
576
+ readme?: string
577
+ dependencies?: ChartDependency[]
578
+ }
579
+ onCopy: (text: string, key: string) => void
580
+ copied: string | null
581
+ }
582
+
583
+ function OverviewTab({ release, onCopy, copied }: OverviewTabProps) {
584
+ return (
585
+ <div className="p-4 space-y-4">
586
+ {/* Chart info */}
587
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
588
+ <h3 className="text-sm font-medium text-theme-text-secondary mb-3">Chart Information</h3>
589
+ <dl className="grid grid-cols-2 gap-3 text-sm">
590
+ <div>
591
+ <dt className="text-theme-text-tertiary">Chart</dt>
592
+ <dd className="text-theme-text-primary font-medium">{release.chart}</dd>
593
+ </div>
594
+ <div>
595
+ <dt className="text-theme-text-tertiary">Chart Version</dt>
596
+ <dd className="text-theme-text-primary">{release.chartVersion}</dd>
597
+ </div>
598
+ <div>
599
+ <dt className="text-theme-text-tertiary">App Version</dt>
600
+ <dd className="text-theme-text-primary">{release.appVersion || '-'}</dd>
601
+ </div>
602
+ <div>
603
+ <dt className="text-theme-text-tertiary">Revision</dt>
604
+ <dd className="text-theme-text-primary">{release.revision}</dd>
605
+ </div>
606
+ <div className="col-span-2">
607
+ <dt className="text-theme-text-tertiary">Updated</dt>
608
+ <dd className="text-theme-text-primary">{formatDate(release.updated)}</dd>
609
+ </div>
610
+ </dl>
611
+ </div>
612
+
613
+ {/* Description */}
614
+ {release.description && (
615
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
616
+ <h3 className="text-sm font-medium text-theme-text-secondary mb-2">Description</h3>
617
+ <p className="text-sm text-theme-text-secondary">{release.description}</p>
618
+ </div>
619
+ )}
620
+
621
+ {/* Notes */}
622
+ {release.notes && (
623
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
624
+ <div className="flex items-center justify-between mb-2">
625
+ <h3 className="text-sm font-medium text-theme-text-secondary">Release Notes</h3>
626
+ <button
627
+ onClick={() => onCopy(release.notes, 'notes')}
628
+ className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
629
+ >
630
+ {copied === 'notes' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
631
+ Copy
632
+ </button>
633
+ </div>
634
+ <div className="text-xs bg-theme-base/50 rounded p-3 max-h-64 overflow-auto">
635
+ <Markdown>{release.notes}</Markdown>
636
+ </div>
637
+ </div>
638
+ )}
639
+
640
+ {/* Dependencies */}
641
+ {release.dependencies && release.dependencies.length > 0 && (
642
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
643
+ <div className="flex items-center gap-2 mb-3">
644
+ <GitFork className="w-4 h-4 text-theme-text-secondary" />
645
+ <h3 className="text-sm font-medium text-theme-text-secondary">Chart Dependencies</h3>
646
+ </div>
647
+ <div className="space-y-2">
648
+ {release.dependencies.map((dep) => (
649
+ <div key={dep.name} className="flex items-center justify-between bg-theme-base/50 rounded p-2 text-sm">
650
+ <div className="flex items-center gap-2">
651
+ <span className="text-theme-text-primary font-medium">{dep.name}</span>
652
+ <span className="text-theme-text-tertiary">{dep.version}</span>
653
+ </div>
654
+ <div className="flex items-center gap-2">
655
+ {dep.condition && (
656
+ <span className="text-xs text-theme-text-tertiary">{dep.condition}</span>
657
+ )}
658
+ <span className={clsx(
659
+ 'badge-sm',
660
+ dep.enabled
661
+ ? SEVERITY_BADGE.success
662
+ : SEVERITY_BADGE.neutral
663
+ )}>
664
+ {dep.enabled ? 'enabled' : 'disabled'}
665
+ </span>
666
+ </div>
667
+ </div>
668
+ ))}
669
+ </div>
670
+ </div>
671
+ )}
672
+
673
+ {/* README */}
674
+ {release.readme && (
675
+ <div className="bg-theme-elevated/30 rounded-lg p-4">
676
+ <div className="flex items-center justify-between mb-2">
677
+ <div className="flex items-center gap-2">
678
+ <BookOpen className="w-4 h-4 text-theme-text-secondary" />
679
+ <h3 className="text-sm font-medium text-theme-text-secondary">Chart README</h3>
680
+ </div>
681
+ <button
682
+ onClick={() => onCopy(release.readme!, 'readme')}
683
+ className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
684
+ >
685
+ {copied === 'readme' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
686
+ Copy
687
+ </button>
688
+ </div>
689
+ <div className="text-xs bg-theme-base/50 rounded p-3 max-h-96 overflow-auto">
690
+ <Markdown>{release.readme}</Markdown>
691
+ </div>
692
+ </div>
693
+ )}
694
+ </div>
695
+ )
696
+ }
697
+
698
+ // Hooks tab content
699
+ interface HooksTabProps {
700
+ hooks: HelmHook[]
701
+ }
702
+
703
+ function HooksTab({ hooks }: HooksTabProps) {
704
+ if (hooks.length === 0) {
705
+ return (
706
+ <div className="flex flex-col items-center justify-center h-48 text-theme-text-tertiary">
707
+ <Anchor className="w-8 h-8 mb-2 opacity-50" />
708
+ <p>No hooks defined for this release</p>
709
+ </div>
710
+ )
711
+ }
712
+
713
+ const getHookStatusColor = (status?: string) => {
714
+ if (!status) return SEVERITY_BADGE.neutral
715
+ switch (status.toLowerCase()) {
716
+ case 'succeeded':
717
+ return SEVERITY_BADGE.success
718
+ case 'failed':
719
+ return SEVERITY_BADGE.error
720
+ case 'running':
721
+ return SEVERITY_BADGE.info
722
+ default:
723
+ return SEVERITY_BADGE.neutral
724
+ }
725
+ }
726
+
727
+ const getEventColor = (event: string) => {
728
+ if (event.includes('delete')) return SEVERITY_BADGE.error
729
+ if (event.includes('install')) return SEVERITY_BADGE.success
730
+ if (event.includes('upgrade')) return SEVERITY_BADGE.info
731
+ if (event.includes('rollback')) return SEVERITY_BADGE.warning
732
+ return SEVERITY_BADGE.neutral
733
+ }
734
+
735
+ return (
736
+ <div className="p-4 space-y-3">
737
+ <p className="text-sm text-theme-text-secondary mb-4">
738
+ Helm hooks are executed at specific points during the release lifecycle.
739
+ </p>
740
+ {hooks.map((hook) => (
741
+ <div key={hook.name} className="bg-theme-elevated/30 rounded-lg p-4">
742
+ <div className="flex items-start justify-between mb-2">
743
+ <div>
744
+ <div className="flex items-center gap-2">
745
+ <span className="text-theme-text-primary font-medium">{hook.name}</span>
746
+ <span className="badge-sm bg-theme-hover/50 text-theme-text-secondary">
747
+ {hook.kind}
748
+ </span>
749
+ </div>
750
+ <div className="flex items-center gap-2 mt-1 text-xs text-theme-text-tertiary">
751
+ <span>Weight: {hook.weight}</span>
752
+ </div>
753
+ </div>
754
+ {hook.status && (
755
+ <span className={clsx('badge', getHookStatusColor(hook.status))}>
756
+ {hook.status}
757
+ </span>
758
+ )}
759
+ </div>
760
+ <div className="flex flex-wrap gap-1.5 mt-2">
761
+ {hook.events.map((event) => (
762
+ <span
763
+ key={event}
764
+ className={clsx('badge', getEventColor(event))}
765
+ >
766
+ {event}
767
+ </span>
768
+ ))}
769
+ </div>
770
+ </div>
771
+ ))}
772
+ </div>
773
+ )
774
+ }