Skip to content

Commit cfcaab5

Browse files
committed
frontend: PortForward: Add custom local port input dialog for port forwarding
Signed-off-by: jaehanbyun <awbrg789@naver.com>
1 parent 31348ec commit cfcaab5

File tree

16 files changed

+441
-134
lines changed

16 files changed

+441
-134
lines changed

frontend/src/components/common/Resource/PortForward.tsx

Lines changed: 150 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import Pod from '../../../lib/k8s/pod';
4040
import Service from '../../../lib/k8s/service';
4141
import ActionButton from '../ActionButton';
4242
export { type PortForward as PortForwardState } from '../../../lib/k8s/api/v1/portForward';
43+
import PortForwardStartDialog from '../../portforward/PortForwardStartDialog';
4344

4445
interface PortForwardKubeObjectProps {
4546
containerPort: number | string;
@@ -57,6 +58,8 @@ type PortForwardProps = PortForwardKubeObjectProps | PortForwardLegacyProps;
5758
export const PORT_FORWARDS_STORAGE_KEY = 'portforwards';
5859
export const PORT_FORWARD_STOP_STATUS = 'Stopped';
5960
export const PORT_FORWARD_RUNNING_STATUS = 'Running';
61+
export const DOCKER_DESKTOP_MIN_PORT = 30000;
62+
export const DOCKER_DESKTOP_MAX_PORT = 32000;
6063

6164
function getPortNumberFromPortName(containers: KubeContainer[], namedPort: string) {
6265
let portNumber = 0;
@@ -107,9 +110,10 @@ function PortForwardContent(props: PortForwardProps) {
107110
const service = !isPod ? (resource as Service) : undefined;
108111
const namespace = resource?.metadata?.namespace || '';
109112
const name = resource?.metadata?.name || '';
110-
const [error, setError] = React.useState(null);
113+
const [error, setError] = React.useState<string | null>(null);
111114
const [portForward, setPortForward] = React.useState<PortForwardState | null>(null);
112115
const [loading, setLoading] = React.useState(false);
116+
const [startDialogOpen, setStartDialogOpen] = React.useState(false);
113117
const { t } = useTranslation(['translation', 'resource']);
114118
const [pods, podsFetchError] = Pod.useList({
115119
namespace,
@@ -173,7 +177,7 @@ function PortForwardContent(props: PortForwardProps) {
173177

174178
localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(serverAndStoragePortForwards));
175179
});
176-
}, []);
180+
}, [cluster, namespace, name, numericContainerPort]);
177181

