@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,375 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { Plug, ChevronDown, Loader2, Globe, Monitor, Copy, Check, X, Terminal } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { useAvailablePorts, useClusterInfo, AvailablePort } from '../../api/client'
5
+ import { useStartPortForward } from './PortForwardManager'
6
+
7
+ interface PortForwardButtonProps {
8
+ type: 'pod' | 'service'
9
+ namespace: string
10
+ name: string
11
+ // For service port forwarding
12
+ serviceName?: string
13
+ className?: string
14
+ }
15
+
16
+ interface KubectlDialogInfo {
17
+ type: 'pod' | 'service'
18
+ namespace: string
19
+ name: string
20
+ port: number
21
+ }
22
+
23
+ function buildKubectlCommand(type: 'pod' | 'service', namespace: string, name: string, localPort: number, remotePort: number) {
24
+ const resource = type === 'pod' ? `pod/${name}` : `svc/${name}`
25
+ const portArg = localPort === remotePort ? `${remotePort}` : `${localPort}:${remotePort}`
26
+ return `kubectl port-forward -n ${namespace} ${resource} ${portArg}`
27
+ }
28
+
29
+ function KubectlCommandDialog({
30
+ info,
31
+ onClose,
32
+ }: {
33
+ info: KubectlDialogInfo
34
+ onClose: () => void
35
+ }) {
36
+ const [copied, setCopied] = useState(false)
37
+ const [copyFallback, setCopyFallback] = useState(false)
38
+ const [localPort, setLocalPort] = useState(info.port)
39
+ const commandRef = useRef<HTMLElement>(null)
40
+ const dialogRef = useRef<HTMLDivElement>(null)
41
+
42
+ const command = buildKubectlCommand(info.type, info.namespace, info.name, localPort, info.port)
43
+
44
+ const handleCopy = async () => {
45
+ try {
46
+ await navigator.clipboard.writeText(command)
47
+ setCopied(true)
48
+ setTimeout(() => setCopied(false), 2000)
49
+ } catch {
50
+ // Clipboard API unavailable (e.g. non-HTTPS context) — select text for manual copy
51
+ if (commandRef.current) {
52
+ const range = document.createRange()
53
+ range.selectNodeContents(commandRef.current)
54
+ const sel = window.getSelection()
55
+ sel?.removeAllRanges()
56
+ sel?.addRange(range)
57
+ }
58
+ setCopyFallback(true)
59
+ setTimeout(() => setCopyFallback(false), 3000)
60
+ }
61
+ }
62
+
63
+ useEffect(() => {
64
+ const handleKeyDown = (e: KeyboardEvent) => {
65
+ if (e.key === 'Escape') onClose()
66
+ }
67
+ document.addEventListener('keydown', handleKeyDown)
68
+ return () => document.removeEventListener('keydown', handleKeyDown)
69
+ }, [onClose])
70
+
71
+ useEffect(() => {
72
+ dialogRef.current?.focus()
73
+ }, [])
74
+
75
+ return (
76
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
77
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
78
+ <div
79
+ ref={dialogRef}
80
+ tabIndex={-1}
81
+ className="relative dialog max-w-lg w-full mx-4 outline-none"
82
+ >
83
+ <div className="flex items-center justify-between p-4 border-b border-theme-border">
84
+ <div className="flex items-center gap-2">
85
+ <Terminal className="w-5 h-5 text-blue-400" />
86
+ <h3 className="text-base font-semibold text-theme-text-primary">Port Forward</h3>
87
+ </div>
88
+ <button
89
+ onClick={onClose}
90
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
91
+ >
92
+ <X className="w-5 h-5" />
93
+ </button>
94
+ </div>
95
+
96
+ <div className="p-4 space-y-3">
97
+ <p className="text-sm text-theme-text-secondary">
98
+ Radar is running in-cluster, so port forwarding must be run from your local terminal.
99
+ </p>
100
+ <div className="flex items-center gap-2 text-sm text-theme-text-secondary">
101
+ <label htmlFor="local-port">Local port:</label>
102
+ <input
103
+ id="local-port"
104
+ type="number"
105
+ min={1}
106
+ max={65535}
107
+ value={localPort}
108
+ onChange={(e) => {
109
+ const val = Number(e.target.value)
110
+ if (val >= 1 && val <= 65535) setLocalPort(val)
111
+ else if (e.target.value === '') setLocalPort(info.port)
112
+ }}
113
+ className="w-20 bg-theme-base border border-theme-border rounded px-2 py-1 text-sm text-theme-text-primary font-mono text-center"
114
+ />
115
+ </div>
116
+ <div className="flex items-center gap-2">
117
+ <code ref={commandRef} className="flex-1 text-sm bg-theme-base rounded px-3 py-2 text-blue-400 font-mono select-all">
118
+ {command}
119
+ </code>
120
+ <button
121
+ onClick={handleCopy}
122
+ className="shrink-0 px-3 py-2 btn-brand text-sm rounded-lg flex items-center gap-1.5"
123
+ >
124
+ {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
125
+ {copied ? 'Copied' : copyFallback ? 'Press Ctrl+C' : 'Copy'}
126
+ </button>
127
+ </div>
128
+ <p className="text-xs text-theme-text-secondary">
129
+ Requires kubectl and authentication to this cluster.
130
+ </p>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ )
135
+ }
136
+
137
+ export function PortForwardButton({
138
+ type,
139
+ namespace,
140
+ name,
141
+ serviceName,
142
+ className,
143
+ }: PortForwardButtonProps) {
144
+ const [isOpen, setIsOpen] = useState(false)
145
+ const [dialogInfo, setDialogInfo] = useState<KubectlDialogInfo | null>(null)
146
+ const [listenAddress, setListenAddress] = useState<'127.0.0.1' | '0.0.0.0'>('127.0.0.1')
147
+ const dropdownRef = useRef<HTMLDivElement>(null)
148
+
149
+ const { data: clusterInfo } = useClusterInfo()
150
+ const { data, isLoading } = useAvailablePorts(type, namespace, name)
151
+ const startPortForward = useStartPortForward()
152
+
153
+ const ports = data?.ports || []
154
+ const inCluster = clusterInfo?.inCluster ?? false
155
+ const isPending = !inCluster && startPortForward.isPending
156
+ const resourceName = type === 'service' ? (serviceName || name) : name
157
+
158
+ // Close dropdown when clicking outside
159
+ useEffect(() => {
160
+ function handleClickOutside(event: MouseEvent) {
161
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
162
+ setIsOpen(false)
163
+ }
164
+ }
165
+ document.addEventListener('mousedown', handleClickOutside)
166
+ return () => document.removeEventListener('mousedown', handleClickOutside)
167
+ }, [])
168
+
169
+ const handlePortSelect = (port: AvailablePort) => {
170
+ setIsOpen(false)
171
+ if (inCluster) {
172
+ setDialogInfo({ type, namespace, name: resourceName, port: port.port })
173
+ } else {
174
+ startPortForward.mutate({
175
+ namespace,
176
+ podName: type === 'pod' ? name : undefined,
177
+ serviceName: type === 'service' ? (serviceName || name) : undefined,
178
+ podPort: port.port,
179
+ listenAddress,
180
+ })
181
+ }
182
+ }
183
+
184
+ function renderButton() {
185
+ // If no ports available, show disabled button
186
+ if (!isLoading && ports.length === 0) {
187
+ return (
188
+ <button
189
+ disabled
190
+ className={clsx(
191
+ 'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg opacity-50 cursor-not-allowed',
192
+ className
193
+ )}
194
+ title="No ports available"
195
+ >
196
+ <Plug className="w-4 h-4" />
197
+ No Ports
198
+ </button>
199
+ )
200
+ }
201
+
202
+ // If only one port, forward directly on click (most common case)
203
+ if (ports.length === 1) {
204
+ return (
205
+ <button
206
+ onClick={() => handlePortSelect(ports[0])}
207
+ disabled={isPending}
208
+ className={clsx(
209
+ 'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg hover:bg-theme-hover transition-colors disabled:opacity-50',
210
+ className
211
+ )}
212
+ title={`Port forward to ${ports[0].port}`}
213
+ >
214
+ {isPending ? (
215
+ <Loader2 className="w-4 h-4 animate-spin" />
216
+ ) : (
217
+ <Plug className="w-4 h-4" />
218
+ )}
219
+ Forward :{ports[0].port}
220
+ </button>
221
+ )
222
+ }
223
+
224
+ // Multiple ports - show dropdown
225
+ return (
226
+ <div className="relative" ref={dropdownRef}>
227
+ <button
228
+ onClick={() => setIsOpen(!isOpen)}
229
+ disabled={isLoading || isPending}
230
+ className={clsx(
231
+ 'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg hover:bg-theme-hover transition-colors disabled:opacity-50',
232
+ className
233
+ )}
234
+ >
235
+ {isLoading || isPending ? (
236
+ <Loader2 className="w-4 h-4 animate-spin" />
237
+ ) : (
238
+ <Plug className="w-4 h-4" />
239
+ )}
240
+ Port Forward
241
+ <ChevronDown className={clsx('w-3 h-3 transition-transform', isOpen && 'rotate-180')} />
242
+ </button>
243
+
244
+ {isOpen && (
245
+ <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">
246
+ {/* Listen address toggle - only for local mode */}
247
+ {!inCluster && (
248
+ <div className="px-3 py-2 border-b border-theme-border">
249
+ <div className="text-xs text-theme-text-disabled mb-2">Listen on</div>
250
+ <div className="flex gap-1">
251
+ <button
252
+ onClick={(e) => { e.stopPropagation(); setListenAddress('127.0.0.1') }}
253
+ className={clsx(
254
+ 'flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors',
255
+ listenAddress === '127.0.0.1'
256
+ ? 'btn-brand-toggle'
257
+ : 'bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-primary'
258
+ )}
259
+ title="Only accessible from this machine"
260
+ >
261
+ <Monitor className="w-3 h-3" />
262
+ localhost
263
+ </button>
264
+ <button
265
+ onClick={(e) => { e.stopPropagation(); setListenAddress('0.0.0.0') }}
266
+ className={clsx(
267
+ 'flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors',
268
+ listenAddress === '0.0.0.0'
269
+ ? 'bg-amber-600 text-white'
270
+ : 'bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-primary'
271
+ )}
272
+ title="Accessible from other machines on the network"
273
+ >
274
+ <Globe className="w-3 h-3" />
275
+ all interfaces
276
+ </button>
277
+ </div>
278
+ </div>
279
+ )}
280
+ <div className="px-2 py-1.5 text-xs text-theme-text-disabled border-b border-theme-border">
281
+ Select port to forward
282
+ </div>
283
+ {ports.map((port, i) => (
284
+ <button
285
+ key={i}
286
+ onClick={() => handlePortSelect(port)}
287
+ className="w-full px-3 py-2 text-left text-sm text-theme-text-primary hover:bg-theme-elevated flex items-center justify-between"
288
+ >
289
+ <span className="flex items-center gap-2 shrink-0">
290
+ <code className="text-accent-text">{port.port}</code>
291
+ <span className="text-theme-text-disabled">/{port.protocol || 'TCP'}</span>
292
+ </span>
293
+ {port.name && (
294
+ <span className="text-xs text-theme-text-disabled truncate max-w-[120px]">{port.name}</span>
295
+ )}
296
+ </button>
297
+ ))}
298
+ </div>
299
+ )}
300
+ </div>
301
+ )
302
+ }
303
+
304
+ return (
305
+ <>
306
+ {renderButton()}
307
+ {dialogInfo && (
308
+ <KubectlCommandDialog info={dialogInfo} onClose={() => setDialogInfo(null)} />
309
+ )}
310
+ </>
311
+ )
312
+ }
313
+
314
+ // Simplified inline button for use in port lists (shows just the port)
315
+ interface PortForwardInlineButtonProps {
316
+ namespace: string
317
+ podName?: string
318
+ serviceName?: string
319
+ port: number
320
+ protocol?: string
321
+ disabled?: boolean
322
+ }
323
+
324
+ export function PortForwardInlineButton({
325
+ namespace,
326
+ podName,
327
+ serviceName,
328
+ port,
329
+ protocol = 'TCP',
330
+ disabled = false,
331
+ }: PortForwardInlineButtonProps) {
332
+ const { data: clusterInfo } = useClusterInfo()
333
+ const startPortForward = useStartPortForward()
334
+ const [dialogInfo, setDialogInfo] = useState<KubectlDialogInfo | null>(null)
335
+
336
+ const inCluster = clusterInfo?.inCluster ?? false
337
+ const isPending = !inCluster && startPortForward.isPending
338
+
339
+ const handleClick = (e: React.MouseEvent) => {
340
+ e.stopPropagation()
341
+ if (inCluster) {
342
+ const resourceType = serviceName ? 'service' : 'pod'
343
+ const resourceName = serviceName || podName || ''
344
+ setDialogInfo({ type: resourceType, namespace, name: resourceName, port })
345
+ } else {
346
+ startPortForward.mutate({
347
+ namespace,
348
+ podName,
349
+ serviceName,
350
+ podPort: port,
351
+ })
352
+ }
353
+ }
354
+
355
+ return (
356
+ <>
357
+ <button
358
+ onClick={handleClick}
359
+ disabled={disabled || isPending}
360
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-theme-elevated hover:bg-accent-muted rounded text-xs transition-colors disabled:opacity-50 disabled:hover:bg-theme-elevated"
361
+ title={`Port forward ${port}`}
362
+ >
363
+ {port}/{protocol}
364
+ {isPending ? (
365
+ <Loader2 className="w-3 h-3 animate-spin" />
366
+ ) : (
367
+ <Plug className="w-3 h-3" />
368
+ )}
369
+ </button>
370
+ {dialogInfo && (
371
+ <KubectlCommandDialog info={dialogInfo} onClose={() => setDialogInfo(null)} />
372
+ )}
373
+ </>
374
+ )
375
+ }