@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,745 @@
1
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { X, Folder, File, Link2, ChevronRight, ChevronDown, AlertTriangle, Loader2, Search, Download, HardDrive, Shield, ShieldCheck, Terminal, Copy, Check, RefreshCw } from 'lucide-react'
4
+ import { clsx } from 'clsx'
5
+ import { useImageMetadata, ApiError } from '../../api/client'
6
+ import type { FileNode, ImageFilesystem } from '../../types'
7
+ import { formatBytes } from '../../utils/format'
8
+ import { downloadBlob, filterTree } from './file-browser-utils'
9
+ import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
10
+
11
+ // Manual fetch function for filesystem (not a hook - gives us full control)
12
+ async function fetchImageFilesystem(
13
+ image: string,
14
+ namespace: string,
15
+ podName: string,
16
+ pullSecrets: string[]
17
+ ): Promise<ImageFilesystem> {
18
+ const params = new URLSearchParams()
19
+ params.set('image', image)
20
+ if (namespace) params.set('namespace', namespace)
21
+ if (podName) params.set('pod', podName)
22
+ if (pullSecrets.length > 0) params.set('pullSecrets', pullSecrets.join(','))
23
+
24
+ const response = await fetch(apiUrl(`/images/inspect?${params.toString()}`), {
25
+ credentials: getCredentialsMode(),
26
+ headers: getAuthHeaders(),
27
+ })
28
+ if (!response.ok) {
29
+ const error = await response.json().catch(() => ({ error: 'Request failed' }))
30
+ throw new Error(error.error || `HTTP ${response.status}`)
31
+ }
32
+ return response.json()
33
+ }
34
+
35
+ interface ImageFilesystemModalProps {
36
+ open: boolean
37
+ onClose: () => void
38
+ image: string
39
+ namespace: string
40
+ podName: string
41
+ pullSecrets: string[]
42
+ onSwitchToPodFiles?: () => void
43
+ }
44
+
45
+ export function ImageFilesystemModal({
46
+ open,
47
+ onClose,
48
+ image,
49
+ namespace,
50
+ podName,
51
+ pullSecrets,
52
+ onSwitchToPodFiles,
53
+ }: ImageFilesystemModalProps) {
54
+ const dialogRef = useRef<HTMLDivElement>(null)
55
+ const [searchQuery, setSearchQuery] = useState('')
56
+
57
+ // Manual fetch state (no automatic React Query fetching)
58
+ const [filesystem, setFilesystem] = useState<ImageFilesystem | null>(null)
59
+ const [isLoadingFilesystem, setIsLoadingFilesystem] = useState(false)
60
+ const [filesystemError, setFilesystemError] = useState<Error | null>(null)
61
+
62
+ // First, fetch metadata (lightweight)
63
+ const {
64
+ data: metadata,
65
+ isLoading: isLoadingMetadata,
66
+ error: metadataError,
67
+ refetch: refetchMetadata,
68
+ } = useImageMetadata(image, namespace, podName, pullSecrets, open)
69
+
70
+ // Detect auth errors from metadata fetch
71
+ const isAuthError = metadataError instanceof ApiError && metadataError.status === 401
72
+ const registryType = isAuthError ? (metadataError.data?.registryType as string) : undefined
73
+
74
+ // Use cached filesystem from metadata if available
75
+ const displayFilesystem: ImageFilesystem | undefined = metadata?.cached
76
+ ? metadata.filesystem
77
+ : filesystem || undefined
78
+
79
+ // Manual fetch triggered by user clicking "Download & View"
80
+ const handleApproveDownload = useCallback(async () => {
81
+ setIsLoadingFilesystem(true)
82
+ setFilesystemError(null)
83
+ try {
84
+ const result = await fetchImageFilesystem(image, namespace, podName, pullSecrets)
85
+ setFilesystem(result)
86
+ } catch (err) {
87
+ setFilesystemError(err instanceof Error ? err : new Error('Failed to fetch filesystem'))
88
+ } finally {
89
+ setIsLoadingFilesystem(false)
90
+ }
91
+ }, [image, namespace, podName, pullSecrets])
92
+
93
+ // Reset state when modal closes or image changes
94
+ useEffect(() => {
95
+ if (!open) {
96
+ setSearchQuery('')
97
+ setFilesystem(null)
98
+ setFilesystemError(null)
99
+ setIsLoadingFilesystem(false)
100
+ }
101
+ }, [open, image])
102
+
103
+ // Handle ESC key
104
+ useEffect(() => {
105
+ if (!open) return
106
+ const handleKeyDown = (e: KeyboardEvent) => {
107
+ if (e.key === 'Escape') { e.stopPropagation(); onClose() }
108
+ }
109
+ document.addEventListener('keydown', handleKeyDown, true)
110
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
111
+ }, [open, onClose])
112
+
113
+ // Focus trap
114
+ useEffect(() => {
115
+ if (open && dialogRef.current) {
116
+ dialogRef.current.focus()
117
+ }
118
+ }, [open])
119
+
120
+ if (!open) return null
121
+
122
+ const error = metadataError || filesystemError
123
+ const isLoading = isLoadingMetadata || isLoadingFilesystem
124
+ // Show confirmation when: metadata loaded, not cached, no filesystem yet, no error
125
+ const showConfirmation = metadata && !metadata.cached && !filesystem && !isLoadingFilesystem && !error
126
+ const showFilesystem = displayFilesystem && displayFilesystem.root
127
+
128
+ return createPortal(
129
+ <div className="fixed inset-0 z-[100] flex items-center justify-center">
130
+ {/* Backdrop */}
131
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
132
+
133
+ {/* Modal */}
134
+ <div
135
+ ref={dialogRef}
136
+ tabIndex={-1}
137
+ className="relative dialog w-full max-w-4xl mx-4 max-h-[85vh] flex flex-col outline-none"
138
+ >
139
+ {/* Header */}
140
+ <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
141
+ <div className="flex-1 min-w-0">
142
+ <h3 className="text-lg font-semibold text-theme-text-primary">Image Filesystem</h3>
143
+ <p className="text-sm text-theme-text-secondary truncate mt-0.5" title={image}>
144
+ {image}
145
+ </p>
146
+ {(displayFilesystem?.platform || metadata?.platform) && (
147
+ <p className="text-xs text-theme-text-tertiary mt-1">
148
+ Platform: {displayFilesystem?.platform || metadata?.platform}
149
+ </p>
150
+ )}
151
+ </div>
152
+ <button
153
+ onClick={onClose}
154
+ className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded ml-4"
155
+ >
156
+ <X className="w-5 h-5" />
157
+ </button>
158
+ </div>
159
+
160
+ {/* Search bar - only show when filesystem is loaded */}
161
+ {showFilesystem && (
162
+ <div className="p-3 border-b border-theme-border shrink-0">
163
+ <div className="relative">
164
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
165
+ <input
166
+ type="text"
167
+ placeholder="Search files..."
168
+ value={searchQuery}
169
+ onChange={(e) => setSearchQuery(e.target.value)}
170
+ className="w-full pl-10 pr-4 py-2 bg-theme-base border border-theme-border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-tertiary focus:outline-none focus:ring-2 focus:ring-blue-500"
171
+ />
172
+ </div>
173
+ </div>
174
+ )}
175
+
176
+ {/* Content */}
177
+ <div className="flex-1 overflow-y-auto p-4">
178
+ {/* Loading state */}
179
+ {isLoading && (
180
+ <div className="flex flex-col items-center justify-center h-64">
181
+ <Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
182
+ <span className="mt-3 text-theme-text-secondary">
183
+ {isLoadingMetadata ? 'Checking image...' : 'Downloading image layers...'}
184
+ </span>
185
+ {isLoadingFilesystem && metadata && (
186
+ <span className="mt-1 text-xs text-theme-text-tertiary">
187
+ This may take a moment for large images
188
+ </span>
189
+ )}
190
+ </div>
191
+ )}
192
+
193
+ {/* Auth error - show guidance instead of generic error */}
194
+ {isAuthError && (
195
+ <AuthenticationHelp
196
+ image={image}
197
+ registryType={registryType}
198
+ onRetry={() => refetchMetadata()}
199
+ />
200
+ )}
201
+
202
+ {/* Non-auth errors keep the existing red error box */}
203
+ {error && !isAuthError && (
204
+ <div className="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
205
+ <div className="flex items-start gap-3">
206
+ <AlertTriangle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
207
+ <div>
208
+ <div className="font-medium text-red-400">Failed to inspect image</div>
209
+ <div className="text-sm text-theme-text-secondary mt-1">
210
+ {error instanceof Error ? error.message : 'Unknown error'}
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ )}
216
+
217
+ {/* Confirmation dialog - image not cached */}
218
+ {showConfirmation && (
219
+ <DownloadConfirmation
220
+ metadata={metadata}
221
+ onConfirm={handleApproveDownload}
222
+ onCancel={onClose}
223
+ />
224
+ )}
225
+
226
+ {/* Filesystem tree */}
227
+ {showFilesystem && (
228
+ <FileTreeView
229
+ root={displayFilesystem.root}
230
+ searchQuery={searchQuery}
231
+ image={image}
232
+ namespace={namespace}
233
+ podName={podName}
234
+ pullSecrets={pullSecrets}
235
+ />
236
+ )}
237
+ </div>
238
+
239
+ {/* Footer with stats */}
240
+ <div className="p-3 border-t border-theme-border text-xs text-theme-text-tertiary flex items-center gap-4 shrink-0">
241
+ {displayFilesystem && (
242
+ <>
243
+ <span>{displayFilesystem.totalFiles.toLocaleString()} files</span>
244
+ <span>{formatBytes(displayFilesystem.totalSize)}</span>
245
+ {displayFilesystem.layers && <span>{displayFilesystem.layers.length} layers</span>}
246
+ {displayFilesystem.digest && (
247
+ <span className="truncate" title={displayFilesystem.digest}>
248
+ Digest: {displayFilesystem.digest.substring(0, 20)}...
249
+ </span>
250
+ )}
251
+ </>
252
+ )}
253
+ {onSwitchToPodFiles && (
254
+ <button
255
+ onClick={() => { onClose(); onSwitchToPodFiles() }}
256
+ className="ml-auto text-blue-400 hover:text-blue-300 hover:underline"
257
+ >
258
+ Browse live files from running pod &rarr;
259
+ </button>
260
+ )}
261
+ </div>
262
+ </div>
263
+ </div>,
264
+ document.body,
265
+ )
266
+ }
267
+
268
+ // ============================================================================
269
+ // Download Confirmation Component
270
+ // ============================================================================
271
+
272
+ interface DownloadConfirmationProps {
273
+ metadata: {
274
+ image: string
275
+ digest: string
276
+ platform: string
277
+ totalSize: number
278
+ layerCount: number
279
+ authMethod: string
280
+ }
281
+ onConfirm: () => void
282
+ onCancel: () => void
283
+ }
284
+
285
+ function DownloadConfirmation({ metadata, onConfirm, onCancel }: DownloadConfirmationProps) {
286
+ const isPublic = metadata.authMethod === 'anonymous'
287
+ const authCommand = !isPublic ? getAuthCommand(metadata.authMethod, metadata.image) : null
288
+ const [copied, setCopied] = useState(false)
289
+
290
+ const handleCopy = async () => {
291
+ if (!authCommand) return
292
+ try {
293
+ await navigator.clipboard.writeText(authCommand)
294
+ setCopied(true)
295
+ setTimeout(() => setCopied(false), 2000)
296
+ } catch (err) {
297
+ console.error('Failed to copy:', err)
298
+ }
299
+ }
300
+
301
+ return (
302
+ <div className="flex flex-col items-center justify-center py-8">
303
+ <HardDrive className="w-16 h-16 text-blue-400 mb-4" />
304
+
305
+ <h4 className="text-lg font-medium text-theme-text-primary mb-2">
306
+ Download Image Layers?
307
+ </h4>
308
+
309
+ <p className="text-sm text-theme-text-secondary text-center max-w-md mb-6">
310
+ This image is not cached locally. To view the filesystem, the image layers need to be downloaded.
311
+ </p>
312
+
313
+ {/* Image info */}
314
+ <div className="bg-theme-base border border-theme-border rounded-lg p-4 mb-6 w-full max-w-sm">
315
+ <div className="space-y-2 text-sm">
316
+ <div className="flex justify-between">
317
+ <span className="text-theme-text-tertiary">Download Size:</span>
318
+ <span className="text-theme-text-primary font-medium">{formatBytes(metadata.totalSize)}</span>
319
+ </div>
320
+ <div className="flex justify-between">
321
+ <span className="text-theme-text-tertiary">Layers:</span>
322
+ <span className="text-theme-text-primary">{metadata.layerCount}</span>
323
+ </div>
324
+ <div className="flex justify-between">
325
+ <span className="text-theme-text-tertiary">Platform:</span>
326
+ <span className="text-theme-text-primary">{metadata.platform}</span>
327
+ </div>
328
+ <div className="flex justify-between items-center">
329
+ <span className="text-theme-text-tertiary">Access:</span>
330
+ <span className={clsx(
331
+ 'flex items-center gap-1',
332
+ isPublic ? 'text-green-400' : 'text-amber-400'
333
+ )}>
334
+ {isPublic ? (
335
+ <>
336
+ <ShieldCheck className="w-3.5 h-3.5" />
337
+ Public
338
+ </>
339
+ ) : (
340
+ <>
341
+ <Shield className="w-3.5 h-3.5" />
342
+ {formatAuthMethod(metadata.authMethod)}
343
+ </>
344
+ )}
345
+ </span>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ {/* Auth command for private registries */}
351
+ {authCommand && (
352
+ <div className="bg-theme-base border border-amber-500/30 rounded-lg p-4 mb-6 w-full max-w-lg">
353
+ <div className="flex items-center gap-2 text-amber-400 text-sm mb-2">
354
+ <Terminal className="w-4 h-4" />
355
+ <span className="font-medium">Authentication Command</span>
356
+ </div>
357
+ <p className="text-xs text-theme-text-secondary mb-3">
358
+ Run this command to configure authentication for this registry:
359
+ </p>
360
+ <div className="relative">
361
+ <pre className="bg-theme-elevated rounded p-3 text-xs text-theme-text-primary overflow-x-auto font-mono">
362
+ {authCommand}
363
+ </pre>
364
+ <button
365
+ onClick={handleCopy}
366
+ className="absolute top-2 right-2 p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-base rounded transition-colors"
367
+ title="Copy to clipboard"
368
+ >
369
+ {copied ? (
370
+ <Check className="w-4 h-4 text-green-400" />
371
+ ) : (
372
+ <Copy className="w-4 h-4" />
373
+ )}
374
+ </button>
375
+ </div>
376
+ </div>
377
+ )}
378
+
379
+ {/* Actions */}
380
+ <div className="flex gap-3">
381
+ <button
382
+ onClick={onCancel}
383
+ className="px-4 py-2 text-sm text-theme-text-secondary hover:text-theme-text-primary border border-theme-border rounded-lg hover:bg-theme-elevated transition-colors"
384
+ >
385
+ Cancel
386
+ </button>
387
+ <button
388
+ onClick={onConfirm}
389
+ className="px-4 py-2 text-sm btn-brand rounded-lg"
390
+ >
391
+ Download & View
392
+ </button>
393
+ </div>
394
+ </div>
395
+ )
396
+ }
397
+
398
+ // ============================================================================
399
+ // Authentication Help Component (shown on 401 errors)
400
+ // ============================================================================
401
+
402
+ interface AuthenticationHelpProps {
403
+ image: string
404
+ registryType?: string
405
+ onRetry: () => void
406
+ }
407
+
408
+ function AuthenticationHelp({ image, registryType, onRetry }: AuthenticationHelpProps) {
409
+ const [copied, setCopied] = useState(false)
410
+ const [retrying, setRetrying] = useState(false)
411
+ const registry = getRegistryHost(image)
412
+ const authMethod = registryType || 'generic'
413
+ const authCommand = getAuthCommand(authMethod, image)
414
+
415
+ const handleCopy = async () => {
416
+ if (!authCommand) return
417
+ try {
418
+ await navigator.clipboard.writeText(authCommand)
419
+ setCopied(true)
420
+ setTimeout(() => setCopied(false), 2000)
421
+ } catch (err) {
422
+ console.error('Failed to copy:', err)
423
+ }
424
+ }
425
+
426
+ const handleRetry = () => {
427
+ setRetrying(true)
428
+ onRetry()
429
+ // Reset after a short delay (the query state will update via React Query)
430
+ setTimeout(() => setRetrying(false), 1000)
431
+ }
432
+
433
+ return (
434
+ <div className="flex flex-col items-center justify-center py-8">
435
+ <Shield className="w-16 h-16 text-amber-400 mb-4" />
436
+
437
+ <h4 className="text-lg font-medium text-theme-text-primary mb-2">
438
+ Authentication Required
439
+ </h4>
440
+
441
+ <p className="text-sm text-theme-text-secondary text-center max-w-md mb-2">
442
+ This image is hosted on a private registry that requires authentication.
443
+ </p>
444
+
445
+ <p className="text-xs text-theme-text-tertiary text-center max-w-md mb-6">
446
+ Registry: <span className="font-mono text-theme-text-secondary">{registry}</span>
447
+ {registryType && registryType !== 'generic' && (
448
+ <> ({formatAuthMethod(registryType)})</>
449
+ )}
450
+ </p>
451
+
452
+ {/* Auth command */}
453
+ {authCommand && (
454
+ <div className="bg-theme-base border border-amber-500/30 rounded-lg p-4 mb-4 w-full max-w-lg">
455
+ <div className="flex items-center gap-2 text-amber-400 text-sm mb-2">
456
+ <Terminal className="w-4 h-4" />
457
+ <span className="font-medium">Run this command to authenticate</span>
458
+ </div>
459
+ <div className="relative">
460
+ <pre className="bg-theme-elevated rounded p-3 text-xs text-theme-text-primary overflow-x-auto font-mono">
461
+ {authCommand}
462
+ </pre>
463
+ <button
464
+ onClick={handleCopy}
465
+ className="absolute top-2 right-2 p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-base rounded transition-colors"
466
+ title="Copy to clipboard"
467
+ >
468
+ {copied ? (
469
+ <Check className="w-4 h-4 text-green-400" />
470
+ ) : (
471
+ <Copy className="w-4 h-4" />
472
+ )}
473
+ </button>
474
+ </div>
475
+ </div>
476
+ )}
477
+
478
+ {/* Explanation */}
479
+ <p className="text-xs text-theme-text-tertiary text-center max-w-md mb-6">
480
+ Radar uses your local Docker credentials (<span className="font-mono">~/.docker/config.json</span>).
481
+ Run the command above in your terminal, then click Retry.
482
+ </p>
483
+
484
+ {/* Retry button */}
485
+ <button
486
+ onClick={handleRetry}
487
+ disabled={retrying}
488
+ className="flex items-center gap-2 px-4 py-2 text-sm btn-brand rounded-lg"
489
+ >
490
+ <RefreshCw className={clsx('w-4 h-4', retrying && 'animate-spin')} />
491
+ {retrying ? 'Retrying...' : 'Retry'}
492
+ </button>
493
+ </div>
494
+ )
495
+ }
496
+
497
+ function formatAuthMethod(method: string): string {
498
+ switch (method) {
499
+ case 'google': return 'Google Cloud'
500
+ case 'aws': return 'AWS ECR'
501
+ case 'azure': return 'Azure ACR'
502
+ case 'github': return 'GitHub'
503
+ case 'docker': return 'Docker Hub'
504
+ case 'quay': return 'Quay.io'
505
+ case 'gitlab': return 'GitLab'
506
+ case 'credentials': return 'Authenticated'
507
+ default: return 'Authenticated'
508
+ }
509
+ }
510
+
511
+ function getRegistryHost(img: string): string {
512
+ // Handle images like "nginx" (Docker Hub official)
513
+ if (!img.includes('/') || !img.split('/')[0].includes('.')) {
514
+ return 'docker.io'
515
+ }
516
+ // Extract hostname from "hostname/path/image:tag"
517
+ return img.split('/')[0]
518
+ }
519
+
520
+ function getAuthCommand(authMethod: string, image: string): string | null {
521
+ const registry = getRegistryHost(image)
522
+
523
+ switch (authMethod) {
524
+ case 'google':
525
+ // Extract the specific registry (gcr.io, us-docker.pkg.dev, etc.)
526
+ return `gcloud auth configure-docker ${registry} --quiet`
527
+
528
+ case 'aws': {
529
+ // Extract region from ECR URL: <account>.dkr.ecr.<region>.amazonaws.com
530
+ const match = registry.match(/\.dkr\.ecr\.([^.]+)\.amazonaws\.com/)
531
+ const region = match ? match[1] : '<region>'
532
+ return `aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${registry}`
533
+ }
534
+
535
+ case 'azure': {
536
+ // Extract ACR name from <name>.azurecr.io
537
+ const acrMatch = registry.match(/^([^.]+)\.azurecr\.io/)
538
+ const acrName = acrMatch ? acrMatch[1] : '<acr-name>'
539
+ return `az acr login --name ${acrName}`
540
+ }
541
+
542
+ case 'github':
543
+ return `echo $GITHUB_TOKEN | docker login ghcr.io -u <username> --password-stdin`
544
+
545
+ case 'docker':
546
+ return `docker login`
547
+
548
+ case 'quay':
549
+ return `docker login quay.io`
550
+
551
+ case 'gitlab':
552
+ return `docker login registry.gitlab.com -u <username> -p <access-token>`
553
+
554
+ case 'generic':
555
+ case 'credentials':
556
+ return `docker login ${registry}`
557
+
558
+ default:
559
+ return `docker login ${registry}`
560
+ }
561
+ }
562
+
563
+ // ============================================================================
564
+ // File Tree View Component
565
+ // ============================================================================
566
+
567
+ interface FileTreeViewProps {
568
+ root: FileNode
569
+ searchQuery: string
570
+ image: string
571
+ namespace: string
572
+ podName: string
573
+ pullSecrets: string[]
574
+ }
575
+
576
+ function FileTreeView({ root, searchQuery, image, namespace, podName, pullSecrets }: FileTreeViewProps) {
577
+ const filteredRoot = useMemo(() => {
578
+ if (!searchQuery.trim()) return root
579
+ return filterTree(root, searchQuery.toLowerCase())
580
+ }, [root, searchQuery])
581
+
582
+ if (!filteredRoot || !filteredRoot.children || filteredRoot.children.length === 0) {
583
+ return (
584
+ <div className="text-center text-theme-text-tertiary py-8">
585
+ {searchQuery ? 'No files match your search' : 'Empty filesystem'}
586
+ </div>
587
+ )
588
+ }
589
+
590
+ return (
591
+ <div className="font-mono text-sm">
592
+ {filteredRoot.children.map((node) => (
593
+ <FileTreeNode
594
+ key={node.path}
595
+ node={node}
596
+ depth={0}
597
+ defaultExpanded={!searchQuery}
598
+ image={image}
599
+ namespace={namespace}
600
+ podName={podName}
601
+ pullSecrets={pullSecrets}
602
+ />
603
+ ))}
604
+ </div>
605
+ )
606
+ }
607
+
608
+ interface FileTreeNodeProps {
609
+ node: FileNode
610
+ depth: number
611
+ defaultExpanded?: boolean
612
+ image: string
613
+ namespace: string
614
+ podName: string
615
+ pullSecrets: string[]
616
+ }
617
+
618
+ function FileTreeNode({ node, depth, defaultExpanded = true, image, namespace, podName, pullSecrets }: FileTreeNodeProps) {
619
+ const [expanded, setExpanded] = useState(defaultExpanded && depth < 2)
620
+ const [downloading, setDownloading] = useState(false)
621
+ const isDir = node.type === 'dir'
622
+ const isSymlink = node.type === 'symlink'
623
+ const isFile = node.type === 'file'
624
+
625
+ const handleDownload = async (e: React.MouseEvent) => {
626
+ e.stopPropagation()
627
+ if (downloading) return
628
+
629
+ setDownloading(true)
630
+ try {
631
+ const params = new URLSearchParams()
632
+ params.set('image', image)
633
+ params.set('path', node.path)
634
+ if (namespace) params.set('namespace', namespace)
635
+ if (podName) params.set('pod', podName)
636
+ if (pullSecrets.length > 0) params.set('pullSecrets', pullSecrets.join(','))
637
+
638
+ const response = await fetch(apiUrl(`/images/file?${params.toString()}`), {
639
+ credentials: getCredentialsMode(),
640
+ headers: getAuthHeaders(),
641
+ })
642
+ if (!response.ok) {
643
+ throw new Error('Failed to download file')
644
+ }
645
+
646
+ const blob = await response.blob()
647
+ await downloadBlob(blob, node.name)
648
+ } catch (err) {
649
+ console.error('Download failed:', err)
650
+ } finally {
651
+ setDownloading(false)
652
+ }
653
+ }
654
+
655
+ const handleClick = () => {
656
+ if (isDir) {
657
+ setExpanded(!expanded)
658
+ }
659
+ }
660
+
661
+ return (
662
+ <div>
663
+ <div
664
+ className={clsx(
665
+ 'flex items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
666
+ isDir && 'font-medium cursor-pointer'
667
+ )}
668
+ style={{ paddingLeft: `${depth * 16 + 4}px` }}
669
+ onClick={handleClick}
670
+ >
671
+ {isDir && (
672
+ <span className="w-4 h-4 flex items-center justify-center">
673
+ {expanded ? (
674
+ <ChevronDown className="w-3.5 h-3.5 text-theme-text-tertiary" />
675
+ ) : (
676
+ <ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary" />
677
+ )}
678
+ </span>
679
+ )}
680
+ {!isDir && <span className="w-4" />}
681
+
682
+ {isDir ? (
683
+ <Folder className="w-4 h-4 text-amber-400 shrink-0" />
684
+ ) : isSymlink ? (
685
+ <Link2 className="w-4 h-4 text-cyan-400 shrink-0" />
686
+ ) : (
687
+ <File className="w-4 h-4 text-theme-text-tertiary shrink-0" />
688
+ )}
689
+
690
+ <span className="text-theme-text-primary truncate flex-1">{node.name}</span>
691
+
692
+ {isSymlink && node.linkTarget && (
693
+ <span className="text-xs text-cyan-400 truncate max-w-48">
694
+ -&gt; {node.linkTarget}
695
+ </span>
696
+ )}
697
+
698
+ {!isDir && node.size !== undefined && (
699
+ <span className="text-xs text-theme-text-tertiary ml-2">
700
+ {formatBytes(node.size)}
701
+ </span>
702
+ )}
703
+
704
+ {node.permissions && (
705
+ <span className="text-xs text-theme-text-tertiary ml-2 font-normal">
706
+ {node.permissions}
707
+ </span>
708
+ )}
709
+
710
+ {isFile && (
711
+ <button
712
+ onClick={handleDownload}
713
+ disabled={downloading}
714
+ className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded ml-1 disabled:opacity-50"
715
+ title="Download file"
716
+ >
717
+ {downloading ? (
718
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
719
+ ) : (
720
+ <Download className="w-3.5 h-3.5" />
721
+ )}
722
+ </button>
723
+ )}
724
+ </div>
725
+
726
+ {isDir && expanded && node.children && (
727
+ <div>
728
+ {node.children.map((child) => (
729
+ <FileTreeNode
730
+ key={child.path}
731
+ node={child}
732
+ depth={depth + 1}
733
+ defaultExpanded={defaultExpanded}
734
+ image={image}
735
+ namespace={namespace}
736
+ podName={podName}
737
+ pullSecrets={pullSecrets}
738
+ />
739
+ ))}
740
+ </div>
741
+ )}
742
+ </div>
743
+ )
744
+ }
745
+