178182
if (!isElectron()) {
179183
return null;
@@ -187,7 +191,7 @@ function PortForwardContent(props: PortForwardProps) {
187191
return null;
188192
}
189193

190-
function handlePortForward() {
194+
function startPortForwardWithSelection(chosenPort?: string) {
191195
if (!namespace || !cluster || !pods) {
192196
return;
193197
}
@@ -199,37 +203,48 @@ function PortForwardContent(props: PortForwardProps) {
199203
const serviceNamespace = namespace;
200204
const serviceName = !isPod ? resourceName : '';
201205
const podName = isPod ? resourceName : pods![0].metadata.name;
202-
var port = portForward?.port;
206+
let port = chosenPort || portForward?.port;
203207

204208
let address = 'localhost';
205209
// In case of docker desktop only a range of ports are open
206210
// so we need to generate a random port from that range
207211
// while making sure that it is not already in use
208212
if (isDockerDesktop()) {
209-
const validMinPort = 30000;
210-
const validMaxPort = 32000;
213+
address = '0.0.0.0';
211214

212-
// create a list of active ports
213-
const activePorts: string[] = [];
214-
const portForwardsInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY);
215-
const parsedPortForwards = JSON.parse(portForwardsInStorage || '[]');
216-
parsedPortForwards.forEach((pf: any) => {
217-
if (pf.status === PORT_FORWARD_RUNNING_STATUS) {
218-
activePorts.push(pf.port);
215+
// Only auto-assign if user didn't specify a port
216+
if (!chosenPort && !portForward?.port) {
217+
// create a list of active ports
218+
const activePorts: string[] = [];
219+
const portForwardsInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY);
220+
const parsedPortForwards = JSON.parse(portForwardsInStorage || '[]');
221+
parsedPortForwards.forEach((pf: any) => {
222+
if (pf.status === PORT_FORWARD_RUNNING_STATUS) {
223+
activePorts.push(pf.port);
224+
}
225+
});
226+
227+
// Generate random port in Docker Desktop range
228+
const portRange = DOCKER_DESKTOP_MAX_PORT - DOCKER_DESKTOP_MIN_PORT + 1;
229+
const maxAttempts = portRange;
230+
let attempts = 0;
231+
232+
while (attempts < maxAttempts) {
233+
const randomPort = (
234+
Math.floor(Math.random() * portRange) + DOCKER_DESKTOP_MIN_PORT
235+
).toString();
236+
if (!activePorts.includes(randomPort)) {
237+
port = randomPort;
238+
break;
239+
}
240+
attempts++;
219241
}
220-
});
221242

222-
// generate random port till it is not in use
223-
while (true) {
224-
const randomPort = (
225-
Math.floor(Math.random() * (validMaxPort - validMinPort + 1)) + validMinPort
226-
).toString();
227-
if (!activePorts.includes(randomPort)) {
228-
port = randomPort;
229-
break;
243+
// Fallback: if all ports seem taken, use a random one anyway
244+
if (!port) {
245+
port = Math.floor(Math.random() * portRange + DOCKER_DESKTOP_MIN_PORT).toString();
230246
}
231247
}
232-
address = '0.0.0.0';
233248
}
234249

235250
setLoading(true);
@@ -261,6 +276,14 @@ function PortForwardContent(props: PortForwardProps) {
261276
});
262277
}
263278

279+
function openStartDialog() {
280+
setStartDialogOpen(true);
281+
}
282+
283+
function closeStartDialog() {
284+
setStartDialogOpen(false);
285+
}
286+
264287
function portForwardStopHandler() {
265288
if (!portForward || !cluster) {
266289
return;
@@ -305,96 +328,118 @@ function PortForwardContent(props: PortForwardProps) {
305328
}
306329

307330
const forwardBaseURL = 'http://127.0.0.1';
331+
const displayPodName = React.useMemo(() => {
332+
return isPod ? name : pods && pods.length > 0 ? pods[0].metadata.name : '';
333+
}, [isPod, name, pods]);
308334

309-
return !portForward ? (
335+
return (
310336
<Box>
311-
{loading ? (
312-
<CircularProgress size={18} />
313-
) : (
314-
<Button
315-
onClick={handlePortForward}
316-
aria-label={t('translation|Start port forward')}
317-
color="primary"
318-
variant="outlined"
319-
style={{
320-
textTransform: 'none',
321-
}}
322-
disabled={loading}
323-
>
324-
<InlineIcon icon="mdi:fast-forward" width={20} />
325-
<Typography>{t('translation|Forward port')}</Typography>
326-
</Button>
327-
)}
328-
{error && (
329-
<Box mt={1}>
330-
{
331-
<Alert
332-
severity="error"
333-
onClose={() => {
334-
setError(null);
337+
{!portForward ? (
338+
<>
339+
{loading ? (
340+
<CircularProgress size={18} />
341+
) : (
342+
<Button
343+
onClick={openStartDialog}
344+
aria-label={t('translation|Start port forward')}
345+
color="primary"
346+
variant="outlined"
347+
style={{
348+
textTransform: 'none',
335349
}}
350+
disabled={loading}
336351
>
337-
<Tooltip title="error">
338-
<Box style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{error}</Box>
339-
</Tooltip>
340-
</Alert>
341-
}
342-
</Box>
343-
)}
344-
</Box>
345-
) : (
346-
<Box>
347-
{portForward.status === PORT_FORWARD_STOP_STATUS ? (
348-
<Box display={'flex'} alignItems="center">
349-
<Typography
350-
style={{
351-
color: grey[500],
352-
}}
353-
>{`${forwardBaseURL}:${portForward.port}`}</Typography>
354-
<ActionButton
355-
onClick={handlePortForward}
356-
description={t('translation|Start port forward')}
357-
color="primary"
358-
icon="mdi:fast-forward"
359-
iconButtonProps={{
360-
size: 'small',
361-
color: 'primary',
362-
disabled: loading,
363-
}}
364-
width={'25'}
365-
/>
366-
<ActionButton
367-
onClick={deletePortForwardHandler}
368-
description={t('translation|Delete port forward')}
369-
color="primary"
370-
icon="mdi:delete-outline"
371-
iconButtonProps={{
372-
size: 'small',
373-
color: 'primary',
374-
disabled: loading,
375-
}}
376-
width={'25'}
377-
/>
378-
</Box>
352+
<InlineIcon icon="mdi:fast-forward" width={20} />
353+
<Typography>{t('translation|Forward port')}</Typography>
354+
</Button>
355+
)}
356+
{error && (
357+
<Box mt={1}>
358+
<Alert
359+
severity="error"
360+
onClose={() => {
361+
setError(null);
362+
}}
363+
>
364+
<Tooltip title="error">
365+
<Box style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{error}</Box>
366+
</Tooltip>
367+
</Alert>
368+
</Box>
369+
)}
370+
</>
379371
) : (
380372
<>
381-
<MuiLink href={`${forwardBaseURL}:${portForward.port}`} target="_blank" color="primary">
382-
{`${forwardBaseURL}:${portForward.port}`}
383-
</MuiLink>
384-
<ActionButton
385-
onClick={portForwardStopHandler}
386-
description={t('translation|Stop port forward')}
387-
color="primary"
388-
icon="mdi:stop-circle-outline"
389-
iconButtonProps={{
390-
size: 'small',
391-
color: 'primary',
392-
disabled: loading,
393-
}}
394-
width={'25'}
395-
/>
373+
{portForward.status === PORT_FORWARD_STOP_STATUS ? (
374+
<Box display={'flex'} alignItems="center">
375+
<Typography
376+
style={{
377+
color: grey[500],
378+
}}
379+
>{`${forwardBaseURL}:${portForward.port}`}</Typography>
380+
<ActionButton
381+
onClick={openStartDialog}
382+
description={t('translation|Start port forward')}
383+
color="primary"
384+
icon="mdi:fast-forward"
385+
iconButtonProps={{
386+
size: 'small',
387+
color: 'primary',
388+
disabled: loading,
389+
}}
390+
width={'25'}
391+
/>
392+
<ActionButton
393+
onClick={deletePortForwardHandler}
394+
description={t('translation|Delete port forward')}
395+
color="primary"
396+
icon="mdi:delete-outline"
397+
iconButtonProps={{
398+
size: 'small',
399+
color: 'primary',
400+
disabled: loading,
401+
}}
402+
width={'25'}
403+
/>
404+
</Box>
405+
) : (
406+
<>
407+
<MuiLink
408+
href={`${forwardBaseURL}:${portForward.port}`}
409+
target="_blank"
410+
color="primary"
411+
>
412+
{`${forwardBaseURL}:${portForward.port}`}
413+
</MuiLink>
414+
<ActionButton
415+
onClick={portForwardStopHandler}
416+
description={t('translation|Stop port forward')}
417+
color="primary"
418+
icon="mdi:stop-circle-outline"
419+
iconButtonProps={{
420+
size: 'small',
421+
color: 'primary',
422+
disabled: loading,
423+
}}
424+
width={'25'}
425+
/>
426+
</>
427+
)}
396428
</>
397429
)}
430+
<PortForwardStartDialog
431+
open={startDialogOpen}
432+
defaultPort={portForward?.port}
433+
podName={displayPodName}
434+
namespace={namespace}
435+
containerPort={numericContainerPort}
436+
isDockerDesktop={isDockerDesktop()}
437+
onCancel={closeStartDialog}
438+
onConfirm={portInput => {
439+
closeStartDialog();
440+
startPortForwardWithSelection(portInput);
441+
}}
442+
/>
398443
</Box>
399444
);
400445
}

0 commit comments

Comments
 (0)