Skip to content

Commit c37ecdb

Browse files
feat: Add port forward button (coder#4167)
1 parent 413bfb8 commit c37ecdb

File tree

11 files changed

+248
-6
lines changed

11 files changed

+248
-6
lines changed

docs/admin/configure.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ should not be localhost.
1414

1515
> Access URL should be a external IP address or domain with DNS records pointing to Coder.
1616
17+
## Wildcard access URL
18+
19+
`CODER_WILDCARD_ACCESS_URL` is necessary for [port forwarding](../networking/port-forwarding.md#dashboard)
20+
via the dashboard or running [coder_apps](../templates.md#coder-apps) on an absolute path. Set this to a wildcard
21+
subdomain that resolves to Coder (e.g. `*.coder.example.com`).
22+
23+
> If you are providing TLS certificates directly to the Coder server, you must use a single certificate for the
24+
> root and wildcard domains. Multi-certificate support [is planned](https://github.com/coder/coder/pull/4150).
25+
1726
## PostgreSQL Database
1827

1928
Coder uses a PostgreSQL database to store users, workspace metadata, and other deployment information.
118 KB
Loading

docs/manifest.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
2-
"versions": ["main", "v0.8.1", "v0.7.12"],
2+
"versions": [
3+
"main",
4+
"v0.8.1",
5+
"v0.7.12"
6+
],
37
"routes": [
48
{
59
"title": "About",

docs/networking/port-forwarding.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ Port forwarding lets developers securely access processes on their Coder
44
workspace from a local machine. A common use case is testing web
55
applications in a browser.
66

7-
There are two ways to forward ports in Coder:
7+
There are three ways to forward ports in Coder:
88

99
- The `coder port-forward` command
10+
- Dashboard
1011
- SSH
1112

1213
The `coder port-forward` command is generally more performant.
@@ -21,6 +22,16 @@ coder port-forward myworkspace --tcp 8000:8080
2122

2223
For more examples, see `coder port-forward --help`.
2324

25+
## Dashboard
26+
27+
> To enable port forwarding via the dashboard, Coder must be configured with a
28+
> [wildcard access URL](./admin/configure#wildcard-access-url).
29+
30+
Use the "Port forward" button in the dashboard to access ports
31+
running on your workspace.
32+
33+
![Port forwarding in the UI](../images/port-forward-dashboard.png)
34+
2435
## SSH
2536

2637
First, [configure SSH](../ides.md#ssh-configuration) on your

site/src/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,3 +495,8 @@ export const getTemplateDAUs = async (
495495
const response = await axios.get(`/api/v2/templates/${templateId}/daus`)
496496
return response.data
497497
}
498+
499+
export const getApplicationsHost = async (): Promise<TypesGen.GetAppHostResponse> => {
500+
const response = await axios.get(`/api/v2/applications/host`)
501+
return response.data
502+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import Button from "@material-ui/core/Button"
2+
import Link from "@material-ui/core/Link"
3+
import Popover from "@material-ui/core/Popover"
4+
import { makeStyles } from "@material-ui/core/styles"
5+
import TextField from "@material-ui/core/TextField"
6+
import OpenInNewOutlined from "@material-ui/icons/OpenInNewOutlined"
7+
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
8+
import { Stack } from "components/Stack/Stack"
9+
import { useRef, useState } from "react"
10+
import { colors } from "theme/colors"
11+
import { CodeExample } from "../CodeExample/CodeExample"
12+
import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip"
13+
14+
export interface PortForwardButtonProps {
15+
host: string
16+
username: string
17+
workspaceName: string
18+
agentName: string
19+
}
20+
21+
const EnabledView: React.FC<PortForwardButtonProps> = (props) => {
22+
const { host, workspaceName, agentName, username } = props
23+
const styles = useStyles()
24+
const [port, setPort] = useState("3000")
25+
const { location } = window
26+
const urlExample = `${location.protocol}//${port}--${agentName}--${workspaceName}--${username}.${host}`
27+
28+
return (
29+
<Stack direction="column" spacing={1}>
30+
<HelpTooltipText>
31+
Access ports running on the agent with the <strong>port, agent name, workspace name</strong>{" "}
32+
and <strong>your username</strong> URL schema, as shown below.
33+
</HelpTooltipText>
34+
35+
<CodeExample code={urlExample} />
36+
37+
<HelpTooltipText>Use the form to open applications in a new tab.</HelpTooltipText>
38+
39+
<Stack direction="row" spacing={1} alignItems="center">
40+
<TextField
41+
label="Port"
42+
type="number"
43+
value={port}
44+
className={styles.portField}
45+
onChange={(e) => {
46+
setPort(e.currentTarget.value)
47+
}}
48+
/>
49+
<Link
50+
underline="none"
51+
href={urlExample}
52+
target="_blank"
53+
rel="noreferrer"
54+
className={styles.openUrlButton}
55+
>
56+
<Button>Open URL</Button>
57+
</Link>
58+
</Stack>
59+
60+
<HelpTooltipLinksGroup>
61+
<HelpTooltipLink href="https://coder.com/docs/coder-oss/latest/networking/port-forward#dashboard">
62+
Learn more about port forward
63+
</HelpTooltipLink>
64+
</HelpTooltipLinksGroup>
65+
</Stack>
66+
)
67+
}
68+
69+
const DisabledView: React.FC<PortForwardButtonProps> = () => {
70+
return (
71+
<Stack direction="column" spacing={1}>
72+
<HelpTooltipText>
73+
<strong>Your deployment does not have port forward enabled.</strong> See the docs for more
74+
details.
75+
</HelpTooltipText>
76+
77+
<HelpTooltipLinksGroup>
78+
<HelpTooltipLink href="https://coder.com/docs/coder-oss/latest/networking/port-forwarding#dashboard">
79+
Learn more about port forward
80+
</HelpTooltipLink>
81+
</HelpTooltipLinksGroup>
82+
</Stack>
83+
)
84+
}
85+
86+
export const PortForwardButton: React.FC<PortForwardButtonProps> = (props) => {
87+
const { host } = props
88+
const anchorRef = useRef<HTMLButtonElement>(null)
89+
const [isOpen, setIsOpen] = useState(false)
90+
const id = isOpen ? "schedule-popover" : undefined
91+
const styles = useStyles()
92+
93+
const onClose = () => {
94+
setIsOpen(false)
95+
}
96+
97+
return (
98+
<>
99+
<Button
100+
startIcon={<OpenInNewOutlined />}
101+
size="small"
102+
ref={anchorRef}
103+
onClick={() => {
104+
setIsOpen(true)
105+
}}
106+
>
107+
Port forward
108+
</Button>
109+
<Popover
110+
classes={{ paper: styles.popoverPaper }}
111+
id={id}
112+
open={isOpen}
113+
anchorEl={anchorRef.current}
114+
onClose={onClose}
115+
anchorOrigin={{
116+
vertical: "bottom",
117+
horizontal: "left",
118+
}}
119+
transformOrigin={{
120+
vertical: "top",
121+
horizontal: "left",
122+
}}
123+
>
124+
<ChooseOne>
125+
<Cond condition={host !== ""}>
126+
<EnabledView {...props} />
127+
</Cond>
128+
<Cond condition={host === ""}>
129+
<DisabledView {...props} />
130+
</Cond>
131+
</ChooseOne>
132+
</Popover>
133+
</>
134+
)
135+
}
136+
137+
const useStyles = makeStyles((theme) => ({
138+
popoverPaper: {
139+
padding: `${theme.spacing(2.5)}px ${theme.spacing(3.5)}px ${theme.spacing(3.5)}px`,
140+
width: theme.spacing(46),
141+
color: theme.palette.text.secondary,
142+
marginTop: theme.spacing(0.25),
143+
},
144+
145+
openUrlButton: {
146+
flexShrink: 0,
147+
},
148+
149+
portField: {
150+
// The default border don't contrast well with the popover
151+
"& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
152+
borderColor: colors.gray[10],
153+
},
154+
},
155+
}))

site/src/components/Resources/Resources.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Skeleton } from "@material-ui/lab"
1010
import useTheme from "@material-ui/styles/useTheme"
1111
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
1212
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
13+
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
1314
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
1415
import { FC, useState } from "react"
1516
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
@@ -42,6 +43,7 @@ interface ResourcesProps {
4243
canUpdateWorkspace: boolean
4344
buildInfo?: BuildInfoResponse | undefined
4445
hideSSHButton?: boolean
46+
applicationsHost?: string
4547
}
4648

4749
export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
@@ -51,6 +53,7 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
5153
canUpdateWorkspace,
5254
buildInfo,
5355
hideSSHButton,
56+
applicationsHost,
5457
}) => {
5558
const styles = useStyles()
5659
const theme: Theme = useTheme()
@@ -150,6 +153,14 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
150153
<div className={styles.accessLinks}>
151154
{canUpdateWorkspace && agent.status === "connected" && (
152155
<>
156+
{applicationsHost !== undefined && (
157+
<PortForwardButton
158+
host={applicationsHost}
159+
workspaceName={workspace.name}
160+
agentName={agent.name}
161+
username={workspace.owner_name}
162+
/>
163+
)}
153164
{!hideSSHButton && (
154165
<SSHButton
155166
workspaceName={workspace.name}

site/src/components/Workspace/Workspace.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface WorkspaceProps {
4646
hideSSHButton?: boolean
4747
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
4848
buildInfo?: TypesGen.BuildInfoResponse
49+
applicationsHost?: string
4950
}
5051

5152
/**
@@ -66,6 +67,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
6667
workspaceErrors,
6768
hideSSHButton,
6869
buildInfo,
70+
applicationsHost,
6971
}) => {
7072
const styles = useStyles()
7173
const navigate = useNavigate()
@@ -140,6 +142,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
140142
canUpdateWorkspace={canUpdateWorkspace}
141143
buildInfo={buildInfo}
142144
hideSSHButton={hideSSHButton}
145+
applicationsHost={applicationsHost}
143146
/>
144147
)}
145148

site/src/pages/WorkspacePage/WorkspacePage.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export const WorkspacePage: FC = () => {
2929
const { t } = useTranslation("workspacePage")
3030
const xServices = useContext(XServiceContext)
3131
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)
32-
3332
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
3433
const {
3534
workspace,
@@ -43,13 +42,11 @@ export const WorkspacePage: FC = () => {
4342
checkPermissionsError,
4443
buildError,
4544
cancellationError,
45+
applicationsHost,
4646
} = workspaceState.context
47-
4847
const canUpdateWorkspace = Boolean(permissions?.updateWorkspace)
49-
5048
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
5149
const [buildInfoState] = useActor(xServices.buildInfoXService)
52-
5350
const styles = useStyles()
5451

5552
/**
@@ -133,6 +130,7 @@ export const WorkspacePage: FC = () => {
133130
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
134131
}}
135132
buildInfo={buildInfoState.context.buildInfo}
133+
applicationsHost={applicationsHost}
136134
/>
137135
<DeleteDialog
138136
entity="workspace"

site/src/testHelpers/handlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,9 @@ export const handlers = [
168168
rest.get("/api/v2/audit/count", (req, res, ctx) => {
169169
return res(ctx.status(200), ctx.json({ count: 1000 }))
170170
}),
171+
172+
// Applications host
173+
rest.get("/api/v2/applications/host", (req, res, ctx) => {
174+
return res(ctx.status(200), ctx.json({ host: "dev.coder.com" }))
175+
}),
171176
]

0 commit comments

Comments
 (0)