Skip to content

Commit 9879476

Browse files
committed
WorkspaceProxy context syncs with coderd on region responses
1 parent 69c5734 commit 9879476

File tree

13 files changed

+88
-198
lines changed

13 files changed

+88
-198
lines changed

site/src/components/AppLink/AppLink.tsx

+8-13
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,27 @@ import * as TypesGen from "../../api/typesGenerated"
1010
import { generateRandomString } from "../../utils/random"
1111
import { BaseIcon } from "./BaseIcon"
1212
import { ShareIcon } from "./ShareIcon"
13-
import { usePreferredProxy } from "hooks/usePreferredProxy"
13+
import { useProxy } from "contexts/ProxyContext"
1414

1515
const Language = {
1616
appTitle: (appName: string, identifier: string): string =>
1717
`${appName} - ${identifier}`,
1818
}
1919

2020
export interface AppLinkProps {
21-
appsHost?: string
2221
workspace: TypesGen.Workspace
2322
app: TypesGen.WorkspaceApp
2423
agent: TypesGen.WorkspaceAgent
2524
}
2625

2726
export const AppLink: FC<AppLinkProps> = ({
28-
appsHost,
2927
app,
3028
workspace,
3129
agent,
3230
}) => {
33-
const preferredProxy = usePreferredProxy()
34-
const preferredPathBase = preferredProxy ? preferredProxy.path_app_url : ""
35-
// Use the proxy host subdomain if it's configured.
36-
appsHost = preferredProxy ? preferredProxy.wildcard_hostname : appsHost
31+
const { proxy } = useProxy()
32+
const preferredPathBase = proxy.preferredPathAppURL
33+
const appsHost = proxy.preferredWildcardHostname
3734

3835
const styles = useStyles()
3936
const username = workspace.owner_name
@@ -49,13 +46,11 @@ export const AppLink: FC<AppLinkProps> = ({
4946

5047
// The backend redirects if the trailing slash isn't included, so we add it
5148
// here to avoid extra roundtrips.
52-
let href = `${preferredPathBase}/@${username}/${workspace.name}.${
53-
agent.name
54-
}/apps/${encodeURIComponent(appSlug)}/`
49+
let href = `${preferredPathBase}/@${username}/${workspace.name}.${agent.name
50+
}/apps/${encodeURIComponent(appSlug)}/`
5551
if (app.command) {
56-
href = `${preferredPathBase}/@${username}/${workspace.name}.${
57-
agent.name
58-
}/terminal?command=${encodeURIComponent(app.command)}`
52+
href = `${preferredPathBase}/@${username}/${workspace.name}.${agent.name
53+
}/terminal?command=${encodeURIComponent(app.command)}`
5954
}
6055

6156
// TODO: @emyrk handle proxy subdomains.

site/src/components/PortForwardButton/PortForwardButton.tsx

+4-7
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,18 @@ export const portForwardURL = (
3535
): string => {
3636
const { location } = window
3737

38-
const subdomain = `${
39-
isNaN(port) ? 3000 : port
40-
}--${agentName}--${workspaceName}--${username}`
38+
const subdomain = `${isNaN(port) ? 3000 : port
39+
}--${agentName}--${workspaceName}--${username}`
4140
return `${location.protocol}//${host}`.replace("*", subdomain)
4241
}
4342

4443
const TooltipView: React.FC<PortForwardButtonProps> = (props) => {
4544
const { host, workspaceName, agentName, agentId, username } = props
46-
const preferredProxy = usePreferredProxy()
47-
const portHost = preferredProxy ? preferredProxy.wildcard_hostname : host
4845

4946
const styles = useStyles()
5047
const [port, setPort] = useState("3000")
5148
const urlExample = portForwardURL(
52-
portHost,
49+
host,
5350
parseInt(port),
5451
agentName,
5552
workspaceName,
@@ -107,7 +104,7 @@ const TooltipView: React.FC<PortForwardButtonProps> = (props) => {
107104
{ports &&
108105
ports.map((p, i) => {
109106
const url = portForwardURL(
110-
portHost,
107+
host,
111108
p.port,
112109
agentName,
113110
workspaceName,

site/src/components/Resources/AgentRow.stories.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ Example.args = {
109109
'set -eux -o pipefail\n\n# install and start code-server\ncurl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.8.3\n/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &\n\n\nif [ ! -d ~/coder ]; then\n mkdir -p ~/coder\n\n git clone https://github.com/coder/coder ~/coder\nfi\n\nsudo service docker start\nDOTFILES_URI=" "\nrm -f ~/.personalize.log\nif [ -n "${DOTFILES_URI// }" ]; then\n coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.personalize.log\nfi\nif [ -x ~/personalize ]; then\n ~/personalize 2>&1 | tee -a ~/.personalize.log\nelif [ -f ~/personalize ]; then\n echo "~/personalize is not executable, skipping..." | tee -a ~/.personalize.log\nfi\n',
110110
},
111111
workspace: MockWorkspace,
112-
applicationsHost: "",
113112
showApps: true,
114113
storybookAgentMetadata: defaultAgentMetadata,
115114
}
@@ -149,7 +148,6 @@ BunchOfApps.args = {
149148
],
150149
},
151150
workspace: MockWorkspace,
152-
applicationsHost: "",
153151
showApps: true,
154152
}
155153

@@ -226,7 +224,7 @@ Off.args = {
226224
export const ShowingPortForward = Template.bind({})
227225
ShowingPortForward.args = {
228226
...Example.args,
229-
applicationsHost: "https://coder.com",
227+
// TODO: @emyrk fix this from the proxy context
230228
}
231229

232230
export const Outdated = Template.bind({})

site/src/components/Resources/AgentRow.tsx

+4-5
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ import { AgentMetadata } from "./AgentMetadata"
4343
import { AgentVersion } from "./AgentVersion"
4444
import { AgentStatus } from "./AgentStatus"
4545
import Collapse from "@material-ui/core/Collapse"
46+
import { useProxy } from "contexts/ProxyContext"
4647

4748
export interface AgentRowProps {
4849
agent: WorkspaceAgent
4950
workspace: Workspace
50-
applicationsHost: string | undefined
5151
showApps: boolean
5252
hideSSHButton?: boolean
5353
sshPrefix?: string
@@ -61,7 +61,6 @@ export interface AgentRowProps {
6161
export const AgentRow: FC<AgentRowProps> = ({
6262
agent,
6363
workspace,
64-
applicationsHost,
6564
showApps,
6665
hideSSHButton,
6766
hideVSCodeDesktopButton,
@@ -96,6 +95,7 @@ export const AgentRow: FC<AgentRowProps> = ({
9695
const hasStartupFeatures =
9796
Boolean(agent.startup_logs_length) ||
9897
Boolean(logsMachine.context.startupLogs?.length)
98+
const { proxy } = useProxy()
9999

100100
const [showStartupLogs, setShowStartupLogs] = useState(
101101
agent.lifecycle_state !== "ready" && hasStartupFeatures,
@@ -228,7 +228,6 @@ export const AgentRow: FC<AgentRowProps> = ({
228228
{agent.apps.map((app) => (
229229
<AppLink
230230
key={app.slug}
231-
appsHost={applicationsHost}
232231
app={app}
233232
agent={agent}
234233
workspace={workspace}
@@ -249,9 +248,9 @@ export const AgentRow: FC<AgentRowProps> = ({
249248
sshPrefix={sshPrefix}
250249
/>
251250
)}
252-
{applicationsHost !== undefined && applicationsHost !== "" && (
251+
{proxy.preferredWildcardHostname !== undefined && proxy.preferredWildcardHostname !== "" && (
253252
<PortForwardButton
254-
host={applicationsHost}
253+
host={proxy.preferredWildcardHostname}
255254
workspaceName={workspace.name}
256255
agentId={agent.id}
257256
agentName={agent.name}

site/src/components/TerminalLink/TerminalLink.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { SecondaryAgentButton } from "components/Resources/AgentButton"
33
import { FC } from "react"
44
import * as TypesGen from "../../api/typesGenerated"
55
import { generateRandomString } from "../../utils/random"
6-
import { usePreferredProxy } from "hooks/usePreferredProxy"
6+
import { useProxy } from "contexts/ProxyContext"
77

88
export const Language = {
99
linkText: "Terminal",
@@ -28,12 +28,10 @@ export const TerminalLink: FC<React.PropsWithChildren<TerminalLinkProps>> = ({
2828
userName = "me",
2929
workspaceName,
3030
}) => {
31-
const preferredProxy = usePreferredProxy()
32-
const preferredPathBase = preferredProxy ? preferredProxy.path_app_url : ""
31+
const { proxy } = useProxy()
3332

34-
const href = `${preferredPathBase}/@${userName}/${workspaceName}${
35-
agentName ? `.${agentName}` : ""
36-
}/terminal`
33+
const href = `${proxy.preferredPathAppURL}/@${userName}/${workspaceName}${agentName ? `.${agentName}` : ""
34+
}/terminal`
3735

3836
return (
3937
<Link

site/src/components/Workspace/Workspace.tsx

-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export interface WorkspaceProps {
5757
hideVSCodeDesktopButton?: boolean
5858
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
5959
buildInfo?: TypesGen.BuildInfoResponse
60-
applicationsHost?: string
6160
sshPrefix?: string
6261
template?: TypesGen.Template
6362
quota_budget?: number
@@ -88,7 +87,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
8887
hideSSHButton,
8988
hideVSCodeDesktopButton,
9089
buildInfo,
91-
applicationsHost,
9290
sshPrefix,
9391
template,
9492
quota_budget,
@@ -240,7 +238,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
240238
key={agent.id}
241239
agent={agent}
242240
workspace={workspace}
243-
applicationsHost={applicationsHost}
244241
sshPrefix={sshPrefix}
245242
showApps={canUpdateWorkspace}
246243
hideSSHButton={hideSSHButton}

site/src/contexts/ProxyContext.tsx

+55-29
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1+
import { useQuery } from "@tanstack/react-query"
2+
import { getApplicationsHost, getWorkspaceProxies } from "api/api"
13
import { Region } from "api/typesGenerated"
24
import { useDashboard } from "components/Dashboard/DashboardProvider"
35
import { createContext, FC, PropsWithChildren, useContext, useState } from "react"
46

57
interface ProxyContextValue {
6-
value: PreferredProxy
7-
setValue: (regions: Region[], selectedRegion: Region | undefined) => void
8+
proxy: PreferredProxy
9+
isLoading: boolean
10+
error?: Error | unknown
11+
setProxy: (regions: Region[], selectedRegion: Region | undefined) => void
812
}
913

1014
interface PreferredProxy {
11-
// Regions is a list of all the regions returned by coderd.
12-
regions: Region[]
1315
// SelectedRegion is the region the user has selected.
16+
// Do not use the fields 'path_app_url' or 'wildcard_hostname' from this
17+
// object. Use the preferred fields.
1418
selectedRegion: Region | undefined
1519
// PreferredPathAppURL is the URL of the proxy or it is the empty string
1620
// to indicte using relative paths. To add a path to this:
1721
// PreferredPathAppURL + "/path/to/app"
1822
preferredPathAppURL: string
1923
// PreferredWildcardHostname is a hostname that includes a wildcard.
20-
// TODO: If the preferred proxy does not have this set, should we default to'
21-
// the primary's?
22-
// Example: "*.example.com"
2324
preferredWildcardHostname: string
2425
}
2526

@@ -32,24 +33,56 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
3233
// Try to load the preferred proxy from local storage.
3334
let savedProxy = loadPreferredProxy()
3435
if (!savedProxy) {
35-
savedProxy = getURLs([], undefined)
36+
savedProxy = getURLs([])
3637
}
3738

3839
// The initial state is no regions and no selected region.
39-
const [state, setState] = useState<PreferredProxy>(savedProxy)
40+
const [proxy, setProxy] = useState<PreferredProxy>(savedProxy)
41+
const setAndSaveProxy = (regions: Region[], selectedRegion: Region | undefined) => {
42+
const preferred = getURLs(regions, selectedRegion)
43+
// Save to local storage to persist the user's preference across reloads
44+
// and other tabs.
45+
savePreferredProxy(preferred)
46+
// Set the state for the current context.
47+
setProxy(preferred)
48+
}
49+
50+
const queryKey = ["get-regions"]
51+
const { error: regionsError, isLoading: regionsLoading } = useQuery({
52+
queryKey,
53+
queryFn: getWorkspaceProxies,
54+
// This onSucccess ensures the local storage is synchronized with the
55+
// regions returned by coderd. If the selected region is not in the list,
56+
// then the user selection is removed.
57+
onSuccess: (data) => {
58+
setAndSaveProxy(data.regions, proxy.selectedRegion)
59+
},
60+
})
4061

4162
// ******************************* //
4263
// ** This code can be removed **
4364
// ** when the experimental is **
4465
// ** dropped ** //
4566
const dashboard = useDashboard()
67+
const appHostQueryKey = ["get-application-host"]
68+
const { data: applicationHostResult, error: appHostError, isLoading: appHostLoading } = useQuery({
69+
queryKey: appHostQueryKey,
70+
queryFn: getApplicationsHost,
71+
})
4672
// If the experiment is disabled, then make the setState do a noop.
4773
// This preserves an empty state, which is the default behavior.
4874
if (!dashboard?.experiments.includes("moons")) {
75+
const value = getURLs([])
76+
4977
return (
5078
<ProxyContext.Provider value={{
51-
value: getURLs([], undefined),
52-
setValue: () => {
79+
proxy: {
80+
...value,
81+
preferredWildcardHostname: applicationHostResult?.host || value.preferredWildcardHostname,
82+
},
83+
isLoading: appHostLoading,
84+
error: appHostError,
85+
setProxy: () => {
5386
// Does a noop
5487
},
5588
}}>
@@ -64,17 +97,12 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
6497

6598
return (
6699
<ProxyContext.Provider value={{
67-
value: state,
100+
proxy: proxy,
101+
isLoading: regionsLoading,
102+
error: regionsError,
68103
// A function that takes the new regions and selected region and updates
69104
// the state with the appropriate urls.
70-
setValue: (regions, selectedRegion) => {
71-
const preferred = getURLs(regions, selectedRegion)
72-
// Save to local storage to persist the user's preference across reloads
73-
// and other tabs.
74-
savePreferredProxy(preferred)
75-
// Set the state for the current context.
76-
setState(preferred)
77-
},
105+
setProxy: setAndSaveProxy,
78106
}}>
79107
{children}
80108
</ProxyContext.Provider >
@@ -99,19 +127,19 @@ export const useProxy = (): ProxyContextValue => {
99127
* @param regions Is the list of regions returned by coderd. If this is empty, default behavior is used.
100128
* @param selectedRegion Is the region the user has selected. If this is undefined, default behavior is used.
101129
*/
102-
const getURLs = (regions: Region[], selectedRegion: Region | undefined): PreferredProxy => {
130+
const getURLs = (regions: Region[], selectedRegion?: Region): PreferredProxy => {
103131
// By default we set the path app to relative and disable wilcard hostnames.
104132
// We will set these values if we find a proxy we can use that supports them.
105133
let pathAppURL = ""
106134
let wildcardHostname = ""
107135

108-
if (selectedRegion === undefined) {
136+
// If a region is selected, make sure it is in the list of regions. If it is not
137+
// we should default to the primary.
138+
selectedRegion = regions.find((region) => selectedRegion && region.id === selectedRegion.id)
139+
140+
if (!selectedRegion) {
109141
// If no region is selected, default to the primary region.
110142
selectedRegion = regions.find((region) => region.name === "primary")
111-
} else {
112-
// If a region is selected, make sure it is in the list of regions. If it is not
113-
// we should default to the primary.
114-
selectedRegion = regions.find((region) => region.id === selectedRegion?.id)
115143
}
116144

117145
// Only use healthy regions.
@@ -126,10 +154,8 @@ const getURLs = (regions: Region[], selectedRegion: Region | undefined): Preferr
126154

127155
// TODO: @emyrk Should we notify the user if they had an unhealthy region selected?
128156

129-
130157
return {
131-
regions: regions,
132-
selectedRegion: selectedRegion,
158+
selectedRegion,
133159
// Trim trailing slashes to be consistent
134160
preferredPathAppURL: pathAppURL.replace(/\/$/, ""),
135161
preferredWildcardHostname: wildcardHostname,

0 commit comments

Comments
 (0)