@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,407 @@
1
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { X, File, Link2, ChevronRight, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
4
+ import { clsx } from 'clsx'
5
+ import type { FileNode } from '../../types'
6
+ import { formatBytes } from '../../utils/format'
7
+ import { downloadBlob, filterTree } from './file-browser-utils'
8
+ import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
9
+
10
+ interface PodFilesystem {
11
+ root: FileNode
12
+ totalFiles: number
13
+ }
14
+
15
+ async function fetchPodFiles(
16
+ namespace: string,
17
+ podName: string,
18
+ container: string,
19
+ dirPath: string,
20
+ ): Promise<PodFilesystem> {
21
+ const params = new URLSearchParams()
22
+ params.set('container', container)
23
+ params.set('path', dirPath)
24
+
25
+ const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files?${params.toString()}`), {
26
+ credentials: getCredentialsMode(),
27
+ headers: getAuthHeaders(),
28
+ })
29
+ if (!response.ok) {
30
+ const error = await response.json().catch(() => ({ error: 'Request failed' }))
31
+ throw new Error(error.error || `HTTP ${response.status}`)
32
+ }
33
+ return response.json()
34
+ }
35
+
36
+ interface PodFilesystemModalProps {
37
+ open: boolean
38
+ onClose: () => void
39
+ namespace: string
40
+ podName: string
41
+ containers: string[]
42
+ initialContainer?: string
43
+ onSwitchToImageFiles?: () => void
44
+ }
45
+
46
+ export function PodFilesystemModal({
47
+ open,
48
+ onClose,
49
+ namespace,
50
+ podName,
51
+ containers,
52
+ initialContainer,
53
+ onSwitchToImageFiles,
54
+ }: PodFilesystemModalProps) {
55
+ const dialogRef = useRef<HTMLDivElement>(null)
56
+ const [searchQuery, setSearchQuery] = useState('')
57
+ const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
58
+ const [currentPath, setCurrentPath] = useState('/')
59
+ const [filesystem, setFilesystem] = useState<PodFilesystem | null>(null)
60
+ const [isLoading, setIsLoading] = useState(false)
61
+ const [error, setError] = useState<string | null>(null)
62
+
63
+ const loadDirectory = useCallback(async (dirPath: string) => {
64
+ setIsLoading(true)
65
+ setError(null)
66
+ try {
67
+ const result = await fetchPodFiles(namespace, podName, selectedContainer, dirPath)
68
+ setFilesystem(result)
69
+ setCurrentPath(dirPath)
70
+ } catch (err) {
71
+ setError(err instanceof Error ? err.message : 'Failed to list files')
72
+ } finally {
73
+ setIsLoading(false)
74
+ }
75
+ }, [namespace, podName, selectedContainer])
76
+
77
+ // Load root on open or container change
78
+ useEffect(() => {
79
+ if (open && selectedContainer) {
80
+ loadDirectory('/')
81
+ }
82
+ }, [open, selectedContainer, loadDirectory])
83
+
84
+ // Reset state when modal closes
85
+ useEffect(() => {
86
+ if (!open) {
87
+ setSearchQuery('')
88
+ setFilesystem(null)
89
+ setError(null)
90
+ setIsLoading(false)
91
+ setCurrentPath('/')
92
+ setSelectedContainer(initialContainer || containers[0] || '')
93
+ }
94
+ }, [open, initialContainer, containers])
95
+
96
+ // Handle ESC key
97
+ useEffect(() => {
98
+ if (!open) return
99
+ const handleKeyDown = (e: KeyboardEvent) => {
100
+ if (e.key === 'Escape') { e.stopPropagation(); onClose() }
101
+ }
102
+ document.addEventListener('keydown', handleKeyDown, true)
103
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
104
+ }, [open, onClose])
105
+
106
+ // Focus trap
107
+ useEffect(() => {
108
+ if (open && dialogRef.current) {
109
+ dialogRef.current.focus()
110
+ }
111
+ }, [open])
112
+
113
+ if (!open) return null
114
+
115
+ const showFilesystem = filesystem && filesystem.root
116
+
117
+ // Build breadcrumb segments
118
+ const pathSegments = currentPath === '/'
119
+ ? ['/']
120
+ : ['/', ...currentPath.split('/').filter(Boolean)]
121
+
122
+ return createPortal(
123
+ <div className="fixed inset-0 z-[100] flex items-center justify-center">
124
+ {/* Backdrop */}
125
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
126
+
127
+ {/* Modal */}
128
+ <div
129
+ ref={dialogRef}
130
+ tabIndex={-1}
131
+ className="relative dialog w-full max-w-4xl mx-4 max-h-[85vh] flex flex-col outline-none"
132
+ >
133
+ {/* Header */}
134
+ <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
135
+ <div className="flex-1 min-w-0">
136
+ <h3 className="text-lg font-semibold text-theme-text-primary">Pod Files</h3>
137
+ <p className="text-sm text-theme-text-secondary truncate mt-0.5">
138
+ {namespace}/{podName}
139
+ </p>
140
+ </div>
141
+
142
+ {/* Container selector */}
143
+ {containers.length > 1 && (
144
+ <select
145
+ value={selectedContainer}
146
+ onChange={(e) => setSelectedContainer(e.target.value)}
147
+ className="mr-4 px-3 py-1.5 text-sm bg-theme-base border border-theme-border rounded-lg text-theme-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500"
148
+ >
149
+ {containers.map((c) => (
150
+ <option key={c} value={c}>{c}</option>
151
+ ))}
152
+ </select>
153
+ )}
154
+
155
+ <button
156
+ onClick={onClose}
157
+ className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded ml-2"
158
+ >
159
+ <X className="w-5 h-5" />
160
+ </button>
161
+ </div>
162
+
163
+ {/* Breadcrumb + Search */}
164
+ <div className="p-3 border-b border-theme-border shrink-0 flex items-center gap-3">
165
+ {/* Breadcrumb */}
166
+ <div className="flex items-center gap-1 text-sm min-w-0 flex-shrink overflow-hidden">
167
+ {pathSegments.map((segment, i) => {
168
+ const segmentPath = i === 0 ? '/' : '/' + pathSegments.slice(1, i + 1).join('/')
169
+ const isLast = i === pathSegments.length - 1
170
+ return (
171
+ <span key={segmentPath} className="flex items-center gap-1">
172
+ {i > 0 && <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />}
173
+ <button
174
+ onClick={() => !isLast && loadDirectory(segmentPath)}
175
+ className={clsx(
176
+ 'truncate',
177
+ isLast
178
+ ? 'text-theme-text-primary font-medium'
179
+ : 'text-blue-400 hover:text-blue-300 hover:underline'
180
+ )}
181
+ >
182
+ {segment === '/' ? '/' : segment}
183
+ </button>
184
+ </span>
185
+ )
186
+ })}
187
+ </div>
188
+
189
+ {/* Search */}
190
+ {showFilesystem && (
191
+ <div className="relative flex-1 min-w-[200px]">
192
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
193
+ <input
194
+ type="text"
195
+ placeholder="Filter files..."
196
+ value={searchQuery}
197
+ onChange={(e) => setSearchQuery(e.target.value)}
198
+ className="w-full pl-10 pr-4 py-1.5 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"
199
+ />
200
+ </div>
201
+ )}
202
+ </div>
203
+
204
+ {/* Content */}
205
+ <div className="flex-1 overflow-y-auto p-4">
206
+ {/* Loading */}
207
+ {isLoading && (
208
+ <div className="flex flex-col items-center justify-center h-64">
209
+ <Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
210
+ <span className="mt-3 text-theme-text-secondary">Loading files...</span>
211
+ </div>
212
+ )}
213
+
214
+ {/* Error */}
215
+ {error && !isLoading && (
216
+ <div className="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
217
+ <div className="flex items-start gap-3">
218
+ <AlertTriangle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
219
+ <div>
220
+ <div className="font-medium text-red-400">Failed to list files</div>
221
+ <div className="text-sm text-theme-text-secondary mt-1">{error}</div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ )}
226
+
227
+ {/* File tree */}
228
+ {showFilesystem && !isLoading && (
229
+ <PodFileTreeView
230
+ root={filesystem.root}
231
+ searchQuery={searchQuery}
232
+ namespace={namespace}
233
+ podName={podName}
234
+ container={selectedContainer}
235
+ onNavigate={loadDirectory}
236
+ />
237
+ )}
238
+ </div>
239
+
240
+ {/* Footer with stats */}
241
+ <div className="p-3 border-t border-theme-border text-xs text-theme-text-tertiary flex items-center gap-4 shrink-0">
242
+ {filesystem && !isLoading && (
243
+ <>
244
+ <span>{filesystem.totalFiles} items</span>
245
+ <span>Container: {selectedContainer}</span>
246
+ </>
247
+ )}
248
+ {onSwitchToImageFiles && (
249
+ <button
250
+ onClick={() => { onClose(); onSwitchToImageFiles() }}
251
+ className="ml-auto text-blue-400 hover:text-blue-300 hover:underline"
252
+ >
253
+ Browse static image from registry &rarr;
254
+ </button>
255
+ )}
256
+ </div>
257
+ </div>
258
+ </div>,
259
+ document.body,
260
+ )
261
+ }
262
+
263
+ // ============================================================================
264
+ // Pod File Tree View
265
+ // ============================================================================
266
+
267
+ interface PodFileTreeViewProps {
268
+ root: FileNode
269
+ searchQuery: string
270
+ namespace: string
271
+ podName: string
272
+ container: string
273
+ onNavigate: (path: string) => void
274
+ }
275
+
276
+ function PodFileTreeView({ root, searchQuery, namespace, podName, container, onNavigate }: PodFileTreeViewProps) {
277
+ const filteredRoot = useMemo(() => {
278
+ if (!searchQuery.trim()) return root
279
+ return filterTree(root, searchQuery.toLowerCase())
280
+ }, [root, searchQuery])
281
+
282
+ if (!filteredRoot || !filteredRoot.children || filteredRoot.children.length === 0) {
283
+ return (
284
+ <div className="text-center text-theme-text-tertiary py-8">
285
+ {searchQuery ? 'No files match your filter' : 'Empty directory'}
286
+ </div>
287
+ )
288
+ }
289
+
290
+ return (
291
+ <div className="font-mono text-sm">
292
+ {filteredRoot.children.map((node) => (
293
+ <PodFileTreeNode
294
+ key={node.path}
295
+ node={node}
296
+ namespace={namespace}
297
+ podName={podName}
298
+ container={container}
299
+ onNavigate={onNavigate}
300
+ />
301
+ ))}
302
+ </div>
303
+ )
304
+ }
305
+
306
+ interface PodFileTreeNodeProps {
307
+ node: FileNode
308
+ namespace: string
309
+ podName: string
310
+ container: string
311
+ onNavigate: (path: string) => void
312
+ }
313
+
314
+ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: PodFileTreeNodeProps) {
315
+ const [downloading, setDownloading] = useState(false)
316
+ const isDir = node.type === 'dir'
317
+ const isSymlink = node.type === 'symlink'
318
+ const isDownloadable = !isDir // files and symlinks can be downloaded
319
+
320
+ const handleDownload = async (e: React.MouseEvent) => {
321
+ e.stopPropagation()
322
+ if (downloading) return
323
+
324
+ setDownloading(true)
325
+ try {
326
+ const params = new URLSearchParams()
327
+ params.set('container', container)
328
+ params.set('path', node.path)
329
+
330
+ const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files/download?${params.toString()}`), {
331
+ credentials: getCredentialsMode(),
332
+ headers: getAuthHeaders(),
333
+ })
334
+ if (!response.ok) {
335
+ const err = await response.json().catch(() => ({ error: 'Download failed' }))
336
+ throw new Error(err.error || `HTTP ${response.status}`)
337
+ }
338
+
339
+ const blob = await response.blob()
340
+ await downloadBlob(blob, node.name)
341
+ } catch (err) {
342
+ console.error('Download failed:', err)
343
+ } finally {
344
+ setDownloading(false)
345
+ }
346
+ }
347
+
348
+ const handleClick = () => {
349
+ if (isDir) {
350
+ onNavigate(node.path)
351
+ }
352
+ }
353
+
354
+ return (
355
+ <div
356
+ className={clsx(
357
+ 'flex items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
358
+ isDir && 'font-medium cursor-pointer'
359
+ )}
360
+ onClick={handleClick}
361
+ >
362
+ {isDir ? (
363
+ <FolderOpen className="w-4 h-4 text-amber-400 shrink-0" />
364
+ ) : isSymlink ? (
365
+ <Link2 className="w-4 h-4 text-cyan-400 shrink-0" />
366
+ ) : (
367
+ <File className="w-4 h-4 text-theme-text-tertiary shrink-0" />
368
+ )}
369
+
370
+ <span className="text-theme-text-primary truncate flex-1">{node.name}</span>
371
+
372
+ {isSymlink && node.linkTarget && (
373
+ <span className="text-xs text-cyan-400 truncate max-w-48">
374
+ -&gt; {node.linkTarget}
375
+ </span>
376
+ )}
377
+
378
+ {!isDir && node.size !== undefined && (
379
+ <span className="text-xs text-theme-text-tertiary ml-2">
380
+ {formatBytes(node.size)}
381
+ </span>
382
+ )}
383
+
384
+ {node.permissions && (
385
+ <span className="text-xs text-theme-text-tertiary ml-2 font-normal">
386
+ {node.permissions}
387
+ </span>
388
+ )}
389
+
390
+ {isDownloadable && (
391
+ <button
392
+ onClick={handleDownload}
393
+ disabled={downloading}
394
+ className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded ml-1 disabled:opacity-50"
395
+ title="Download file"
396
+ >
397
+ {downloading ? (
398
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
399
+ ) : (
400
+ <Download className="w-3.5 h-3.5" />
401
+ )}
402
+ </button>
403
+ )}
404
+ </div>
405
+ )
406
+ }
407
+
@@ -0,0 +1,43 @@
1
+ import { ResourceDetailDrawer as BaseResourceDetailDrawer } from '@skyhook-io/k8s-ui'
2
+ import type { SelectedResource } from '../../types'
3
+ import { WorkloadView } from '../workload/WorkloadView'
4
+
5
+ interface ResourceDetailDrawerProps {
6
+ resource: SelectedResource
7
+ onClose: () => void
8
+ onNavigate?: (resource: SelectedResource) => void
9
+ /** Open directly to YAML view */
10
+ initialTab?: 'detail' | 'yaml'
11
+ /** Controls slide-in/out animation (driven by useAnimatedUnmount) */
12
+ isOpen?: boolean
13
+ /** Whether the drawer is expanded to full-screen WorkloadView */
14
+ expanded?: boolean
15
+ /** Called when user clicks collapse in expanded mode */
16
+ onCollapse?: () => void
17
+ /** Called when user clicks expand button */
18
+ onExpand?: (resource: SelectedResource) => void
19
+ /** Navigate to another resource within expanded WorkloadView */
20
+ onNavigateToResource?: (resource: SelectedResource) => void
21
+ }
22
+
23
+ export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
24
+ return (
25
+ <BaseResourceDetailDrawer {...props}>
26
+ {({ resource, expanded, initialTab, onClose, onExpand, onBack, onNavigateToResource, onCollapseToDrawer }) => (
27
+ <WorkloadView
28
+ kind={resource.kind}
29
+ namespace={resource.namespace}
30
+ name={resource.name}
31
+ group={resource.group}
32
+ expanded={expanded}
33
+ initialTab={initialTab}
34
+ onClose={onClose}
35
+ onExpand={onExpand}
36
+ onBack={onBack ?? (() => {})}
37
+ onNavigateToResource={onNavigateToResource}
38
+ onCollapseToDrawer={onCollapseToDrawer}
39
+ />
40
+ )}
41
+ </BaseResourceDetailDrawer>
42
+ )
43
+ }
@@ -0,0 +1,190 @@
1
+ import { useState, useMemo, useCallback, useEffect } from 'react'
2
+ import { useLocation, useNavigate } from 'react-router-dom'
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { ApiError, fetchJSON, isForbiddenError, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics } from '../../api/client'
5
+ import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
6
+ import { useAPIResources } from '../../api/apiResources'
7
+ import { initNavigationMap } from '@skyhook-io/k8s-ui'
8
+ import { usePinnedKinds } from '../../hooks/useFavorites'
9
+ import { useOpenLogs, useOpenWorkloadLogs } from '../dock'
10
+ import {
11
+ ResourcesView as BaseResourcesView,
12
+ CORE_RESOURCES,
13
+ } from '@skyhook-io/k8s-ui'
14
+ import type { ResourceQueryResult } from '@skyhook-io/k8s-ui'
15
+ import type { SelectedResource } from '../../types'
16
+ import type { NavigateToResource } from '../../utils/navigation'
17
+ import { CreateResourceDialog } from '../shared/CreateResourceDialog'
18
+ import { getSkeletonYaml } from '../../utils/skeleton-yaml'
19
+
20
+ interface ResourceCountsResponse {
21
+ counts: Record<string, number>
22
+ forbidden?: string[]
23
+ }
24
+
25
+ interface ResourcesViewProps {
26
+ namespaces: string[]
27
+ selectedResource?: SelectedResource | null
28
+ onResourceClick?: (resource: SelectedResource | null) => void
29
+ onResourceClickYaml?: NavigateToResource
30
+ onKindChange?: () => void
31
+ }
32
+
33
+ export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange }: ResourcesViewProps) {
34
+ const location = useLocation()
35
+ const navigate = useNavigate()
36
+
37
+ // API resources discovery
38
+ const { data: apiResources } = useAPIResources()
39
+
40
+ // Initialize navigation kind↔plural maps from discovered API resources
41
+ useEffect(() => {
42
+ if (apiResources) initNavigationMap(apiResources)
43
+ }, [apiResources])
44
+
45
+ // Track the selected kind from the k8s-ui component
46
+ const [selectedKind, setSelectedKind] = useState<{ name: string; kind: string; group: string } | null>(null)
47
+
48
+ // Lightweight resource counts for sidebar badges (~2KB instead of ~608MB)
49
+ const namespacesParam = namespaces.join(',')
50
+ const { data: countsData } = useQuery({
51
+ queryKey: ['resource-counts', namespacesParam],
52
+ queryFn: async () => {
53
+ const params = new URLSearchParams()
54
+ if (namespaces.length > 0) params.set('namespaces', namespacesParam)
55
+ return fetchJSON<ResourceCountsResponse>(`/resource-counts?${params}`)
56
+ },
57
+ staleTime: 10000,
58
+ refetchInterval: 60000, // Safety net — SSE k8s_event drives near-real-time invalidation
59
+ })
60
+
61
+ // Determine if selected kind is a CRD (only CRDs should send ?group= to backend)
62
+ const isSelectedCrd = useMemo(() => {
63
+ if (!selectedKind) return false
64
+ // Check API resources first, fall back to CORE_RESOURCES
65
+ const match = apiResources?.find(r => r.name === selectedKind.name && r.group === selectedKind.group)
66
+ ?? CORE_RESOURCES.find(r => r.name === selectedKind.name && r.group === selectedKind.group)
67
+ return match?.isCrd ?? (!!selectedKind.group) // default: has group = likely CRD
68
+ }, [selectedKind, apiResources])
69
+
70
+ // Fetch full data only for the selected kind
71
+ const selectedKindQuery = useQuery({
72
+ queryKey: ['resources', selectedKind?.name, isSelectedCrd ? selectedKind?.group : '', namespaces],
73
+ queryFn: async () => {
74
+ if (!selectedKind) return []
75
+ const params = new URLSearchParams()
76
+ if (namespaces.length > 0) params.set('namespaces', namespacesParam)
77
+ if (isSelectedCrd && selectedKind.group) params.set('group', selectedKind.group)
78
+ const res = await fetch(apiUrl(`/resources/${selectedKind.name}?${params}`), {
79
+ credentials: getCredentialsMode(),
80
+ headers: getAuthHeaders(),
81
+ })
82
+ if (!res.ok) {
83
+ const errorData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
84
+ throw new ApiError(errorData.error || `Failed to fetch ${selectedKind.name}`, res.status, errorData)
85
+ }
86
+ return res.json()
87
+ },
88
+ enabled: !!selectedKind,
89
+ staleTime: 30000,
90
+ refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
91
+ retry: (failureCount: number, error: Error) => {
92
+ if (isForbiddenError(error)) return false
93
+ return failureCount < 3
94
+ },
95
+ })
96
+
97
+ // Map to ResourceQueryResult shape
98
+ const selectedKindQueryResult: ResourceQueryResult | undefined = useMemo(() => {
99
+ if (!selectedKind) return undefined
100
+ return {
101
+ data: selectedKindQuery.data as any[] | undefined,
102
+ isLoading: selectedKindQuery.isLoading,
103
+ error: selectedKindQuery.error,
104
+ refetch: selectedKindQuery.refetch,
105
+ dataUpdatedAt: selectedKindQuery.dataUpdatedAt,
106
+ }
107
+ }, [selectedKind, selectedKindQuery.data, selectedKindQuery.isLoading, selectedKindQuery.error, selectedKindQuery.refetch, selectedKindQuery.dataUpdatedAt])
108
+
109
+ // Metrics
110
+ const { data: topPodMetrics } = useTopPodMetrics()
111
+ const { data: topNodeMetrics } = useTopNodeMetrics()
112
+
113
+ // Certificate expiry
114
+ const { data: certExpiry, isError: certExpiryError } = useSecretCertExpiry()
115
+
116
+ // Pinned kinds
117
+ const { pinned, togglePin, isPinned } = usePinnedKinds()
118
+
119
+ // Dock actions
120
+ const openLogs = useOpenLogs()
121
+ const openWorkloadLogs = useOpenWorkloadLogs()
122
+
123
+ // Navigation adapter
124
+ const handleNavigate = useMemo(() => {
125
+ return (path: string, options?: { replace?: boolean }) => {
126
+ navigate(path, { replace: options?.replace })
127
+ }
128
+ }, [navigate])
129
+
130
+ // Create resource dialog
131
+ const [createDialogOpen, setCreateDialogOpen] = useState(false)
132
+ const [createDialogYaml, setCreateDialogYaml] = useState('')
133
+ const [createDialogTitle, setCreateDialogTitle] = useState<string | undefined>()
134
+
135
+ const handleCreateResource = useCallback((kind: { name: string; kind: string; group: string } | null) => {
136
+ if (kind?.kind) {
137
+ setCreateDialogYaml(getSkeletonYaml(kind.kind, kind.group))
138
+ setCreateDialogTitle(`Create ${kind.kind}`)
139
+ } else {
140
+ setCreateDialogYaml('')
141
+ setCreateDialogTitle(undefined)
142
+ }
143
+ setCreateDialogOpen(true)
144
+ }, [])
145
+
146
+ return (
147
+ <>
148
+ <BaseResourcesView
149
+ namespaces={namespaces}
150
+ selectedResource={selectedResource}
151
+ onResourceClick={onResourceClick}
152
+ onResourceClickYaml={onResourceClickYaml}
153
+ onKindChange={onKindChange}
154
+ // Injected data
155
+ apiResources={apiResources}
156
+ // Lightweight counts for sidebar (replaces 233 parallel queries)
157
+ resourceCounts={countsData?.counts}
158
+ resourceForbidden={countsData?.forbidden}
159
+ selectedKindQuery={selectedKindQueryResult}
160
+ onSelectedKindChange={setSelectedKind}
161
+ topPodMetrics={topPodMetrics}
162
+ topNodeMetrics={topNodeMetrics}
163
+ certExpiry={certExpiry}
164
+ certExpiryError={certExpiryError}
165
+ // Pinned kinds
166
+ pinned={pinned}
167
+ togglePin={togglePin}
168
+ isPinned={(kind: string, group?: string) => isPinned(kind, group ?? '')}
169
+ // Navigation
170
+ locationSearch={location.search}
171
+ locationPathname={location.pathname}
172
+ onNavigate={handleNavigate}
173
+ // Dock actions
174
+ onOpenLogs={openLogs}
175
+ onOpenWorkloadLogs={openWorkloadLogs}
176
+ // Create resource
177
+ onCreateResource={handleCreateResource}
178
+ />
179
+ <CreateResourceDialog
180
+ open={createDialogOpen}
181
+ onClose={() => setCreateDialogOpen(false)}
182
+ initialYaml={createDialogYaml}
183
+ title={createDialogTitle}
184
+ onCreated={(result) => {
185
+ onResourceClick?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
186
+ }}
187
+ />
188
+ </>
189
+ )
190
+ }
@@ -0,0 +1 @@
1
+ export * from '@skyhook-io/k8s-ui/components/ui/drawer-components'
@@ -0,0 +1,35 @@
1
+ import type { FileNode } from '../../types'
2
+ import { isDesktopApp, desktopSaveBlob } from '../../utils/desktop-download'
3
+
4
+ /** Trigger a file download from a Blob. Uses native save dialog on desktop. */
5
+ export async function downloadBlob(blob: Blob, filename: string): Promise<void> {
6
+ if (await isDesktopApp()) {
7
+ await desktopSaveBlob(blob, filename)
8
+ return
9
+ }
10
+ const url = URL.createObjectURL(blob)
11
+ const a = document.createElement('a')
12
+ a.href = url
13
+ a.download = filename
14
+ a.click()
15
+ URL.revokeObjectURL(url)
16
+ }
17
+
18
+ /** Recursively filter a FileNode tree by name substring match. */
19
+ export function filterTree(node: FileNode, query: string): FileNode | null {
20
+ if (node.name.toLowerCase().includes(query)) {
21
+ return node
22
+ }
23
+
24
+ if (node.type === 'dir' && node.children) {
25
+ const filteredChildren = node.children
26
+ .map((child) => filterTree(child, query))
27
+ .filter((child): child is FileNode => child !== null)
28
+
29
+ if (filteredChildren.length > 0) {
30
+ return { ...node, children: filteredChildren }
31
+ }
32
+ }
33
+
34
+ return null
35
+ }
@@ -0,0 +1 @@
1
+ export * from '@skyhook-io/k8s-ui/components/resources/renderers/AlertRenderer'