Skip to content

Commit 4a9d1c1

Browse files
Emyrkdeansheather
andauthored
chore: UI/UX for regions (#7283)
* chore: Allow regular users to query for all workspaces * FE to add workspace proxy options to account settings * WorkspaceProxy context syncs with coderd on region responses --------- Co-authored-by: Dean Sheather <dean@deansheather.com>
1 parent c00f5e4 commit 4a9d1c1

31 files changed

+984
-168
lines changed

coderd/workspaceapps/proxy.go

+6
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,12 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
618618

619619
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
620620
CompressionMode: websocket.CompressionDisabled,
621+
// Always allow websockets from the primary dashboard URL.
622+
// Terminals are opened there and connect to the proxy.
623+
OriginPatterns: []string{
624+
s.DashboardURL.Host,
625+
s.AccessURL.Host,
626+
},
621627
})
622628
if err != nil {
623629
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{

enterprise/coderd/workspaceproxy.go

+39-19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"net/http"
99
"net/url"
10+
"strings"
1011
"time"
1112

1213
"github.com/google/uuid"
@@ -57,26 +58,31 @@ func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
5758
return
5859
}
5960

60-
proxyHealth := api.ProxyHealth.HealthStatus()
61-
for _, proxy := range proxies {
62-
if proxy.Deleted {
63-
continue
64-
}
65-
66-
health, ok := proxyHealth[proxy.ID]
67-
if !ok {
68-
health.Status = proxyhealth.Unknown
61+
// Only add additional regions if the proxy health is enabled.
62+
// If it is nil, it is because the moons feature flag is not on.
63+
// By default, we still want to return the primary region.
64+
if api.ProxyHealth != nil {
65+
proxyHealth := api.ProxyHealth.HealthStatus()
66+
for _, proxy := range proxies {
67+
if proxy.Deleted {
68+
continue
69+
}
70+
71+
health, ok := proxyHealth[proxy.ID]
72+
if !ok {
73+
health.Status = proxyhealth.Unknown
74+
}
75+
76+
regions = append(regions, codersdk.Region{
77+
ID: proxy.ID,
78+
Name: proxy.Name,
79+
DisplayName: proxy.DisplayName,
80+
IconURL: proxy.Icon,
81+
Healthy: health.Status == proxyhealth.Healthy,
82+
PathAppURL: proxy.Url,
83+
WildcardHostname: proxy.WildcardHostname,
84+
})
6985
}
70-
71-
regions = append(regions, codersdk.Region{
72-
ID: proxy.ID,
73-
Name: proxy.Name,
74-
DisplayName: proxy.DisplayName,
75-
IconURL: proxy.Icon,
76-
Healthy: health.Status == proxyhealth.Healthy,
77-
PathAppURL: proxy.Url,
78-
WildcardHostname: proxy.WildcardHostname,
79-
})
8086
}
8187

8288
httpapi.Write(ctx, rw, http.StatusOK, codersdk.RegionsResponse{
@@ -156,6 +162,20 @@ func (api *API) postWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
156162
return
157163
}
158164

165+
if strings.ToLower(req.Name) == "primary" {
166+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
167+
Message: `The name "primary" is reserved for the primary region.`,
168+
Detail: "Cannot name a workspace proxy 'primary'.",
169+
Validations: []codersdk.ValidationError{
170+
{
171+
Field: "name",
172+
Detail: "Reserved name",
173+
},
174+
},
175+
})
176+
return
177+
}
178+
159179
id := uuid.New()
160180
secret, err := cryptorand.HexString(64)
161181
if err != nil {

site/src/AppRouter.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const SSHKeysPage = lazy(
3838
const TokensPage = lazy(
3939
() => import("./pages/UserSettingsPage/TokensPage/TokensPage"),
4040
)
41+
const WorkspaceProxyPage = lazy(
42+
() =>
43+
import("./pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyPage"),
44+
)
4145
const CreateUserPage = lazy(
4246
() => import("./pages/UsersPage/CreateUserPage/CreateUserPage"),
4347
)
@@ -272,6 +276,10 @@ export const AppRouter: FC = () => {
272276
<Route index element={<TokensPage />} />
273277
<Route path="new" element={<CreateTokenPage />} />
274278
</Route>
279+
<Route
280+
path="workspace-proxies"
281+
element={<WorkspaceProxyPage />}
282+
/>
275283
</Route>
276284

277285
<Route path="/@:username">

site/src/api/api.ts

+18
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,14 @@ export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
944944
return response.data
945945
}
946946

947+
export const getWorkspaceProxies =
948+
async (): Promise<TypesGen.RegionsResponse> => {
949+
const response = await axios.get<TypesGen.RegionsResponse>(
950+
`/api/v2/regions`,
951+
)
952+
return response.data
953+
}
954+
947955
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
948956
try {
949957
const response = await axios.get(`/api/v2/appearance`)
@@ -1292,3 +1300,13 @@ export const watchBuildLogsByBuildId = (
12921300
})
12931301
return socket
12941302
}
1303+
1304+
export const issueReconnectingPTYSignedToken = async (
1305+
params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
1306+
): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
1307+
const response = await axios.post(
1308+
"/api/v2/applications/reconnecting-pty-signed-token",
1309+
params,
1310+
)
1311+
return response.data
1312+
}

site/src/components/AppLink/AppLink.stories.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
import { Story } from "@storybook/react"
22
import {
3+
MockPrimaryWorkspaceProxy,
4+
MockWorkspaceProxies,
35
MockWorkspace,
46
MockWorkspaceAgent,
57
MockWorkspaceApp,
68
} from "testHelpers/entities"
79
import { AppLink, AppLinkProps } from "./AppLink"
10+
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
811

912
export default {
1013
title: "components/AppLink",
1114
component: AppLink,
1215
}
1316

14-
const Template: Story<AppLinkProps> = (args) => <AppLink {...args} />
17+
const Template: Story<AppLinkProps> = (args) => (
18+
<ProxyContext.Provider
19+
value={{
20+
proxy: getPreferredProxy(MockWorkspaceProxies, MockPrimaryWorkspaceProxy),
21+
proxies: MockWorkspaceProxies,
22+
isLoading: false,
23+
isFetched: true,
24+
setProxy: () => {
25+
return
26+
},
27+
}}
28+
>
29+
<AppLink {...args} />
30+
</ProxyContext.Provider>
31+
)
1532

1633
export const WithIcon = Template.bind({})
1734
WithIcon.args = {

site/src/components/AppLink/AppLink.tsx

+9-9
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,24 @@ import * as TypesGen from "../../api/typesGenerated"
1010
import { generateRandomString } from "../../utils/random"
1111
import { BaseIcon } from "./BaseIcon"
1212
import { ShareIcon } from "./ShareIcon"
13+
import { useProxy } from "contexts/ProxyContext"
1314

1415
const Language = {
1516
appTitle: (appName: string, identifier: string): string =>
1617
`${appName} - ${identifier}`,
1718
}
1819

1920
export interface AppLinkProps {
20-
appsHost?: string
2121
workspace: TypesGen.Workspace
2222
app: TypesGen.WorkspaceApp
2323
agent: TypesGen.WorkspaceAgent
2424
}
2525

26-
export const AppLink: FC<AppLinkProps> = ({
27-
appsHost,
28-
app,
29-
workspace,
30-
agent,
31-
}) => {
26+
export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
27+
const { proxy } = useProxy()
28+
const preferredPathBase = proxy.preferredPathAppURL
29+
const appsHost = proxy.preferredWildcardHostname
30+
3231
const styles = useStyles()
3332
const username = workspace.owner_name
3433

@@ -43,14 +42,15 @@ export const AppLink: FC<AppLinkProps> = ({
4342

4443
// The backend redirects if the trailing slash isn't included, so we add it
4544
// here to avoid extra roundtrips.
46-
let href = `/@${username}/${workspace.name}.${
45+
let href = `${preferredPathBase}/@${username}/${workspace.name}.${
4746
agent.name
4847
}/apps/${encodeURIComponent(appSlug)}/`
4948
if (app.command) {
50-
href = `/@${username}/${workspace.name}.${
49+
href = `${preferredPathBase}/@${username}/${workspace.name}.${
5150
agent.name
5251
}/terminal?command=${encodeURIComponent(app.command)}`
5352
}
53+
5454
if (appsHost && app.subdomain) {
5555
const subdomain = `${appSlug}--${agent.name}--${workspace.name}--${username}`
5656
href = `${window.location.protocol}//${appsHost}/`.replace("*", subdomain)

site/src/components/Dashboard/DashboardLayout.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Outlet } from "react-router-dom"
1313
import { dashboardContentBottomPadding } from "theme/constants"
1414
import { updateCheckMachine } from "xServices/updateCheck/updateCheckXService"
1515
import { Navbar } from "../Navbar/Navbar"
16-
import { DashboardProvider } from "./DashboardProvider"
1716

1817
export const DashboardLayout: FC = () => {
1918
const styles = useStyles()
@@ -28,7 +27,7 @@ export const DashboardLayout: FC = () => {
2827
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
2928

3029
return (
31-
<DashboardProvider>
30+
<>
3231
<ServiceBanner />
3332
{canViewDeployment && <LicenseBanner />}
3433

@@ -57,7 +56,7 @@ export const DashboardLayout: FC = () => {
5756

5857
<DeploymentBanner />
5958
</div>
60-
</DashboardProvider>
59+
</>
6160
)
6261
}
6362

site/src/components/DeploySettingsLayout/Badges.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ export const EntitledBadge: FC = () => {
2222
)
2323
}
2424

25+
export const HealthyBadge: FC = () => {
26+
const styles = useStyles()
27+
return (
28+
<span className={combineClasses([styles.badge, styles.enabledBadge])}>
29+
Healthy
30+
</span>
31+
)
32+
}
33+
34+
export const NotHealthyBadge: FC = () => {
35+
const styles = useStyles()
36+
return (
37+
<span className={combineClasses([styles.badge, styles.errorBadge])}>
38+
Unhealthy
39+
</span>
40+
)
41+
}
42+
2543
export const DisabledBadge: FC = () => {
2644
const styles = useStyles()
2745
return (
@@ -92,6 +110,11 @@ const useStyles = makeStyles((theme) => ({
92110
backgroundColor: theme.palette.success.dark,
93111
},
94112

113+
errorBadge: {
114+
border: `1px solid ${theme.palette.error.light}`,
115+
backgroundColor: theme.palette.error.dark,
116+
},
117+
95118
disabledBadge: {
96119
border: `1px solid ${theme.palette.divider}`,
97120
backgroundColor: theme.palette.background.paper,

site/src/components/PortForwardButton/PortForwardButton.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const portForwardURL = (
4343

4444
const TooltipView: React.FC<PortForwardButtonProps> = (props) => {
4545
const { host, workspaceName, agentName, agentId, username } = props
46+
4647
const styles = useStyles()
4748
const [port, setPort] = useState("3000")
4849
const urlExample = portForwardURL(

site/src/components/RequireAuth/RequireAuth.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { Navigate, useLocation } from "react-router"
44
import { Outlet } from "react-router-dom"
55
import { embedRedirect } from "../../utils/redirect"
66
import { FullScreenLoader } from "../Loader/FullScreenLoader"
7+
import { DashboardProvider } from "components/Dashboard/DashboardProvider"
8+
import { ProxyProvider } from "contexts/ProxyContext"
79

810
export const RequireAuth: FC = () => {
911
const [authState] = useAuth()
@@ -21,6 +23,14 @@ export const RequireAuth: FC = () => {
2123
) {
2224
return <FullScreenLoader />
2325
} else {
24-
return <Outlet />
26+
// Authenticated pages have access to some contexts for knowing enabled experiments
27+
// and where to route workspace connections.
28+
return (
29+
<DashboardProvider>
30+
<ProxyProvider>
31+
<Outlet />
32+
</ProxyProvider>
33+
</DashboardProvider>
34+
)
2535
}
2636
}

0 commit comments

Comments
 (0)