@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.
- package/README.md +67 -0
- package/package.json +80 -0
- package/src/App.tsx +1538 -0
- package/src/RadarApp.tsx +145 -0
- package/src/api/apiResources.ts +28 -0
- package/src/api/client.ts +2583 -0
- package/src/api/config.ts +116 -0
- package/src/api/traffic.ts +139 -0
- package/src/components/ConnectionErrorView.tsx +272 -0
- package/src/components/ContextSwitcher.tsx +481 -0
- package/src/components/DebugOverlay.tsx +94 -0
- package/src/components/UserMenu.tsx +87 -0
- package/src/components/audit/AuditSettingsDialog.tsx +162 -0
- package/src/components/audit/AuditView.tsx +123 -0
- package/src/components/cost/CostTrendChart.tsx +388 -0
- package/src/components/cost/CostView.tsx +545 -0
- package/src/components/dock/BottomDock.tsx +96 -0
- package/src/components/dock/DockContext.tsx +11 -0
- package/src/components/dock/LocalTerminalTab.tsx +22 -0
- package/src/components/dock/LogsTab.tsx +26 -0
- package/src/components/dock/NodeTerminalTab.tsx +50 -0
- package/src/components/dock/TerminalTab.tsx +42 -0
- package/src/components/dock/TrafficFlowListTab.tsx +18 -0
- package/src/components/dock/WorkloadLogsTab.tsx +23 -0
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsActions.tsx +1 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
- package/src/components/gitops/ManagedResourcesList.tsx +1 -0
- package/src/components/gitops/SyncCountdown.tsx +1 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/helm/ChartBrowser.tsx +580 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
- package/src/components/helm/HelmView.tsx +475 -0
- package/src/components/helm/InstallWizard.tsx +1060 -0
- package/src/components/helm/ManifestDiffViewer.tsx +91 -0
- package/src/components/helm/ManifestViewer.tsx +61 -0
- package/src/components/helm/OwnedResources.tsx +465 -0
- package/src/components/helm/RevisionHistory.tsx +167 -0
- package/src/components/helm/ValuesDiffPreview.tsx +190 -0
- package/src/components/helm/ValuesViewer.tsx +365 -0
- package/src/components/helm/helm-utils.ts +37 -0
- package/src/components/home/ActivitySummary.tsx +262 -0
- package/src/components/home/CertificateHealthCard.tsx +105 -0
- package/src/components/home/ClusterHealthCard.tsx +483 -0
- package/src/components/home/CostCard.tsx +112 -0
- package/src/components/home/HealthRing.tsx +1 -0
- package/src/components/home/HelmSummary.tsx +129 -0
- package/src/components/home/HomeView.tsx +224 -0
- package/src/components/home/MCPSetupDialog.tsx +417 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
- package/src/components/home/TopologyPreview.tsx +219 -0
- package/src/components/home/TrafficSummary.tsx +154 -0
- package/src/components/logs/JsonLogLine.tsx +1 -0
- package/src/components/logs/LogCore.tsx +2 -0
- package/src/components/logs/LogsViewer.tsx +44 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
- package/src/components/logs/useLogBuffer.ts +2 -0
- package/src/components/logs/useLogSearch.ts +1 -0
- package/src/components/portforward/PortForwardButton.tsx +375 -0
- package/src/components/portforward/PortForwardManager.tsx +871 -0
- package/src/components/resource/PrometheusCharts.tsx +687 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
- package/src/components/resources/ImageFilesystemModal.tsx +745 -0
- package/src/components/resources/PodFilesystemModal.tsx +407 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
- package/src/components/resources/ResourcesView.tsx +190 -0
- package/src/components/resources/drawer-components.tsx +1 -0
- package/src/components/resources/file-browser-utils.ts +35 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/EventRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +1 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/JobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodRenderer.tsx +94 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/VPARenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
- package/src/components/resources/renderers/argo-cells.tsx +1 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
- package/src/components/resources/renderers/eso-cells.tsx +1 -0
- package/src/components/resources/renderers/flux-cells.tsx +1 -0
- package/src/components/resources/renderers/index.ts +91 -0
- package/src/components/resources/renderers/istio-cells.tsx +1 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
- package/src/components/resources/renderers/keda-cells.tsx +1 -0
- package/src/components/resources/renderers/knative-cells.tsx +1 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
- package/src/components/resources/renderers/traefik-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-shared.tsx +1 -0
- package/src/components/resources/renderers/velero-cells.tsx +1 -0
- package/src/components/resources/resource-utils-argo.ts +2 -0
- package/src/components/resources/resource-utils-certmanager.ts +2 -0
- package/src/components/resources/resource-utils-cnpg.ts +2 -0
- package/src/components/resources/resource-utils-eso.ts +2 -0
- package/src/components/resources/resource-utils-flux.ts +2 -0
- package/src/components/resources/resource-utils-istio.ts +2 -0
- package/src/components/resources/resource-utils-karpenter.ts +2 -0
- package/src/components/resources/resource-utils-keda.ts +2 -0
- package/src/components/resources/resource-utils-knative.ts +2 -0
- package/src/components/resources/resource-utils-kyverno.ts +2 -0
- package/src/components/resources/resource-utils-prometheus.ts +2 -0
- package/src/components/resources/resource-utils-traefik.ts +1 -0
- package/src/components/resources/resource-utils-trivy.ts +2 -0
- package/src/components/resources/resource-utils-velero.ts +2 -0
- package/src/components/resources/resource-utils.ts +5 -0
- package/src/components/settings/SettingsDialog.tsx +537 -0
- package/src/components/shared/CreateResourceDialog.tsx +17 -0
- package/src/components/shared/EditableYamlView.tsx +24 -0
- package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
- package/src/components/timeline/DiffViewer.tsx +1 -0
- package/src/components/timeline/TimelineList.tsx +69 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
- package/src/components/timeline/TimelineView.tsx +157 -0
- package/src/components/timeline/shared.tsx +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
- package/src/components/traffic/TrafficFlowList.tsx +415 -0
- package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
- package/src/components/traffic/TrafficGraph.tsx +1546 -0
- package/src/components/traffic/TrafficView.tsx +1213 -0
- package/src/components/traffic/TrafficWizard.tsx +386 -0
- package/src/components/traffic/index.ts +3 -0
- package/src/components/ui/CodeViewer.tsx +8 -0
- package/src/components/ui/CommandPalette.tsx +460 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
- package/src/components/ui/ErrorBoundary.tsx +46 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
- package/src/components/ui/Markdown.tsx +108 -0
- package/src/components/ui/MetricsChart.tsx +1 -0
- package/src/components/ui/NamespaceSelector.tsx +436 -0
- package/src/components/ui/ResourceBar.tsx +1 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
- package/src/components/ui/Toast.tsx +1 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/UpdateNotification.tsx +299 -0
- package/src/components/ui/YamlEditor.tsx +1 -0
- package/src/components/workload/WorkloadView.tsx +532 -0
- package/src/context/ConnectionContext.tsx +173 -0
- package/src/context/ContextSwitchContext.tsx +56 -0
- package/src/context/NavCustomization.tsx +62 -0
- package/src/context/ThemeContext.tsx +97 -0
- package/src/contexts/CapabilitiesContext.tsx +130 -0
- package/src/hooks/useAnimatedUnmount.ts +1 -0
- package/src/hooks/useDesktopDownload.ts +41 -0
- package/src/hooks/useEventSource.ts +262 -0
- package/src/hooks/useFavorites.ts +69 -0
- package/src/hooks/useKeyboardShortcuts.tsx +7 -0
- package/src/hooks/useRefreshAnimation.ts +1 -0
- package/src/index.css +243 -0
- package/src/index.ts +17 -0
- package/src/main.tsx +158 -0
- package/src/types/gitops.ts +2 -0
- package/src/types.ts +3 -0
- package/src/utils/animation.ts +2 -0
- package/src/utils/badge-colors.ts +2 -0
- package/src/utils/context-name.ts +2 -0
- package/src/utils/desktop-download.ts +66 -0
- package/src/utils/desktop-open-folder.ts +21 -0
- package/src/utils/format.ts +2 -0
- package/src/utils/log-format.ts +12 -0
- package/src/utils/navigation.ts +23 -0
- package/src/utils/resource-hierarchy.ts +2 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.ts +2 -0
- package/src/utils/traffic-colors.ts +54 -0
- 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
|
+
}
|