@@ -40,6 +40,7 @@ import Pod from '../../../lib/k8s/pod';
4040import Service from '../../../lib/k8s/service' ;
4141import ActionButton from '../ActionButton' ;
4242export { type PortForward as PortForwardState } from '../../../lib/k8s/api/v1/portForward' ;
43+ import PortForwardStartDialog from '../../portforward/PortForwardStartDialog' ;
4344
4445interface PortForwardKubeObjectProps {
4546 containerPort : number | string ;
@@ -57,6 +58,8 @@ type PortForwardProps = PortForwardKubeObjectProps | PortForwardLegacyProps;
5758export const PORT_FORWARDS_STORAGE_KEY = 'portforwards' ;
5859export const PORT_FORWARD_STOP_STATUS = 'Stopped' ;
5960export const PORT_FORWARD_RUNNING_STATUS = 'Running' ;
61+ export const DOCKER_DESKTOP_MIN_PORT = 30000 ;
62+ export const DOCKER_DESKTOP_MAX_PORT = 32000 ;
6063
6164function 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