@@ -16,7 +16,7 @@ import Stack from "@mui/material/Stack";
16
16
import TextField from "@mui/material/TextField" ;
17
17
import Tooltip from "@mui/material/Tooltip" ;
18
18
import { type FormikContextType , useFormik } from "formik" ;
19
- import type { FC } from "react" ;
19
+ import { useState , type FC } from "react" ;
20
20
import { useQuery , useMutation } from "react-query" ;
21
21
import * as Yup from "yup" ;
22
22
import { getAgentListeningPorts } from "api/api" ;
@@ -48,7 +48,11 @@ import { type ClassName, useClassName } from "hooks/useClassName";
48
48
import { useDashboard } from "modules/dashboard/useDashboard" ;
49
49
import { docs } from "utils/docs" ;
50
50
import { getFormHelpers } from "utils/formUtils" ;
51
- import { portForwardURL } from "utils/portForward" ;
51
+ import {
52
+ getWorkspaceListeningPortsProtocol ,
53
+ portForwardURL ,
54
+ saveWorkspaceListeningPortsProtocol ,
55
+ } from "utils/portForward" ;
52
56
53
57
export interface PortForwardButtonProps {
54
58
host : string ;
@@ -135,6 +139,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
135
139
portSharingControlsEnabled,
136
140
} ) => {
137
141
const theme = useTheme ( ) ;
142
+ const [ listeningPortProtocol , setListeningPortProtocol ] = useState (
143
+ getWorkspaceListeningPortsProtocol ( workspaceID ) ,
144
+ ) ;
138
145
139
146
const sharedPortsQuery = useQuery ( {
140
147
...workspacePortShares ( workspaceID ) ,
@@ -189,15 +196,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
189
196
( port ) => port . agent_name === agent . name ,
190
197
) ;
191
198
// we don't want to show listening ports if it's a shared port
192
- const filteredListeningPorts = listeningPorts ?. filter ( ( port ) => {
193
- for ( let i = 0 ; i < filteredSharedPorts . length ; i ++ ) {
194
- if ( filteredSharedPorts [ i ] . port === port . port ) {
195
- return false ;
196
- }
197
- }
198
-
199
- return true ;
200
- } ) ;
199
+ const filteredListeningPorts = ( listeningPorts ?? [ ] ) . filter ( ( port ) =>
200
+ filteredSharedPorts . every ( ( sharedPort ) => sharedPort . port !== port . port ) ,
201
+ ) ;
201
202
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
202
203
const canSharePorts =
203
204
portSharingExperimentEnabled &&
@@ -224,95 +225,117 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
224
225
overflowY : "auto" ,
225
226
} }
226
227
>
227
- < header
228
- css = { ( theme ) => ( {
228
+ < Stack
229
+ direction = "column"
230
+ css = { {
229
231
padding : 20 ,
230
- paddingBottom : 10 ,
231
- position : "sticky" ,
232
- top : 0 ,
233
- background : theme . palette . background . paper ,
234
- // For some reason the Share button label has a higher z-index than
235
- // the header. Probably some tricky stuff from MUI.
236
- zIndex : 1 ,
237
- } ) }
232
+ } }
238
233
>
239
234
< Stack
240
235
direction = "row"
241
236
justifyContent = "space-between"
242
237
alignItems = "start"
243
238
>
244
- < HelpTooltipTitle > Listening ports </ HelpTooltipTitle >
239
+ < HelpTooltipTitle > Listening Ports </ HelpTooltipTitle >
245
240
< HelpTooltipLink
246
241
href = { docs ( "/networking/port-forwarding#dashboard" ) }
247
242
>
248
243
Learn more
249
244
</ HelpTooltipLink >
250
245
</ Stack >
251
- < HelpTooltipText css = { { color : theme . palette . text . secondary } } >
252
- { filteredListeningPorts ?. length === 0
253
- ? "No open ports were detected."
254
- : "The listening ports are exclusively accessible to you." }
255
- </ HelpTooltipText >
256
- < form
257
- css = { styles . newPortForm }
258
- onSubmit = { ( e ) => {
259
- e . preventDefault ( ) ;
260
- const formData = new FormData ( e . currentTarget ) ;
261
- const port = Number ( formData . get ( "portNumber" ) ) ;
262
- const url = portForwardURL (
263
- host ,
264
- port ,
265
- agent . name ,
266
- workspaceName ,
267
- username ,
268
- ) ;
269
- window . open ( url , "_blank" ) ;
270
- } }
271
- >
272
- < input
273
- aria-label = "Port number"
274
- name = "portNumber"
275
- type = "number"
276
- placeholder = "Connect to port..."
277
- min = { 9 }
278
- max = { 65535 }
279
- required
280
- css = { styles . newPortInput }
281
- />
282
- < Button
283
- type = "submit"
284
- size = "small"
285
- variant = "text"
246
+ < Stack direction = "column" gap = { 1 } >
247
+ < HelpTooltipText css = { { color : theme . palette . text . secondary } } >
248
+ The listening ports are exclusively accessible to you. Selecting
249
+ HTTP/S will change the protocol for all listening ports.
250
+ </ HelpTooltipText >
251
+ < Stack
252
+ direction = "row"
253
+ gap = { 2 }
286
254
css = { {
287
- paddingLeft : 12 ,
288
- paddingRight : 12 ,
289
- minWidth : 0 ,
255
+ paddingBottom : 8 ,
290
256
} }
291
257
>
292
- < OpenInNewOutlined
293
- css = { {
294
- flexShrink : 0 ,
295
- width : 14 ,
296
- height : 14 ,
297
- color : theme . palette . text . primary ,
258
+ < FormControl size = "small" css = { styles . protocolFormControl } >
259
+ < Select
260
+ css = { styles . listeningPortProtocol }
261
+ value = { listeningPortProtocol }
262
+ onChange = { async ( event ) => {
263
+ const selectedProtocol = event . target . value as
264
+ | "http"
265
+ | "https" ;
266
+ setListeningPortProtocol ( selectedProtocol ) ;
267
+ saveWorkspaceListeningPortsProtocol (
268
+ workspaceID ,
269
+ selectedProtocol ,
270
+ ) ;
271
+ } }
272
+ >
273
+ < MenuItem value = "http" > HTTP</ MenuItem >
274
+ < MenuItem value = "https" > HTTPS</ MenuItem >
275
+ </ Select >
276
+ </ FormControl >
277
+ < form
278
+ css = { styles . newPortForm }
279
+ onSubmit = { ( e ) => {
280
+ e . preventDefault ( ) ;
281
+ const formData = new FormData ( e . currentTarget ) ;
282
+ const port = Number ( formData . get ( "portNumber" ) ) ;
283
+ const url = portForwardURL (
284
+ host ,
285
+ port ,
286
+ agent . name ,
287
+ workspaceName ,
288
+ username ,
289
+ listeningPortProtocol ,
290
+ ) ;
291
+ window . open ( url , "_blank" ) ;
298
292
} }
299
- />
300
- </ Button >
301
- </ form >
302
- </ header >
303
- < div
304
- css = { {
305
- padding : 20 ,
306
- paddingTop : 0 ,
307
- } }
308
- >
309
- { filteredListeningPorts ?. map ( ( port ) => {
293
+ >
294
+ < input
295
+ aria-label = "Port number"
296
+ name = "portNumber"
297
+ type = "number"
298
+ placeholder = "Connect to port..."
299
+ min = { 9 }
300
+ max = { 65535 }
301
+ required
302
+ css = { styles . newPortInput }
303
+ />
304
+ < Button
305
+ type = "submit"
306
+ size = "small"
307
+ variant = "text"
308
+ css = { {
309
+ paddingLeft : 12 ,
310
+ paddingRight : 12 ,
311
+ minWidth : 0 ,
312
+ } }
313
+ >
314
+ < OpenInNewOutlined
315
+ css = { {
316
+ flexShrink : 0 ,
317
+ width : 14 ,
318
+ height : 14 ,
319
+ color : theme . palette . text . primary ,
320
+ } }
321
+ />
322
+ </ Button >
323
+ </ form >
324
+ </ Stack >
325
+ </ Stack >
326
+ { filteredListeningPorts . length === 0 && (
327
+ < HelpTooltipText css = { styles . noPortText } >
328
+ No open ports were detected.
329
+ </ HelpTooltipText >
330
+ ) }
331
+ { filteredListeningPorts . map ( ( port ) => {
310
332
const url = portForwardURL (
311
333
host ,
312
334
port . port ,
313
335
agent . name ,
314
336
workspaceName ,
315
337
username ,
338
+ listeningPortProtocol ,
316
339
) ;
317
340
const label =
318
341
port . process_name !== "" ? port . process_name : port . port ;
@@ -323,31 +346,33 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
323
346
alignItems = "center"
324
347
justifyContent = "space-between"
325
348
>
326
- < Link
327
- underline = "none"
328
- css = { styles . portLink }
329
- href = { url }
330
- target = "_blank"
331
- rel = "noreferrer"
332
- >
333
- < SensorsIcon css = { { width : 14 , height : 14 } } />
334
- { label }
335
- </ Link >
336
- < Stack
337
- direction = "row"
338
- gap = { 2 }
339
- justifyContent = "flex-end"
340
- alignItems = "center"
341
- >
349
+ < Stack direction = "row" gap = { 3 } >
342
350
< Link
343
351
underline = "none"
344
352
css = { styles . portLink }
345
353
href = { url }
346
354
target = "_blank"
347
355
rel = "noreferrer"
348
356
>
349
- < span css = { styles . portNumber } > { port . port } </ span >
357
+ < SensorsIcon css = { { width : 14 , height : 14 } } />
358
+ { port . port }
350
359
</ Link >
360
+ < Link
361
+ underline = "none"
362
+ css = { styles . portLink }
363
+ href = { url }
364
+ target = "_blank"
365
+ rel = "noreferrer"
366
+ >
367
+ { label }
368
+ </ Link >
369
+ </ Stack >
370
+ < Stack
371
+ direction = "row"
372
+ gap = { 2 }
373
+ justifyContent = "flex-end"
374
+ alignItems = "center"
375
+ >
351
376
{ canSharePorts && (
352
377
< Button
353
378
size = "small"
@@ -356,7 +381,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
356
381
await upsertSharedPortMutation . mutateAsync ( {
357
382
agent_name : agent . name ,
358
383
port : port . port ,
359
- protocol : "http" ,
384
+ protocol : listeningPortProtocol ,
360
385
share_level : "authenticated" ,
361
386
} ) ;
362
387
await sharedPortsQuery . refetch ( ) ;
@@ -369,7 +394,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
369
394
</ Stack >
370
395
) ;
371
396
} ) }
372
- </ div >
397
+ </ Stack >
373
398
</ div >
374
399
{ portSharingExperimentEnabled && (
375
400
< div
@@ -393,7 +418,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
393
418
agent . name ,
394
419
workspaceName ,
395
420
username ,
396
- share . protocol === "https" ,
421
+ share . protocol ,
397
422
) ;
398
423
const label = share . port ;
399
424
return (
@@ -619,6 +644,22 @@ const styles = {
619
644
"&:focus-within" : {
620
645
borderColor : theme . palette . primary . main ,
621
646
} ,
647
+ width : "100%" ,
648
+ } ) ,
649
+
650
+ listeningPortProtocol : ( theme ) => ( {
651
+ boxShadow : "none" ,
652
+ ".MuiOutlinedInput-notchedOutline" : { border : 0 } ,
653
+ "&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline" : {
654
+ border : 0 ,
655
+ } ,
656
+ "&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline" : {
657
+ border : 0 ,
658
+ } ,
659
+ border : `1px solid ${ theme . palette . divider } ` ,
660
+ borderRadius : "4px" ,
661
+ marginTop : 8 ,
662
+ minWidth : "100px" ,
622
663
} ) ,
623
664
624
665
newPortInput : ( theme ) => ( {
@@ -633,6 +674,12 @@ const styles = {
633
674
display : "block" ,
634
675
width : "100%" ,
635
676
} ) ,
677
+ noPortText : ( theme ) => ( {
678
+ color : theme . palette . text . secondary ,
679
+ paddingTop : 20 ,
680
+ paddingBottom : 10 ,
681
+ textAlign : "center" ,
682
+ } ) ,
636
683
sharedPortLink : ( ) => ( {
637
684
minWidth : 80 ,
638
685
} ) ,
0 commit comments