diff --git a/site/src/modules/resources/PortForwardButton.tsx b/site/src/modules/resources/PortForwardButton.tsx index 7c0def4262..1e4e2c2f59 100644 --- a/site/src/modules/resources/PortForwardButton.tsx +++ b/site/src/modules/resources/PortForwardButton.tsx @@ -16,7 +16,7 @@ import Stack from "@mui/material/Stack"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import { type FormikContextType, useFormik } from "formik"; -import type { FC } from "react"; +import { useState, type FC } from "react"; import { useQuery, useMutation } from "react-query"; import * as Yup from "yup"; import { getAgentListeningPorts } from "api/api"; @@ -48,7 +48,11 @@ import { type ClassName, useClassName } from "hooks/useClassName"; import { useDashboard } from "modules/dashboard/useDashboard"; import { docs } from "utils/docs"; import { getFormHelpers } from "utils/formUtils"; -import { portForwardURL } from "utils/portForward"; +import { + getWorkspaceListeningPortsProtocol, + portForwardURL, + saveWorkspaceListeningPortsProtocol, +} from "utils/portForward"; export interface PortForwardButtonProps { host: string; @@ -135,6 +139,9 @@ export const PortForwardPopoverView: FC = ({ portSharingControlsEnabled, }) => { const theme = useTheme(); + const [listeningPortProtocol, setListeningPortProtocol] = useState( + getWorkspaceListeningPortsProtocol(workspaceID), + ); const sharedPortsQuery = useQuery({ ...workspacePortShares(workspaceID), @@ -189,15 +196,9 @@ export const PortForwardPopoverView: FC = ({ (port) => port.agent_name === agent.name, ); // we don't want to show listening ports if it's a shared port - const filteredListeningPorts = listeningPorts?.filter((port) => { - for (let i = 0; i < filteredSharedPorts.length; i++) { - if (filteredSharedPorts[i].port === port.port) { - return false; - } - } - - return true; - }); + const filteredListeningPorts = (listeningPorts ?? []).filter((port) => + filteredSharedPorts.every((sharedPort) => sharedPort.port !== port.port), + ); // only disable the form if shared port controls are entitled and the template doesn't allow sharing ports const canSharePorts = portSharingExperimentEnabled && @@ -224,95 +225,117 @@ export const PortForwardPopoverView: FC = ({ overflowY: "auto", }} > -
({ + - Listening ports + Listening Ports Learn more - - {filteredListeningPorts?.length === 0 - ? "No open ports were detected." - : "The listening ports are exclusively accessible to you."} - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const port = Number(formData.get("portNumber")); - const url = portForwardURL( - host, - port, - agent.name, - workspaceName, - username, - ); - window.open(url, "_blank"); - }} - > - - -
-
-
- {filteredListeningPorts?.map((port) => { + > + + + + + + {filteredListeningPorts.length === 0 && ( + + No open ports were detected. + + )} + {filteredListeningPorts.map((port) => { const url = portForwardURL( host, port.port, agent.name, workspaceName, username, + listeningPortProtocol, ); const label = port.process_name !== "" ? port.process_name : port.port; @@ -323,22 +346,7 @@ export const PortForwardPopoverView: FC = ({ alignItems="center" justifyContent="space-between" > - - - {label} - - + = ({ target="_blank" rel="noreferrer" > - {port.port} + + {port.port} + + {label} + + + {canSharePorts && (
+ {portSharingExperimentEnabled && (
= ({ agent.name, workspaceName, username, - share.protocol === "https", + share.protocol, ); const label = share.port; return ( @@ -619,6 +644,22 @@ const styles = { "&:focus-within": { borderColor: theme.palette.primary.main, }, + width: "100%", + }), + + listeningPortProtocol: (theme) => ({ + boxShadow: "none", + ".MuiOutlinedInput-notchedOutline": { border: 0 }, + "&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline": { + border: 0, + }, + "&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline": { + border: 0, + }, + border: `1px solid ${theme.palette.divider}`, + borderRadius: "4px", + marginTop: 8, + minWidth: "100px", }), newPortInput: (theme) => ({ @@ -633,6 +674,12 @@ const styles = { display: "block", width: "100%", }), + noPortText: (theme) => ({ + color: theme.palette.text.secondary, + paddingTop: 20, + paddingBottom: 10, + textAlign: "center", + }), sharedPortLink: () => ({ minWidth: 80, }), diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index d48cb2aeb3..3d9f837ab3 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3261,7 +3261,7 @@ export const MockHealth: TypesGen.HealthcheckReport = { export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = { ports: [ - { process_name: "webb", network: "", port: 3000 }, + { process_name: "webb", network: "", port: 30000 }, { process_name: "gogo", network: "", port: 8080 }, { process_name: "", network: "", port: 8081 }, ], diff --git a/site/src/utils/portForward.ts b/site/src/utils/portForward.ts index bd666823b2..48ef5f39de 100644 --- a/site/src/utils/portForward.ts +++ b/site/src/utils/portForward.ts @@ -1,13 +1,15 @@ +import type { WorkspaceAgentPortShareProtocol } from "api/typesGenerated"; + export const portForwardURL = ( host: string, port: number, agentName: string, workspaceName: string, username: string, - https = false, + protocol: WorkspaceAgentPortShareProtocol, ): string => { const { location } = window; - const suffix = https ? "s" : ""; + const suffix = protocol === "https" ? "s" : ""; const subdomain = `${port}${suffix}--${agentName}--${workspaceName}--${username}`; return `${location.protocol}//${host}`.replace("*", subdomain); @@ -56,9 +58,28 @@ export const openMaybePortForwardedURL = ( agentName, workspaceName, username, + url.protocol.replace(":", "") as WorkspaceAgentPortShareProtocol, ) + url.pathname, ); } catch (ex) { open(uri); } }; + +export const saveWorkspaceListeningPortsProtocol = ( + workspaceID: string, + protocol: WorkspaceAgentPortShareProtocol, +) => { + localStorage.setItem( + `listening-ports-protocol-workspace-${workspaceID}`, + protocol, + ); +}; + +export const getWorkspaceListeningPortsProtocol = ( + workspaceID: string, +): WorkspaceAgentPortShareProtocol => { + return (localStorage.getItem( + `listening-ports-protocol-workspace-${workspaceID}`, + ) || "http") as WorkspaceAgentPortShareProtocol; +};