Skip to content

Commit 3ab5a51

Browse files
authored
feat: add listening ports protocol selector (#12915)
1 parent 4968916 commit 3ab5a51

File tree

3 files changed

+168
-100
lines changed

3 files changed

+168
-100
lines changed

site/src/modules/resources/PortForwardButton.tsx

+144-97
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Stack from "@mui/material/Stack";
1616
import TextField from "@mui/material/TextField";
1717
import Tooltip from "@mui/material/Tooltip";
1818
import { type FormikContextType, useFormik } from "formik";
19-
import type { FC } from "react";
19+
import { useState, type FC } from "react";
2020
import { useQuery, useMutation } from "react-query";
2121
import * as Yup from "yup";
2222
import { getAgentListeningPorts } from "api/api";
@@ -48,7 +48,11 @@ import { type ClassName, useClassName } from "hooks/useClassName";
4848
import { useDashboard } from "modules/dashboard/useDashboard";
4949
import { docs } from "utils/docs";
5050
import { getFormHelpers } from "utils/formUtils";
51-
import { portForwardURL } from "utils/portForward";
51+
import {
52+
getWorkspaceListeningPortsProtocol,
53+
portForwardURL,
54+
saveWorkspaceListeningPortsProtocol,
55+
} from "utils/portForward";
5256

5357
export interface PortForwardButtonProps {
5458
host: string;
@@ -135,6 +139,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
135139
portSharingControlsEnabled,
136140
}) => {
137141
const theme = useTheme();
142+
const [listeningPortProtocol, setListeningPortProtocol] = useState(
143+
getWorkspaceListeningPortsProtocol(workspaceID),
144+
);
138145

139146
const sharedPortsQuery = useQuery({
140147
...workspacePortShares(workspaceID),
@@ -189,15 +196,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
189196
(port) => port.agent_name === agent.name,
190197
);
191198
// 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+
);
201202
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
202203
const canSharePorts =
203204
portSharingExperimentEnabled &&
@@ -224,95 +225,117 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
224225
overflowY: "auto",
225226
}}
226227
>
227-
<header
228-
css={(theme) => ({
228+
<Stack
229+
direction="column"
230+
css={{
229231
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+
}}
238233
>
239234
<Stack
240235
direction="row"
241236
justifyContent="space-between"
242237
alignItems="start"
243238
>
244-
<HelpTooltipTitle>Listening ports</HelpTooltipTitle>
239+
<HelpTooltipTitle>Listening Ports</HelpTooltipTitle>
245240
<HelpTooltipLink
246241
href={docs("/networking/port-forwarding#dashboard")}
247242
>
248243
Learn more
249244
</HelpTooltipLink>
250245
</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}
286254
css={{
287-
paddingLeft: 12,
288-
paddingRight: 12,
289-
minWidth: 0,
255+
paddingBottom: 8,
290256
}}
291257
>
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");
298292
}}
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) => {
310332
const url = portForwardURL(
311333
host,
312334
port.port,
313335
agent.name,
314336
workspaceName,
315337
username,
338+
listeningPortProtocol,
316339
);
317340
const label =
318341
port.process_name !== "" ? port.process_name : port.port;
@@ -323,31 +346,33 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
323346
alignItems="center"
324347
justifyContent="space-between"
325348
>
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}>
342350
<Link
343351
underline="none"
344352
css={styles.portLink}
345353
href={url}
346354
target="_blank"
347355
rel="noreferrer"
348356
>
349-
<span css={styles.portNumber}>{port.port}</span>
357+
<SensorsIcon css={{ width: 14, height: 14 }} />
358+
{port.port}
350359
</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+
>
351376
{canSharePorts && (
352377
<Button
353378
size="small"
@@ -356,7 +381,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
356381
await upsertSharedPortMutation.mutateAsync({
357382
agent_name: agent.name,
358383
port: port.port,
359-
protocol: "http",
384+
protocol: listeningPortProtocol,
360385
share_level: "authenticated",
361386
});
362387
await sharedPortsQuery.refetch();
@@ -369,7 +394,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
369394
</Stack>
370395
);
371396
})}
372-
</div>
397+
</Stack>
373398
</div>
374399
{portSharingExperimentEnabled && (
375400
<div
@@ -393,7 +418,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
393418
agent.name,
394419
workspaceName,
395420
username,
396-
share.protocol === "https",
421+
share.protocol,
397422
);
398423
const label = share.port;
399424
return (
@@ -619,6 +644,22 @@ const styles = {
619644
"&:focus-within": {
620645
borderColor: theme.palette.primary.main,
621646
},
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",
622663
}),
623664

624665
newPortInput: (theme) => ({
@@ -633,6 +674,12 @@ const styles = {
633674
display: "block",
634675
width: "100%",
635676
}),
677+
noPortText: (theme) => ({
678+
color: theme.palette.text.secondary,
679+
paddingTop: 20,
680+
paddingBottom: 10,
681+
textAlign: "center",
682+
}),
636683
sharedPortLink: () => ({
637684
minWidth: 80,
638685
}),

site/src/testHelpers/entities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3261,7 +3261,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
32613261
export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse =
32623262
{
32633263
ports: [
3264-
{ process_name: "webb", network: "", port: 3000 },
3264+
{ process_name: "webb", network: "", port: 30000 },
32653265
{ process_name: "gogo", network: "", port: 8080 },
32663266
{ process_name: "", network: "", port: 8081 },
32673267
],

0 commit comments

Comments
 (0)