feat: add listening ports protocol selector (#12915)

This commit is contained in:
Garrett Delfosse 2024-04-15 15:00:24 -04:00 committed by GitHub
parent 49689162bb
commit 3ab5a51ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 168 additions and 100 deletions

View File

@ -16,7 +16,7 @@ import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import { type FormikContextType, useFormik } from "formik"; import { type FormikContextType, useFormik } from "formik";
import type { FC } from "react"; import { useState, type FC } from "react";
import { useQuery, useMutation } from "react-query"; import { useQuery, useMutation } from "react-query";
import * as Yup from "yup"; import * as Yup from "yup";
import { getAgentListeningPorts } from "api/api"; import { getAgentListeningPorts } from "api/api";
@ -48,7 +48,11 @@ import { type ClassName, useClassName } from "hooks/useClassName";
import { useDashboard } from "modules/dashboard/useDashboard"; import { useDashboard } from "modules/dashboard/useDashboard";
import { docs } from "utils/docs"; import { docs } from "utils/docs";
import { getFormHelpers } from "utils/formUtils"; import { getFormHelpers } from "utils/formUtils";
import { portForwardURL } from "utils/portForward"; import {
getWorkspaceListeningPortsProtocol,
portForwardURL,
saveWorkspaceListeningPortsProtocol,
} from "utils/portForward";
export interface PortForwardButtonProps { export interface PortForwardButtonProps {
host: string; host: string;
@ -135,6 +139,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
portSharingControlsEnabled, portSharingControlsEnabled,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const [listeningPortProtocol, setListeningPortProtocol] = useState(
getWorkspaceListeningPortsProtocol(workspaceID),
);
const sharedPortsQuery = useQuery({ const sharedPortsQuery = useQuery({
...workspacePortShares(workspaceID), ...workspacePortShares(workspaceID),
@ -189,15 +196,9 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
(port) => port.agent_name === agent.name, (port) => port.agent_name === agent.name,
); );
// we don't want to show listening ports if it's a shared port // we don't want to show listening ports if it's a shared port
const filteredListeningPorts = listeningPorts?.filter((port) => { const filteredListeningPorts = (listeningPorts ?? []).filter((port) =>
for (let i = 0; i < filteredSharedPorts.length; i++) { filteredSharedPorts.every((sharedPort) => sharedPort.port !== port.port),
if (filteredSharedPorts[i].port === port.port) { );
return false;
}
}
return true;
});
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports // only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
const canSharePorts = const canSharePorts =
portSharingExperimentEnabled && portSharingExperimentEnabled &&
@ -224,95 +225,117 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
overflowY: "auto", overflowY: "auto",
}} }}
> >
<header <Stack
css={(theme) => ({ direction="column"
css={{
padding: 20, padding: 20,
paddingBottom: 10, }}
position: "sticky",
top: 0,
background: theme.palette.background.paper,
// For some reason the Share button label has a higher z-index than
// the header. Probably some tricky stuff from MUI.
zIndex: 1,
})}
> >
<Stack <Stack
direction="row" direction="row"
justifyContent="space-between" justifyContent="space-between"
alignItems="start" alignItems="start"
> >
<HelpTooltipTitle>Listening ports</HelpTooltipTitle> <HelpTooltipTitle>Listening Ports</HelpTooltipTitle>
<HelpTooltipLink <HelpTooltipLink
href={docs("/networking/port-forwarding#dashboard")} href={docs("/networking/port-forwarding#dashboard")}
> >
Learn more Learn more
</HelpTooltipLink> </HelpTooltipLink>
</Stack> </Stack>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}> <Stack direction="column" gap={1}>
{filteredListeningPorts?.length === 0 <HelpTooltipText css={{ color: theme.palette.text.secondary }}>
? "No open ports were detected." The listening ports are exclusively accessible to you. Selecting
: "The listening ports are exclusively accessible to you."} HTTP/S will change the protocol for all listening ports.
</HelpTooltipText> </HelpTooltipText>
<form <Stack
css={styles.newPortForm} direction="row"
onSubmit={(e) => { gap={2}
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");
}}
>
<input
aria-label="Port number"
name="portNumber"
type="number"
placeholder="Connect to port..."
min={9}
max={65535}
required
css={styles.newPortInput}
/>
<Button
type="submit"
size="small"
variant="text"
css={{ css={{
paddingLeft: 12, paddingBottom: 8,
paddingRight: 12,
minWidth: 0,
}} }}
> >
<OpenInNewOutlined <FormControl size="small" css={styles.protocolFormControl}>
css={{ <Select
flexShrink: 0, css={styles.listeningPortProtocol}
width: 14, value={listeningPortProtocol}
height: 14, onChange={async (event) => {
color: theme.palette.text.primary, const selectedProtocol = event.target.value as
| "http"
| "https";
setListeningPortProtocol(selectedProtocol);
saveWorkspaceListeningPortsProtocol(
workspaceID,
selectedProtocol,
);
}}
>
<MenuItem value="http">HTTP</MenuItem>
<MenuItem value="https">HTTPS</MenuItem>
</Select>
</FormControl>
<form
css={styles.newPortForm}
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const port = Number(formData.get("portNumber"));
const url = portForwardURL(
host,
port,
agent.name,
workspaceName,
username,
listeningPortProtocol,
);
window.open(url, "_blank");
}} }}
/> >
</Button> <input
</form> aria-label="Port number"
</header> name="portNumber"
<div type="number"
css={{ placeholder="Connect to port..."
padding: 20, min={9}
paddingTop: 0, max={65535}
}} required
> css={styles.newPortInput}
{filteredListeningPorts?.map((port) => { />
<Button
type="submit"
size="small"
variant="text"
css={{
paddingLeft: 12,
paddingRight: 12,
minWidth: 0,
}}
>
<OpenInNewOutlined
css={{
flexShrink: 0,
width: 14,
height: 14,
color: theme.palette.text.primary,
}}
/>
</Button>
</form>
</Stack>
</Stack>
{filteredListeningPorts.length === 0 && (
<HelpTooltipText css={styles.noPortText}>
No open ports were detected.
</HelpTooltipText>
)}
{filteredListeningPorts.map((port) => {
const url = portForwardURL( const url = portForwardURL(
host, host,
port.port, port.port,
agent.name, agent.name,
workspaceName, workspaceName,
username, username,
listeningPortProtocol,
); );
const label = const label =
port.process_name !== "" ? port.process_name : port.port; port.process_name !== "" ? port.process_name : port.port;
@ -323,22 +346,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
> >
<Link <Stack direction="row" gap={3}>
underline="none"
css={styles.portLink}
href={url}
target="_blank"
rel="noreferrer"
>
<SensorsIcon css={{ width: 14, height: 14 }} />
{label}
</Link>
<Stack
direction="row"
gap={2}
justifyContent="flex-end"
alignItems="center"
>
<Link <Link
underline="none" underline="none"
css={styles.portLink} css={styles.portLink}
@ -346,8 +354,25 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<span css={styles.portNumber}>{port.port}</span> <SensorsIcon css={{ width: 14, height: 14 }} />
{port.port}
</Link> </Link>
<Link
underline="none"
css={styles.portLink}
href={url}
target="_blank"
rel="noreferrer"
>
{label}
</Link>
</Stack>
<Stack
direction="row"
gap={2}
justifyContent="flex-end"
alignItems="center"
>
{canSharePorts && ( {canSharePorts && (
<Button <Button
size="small" size="small"
@ -356,7 +381,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
await upsertSharedPortMutation.mutateAsync({ await upsertSharedPortMutation.mutateAsync({
agent_name: agent.name, agent_name: agent.name,
port: port.port, port: port.port,
protocol: "http", protocol: listeningPortProtocol,
share_level: "authenticated", share_level: "authenticated",
}); });
await sharedPortsQuery.refetch(); await sharedPortsQuery.refetch();
@ -369,7 +394,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
</Stack> </Stack>
); );
})} })}
</div> </Stack>
</div> </div>
{portSharingExperimentEnabled && ( {portSharingExperimentEnabled && (
<div <div
@ -393,7 +418,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
agent.name, agent.name,
workspaceName, workspaceName,
username, username,
share.protocol === "https", share.protocol,
); );
const label = share.port; const label = share.port;
return ( return (
@ -619,6 +644,22 @@ const styles = {
"&:focus-within": { "&:focus-within": {
borderColor: theme.palette.primary.main, 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) => ({ newPortInput: (theme) => ({
@ -633,6 +674,12 @@ const styles = {
display: "block", display: "block",
width: "100%", width: "100%",
}), }),
noPortText: (theme) => ({
color: theme.palette.text.secondary,
paddingTop: 20,
paddingBottom: 10,
textAlign: "center",
}),
sharedPortLink: () => ({ sharedPortLink: () => ({
minWidth: 80, minWidth: 80,
}), }),

View File

@ -3261,7 +3261,7 @@ export const MockHealth: TypesGen.HealthcheckReport = {
export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse = export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsResponse =
{ {
ports: [ ports: [
{ process_name: "webb", network: "", port: 3000 }, { process_name: "webb", network: "", port: 30000 },
{ process_name: "gogo", network: "", port: 8080 }, { process_name: "gogo", network: "", port: 8080 },
{ process_name: "", network: "", port: 8081 }, { process_name: "", network: "", port: 8081 },
], ],

View File

@ -1,13 +1,15 @@
import type { WorkspaceAgentPortShareProtocol } from "api/typesGenerated";
export const portForwardURL = ( export const portForwardURL = (
host: string, host: string,
port: number, port: number,
agentName: string, agentName: string,
workspaceName: string, workspaceName: string,
username: string, username: string,
https = false, protocol: WorkspaceAgentPortShareProtocol,
): string => { ): string => {
const { location } = window; const { location } = window;
const suffix = https ? "s" : ""; const suffix = protocol === "https" ? "s" : "";
const subdomain = `${port}${suffix}--${agentName}--${workspaceName}--${username}`; const subdomain = `${port}${suffix}--${agentName}--${workspaceName}--${username}`;
return `${location.protocol}//${host}`.replace("*", subdomain); return `${location.protocol}//${host}`.replace("*", subdomain);
@ -56,9 +58,28 @@ export const openMaybePortForwardedURL = (
agentName, agentName,
workspaceName, workspaceName,
username, username,
url.protocol.replace(":", "") as WorkspaceAgentPortShareProtocol,
) + url.pathname, ) + url.pathname,
); );
} catch (ex) { } catch (ex) {
open(uri); 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;
};