@skyhook-io/radar-app 0.2.2 → 0.3.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 +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { Plug, ChevronDown, Loader2, Globe, Monitor, Copy, Check, X, Terminal } from 'lucide-react'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
4
|
-
import { useAvailablePorts,
|
|
5
|
+
import { useAvailablePorts, AvailablePort } from '../../api/client'
|
|
5
6
|
import { useStartPortForward } from './PortForwardManager'
|
|
7
|
+
import { useIsLocalDeployment } from '../../contexts/CapabilitiesContext'
|
|
8
|
+
import { validatePort } from '@skyhook-io/k8s-ui/utils/validators'
|
|
9
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
10
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
6
11
|
|
|
7
12
|
interface PortForwardButtonProps {
|
|
8
13
|
type: 'pod' | 'service'
|
|
@@ -20,6 +25,13 @@ interface KubectlDialogInfo {
|
|
|
20
25
|
port: number
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
// kubectl port-forward (and the live forward, which uses the same transport) is
|
|
29
|
+
// TCP-only — UDP/SCTP can't be forwarded (kubernetes/kubernetes#47862). Treat an
|
|
30
|
+
// unset protocol as TCP.
|
|
31
|
+
export function isPortForwardable(protocol?: string): boolean {
|
|
32
|
+
return (protocol || 'TCP').toUpperCase() === 'TCP'
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
function buildKubectlCommand(type: 'pod' | 'service', namespace: string, name: string, localPort: number, remotePort: number) {
|
|
24
36
|
const resource = type === 'pod' ? `pod/${name}` : `svc/${name}`
|
|
25
37
|
const portArg = localPort === remotePort ? `${remotePort}` : `${localPort}:${remotePort}`
|
|
@@ -35,7 +47,13 @@ function KubectlCommandDialog({
|
|
|
35
47
|
}) {
|
|
36
48
|
const [copied, setCopied] = useState(false)
|
|
37
49
|
const [copyFallback, setCopyFallback] = useState(false)
|
|
38
|
-
|
|
50
|
+
// Track raw input separately from the validated port so the user
|
|
51
|
+
// always sees the characters they typed; the validated port (used to
|
|
52
|
+
// build the command) only updates when the input parses cleanly.
|
|
53
|
+
const [portInput, setPortInput] = useState(String(info.port))
|
|
54
|
+
const portValidation = validatePort(portInput)
|
|
55
|
+
const localPort = portValidation.valid ? portValidation.value : info.port
|
|
56
|
+
const portError = portValidation.valid ? null : portValidation.error
|
|
39
57
|
const commandRef = useRef<HTMLElement>(null)
|
|
40
58
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
41
59
|
|
|
@@ -62,17 +80,23 @@ function KubectlCommandDialog({
|
|
|
62
80
|
|
|
63
81
|
useEffect(() => {
|
|
64
82
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
65
|
-
if (e.key
|
|
83
|
+
if (e.key !== 'Escape') return
|
|
84
|
+
// Capture + stopPropagation so Escape closes only this dialog, not the
|
|
85
|
+
// drawer behind it (whose Escape shortcut listens in the bubble phase).
|
|
86
|
+
e.stopPropagation()
|
|
87
|
+
onClose()
|
|
66
88
|
}
|
|
67
|
-
document.addEventListener('keydown', handleKeyDown)
|
|
68
|
-
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
89
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
90
|
+
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
|
69
91
|
}, [onClose])
|
|
70
92
|
|
|
71
93
|
useEffect(() => {
|
|
72
94
|
dialogRef.current?.focus()
|
|
73
95
|
}, [])
|
|
74
96
|
|
|
75
|
-
|
|
97
|
+
// Portal to <body>: the drawer is a transformed ancestor that would otherwise
|
|
98
|
+
// trap this position:fixed dialog inside the drawer instead of centering it.
|
|
99
|
+
return createPortal(
|
|
76
100
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
77
101
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
78
102
|
<div
|
|
@@ -95,23 +119,36 @@ function KubectlCommandDialog({
|
|
|
95
119
|
|
|
96
120
|
<div className="p-4 space-y-3">
|
|
97
121
|
<p className="text-sm text-theme-text-secondary">
|
|
98
|
-
|
|
122
|
+
Forward this port to your own machine — run it from your terminal:
|
|
99
123
|
</p>
|
|
100
|
-
<div className="flex
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
<div className="flex flex-col gap-1">
|
|
125
|
+
<div className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
126
|
+
<label htmlFor="local-port">Local port:</label>
|
|
127
|
+
<Input
|
|
128
|
+
id="local-port"
|
|
129
|
+
inputMode="numeric"
|
|
130
|
+
value={portInput}
|
|
131
|
+
onChange={(e) => setPortInput(e.target.value)}
|
|
132
|
+
aria-invalid={portError ? true : undefined}
|
|
133
|
+
aria-describedby="local-port-help"
|
|
134
|
+
className={clsx(
|
|
135
|
+
'w-24 bg-theme-base border rounded px-2 py-1 text-sm text-theme-text-primary font-mono text-center',
|
|
136
|
+
portError
|
|
137
|
+
? 'border-red-500/60 focus:outline-none focus:ring-2 focus:ring-red-500'
|
|
138
|
+
: 'border-theme-border',
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
{portError && (
|
|
142
|
+
<span className="text-xs text-red-400">
|
|
143
|
+
using {info.port}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
{portError && (
|
|
148
|
+
<p id="local-port-help" className="text-xs text-red-400">
|
|
149
|
+
{portError.charAt(0).toUpperCase() + portError.slice(1)}.
|
|
150
|
+
</p>
|
|
151
|
+
)}
|
|
115
152
|
</div>
|
|
116
153
|
<div className="flex items-center gap-2">
|
|
117
154
|
<code ref={commandRef} className="flex-1 text-sm bg-theme-base rounded px-3 py-2 text-blue-400 font-mono select-all">
|
|
@@ -125,12 +162,13 @@ function KubectlCommandDialog({
|
|
|
125
162
|
{copied ? 'Copied' : copyFallback ? 'Press Ctrl+C' : 'Copy'}
|
|
126
163
|
</button>
|
|
127
164
|
</div>
|
|
128
|
-
<p className="text-xs text-theme-text-
|
|
129
|
-
|
|
165
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
166
|
+
You'll need <code className="inline-code">kubectl</code> and access to this cluster.
|
|
130
167
|
</p>
|
|
131
168
|
</div>
|
|
132
169
|
</div>
|
|
133
|
-
</div
|
|
170
|
+
</div>,
|
|
171
|
+
document.body,
|
|
134
172
|
)
|
|
135
173
|
}
|
|
136
174
|
|
|
@@ -146,12 +184,16 @@ export function PortForwardButton({
|
|
|
146
184
|
const [listenAddress, setListenAddress] = useState<'127.0.0.1' | '0.0.0.0'>('127.0.0.1')
|
|
147
185
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
148
186
|
|
|
149
|
-
const
|
|
187
|
+
const isLocal = useIsLocalDeployment()
|
|
150
188
|
const { data, isLoading } = useAvailablePorts(type, namespace, name)
|
|
151
189
|
const startPortForward = useStartPortForward()
|
|
152
190
|
|
|
153
191
|
const ports = data?.ports || []
|
|
154
|
-
|
|
192
|
+
// Decide copy-command vs live forward from the SAME deployment signal that
|
|
193
|
+
// gates whether the button shows at all — so the two can't disagree (and we
|
|
194
|
+
// don't race a separate /cluster-info fetch that defaults to "not in-cluster").
|
|
195
|
+
// Cloud runs in-cluster too, so anything not-local uses the copy command.
|
|
196
|
+
const inCluster = !isLocal
|
|
155
197
|
const isPending = !inCluster && startPortForward.isPending
|
|
156
198
|
const resourceName = type === 'service' ? (serviceName || name) : name
|
|
157
199
|
|
|
@@ -182,42 +224,49 @@ export function PortForwardButton({
|
|
|
182
224
|
}
|
|
183
225
|
|
|
184
226
|
function renderButton() {
|
|
185
|
-
//
|
|
186
|
-
|
|
227
|
+
// kubectl port-forward is TCP-only — never offer UDP/SCTP ports as targets.
|
|
228
|
+
const forwardable = ports.filter((p) => isPortForwardable(p.protocol))
|
|
229
|
+
|
|
230
|
+
// No forwardable ports: disabled button. Distinguish "no ports at all" from
|
|
231
|
+
// "ports exist but are all UDP" so the operator isn't left guessing.
|
|
232
|
+
if (!isLoading && forwardable.length === 0) {
|
|
233
|
+
const udpOnly = ports.length > 0
|
|
187
234
|
return (
|
|
235
|
+
<Tooltip content={udpOnly ? "kubectl port-forward doesn't support UDP" : 'No ports available'}>
|
|
188
236
|
<button
|
|
189
237
|
disabled
|
|
190
238
|
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',
|
|
239
|
+
'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 disabled:pointer-events-none',
|
|
192
240
|
className
|
|
193
241
|
)}
|
|
194
|
-
title="No ports available"
|
|
195
242
|
>
|
|
196
243
|
<Plug className="w-4 h-4" />
|
|
197
|
-
No Ports
|
|
244
|
+
{udpOnly ? 'No TCP Ports' : 'No Ports'}
|
|
198
245
|
</button>
|
|
246
|
+
</Tooltip>
|
|
199
247
|
)
|
|
200
248
|
}
|
|
201
249
|
|
|
202
|
-
// If only one port, forward directly on click (most common case)
|
|
203
|
-
if (
|
|
250
|
+
// If only one forwardable port, forward directly on click (most common case)
|
|
251
|
+
if (forwardable.length === 1) {
|
|
204
252
|
return (
|
|
253
|
+
<Tooltip content={`Port forward to ${forwardable[0].port}`}>
|
|
205
254
|
<button
|
|
206
|
-
onClick={() => handlePortSelect(
|
|
255
|
+
onClick={() => handlePortSelect(forwardable[0])}
|
|
207
256
|
disabled={isPending}
|
|
208
257
|
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',
|
|
258
|
+
'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 disabled:pointer-events-none',
|
|
210
259
|
className
|
|
211
260
|
)}
|
|
212
|
-
title={`Port forward to ${ports[0].port}`}
|
|
213
261
|
>
|
|
214
262
|
{isPending ? (
|
|
215
263
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
216
264
|
) : (
|
|
217
265
|
<Plug className="w-4 h-4" />
|
|
218
266
|
)}
|
|
219
|
-
Forward :{
|
|
267
|
+
Forward :{forwardable[0].port}
|
|
220
268
|
</button>
|
|
269
|
+
</Tooltip>
|
|
221
270
|
)
|
|
222
271
|
}
|
|
223
272
|
|
|
@@ -248,6 +297,7 @@ export function PortForwardButton({
|
|
|
248
297
|
<div className="px-3 py-2 border-b border-theme-border">
|
|
249
298
|
<div className="text-xs text-theme-text-disabled mb-2">Listen on</div>
|
|
250
299
|
<div className="flex gap-1">
|
|
300
|
+
<Tooltip content="Only accessible from this machine" wrapperClassName="flex-1">
|
|
251
301
|
<button
|
|
252
302
|
onClick={(e) => { e.stopPropagation(); setListenAddress('127.0.0.1') }}
|
|
253
303
|
className={clsx(
|
|
@@ -256,11 +306,12 @@ export function PortForwardButton({
|
|
|
256
306
|
? 'btn-brand-toggle'
|
|
257
307
|
: 'bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-primary'
|
|
258
308
|
)}
|
|
259
|
-
title="Only accessible from this machine"
|
|
260
309
|
>
|
|
261
310
|
<Monitor className="w-3 h-3" />
|
|
262
311
|
localhost
|
|
263
312
|
</button>
|
|
313
|
+
</Tooltip>
|
|
314
|
+
<Tooltip content="Accessible from other machines on the network" wrapperClassName="flex-1">
|
|
264
315
|
<button
|
|
265
316
|
onClick={(e) => { e.stopPropagation(); setListenAddress('0.0.0.0') }}
|
|
266
317
|
className={clsx(
|
|
@@ -269,25 +320,25 @@ export function PortForwardButton({
|
|
|
269
320
|
? 'bg-amber-600 text-white'
|
|
270
321
|
: 'bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-primary'
|
|
271
322
|
)}
|
|
272
|
-
title="Accessible from other machines on the network"
|
|
273
323
|
>
|
|
274
324
|
<Globe className="w-3 h-3" />
|
|
275
325
|
all interfaces
|
|
276
326
|
</button>
|
|
327
|
+
</Tooltip>
|
|
277
328
|
</div>
|
|
278
329
|
</div>
|
|
279
330
|
)}
|
|
280
331
|
<div className="px-2 py-1.5 text-xs text-theme-text-disabled border-b border-theme-border">
|
|
281
332
|
Select port to forward
|
|
282
333
|
</div>
|
|
283
|
-
{
|
|
334
|
+
{forwardable.map((port, i) => (
|
|
284
335
|
<button
|
|
285
336
|
key={i}
|
|
286
337
|
onClick={() => handlePortSelect(port)}
|
|
287
338
|
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
339
|
>
|
|
289
340
|
<span className="flex items-center gap-2 shrink-0">
|
|
290
|
-
<code className="
|
|
341
|
+
<code className="inline-code">{port.port}</code>
|
|
291
342
|
<span className="text-theme-text-disabled">/{port.protocol || 'TCP'}</span>
|
|
292
343
|
</span>
|
|
293
344
|
{port.name && (
|
|
@@ -329,11 +380,15 @@ export function PortForwardInlineButton({
|
|
|
329
380
|
protocol = 'TCP',
|
|
330
381
|
disabled = false,
|
|
331
382
|
}: PortForwardInlineButtonProps) {
|
|
332
|
-
const
|
|
383
|
+
const isLocal = useIsLocalDeployment()
|
|
333
384
|
const startPortForward = useStartPortForward()
|
|
334
385
|
const [dialogInfo, setDialogInfo] = useState<KubectlDialogInfo | null>(null)
|
|
335
386
|
|
|
336
|
-
|
|
387
|
+
// Decide copy-command vs live forward from the SAME deployment signal that
|
|
388
|
+
// gates whether the button shows at all — so the two can't disagree (and we
|
|
389
|
+
// don't race a separate /cluster-info fetch that defaults to "not in-cluster").
|
|
390
|
+
// Cloud runs in-cluster too, so anything not-local uses the copy command.
|
|
391
|
+
const inCluster = !isLocal
|
|
337
392
|
const isPending = !inCluster && startPortForward.isPending
|
|
338
393
|
|
|
339
394
|
const handleClick = (e: React.MouseEvent) => {
|
|
@@ -352,21 +407,37 @@ export function PortForwardInlineButton({
|
|
|
352
407
|
}
|
|
353
408
|
}
|
|
354
409
|
|
|
410
|
+
// UDP/SCTP can't be port-forwarded — show a muted, non-interactive hint that
|
|
411
|
+
// explains why rather than a button that would copy a command that can't work.
|
|
412
|
+
if (!isPortForwardable(protocol)) {
|
|
413
|
+
return (
|
|
414
|
+
<Tooltip content="kubectl port-forward doesn't support UDP">
|
|
415
|
+
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-theme-elevated rounded text-xs text-theme-text-tertiary opacity-60 cursor-default">
|
|
416
|
+
{port}/{protocol}
|
|
417
|
+
<Plug className="w-3 h-3" />
|
|
418
|
+
</span>
|
|
419
|
+
</Tooltip>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
355
423
|
return (
|
|
356
424
|
<>
|
|
425
|
+
<Tooltip content={inCluster ? 'Copy a kubectl port-forward command' : `Port forward ${port}`}>
|
|
357
426
|
<button
|
|
358
427
|
onClick={handleClick}
|
|
359
428
|
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}`}
|
|
429
|
+
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 disabled:pointer-events-none"
|
|
362
430
|
>
|
|
363
|
-
{
|
|
431
|
+
{/* In-cluster this opens a copy-command dialog rather than forwarding now;
|
|
432
|
+
the trailing "…" signals "opens a dialog" (it doesn't fire immediately). */}
|
|
433
|
+
{port}/{protocol}{inCluster ? '…' : ''}
|
|
364
434
|
{isPending ? (
|
|
365
435
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
366
436
|
) : (
|
|
367
437
|
<Plug className="w-3 h-3" />
|
|
368
438
|
)}
|
|
369
439
|
</button>
|
|
440
|
+
</Tooltip>
|
|
370
441
|
{dialogInfo && (
|
|
371
442
|
<KubectlCommandDialog info={dialogInfo} onClose={() => setDialogInfo(null)} />
|
|
372
443
|
)}
|