Skip to content

feat: Add port forward button #4167

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 9 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions docs/admin/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ should not be localhost.

> Access URL should be a external IP address or domain with DNS records pointing to Coder.

## Wildcard access URL

`CODER_WILDCARD_ACCESS_URL` is necessary for [port forwarding](../networking/port-forwarding.md#dashboard)
via the dashboard or running [coder_apps](../templates.md#coder-apps) on an absolute path. Set this to a wildcard
subdomain that resolves to Coder (e.g. `*.coder.example.com`).

> If you are providing TLS certificates directly to the Coder server, you must use a single certificate for the
> root and wildcard domains. Multi-certificate support [is planned](https://github.com/coder/coder/pull/4150).

## PostgreSQL Database

Coder uses a PostgreSQL database to store users, workspace metadata, and other deployment information.
Expand Down
Binary file added docs/images/port-forward-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion docs/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"versions": ["main", "v0.8.1", "v0.7.12"],
"versions": [
"main",
"v0.8.1",
"v0.7.12"
],
"routes": [
{
"title": "About",
Expand Down
13 changes: 12 additions & 1 deletion docs/networking/port-forwarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ Port forwarding lets developers securely access processes on their Coder
workspace from a local machine. A common use case is testing web
applications in a browser.

There are two ways to forward ports in Coder:
There are three ways to forward ports in Coder:

- The `coder port-forward` command
- Dashboard
- SSH

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

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

## Dashboard

> To enable port forwarding via the dashboard, Coder must be configured with a
> [wildcard access URL](./admin/configure#wildcard-access-url).

Use the "Port forward" button in the dashboard to access ports
running on your workspace.

![Port forwarding in the UI](../images/port-forward-dashboard.png)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot should read port, agent name, workspace name and your username (agent name before workspace name)


## SSH

First, [configure SSH](../ides.md#ssh-configuration) on your
Expand Down
5 changes: 5 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,8 @@ export const getTemplateDAUs = async (
const response = await axios.get(`/api/v2/templates/${templateId}/daus`)
return response.data
}

export const getApplicationsHost = async (): Promise<TypesGen.GetAppHostResponse> => {
const response = await axios.get(`/api/v2/applications/host`)
return response.data
}
155 changes: 155 additions & 0 deletions site/src/components/PortForwardButton/PortForwardButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import Button from "@material-ui/core/Button"
import Link from "@material-ui/core/Link"
import Popover from "@material-ui/core/Popover"
import { makeStyles } from "@material-ui/core/styles"
import TextField from "@material-ui/core/TextField"
import OpenInNewOutlined from "@material-ui/icons/OpenInNewOutlined"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { Stack } from "components/Stack/Stack"
import { useRef, useState } from "react"
import { colors } from "theme/colors"
import { CodeExample } from "../CodeExample/CodeExample"
import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip"

export interface PortForwardButtonProps {
host: string
username: string
workspaceName: string
agentName: string
}

const EnabledView: React.FC<PortForwardButtonProps> = (props) => {
const { host, workspaceName, agentName, username } = props
const styles = useStyles()
const [port, setPort] = useState("3000")
const { location } = window
const urlExample = `${location.protocol}//${port}--${agentName}--${workspaceName}--${username}.${host}`

return (
<Stack direction="column" spacing={1}>
<HelpTooltipText>
Access ports running on the agent with the <strong>port, agent name, workspace name</strong>{" "}
and <strong>your username</strong> URL schema, as shown below.
</HelpTooltipText>

<CodeExample code={urlExample} />

<HelpTooltipText>Use the form to open applications in a new tab.</HelpTooltipText>

<Stack direction="row" spacing={1} alignItems="center">
<TextField
label="Port"
type="number"
value={port}
className={styles.portField}
onChange={(e) => {
setPort(e.currentTarget.value)
}}
/>
<Link
underline="none"
href={urlExample}
target="_blank"
rel="noreferrer"
className={styles.openUrlButton}
>
<Button>Open URL</Button>
</Link>
</Stack>

<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://coder.com/docs/coder-oss/latest/networking/port-forward#dashboard">
Learn more about port forward
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</Stack>
)
}

const DisabledView: React.FC<PortForwardButtonProps> = () => {
return (
<Stack direction="column" spacing={1}>
<HelpTooltipText>
<strong>Your deployment does not have port forward enabled.</strong> See the docs for more
details.
</HelpTooltipText>

<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://coder.com/docs/coder-oss/latest/networking/port-forwarding#dashboard">
Learn more about port forward
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</Stack>
)
}

export const PortForwardButton: React.FC<PortForwardButtonProps> = (props) => {
const { host } = props
const anchorRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const id = isOpen ? "schedule-popover" : undefined
const styles = useStyles()

const onClose = () => {
setIsOpen(false)
}

return (
<>
<Button
startIcon={<OpenInNewOutlined />}
size="small"
ref={anchorRef}
onClick={() => {
setIsOpen(true)
}}
>
Port forward
</Button>
<Popover
classes={{ paper: styles.popoverPaper }}
id={id}
open={isOpen}
anchorEl={anchorRef.current}
onClose={onClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
<ChooseOne>
<Cond condition={host !== ""}>
<EnabledView {...props} />
</Cond>
<Cond condition={host === ""}>
<DisabledView {...props} />
</Cond>
</ChooseOne>
</Popover>
</>
)
}

const useStyles = makeStyles((theme) => ({
popoverPaper: {
padding: `${theme.spacing(2.5)}px ${theme.spacing(3.5)}px ${theme.spacing(3.5)}px`,
width: theme.spacing(46),
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.25),
},

openUrlButton: {
flexShrink: 0,
},

portField: {
// The default border don't contrast well with the popover
"& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
borderColor: colors.gray[10],
},
},
}))
11 changes: 11 additions & 0 deletions site/src/components/Resources/Resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Skeleton } from "@material-ui/lab"
import useTheme from "@material-ui/styles/useTheme"
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
import { FC, useState } from "react"
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
Expand Down Expand Up @@ -42,6 +43,7 @@ interface ResourcesProps {
canUpdateWorkspace: boolean
buildInfo?: BuildInfoResponse | undefined
hideSSHButton?: boolean
applicationsHost?: string
}

export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
Expand All @@ -51,6 +53,7 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
canUpdateWorkspace,
buildInfo,
hideSSHButton,
applicationsHost,
}) => {
const styles = useStyles()
const theme: Theme = useTheme()
Expand Down Expand Up @@ -151,6 +154,14 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
<div className={styles.accessLinks}>
{canUpdateWorkspace && agent.status === "connected" && (
<>
{applicationsHost !== undefined && (
<PortForwardButton
host={applicationsHost}
workspaceName={workspace.name}
agentName={agent.name}
username={workspace.owner_name}
/>
)}
{!hideSSHButton && (
<SSHButton
workspaceName={workspace.name}
Expand Down
3 changes: 3 additions & 0 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface WorkspaceProps {
hideSSHButton?: boolean
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
buildInfo?: TypesGen.BuildInfoResponse
applicationsHost?: string
}

/**
Expand All @@ -66,6 +67,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
workspaceErrors,
hideSSHButton,
buildInfo,
applicationsHost,
}) => {
const styles = useStyles()
const navigate = useNavigate()
Expand Down Expand Up @@ -140,6 +142,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
canUpdateWorkspace={canUpdateWorkspace}
buildInfo={buildInfo}
hideSSHButton={hideSSHButton}
applicationsHost={applicationsHost}
/>
)}

Expand Down
6 changes: 2 additions & 4 deletions site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const WorkspacePage: FC = () => {
const { t } = useTranslation("workspacePage")
const xServices = useContext(XServiceContext)
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)

const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
const {
workspace,
Expand All @@ -43,13 +42,11 @@ export const WorkspacePage: FC = () => {
checkPermissionsError,
buildError,
cancellationError,
applicationsHost,
} = workspaceState.context

const canUpdateWorkspace = Boolean(permissions?.updateWorkspace)

const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
const [buildInfoState] = useActor(xServices.buildInfoXService)

const styles = useStyles()

/**
Expand Down Expand Up @@ -133,6 +130,7 @@ export const WorkspacePage: FC = () => {
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
}}
buildInfo={buildInfoState.context.buildInfo}
applicationsHost={applicationsHost}
/>
<DeleteDialog
entity="workspace"
Expand Down
5 changes: 5 additions & 0 deletions site/src/testHelpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,9 @@ export const handlers = [
rest.get("/api/v2/audit/count", (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ count: 1000 }))
}),

// Applications host
rest.get("/api/v2/applications/host", (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ host: "dev.coder.com" }))
}),
]
Loading