diff --git a/docs/admin/configure.md b/docs/admin/configure.md index 68e2273407224..63c6a44930d59 100644 --- a/docs/admin/configure.md +++ b/docs/admin/configure.md @@ -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. diff --git a/docs/images/port-forward-dashboard.png b/docs/images/port-forward-dashboard.png new file mode 100644 index 0000000000000..2a2862d542d65 Binary files /dev/null and b/docs/images/port-forward-dashboard.png differ diff --git a/docs/manifest.json b/docs/manifest.json index a0aa731d77d61..6d9de148b1889 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1,5 +1,9 @@ { - "versions": ["main", "v0.8.1", "v0.7.12"], + "versions": [ + "main", + "v0.8.1", + "v0.7.12" + ], "routes": [ { "title": "About", diff --git a/docs/networking/port-forwarding.md b/docs/networking/port-forwarding.md index 5e925b8dedbfa..29c3c624a34cd 100644 --- a/docs/networking/port-forwarding.md +++ b/docs/networking/port-forwarding.md @@ -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. @@ -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) + ## SSH First, [configure SSH](../ides.md#ssh-configuration) on your diff --git a/site/src/api/api.ts b/site/src/api/api.ts index c907ecccca10d..68bc007442d75 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -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 => { + const response = await axios.get(`/api/v2/applications/host`) + return response.data +} diff --git a/site/src/components/PortForwardButton/PortForwardButton.tsx b/site/src/components/PortForwardButton/PortForwardButton.tsx new file mode 100644 index 0000000000000..b20b9343741ff --- /dev/null +++ b/site/src/components/PortForwardButton/PortForwardButton.tsx @@ -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 = (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 ( + + + Access ports running on the agent with the port, agent name, workspace name{" "} + and your username URL schema, as shown below. + + + + + Use the form to open applications in a new tab. + + + { + setPort(e.currentTarget.value) + }} + /> + + + + + + + + Learn more about port forward + + + + ) +} + +const DisabledView: React.FC = () => { + return ( + + + Your deployment does not have port forward enabled. See the docs for more + details. + + + + + Learn more about port forward + + + + ) +} + +export const PortForwardButton: React.FC = (props) => { + const { host } = props + const anchorRef = useRef(null) + const [isOpen, setIsOpen] = useState(false) + const id = isOpen ? "schedule-popover" : undefined + const styles = useStyles() + + const onClose = () => { + setIsOpen(false) + } + + return ( + <> + + + + + + + + + + + + + ) +} + +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], + }, + }, +})) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index d935f2096d1bc..cd0d167a7a781 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -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" @@ -42,6 +43,7 @@ interface ResourcesProps { canUpdateWorkspace: boolean buildInfo?: BuildInfoResponse | undefined hideSSHButton?: boolean + applicationsHost?: string } export const Resources: FC> = ({ @@ -51,6 +53,7 @@ export const Resources: FC> = ({ canUpdateWorkspace, buildInfo, hideSSHButton, + applicationsHost, }) => { const styles = useStyles() const theme: Theme = useTheme() @@ -151,6 +154,14 @@ export const Resources: FC> = ({
{canUpdateWorkspace && agent.status === "connected" && ( <> + {applicationsHost !== undefined && ( + + )} {!hideSSHButton && ( > buildInfo?: TypesGen.BuildInfoResponse + applicationsHost?: string } /** @@ -66,6 +67,7 @@ export const Workspace: FC> = ({ workspaceErrors, hideSSHButton, buildInfo, + applicationsHost, }) => { const styles = useStyles() const navigate = useNavigate() @@ -140,6 +142,7 @@ export const Workspace: FC> = ({ canUpdateWorkspace={canUpdateWorkspace} buildInfo={buildInfo} hideSSHButton={hideSSHButton} + applicationsHost={applicationsHost} /> )} diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 260cf7edb9bb5..a7f5abdf1e5fd 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -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, @@ -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() /** @@ -133,6 +130,7 @@ export const WorkspacePage: FC = () => { [WorkspaceErrors.CANCELLATION_ERROR]: cancellationError, }} buildInfo={buildInfoState.context.buildInfo} + applicationsHost={applicationsHost} /> { 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" })) + }), ] diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 10610db86502d..13bb3accb9ec3 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from "api/errors" import { assign, createMachine, send } from "xstate" import * as API from "../../api/api" import * as Types from "../../api/types" @@ -61,6 +62,8 @@ export interface WorkspaceContext { // permissions permissions?: Permissions checkPermissionsError?: Error | unknown + // applications + applicationsHost?: string } export type WorkspaceEvent = @@ -139,6 +142,9 @@ export const workspaceMachine = createMachine( checkPermissions: { data: TypesGen.AuthorizationResponse } + getApplicationsHost: { + data: TypesGen.GetAppHostResponse + } }, }, initial: "idle", @@ -391,6 +397,30 @@ export const workspaceMachine = createMachine( }, }, }, + applications: { + initial: "gettingApplicationsHost", + states: { + gettingApplicationsHost: { + invoke: { + src: "getApplicationsHost", + onDone: { + target: "success", + actions: ["assignApplicationsHost"], + }, + onError: { + target: "error", + actions: ["displayApplicationsHostError"], + }, + }, + }, + error: { + type: "final", + }, + success: { + type: "final", + }, + }, + }, }, }, error: { @@ -494,6 +524,14 @@ export const workspaceMachine = createMachine( clearGetBuildsError: assign({ getBuildsError: (_) => undefined, }), + // Applications + assignApplicationsHost: assign({ + applicationsHost: (_, { data }) => data.host, + }), + displayApplicationsHostError: (_, { data }) => { + const message = getErrorMessage(data, "Error getting the applications host.") + displayError(message) + }, }, guards: { moreBuildsAvailable, @@ -603,6 +641,9 @@ export const workspaceMachine = createMachine( throw Error("Cannot check permissions workspace id") } }, + getApplicationsHost: async () => { + return API.getApplicationsHost() + }, }, }, )