Skip to content

feat: add port sharing frontend #12119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add query:
  • Loading branch information
f0ssel committed Feb 13, 2024
commit fb23e77eb14f2805b01341b5002c584ea35f09c6
9 changes: 9 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,15 @@ export const getAgentListeningPorts = async (
return response.data;
};

export const getWorkspaceAgentSharedPorts = async (
workspaceID: string,
): Promise<TypesGen.WorkspaceAgentPortShares> => {
const response = await axios.get(
`/api/v2/workspaces/${workspaceID}/shared-ports`,
);
return response.data;
};

// getDeploymentSSHConfig is used by the VSCode-Extension.
export const getDeploymentSSHConfig =
async (): Promise<TypesGen.SSHConfigResponse> => {
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export const PopoverContent: FC<PopoverContentProps> = ({
marginTop: hoverMode ? undefined : 8,
pointerEvents: hoverMode ? "none" : undefined,
"& .MuiPaper-root": {
minWidth: 520,
minWidth: 320,
fontSize: 14,
pointerEvents: hoverMode ? "auto" : undefined,
},
Expand Down
1 change: 1 addition & 0 deletions site/src/modules/resources/AgentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export const AgentRow: FC<AgentRowProps> = ({
workspaceName={workspace.name}
agent={agent}
username={workspace.owner_name}
workspaceID={workspace.id}
/>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/resources/PortForwardButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Story = StoryObj<typeof PortForwardButton>;
export const Example: Story = {
args: {
storybook: {
portsQueryData: MockListeningPortsResponse,
listeningPortsQueryData: MockListeningPortsResponse,
},
},
};
Expand Down
198 changes: 107 additions & 91 deletions site/src/modules/resources/PortForwardButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import type { FC } from "react";
import { useQuery } from "react-query";
import { docs } from "utils/docs";
import { getAgentListeningPorts } from "api/api";
import { getAgentListeningPorts, getWorkspaceAgentSharedPorts } from "api/api";
import type {
WorkspaceAgent,
WorkspaceAgentListeningPort,
WorkspaceAgentListeningPortsResponse,
WorkspaceAgentPortShare,
WorkspaceAgentPortShares,
} from "api/typesGenerated";
import { portForwardURL } from "utils/portForward";
import { type ClassName, useClassName } from "hooks/useClassName";
import {
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipText,
HelpTooltipTitle,
} from "components/HelpTooltip/HelpTooltip";
Expand All @@ -31,29 +32,28 @@ import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import TextField from "@mui/material/TextField";
import SensorsIcon from '@mui/icons-material/Sensors';
import Add from '@mui/icons-material/Add';
import IconButton from "@mui/material/IconButton";
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import DeleteIcon from '@mui/icons-material/Delete';
import SensorsIcon from "@mui/icons-material/Sensors";
import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen";

export interface PortForwardButtonProps {
host: string;
username: string;
workspaceName: string;
workspaceID: string;
agent: WorkspaceAgent;

/**
* Only for use in Storybook
*/
storybook?: {
portsQueryData?: WorkspaceAgentListeningPortsResponse;
listeningPortsQueryData?: WorkspaceAgentListeningPortsResponse;
sharedPortsQueryData?: WorkspaceAgentPortShares;
};
}

export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
const { agent, storybook } = props;
const { agent, workspaceID, storybook } = props;

const paper = useClassName(classNames.paper, []);

Expand All @@ -64,21 +64,34 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
refetchInterval: 5_000,
});

const data = storybook ? storybook.portsQueryData : portsQuery.data;
const sharedPortsQuery = useQuery({
queryKey: ["sharedPorts", agent.id],
queryFn: () => getWorkspaceAgentSharedPorts(workspaceID),
enabled: !storybook && agent.status === "connected",
});

const listeningPorts = storybook
? storybook.listeningPortsQueryData
: portsQuery.data;
const sharedPorts = storybook
? storybook.sharedPortsQueryData
: sharedPortsQuery.data;

return (
<Popover>
<PopoverTrigger>
<Button
disabled={!data}
disabled={!listeningPorts}
size="small"
variant="text"
endIcon={<KeyboardArrowDown />}
css={{ fontSize: 13, padding: "8px 12px" }}
startIcon={
data ? (
listeningPorts ? (
<div>
<span css={styles.portCount}>{data.ports.length}</span>
<span css={styles.portCount}>
{listeningPorts.ports.length}
</span>
</div>
) : (
<CircularProgress size={10} />
Expand All @@ -89,34 +102,30 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
</Button>
</PopoverTrigger>
<PopoverContent horizontal="right" classes={{ paper }}>
<PortForwardPopoverView {...props} ports={data?.ports} />
<PortForwardPopoverView
{...props}
listeningPorts={listeningPorts?.ports}
sharedPorts={sharedPorts?.shares}
/>
</PopoverContent>
</Popover>
);
};

interface PortForwardPopoverViewProps extends PortForwardButtonProps {
ports?: WorkspaceAgentListeningPort[];
listeningPorts?: WorkspaceAgentListeningPort[];
sharedPorts?: WorkspaceAgentPortShare[];
}

export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
host,
workspaceName,
agent,
username,
ports,
listeningPorts,
sharedPorts,
}) => {
const theme = useTheme();
const sharedPorts = [
{
port: 8090,
share_level: "Authenticated",
},
{
port: 8091,
share_level: "Public",
}
];

return (
<>
Expand All @@ -126,18 +135,20 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="start">
<HelpTooltipTitle>Listening ports</HelpTooltipTitle>
<Stack
direction="row"
justifyContent="space-between"
alignItems="start"
>
<HelpTooltipTitle>Listening ports</HelpTooltipTitle>
<HelpTooltipLink href={docs("/networking/port-forwarding#dashboard")}>
Learn more
</HelpTooltipLink>
</Stack>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
{ports?.length === 0
{listeningPorts?.length === 0
? "No open ports were detected."
: "The listening ports are exclusively accessible to you."
}

: "The listening ports are exclusively accessible to you."}
</HelpTooltipText>
<form
css={styles.newPortForm}
Expand Down Expand Up @@ -186,10 +197,11 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
</Button>
</form>
<div
css={{
paddingTop: 10,
}}>
{ports?.map((port) => {
css={{
paddingTop: 10,
}}
>
{listeningPorts?.map((port) => {
const url = portForwardURL(
host,
port.port,
Expand All @@ -200,7 +212,12 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
const label =
port.process_name !== "" ? port.process_name : port.port;
return (
<Stack key={port.port} direction="row" justifyContent="space-between" alignItems="center">
<Stack
key={port.port}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Link
underline="none"
css={styles.portLink}
Expand All @@ -227,95 +244,94 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
);
})}
</div>
</div>
<div css={{
</div>
<div
css={{
padding: 20,
}}>
}}
>
<HelpTooltipTitle>Shared Ports</HelpTooltipTitle>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
{ports?.length === 0
{listeningPorts?.length === 0
? "No ports are shared."
: "Ports can be shared with other Coder users or with the public."}
</HelpTooltipText>
<div>
{sharedPorts?.map((port) => {
{sharedPorts?.map((share) => {
const url = portForwardURL(
host,
port.port,
share.port,
agent.name,
workspaceName,
username,
);
const label = port.port;
const label = share.port;
return (
<Stack key={port.port} direction="row" justifyContent="space-between" alignItems="center">
<Stack
key={share.port}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Link
underline="none"
css={styles.portLink}
href={url}
target="_blank"
rel="noreferrer"
>
{port.share_level === "Public" ?
(
{share.share_level === "public" ? (
<LockOpenIcon css={{ width: 14, height: 14 }} />
)
: (
) : (
<LockIcon css={{ width: 14, height: 14 }} />
)}
{label}
</Link>
<Stack direction="row" gap={1}>
<FormControl size="small">
<Select
sx={{
boxShadow: "none",
".MuiOutlinedInput-notchedOutline": { border: 0 },
"&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline":
{
border: 0,
},
"&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: 0,
},
}}
value={port.share_level}
>
<MenuItem value="Owner">Owner</MenuItem>
<MenuItem value="Authenticated">Authenticated</MenuItem>
<MenuItem value="Public">Public</MenuItem>
</Select>
</FormControl>
<FormControl size="small">
<Select
sx={{
boxShadow: "none",
".MuiOutlinedInput-notchedOutline": { border: 0 },
"&.MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline":
{
border: 0,
},
"&.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline":
{
border: 0,
},
}}
value={share.share_level}
>
<MenuItem value="Authenticated">Authenticated</MenuItem>
<MenuItem value="Public">Public</MenuItem>
</Select>
</FormControl>
</Stack>

</Stack>
);
})}
</div>
<Stack direction="column" gap={1} justifyContent="flex-end" sx={{
marginTop: 2,
}}>
<TextField
label="Port"
variant="outlined"
size="small"
/>
</div>
<Stack
direction="column"
gap={1}
justifyContent="flex-end"
sx={{
marginTop: 2,
}}
>
<TextField label="Port" variant="outlined" size="small" />
<FormControl size="small">
<Select
value="Authenticated"
>
<MenuItem value="Authenticated">Authenticated</MenuItem>
<MenuItem value="Public">Public</MenuItem>
</Select>
<Select value="Authenticated">
<MenuItem value="Authenticated">Authenticated</MenuItem>
<MenuItem value="Public">Public</MenuItem>
</Select>
</FormControl>
<Button variant="contained">
Add Shared Port
</Button>
<Button variant="contained">Add Shared Port</Button>
</Stack>
</div>


{/* <div css={{ padding: 20 }}>
<HelpTooltipTitle>Forward port</HelpTooltipTitle>
<HelpTooltipText css={{ color: theme.palette.text.secondary }}>
Expand Down
Loading