@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,580 @@
1
+ import { useState, useMemo } from 'react'
2
+ import { Search, RefreshCw, Package, Database, AlertCircle, ExternalLink, ChevronDown, Star, Shield, BadgeCheck, Building2, Globe, ArrowUpDown, FileJson, PenTool } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { useHelmRepositories, useSearchCharts, useUpdateRepository, useArtifactHubSearch, type ArtifactHubSortOption } from '../../api/client'
5
+ import { useCanHelmWrite } from '../../contexts/CapabilitiesContext'
6
+ import type { ChartInfo, HelmRepository, ArtifactHubChart, ChartSource } from '../../types'
7
+ import { formatAge } from './helm-utils'
8
+ import { SEVERITY_BADGE } from '../../utils/badge-colors'
9
+ import { Tooltip } from '../ui/Tooltip'
10
+
11
+ interface ChartBrowserProps {
12
+ onChartSelect: (repo: string, chart: string, version: string, source: ChartSource) => void
13
+ }
14
+
15
+ export function ChartBrowser({ onChartSelect }: ChartBrowserProps) {
16
+ const [chartSource, setChartSource] = useState<ChartSource>('local')
17
+ const [searchTerm, setSearchTerm] = useState('')
18
+ const [selectedRepo, setSelectedRepo] = useState<string>('all')
19
+ const [showAllVersions, setShowAllVersions] = useState(false)
20
+ const [repoDropdownOpen, setRepoDropdownOpen] = useState(false)
21
+ // ArtifactHub filters
22
+ const [showOfficialOnly, setShowOfficialOnly] = useState(false)
23
+ const [showVerifiedOnly, setShowVerifiedOnly] = useState(false)
24
+ const [artifactHubSort, setArtifactHubSort] = useState<ArtifactHubSortOption>('relevance')
25
+
26
+ const canHelmWrite = useCanHelmWrite()
27
+
28
+ // Local repo hooks
29
+ const { data: repositories, isLoading: reposLoading } = useHelmRepositories()
30
+ const { data: searchResult, isLoading: chartsLoading, refetch: refetchCharts } = useSearchCharts(
31
+ searchTerm,
32
+ showAllVersions,
33
+ chartSource === 'local'
34
+ )
35
+ const updateRepoMutation = useUpdateRepository()
36
+
37
+ // ArtifactHub hook - only search when there's a search term
38
+ const { data: artifactHubResult, isLoading: artifactHubLoading } = useArtifactHubSearch(
39
+ searchTerm,
40
+ { official: showOfficialOnly, verified: showVerifiedOnly, limit: 60, sort: artifactHubSort },
41
+ chartSource === 'artifacthub' && searchTerm.length > 0
42
+ )
43
+
44
+ // Filter local charts by selected repository
45
+ const filteredLocalCharts = useMemo(() => {
46
+ if (!searchResult?.charts) return []
47
+ if (selectedRepo === 'all') return searchResult.charts
48
+ return searchResult.charts.filter(c => c.repository === selectedRepo)
49
+ }, [searchResult?.charts, selectedRepo])
50
+
51
+ // Group local charts by repository for display
52
+ const chartsByRepo = useMemo(() => {
53
+ const groups = new Map<string, ChartInfo[]>()
54
+ for (const chart of filteredLocalCharts) {
55
+ const existing = groups.get(chart.repository) || []
56
+ existing.push(chart)
57
+ groups.set(chart.repository, existing)
58
+ }
59
+ return groups
60
+ }, [filteredLocalCharts])
61
+
62
+ const handleUpdateRepo = async (repoName: string) => {
63
+ await updateRepoMutation.mutateAsync(repoName)
64
+ refetchCharts()
65
+ }
66
+
67
+ const handleUpdateAllRepos = async () => {
68
+ if (!repositories) return
69
+ for (const repo of repositories) {
70
+ try {
71
+ await updateRepoMutation.mutateAsync(repo.name)
72
+ } catch {
73
+ // Continue with other repos
74
+ }
75
+ }
76
+ refetchCharts()
77
+ }
78
+
79
+ const isLoading = chartSource === 'local' ? chartsLoading : artifactHubLoading
80
+ const totalCount = chartSource === 'local'
81
+ ? filteredLocalCharts.length
82
+ : (artifactHubResult?.charts.length ?? 0)
83
+
84
+ return (
85
+ <div className="flex flex-col h-full">
86
+ {/* Toolbar */}
87
+ <div className="flex items-center gap-4 px-4 py-3 border-b border-theme-border bg-theme-surface/50 shrink-0">
88
+ <div className="flex items-center gap-2 text-theme-text-secondary">
89
+ <Package className="w-5 h-5" />
90
+ <span className="font-medium">Charts</span>
91
+ {!isLoading && (
92
+ <span className="badge bg-theme-elevated">
93
+ {totalCount}
94
+ </span>
95
+ )}
96
+ </div>
97
+
98
+ {/* Source toggle */}
99
+ <div className="flex items-center bg-theme-elevated rounded-lg p-0.5 border border-theme-border-light">
100
+ <button
101
+ onClick={() => setChartSource('local')}
102
+ className={clsx(
103
+ 'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors',
104
+ chartSource === 'local'
105
+ ? `${SEVERITY_BADGE.info} font-medium`
106
+ : 'text-theme-text-secondary hover:text-theme-text-primary'
107
+ )}
108
+ >
109
+ <Database className="w-3.5 h-3.5" />
110
+ My Repos
111
+ </button>
112
+ <button
113
+ onClick={() => setChartSource('artifacthub')}
114
+ className={clsx(
115
+ 'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md transition-colors',
116
+ chartSource === 'artifacthub'
117
+ ? `${SEVERITY_BADGE.info} font-medium`
118
+ : 'text-theme-text-secondary hover:text-theme-text-primary'
119
+ )}
120
+ >
121
+ <Globe className="w-3.5 h-3.5" />
122
+ ArtifactHub
123
+ </button>
124
+ </div>
125
+
126
+ {/* Repository filter dropdown (only for local) */}
127
+ {chartSource === 'local' && (
128
+ <div className="relative">
129
+ <button
130
+ onClick={() => setRepoDropdownOpen(!repoDropdownOpen)}
131
+ className="flex items-center gap-2 px-3 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary hover:bg-theme-hover transition-colors"
132
+ >
133
+ <Database className="w-4 h-4 text-theme-text-tertiary" />
134
+ <span>{selectedRepo === 'all' ? 'All Repositories' : selectedRepo}</span>
135
+ <ChevronDown className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform', repoDropdownOpen && 'rotate-180')} />
136
+ </button>
137
+
138
+ {repoDropdownOpen && (
139
+ <div className="absolute top-full left-0 mt-1 w-64 bg-theme-surface border border-theme-border rounded-lg shadow-xl z-50 py-1 max-h-64 overflow-auto">
140
+ <button
141
+ onClick={() => { setSelectedRepo('all'); setRepoDropdownOpen(false) }}
142
+ className={clsx(
143
+ 'w-full px-3 py-2 text-left text-sm hover:bg-theme-hover flex items-center justify-between',
144
+ selectedRepo === 'all' ? 'text-blue-400' : 'text-theme-text-primary'
145
+ )}
146
+ >
147
+ <span>All Repositories</span>
148
+ {selectedRepo === 'all' && <span className="text-xs">✓</span>}
149
+ </button>
150
+ <div className="border-t border-theme-border my-1" />
151
+ {reposLoading ? (
152
+ <div className="px-3 py-2 text-sm text-theme-text-tertiary">Loading...</div>
153
+ ) : repositories?.length === 0 ? (
154
+ <div className="px-3 py-2 text-sm text-theme-text-tertiary">No repositories configured</div>
155
+ ) : (
156
+ repositories?.map(repo => (
157
+ <RepoDropdownItem
158
+ key={repo.name}
159
+ repo={repo}
160
+ isSelected={selectedRepo === repo.name}
161
+ onSelect={() => { setSelectedRepo(repo.name); setRepoDropdownOpen(false) }}
162
+ onUpdate={() => handleUpdateRepo(repo.name)}
163
+ isUpdating={updateRepoMutation.isPending}
164
+ canUpdate={canHelmWrite}
165
+ />
166
+ ))
167
+ )}
168
+ </div>
169
+ )}
170
+ </div>
171
+ )}
172
+
173
+ {/* ArtifactHub filters */}
174
+ {chartSource === 'artifacthub' && (
175
+ <div className="flex items-center gap-3">
176
+ <label className="flex items-center gap-1.5 text-sm text-theme-text-secondary">
177
+ <input
178
+ type="checkbox"
179
+ checked={showOfficialOnly}
180
+ onChange={(e) => setShowOfficialOnly(e.target.checked)}
181
+ className="rounded border-theme-border text-blue-500 focus:ring-blue-500"
182
+ />
183
+ <BadgeCheck className="w-3.5 h-3.5 text-blue-400" />
184
+ Official
185
+ </label>
186
+ <label className="flex items-center gap-1.5 text-sm text-theme-text-secondary">
187
+ <input
188
+ type="checkbox"
189
+ checked={showVerifiedOnly}
190
+ onChange={(e) => setShowVerifiedOnly(e.target.checked)}
191
+ className="rounded border-theme-border text-blue-500 focus:ring-blue-500"
192
+ />
193
+ <Shield className="w-3.5 h-3.5 text-green-400" />
194
+ Verified
195
+ </label>
196
+ {/* Sort dropdown */}
197
+ <div className="flex items-center gap-1.5">
198
+ <ArrowUpDown className="w-3.5 h-3.5 text-theme-text-tertiary" />
199
+ <select
200
+ value={artifactHubSort}
201
+ onChange={(e) => setArtifactHubSort(e.target.value as ArtifactHubSortOption)}
202
+ className="bg-theme-elevated border border-theme-border-light rounded px-2 py-1 text-sm text-theme-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500"
203
+ >
204
+ <option value="relevance">Relevance</option>
205
+ <option value="stars">Stars</option>
206
+ <option value="last_updated">Last Updated</option>
207
+ </select>
208
+ </div>
209
+ </div>
210
+ )}
211
+
212
+ {/* Search */}
213
+ <div className="flex-1 relative">
214
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
215
+ <input
216
+ type="text"
217
+ placeholder={chartSource === 'local' ? "Search charts..." : "Search ArtifactHub..."}
218
+ value={searchTerm}
219
+ onChange={(e) => setSearchTerm(e.target.value)}
220
+ className="w-full max-w-md pl-10 pr-4 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"
221
+ />
222
+ </div>
223
+
224
+ {/* Options (only for local) */}
225
+ {chartSource === 'local' && (
226
+ <>
227
+ <label className="flex items-center gap-2 text-sm text-theme-text-secondary">
228
+ <input
229
+ type="checkbox"
230
+ checked={showAllVersions}
231
+ onChange={(e) => setShowAllVersions(e.target.checked)}
232
+ className="rounded border-theme-border text-blue-500 focus:ring-blue-500"
233
+ />
234
+ All versions
235
+ </label>
236
+
237
+ {/* Refresh button */}
238
+ <Tooltip content={canHelmWrite ? "Update all repositories" : "Helm write permissions required (rbac.helm=true)"}>
239
+ <button
240
+ onClick={handleUpdateAllRepos}
241
+ disabled={updateRepoMutation.isPending || !canHelmWrite}
242
+ className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg disabled:opacity-50"
243
+ >
244
+ <RefreshCw className={clsx('w-4 h-4', updateRepoMutation.isPending && 'animate-spin')} />
245
+ </button>
246
+ </Tooltip>
247
+ </>
248
+ )}
249
+ </div>
250
+
251
+ {/* Chart grid */}
252
+ <div className="flex-1 overflow-auto p-4">
253
+ {isLoading ? (
254
+ <div className="flex items-center justify-center h-32 text-theme-text-tertiary">
255
+ Loading charts...
256
+ </div>
257
+ ) : chartSource === 'local' ? (
258
+ // Local charts view
259
+ filteredLocalCharts.length === 0 ? (
260
+ <div className="flex flex-col items-center justify-center h-64 text-theme-text-tertiary gap-3">
261
+ <AlertCircle className="w-12 h-12 text-theme-text-disabled" />
262
+ <div className="text-center">
263
+ <p className="text-lg font-medium text-theme-text-secondary">No charts found</p>
264
+ {searchTerm ? (
265
+ <p className="text-sm mt-1">Try a different search term</p>
266
+ ) : repositories?.length === 0 ? (
267
+ <div className="text-sm mt-1">
268
+ <p>No Helm repositories configured.</p>
269
+ <p className="mt-1">
270
+ Add repositories using <code className="bg-theme-elevated px-1 rounded">helm repo add</code>
271
+ </p>
272
+ <p className="mt-2">
273
+ Or try searching on <button onClick={() => setChartSource('artifacthub')} className="text-blue-400 hover:underline">ArtifactHub</button>
274
+ </p>
275
+ </div>
276
+ ) : (
277
+ <p className="text-sm mt-1">Try updating your repositories</p>
278
+ )}
279
+ </div>
280
+ </div>
281
+ ) : selectedRepo === 'all' ? (
282
+ // Grouped by repository
283
+ <div className="space-y-6">
284
+ {Array.from(chartsByRepo.entries()).map(([repoName, charts]) => (
285
+ <div key={repoName}>
286
+ <div className="flex items-center gap-2 mb-3">
287
+ <Database className="w-4 h-4 text-theme-text-tertiary" />
288
+ <h3 className="text-sm font-medium text-theme-text-secondary">{repoName}</h3>
289
+ <span className="text-xs text-theme-text-tertiary">({charts.length})</span>
290
+ </div>
291
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
292
+ {charts.map((chart, idx) => (
293
+ <LocalChartCard
294
+ key={`${chart.repository}-${chart.name}-${chart.version}-${idx}`}
295
+ chart={chart}
296
+ onSelect={() => onChartSelect(chart.repository, chart.name, chart.version, 'local')}
297
+ />
298
+ ))}
299
+ </div>
300
+ </div>
301
+ ))}
302
+ </div>
303
+ ) : (
304
+ // Single repository view
305
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
306
+ {filteredLocalCharts.map((chart, idx) => (
307
+ <LocalChartCard
308
+ key={`${chart.repository}-${chart.name}-${chart.version}-${idx}`}
309
+ chart={chart}
310
+ onSelect={() => onChartSelect(chart.repository, chart.name, chart.version, 'local')}
311
+ />
312
+ ))}
313
+ </div>
314
+ )
315
+ ) : (
316
+ // ArtifactHub view
317
+ !searchTerm ? (
318
+ // No search term yet - show prompt
319
+ <div className="flex flex-col items-center justify-center h-64 text-theme-text-tertiary gap-3">
320
+ <Globe className="w-12 h-12 text-theme-text-disabled" />
321
+ <div className="text-center">
322
+ <p className="text-lg font-medium text-theme-text-secondary">Search ArtifactHub</p>
323
+ <p className="text-sm mt-1">Enter a search term to find charts from the community</p>
324
+ </div>
325
+ </div>
326
+ ) : !artifactHubResult?.charts.length ? (
327
+ // Searched but no results
328
+ <div className="flex flex-col items-center justify-center h-64 text-theme-text-tertiary gap-3">
329
+ <AlertCircle className="w-12 h-12 text-theme-text-disabled" />
330
+ <div className="text-center">
331
+ <p className="text-lg font-medium text-theme-text-secondary">No charts found</p>
332
+ <p className="text-sm mt-1">Try a different search term or adjust filters</p>
333
+ </div>
334
+ </div>
335
+ ) : (
336
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
337
+ {artifactHubResult.charts.map((chart, idx) => (
338
+ <ArtifactHubChartCard
339
+ key={`${chart.repository.name}-${chart.name}-${chart.version}-${idx}`}
340
+ chart={chart}
341
+ onSelect={() => onChartSelect(chart.repository.name, chart.name, chart.version, 'artifacthub')}
342
+ />
343
+ ))}
344
+ </div>
345
+ )
346
+ )}
347
+ </div>
348
+ </div>
349
+ )
350
+ }
351
+
352
+ interface RepoDropdownItemProps {
353
+ repo: HelmRepository
354
+ isSelected: boolean
355
+ onSelect: () => void
356
+ onUpdate: () => void
357
+ isUpdating: boolean
358
+ canUpdate: boolean
359
+ }
360
+
361
+ function RepoDropdownItem({ repo, isSelected, onSelect, onUpdate, isUpdating, canUpdate }: RepoDropdownItemProps) {
362
+ return (
363
+ <div className="flex items-center justify-between px-3 py-2 hover:bg-theme-hover group">
364
+ <button
365
+ onClick={onSelect}
366
+ className={clsx(
367
+ 'flex-1 text-left text-sm truncate',
368
+ isSelected ? 'text-blue-400' : 'text-theme-text-primary'
369
+ )}
370
+ >
371
+ {repo.name}
372
+ {repo.lastUpdated && (
373
+ <span className="text-xs text-theme-text-tertiary ml-2">
374
+ {formatAge(repo.lastUpdated)}
375
+ </span>
376
+ )}
377
+ </button>
378
+ <button
379
+ onClick={(e) => { e.stopPropagation(); onUpdate() }}
380
+ disabled={isUpdating || !canUpdate}
381
+ className="p-1 text-theme-text-tertiary hover:text-theme-text-primary opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-50"
382
+ title={canUpdate ? "Update repository" : "Helm write permissions required (rbac.helm=true)"}
383
+ >
384
+ <RefreshCw className={clsx('w-3.5 h-3.5', isUpdating && 'animate-spin')} />
385
+ </button>
386
+ </div>
387
+ )
388
+ }
389
+
390
+ interface LocalChartCardProps {
391
+ chart: ChartInfo
392
+ onSelect: () => void
393
+ }
394
+
395
+ function LocalChartCard({ chart, onSelect }: LocalChartCardProps) {
396
+ return (
397
+ <button
398
+ onClick={onSelect}
399
+ className="flex flex-col p-3 bg-theme-elevated/30 hover:bg-theme-elevated/50 border border-theme-border-light rounded-lg text-left transition-colors group"
400
+ >
401
+ <div className="flex items-start gap-3">
402
+ {chart.icon ? (
403
+ <img
404
+ src={chart.icon}
405
+ alt=""
406
+ className="w-10 h-10 rounded object-contain bg-white/10 p-1"
407
+ onError={(e) => {
408
+ (e.target as HTMLImageElement).style.display = 'none'
409
+ }}
410
+ />
411
+ ) : (
412
+ <div className="w-10 h-10 rounded bg-theme-hover flex items-center justify-center">
413
+ <Package className="w-5 h-5 text-theme-text-tertiary" />
414
+ </div>
415
+ )}
416
+ <div className="flex-1 min-w-0">
417
+ <div className="flex items-center gap-2">
418
+ <h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
419
+ {chart.deprecated && (
420
+ <span className={clsx('px-1 py-0.5 text-[10px] rounded', SEVERITY_BADGE.warning)}>
421
+ deprecated
422
+ </span>
423
+ )}
424
+ </div>
425
+ <div className="flex items-center gap-2 mt-0.5">
426
+ <span className="text-xs text-theme-text-tertiary">{chart.version}</span>
427
+ {chart.appVersion && (
428
+ <>
429
+ <span className="text-xs text-theme-text-disabled">|</span>
430
+ <span className="text-xs text-theme-text-tertiary">App: {chart.appVersion}</span>
431
+ </>
432
+ )}
433
+ </div>
434
+ </div>
435
+ <ExternalLink className="w-4 h-4 text-theme-text-tertiary opacity-0 group-hover:opacity-100 shrink-0" />
436
+ </div>
437
+ {chart.description && (
438
+ <p className="mt-2 text-xs text-theme-text-secondary line-clamp-2">
439
+ {chart.description}
440
+ </p>
441
+ )}
442
+ </button>
443
+ )
444
+ }
445
+
446
+ interface ArtifactHubChartCardProps {
447
+ chart: ArtifactHubChart
448
+ onSelect: () => void
449
+ }
450
+
451
+ function ArtifactHubChartCard({ chart, onSelect }: ArtifactHubChartCardProps) {
452
+ // Format Unix timestamp to relative age
453
+ const lastUpdated = chart.updatedAt
454
+ ? formatAge(new Date(chart.updatedAt * 1000).toISOString())
455
+ : null
456
+
457
+ return (
458
+ <button
459
+ onClick={onSelect}
460
+ className="flex flex-col p-4 bg-theme-elevated/30 hover:bg-theme-elevated/50 border border-theme-border-light rounded-lg text-left transition-colors group"
461
+ >
462
+ {/* Header row */}
463
+ <div className="flex items-start gap-3">
464
+ {/* Logo */}
465
+ {chart.logoUrl ? (
466
+ <img
467
+ src={chart.logoUrl}
468
+ alt=""
469
+ className="w-12 h-12 rounded object-contain bg-white/10 p-1 shrink-0"
470
+ onError={(e) => {
471
+ (e.target as HTMLImageElement).style.display = 'none'
472
+ }}
473
+ />
474
+ ) : (
475
+ <div className="w-12 h-12 rounded bg-theme-hover flex items-center justify-center shrink-0">
476
+ <Package className="w-6 h-6 text-theme-text-tertiary" />
477
+ </div>
478
+ )}
479
+
480
+ {/* Name and org */}
481
+ <div className="flex-1 min-w-0">
482
+ <h4 className="text-sm font-medium text-theme-text-primary truncate">{chart.name}</h4>
483
+ <div className="flex items-center gap-2 mt-0.5 text-xs text-theme-text-tertiary">
484
+ <span className="flex items-center gap-1">
485
+ <Building2 className="w-3 h-3" />
486
+ {chart.repository.organizationName || chart.repository.name}
487
+ </span>
488
+ <span className="flex items-center gap-1">
489
+ <Globe className="w-3 h-3" />
490
+ {chart.repository.name}
491
+ </span>
492
+ </div>
493
+ </div>
494
+
495
+ {/* Stats - top right */}
496
+ <div className="text-right shrink-0">
497
+ <div className="flex items-center justify-end gap-2">
498
+ {chart.stars > 0 && (
499
+ <span className="flex items-center gap-1 px-2 py-0.5 text-xs border border-theme-border rounded">
500
+ <Star className="w-3 h-3" />
501
+ {chart.stars}
502
+ </span>
503
+ )}
504
+ </div>
505
+ {lastUpdated && (
506
+ <p className="text-xs text-theme-text-tertiary mt-1">Updated {lastUpdated}</p>
507
+ )}
508
+ <p className="text-xs text-theme-text-secondary mt-0.5">Version {chart.version}</p>
509
+ </div>
510
+ </div>
511
+
512
+ {/* Description */}
513
+ {chart.description && (
514
+ <p className="mt-3 text-xs text-theme-text-secondary line-clamp-2">
515
+ {chart.description}
516
+ </p>
517
+ )}
518
+
519
+ {/* Footer - badges row */}
520
+ <div className="flex items-center justify-between mt-3 pt-3 border-t border-theme-border-light/50">
521
+ {/* Keywords/category */}
522
+ <div className="flex items-center gap-2">
523
+ {chart.deprecated && (
524
+ <span className={clsx('px-2 py-0.5 text-[10px] rounded', SEVERITY_BADGE.warning)}>
525
+ deprecated
526
+ </span>
527
+ )}
528
+ {chart.keywords && chart.keywords.length > 0 && (
529
+ <span className="px-2 py-0.5 text-[10px] rounded bg-theme-elevated text-theme-text-tertiary border border-theme-border-light truncate max-w-[150px]">
530
+ {chart.keywords[0]}
531
+ </span>
532
+ )}
533
+ </div>
534
+
535
+ {/* Feature badges */}
536
+ <div className="flex items-center gap-1">
537
+ {/* Values Schema */}
538
+ <Tooltip content={chart.hasValuesSchema ? 'Has values schema' : 'No values schema'}>
539
+ <span className={clsx(
540
+ 'p-1.5 rounded',
541
+ chart.hasValuesSchema ? SEVERITY_BADGE.warning : 'bg-theme-elevated/50 text-theme-text-disabled'
542
+ )}>
543
+ <FileJson className="w-4 h-4" />
544
+ </span>
545
+ </Tooltip>
546
+
547
+ {/* Signed */}
548
+ <Tooltip content={chart.signed ? 'Signed package' : 'Not signed'}>
549
+ <span className={clsx(
550
+ 'p-1.5 rounded',
551
+ chart.signed ? SEVERITY_BADGE.info : 'bg-theme-elevated/50 text-theme-text-disabled'
552
+ )}>
553
+ <PenTool className="w-4 h-4" />
554
+ </span>
555
+ </Tooltip>
556
+
557
+ {/* Verified Publisher */}
558
+ <Tooltip content={chart.repository.verifiedPublisher ? 'Verified publisher' : 'Not verified'}>
559
+ <span className={clsx(
560
+ 'p-1.5 rounded',
561
+ chart.repository.verifiedPublisher ? SEVERITY_BADGE.success : 'bg-theme-elevated/50 text-theme-text-disabled'
562
+ )}>
563
+ <Shield className="w-4 h-4" />
564
+ </span>
565
+ </Tooltip>
566
+
567
+ {/* Official */}
568
+ <Tooltip content={chart.repository.official ? 'Official package' : 'Community package'}>
569
+ <span className={clsx(
570
+ 'p-1.5 rounded',
571
+ chart.repository.official ? SEVERITY_BADGE.success : 'bg-theme-elevated/50 text-theme-text-disabled'
572
+ )}>
573
+ <Star className={clsx('w-4 h-4', chart.repository.official && 'fill-current')} />
574
+ </span>
575
+ </Tooltip>
576
+ </div>
577
+ </div>
578
+ </button>
579
+ )
580
+ }