feat: move proxy settings page to deployment options (#8246)

* feat: Move workspace proxy page to deployment options

Workspace proxy settings page is now an admin feature

* WorkspaceProxy response extends region
This commit is contained in:
Steven Masley
2023-06-30 11:32:35 -04:00
committed by GitHub
parent 1e8cc2ca8d
commit f0bd258ff1
39 changed files with 645 additions and 342 deletions
+3 -3
View File
@@ -7,7 +7,7 @@ import { TextEncoder, TextDecoder } from "util"
import { Blob } from "buffer"
import jestFetchMock from "jest-fetch-mock"
import { ProxyLatencyReport } from "contexts/useProxyLatency"
import { RegionsResponse } from "api/typesGenerated"
import { Region } from "api/typesGenerated"
import { useMemo } from "react"
jestFetchMock.enableMocks()
@@ -16,14 +16,14 @@ jestFetchMock.enableMocks()
// This would fail unit testing, or at least make it very slow with
// actual network requests. So just globally mock this hook.
jest.mock("contexts/useProxyLatency", () => ({
useProxyLatency: (proxies?: RegionsResponse) => {
useProxyLatency: (proxies?: Region[]) => {
// Must use `useMemo` here to avoid infinite loop.
// Mocking the hook with a hook.
const proxyLatencies = useMemo(() => {
if (!proxies) {
return {} as Record<string, ProxyLatencyReport>
}
return proxies.regions.reduce((acc, proxy) => {
return proxies.reduce((acc, proxy) => {
acc[proxy.id] = {
accurate: true,
// Return a constant latency of 8ms.
+3 -1
View File
@@ -146,7 +146,9 @@ type Handler struct {
buildInfoJSON string
AppearanceFetcher func(ctx context.Context) (codersdk.AppearanceConfig, error)
RegionsFetcher func(ctx context.Context) (codersdk.RegionsResponse, error)
// RegionsFetcher will attempt to fetch the more detailed WorkspaceProxy data, but will fall back to the
// regions if the user does not have the correct permissions.
RegionsFetcher func(ctx context.Context) (any, error)
Entitlements atomic.Pointer[codersdk.Entitlements]
Experiments atomic.Pointer[codersdk.Experiments]
+4 -4
View File
@@ -276,6 +276,10 @@ export const AppRouter: FC = () => {
<Route path="network" element={<NetworkSettingsPage />} />
<Route path="userauth" element={<UserAuthSettingsPage />} />
<Route path="gitauth" element={<GitAuthSettingsPage />} />
<Route
path="workspace-proxies"
element={<WorkspaceProxyPage />}
/>
</Route>
<Route path="settings" element={<SettingsLayout />}>
@@ -286,10 +290,6 @@ export const AppRouter: FC = () => {
<Route index element={<TokensPage />} />
<Route path="new" element={<CreateTokenPage />} />
</Route>
<Route
path="workspace-proxies"
element={<WorkspaceProxyPage />}
/>
</Route>
<Route path="/:username">
+17 -7
View File
@@ -966,13 +966,23 @@ export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
return response.data
}
export const getWorkspaceProxies =
async (): Promise<TypesGen.RegionsResponse> => {
const response = await axios.get<TypesGen.RegionsResponse>(
`/api/v2/regions`,
)
return response.data
}
export const getWorkspaceProxyRegions = async (): Promise<
TypesGen.RegionsResponse<TypesGen.Region>
> => {
const response = await axios.get<TypesGen.RegionsResponse<TypesGen.Region>>(
`/api/v2/regions`,
)
return response.data
}
export const getWorkspaceProxies = async (): Promise<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
> => {
const response = await axios.get<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
>(`/api/v2/workspaceproxies`)
return response.data
}
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
try {
+7 -10
View File
@@ -747,8 +747,8 @@ export interface Region {
}
// From codersdk/workspaceproxy.go
export interface RegionsResponse {
readonly regions: Region[]
export interface RegionsResponse<R extends RegionTypes> {
readonly regions: R[]
}
// From codersdk/replicas.go
@@ -1307,17 +1307,11 @@ export interface WorkspaceOptions {
}
// From codersdk/workspaceproxy.go
export interface WorkspaceProxy {
readonly id: string
readonly name: string
readonly display_name: string
readonly icon: string
readonly url: string
readonly wildcard_hostname: string
export interface WorkspaceProxy extends Region {
readonly status?: WorkspaceProxyStatus
readonly created_at: string
readonly updated_at: string
readonly deleted: boolean
readonly status?: WorkspaceProxyStatus
}
// From codersdk/deployment.go
@@ -1732,3 +1726,6 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [
"start",
"stop",
]
// From codersdk/workspaceproxy.go
export type RegionTypes = Region | WorkspaceProxy
@@ -40,6 +40,24 @@ export const NotHealthyBadge: FC = () => {
)
}
export const NotRegisteredBadge: FC = () => {
const styles = useStyles()
return (
<span className={combineClasses([styles.badge, styles.warnBadge])}>
Not Registered
</span>
)
}
export const NotReachableBadge: FC = () => {
const styles = useStyles()
return (
<span className={combineClasses([styles.badge, styles.errorBadge])}>
Not Reachable
</span>
)
}
export const DisabledBadge: FC = () => {
const styles = useStyles()
return (
@@ -88,6 +106,7 @@ const useStyles = makeStyles((theme) => ({
display: "flex",
alignItems: "center",
width: "fit-content",
whiteSpace: "nowrap",
},
enterpriseBadge: {
@@ -115,6 +134,11 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: theme.palette.error.dark,
},
warnBadge: {
border: `1px solid ${theme.palette.warning.light}`,
backgroundColor: theme.palette.warning.dark,
},
disabledBadge: {
border: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
@@ -4,12 +4,14 @@ import LaunchOutlined from "@mui/icons-material/LaunchOutlined"
import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined"
import LockRounded from "@mui/icons-material/LockOutlined"
import Globe from "@mui/icons-material/PublicOutlined"
import HubOutlinedIcon from "@mui/icons-material/HubOutlined"
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"
import { GitIcon } from "components/Icons/GitIcon"
import { Stack } from "components/Stack/Stack"
import { ElementType, PropsWithChildren, ReactNode, FC } from "react"
import { NavLink } from "react-router-dom"
import { combineClasses } from "utils/combineClasses"
import { useDashboard } from "components/Dashboard/DashboardProvider"
const SidebarNavItem: FC<
PropsWithChildren<{ href: string; icon: ReactNode }>
@@ -40,6 +42,7 @@ const SidebarNavItemIcon: FC<{ icon: ElementType }> = ({ icon: Icon }) => {
export const Sidebar: React.FC = () => {
const styles = useStyles()
const dashboard = useDashboard()
return (
<nav className={styles.sidebar}>
@@ -76,6 +79,14 @@ export const Sidebar: React.FC = () => {
<SidebarNavItem href="network" icon={<SidebarNavItemIcon icon={Globe} />}>
Network
</SidebarNavItem>
{dashboard.experiments.includes("moons") && (
<SidebarNavItem
href="workspace-proxies"
icon={<SidebarNavItemIcon icon={HubOutlinedIcon} />}
>
Workspace Proxy
</SidebarNavItem>
)}
<SidebarNavItem
href="security"
icon={<SidebarNavItemIcon icon={LockRounded} />}
@@ -4,7 +4,7 @@ import {
MockUser,
MockUser2,
} from "../../testHelpers/entities"
import { render } from "../../testHelpers/renderHelpers"
import { renderWithAuth } from "../../testHelpers/renderHelpers"
import { Language as navLanguage, NavbarView } from "./NavbarView"
import { ProxyContextValue } from "contexts/ProxyContext"
import { action } from "@storybook/addon-actions"
@@ -41,7 +41,7 @@ describe("NavbarView", () => {
})
it("workspaces nav link has the correct href", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser}
@@ -55,7 +55,7 @@ describe("NavbarView", () => {
})
it("templates nav link has the correct href", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser}
@@ -69,7 +69,7 @@ describe("NavbarView", () => {
})
it("users nav link has the correct href", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser}
@@ -91,7 +91,7 @@ describe("NavbarView", () => {
}
// When
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={mockUser}
@@ -108,7 +108,7 @@ describe("NavbarView", () => {
})
it("audit nav link has the correct href", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser}
@@ -122,7 +122,7 @@ describe("NavbarView", () => {
})
it("audit nav link is hidden for members", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser2}
@@ -136,7 +136,7 @@ describe("NavbarView", () => {
})
it("deployment nav link has the correct href", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser}
@@ -152,7 +152,7 @@ describe("NavbarView", () => {
})
it("deployment nav link is hidden for members", async () => {
render(
renderWithAuth(
<NavbarView
proxyContextValue={proxyContextValue}
user={MockUser2}
+17 -5
View File
@@ -23,6 +23,7 @@ import Divider from "@mui/material/Divider"
import Skeleton from "@mui/material/Skeleton"
import { BUTTON_SM_HEIGHT } from "theme/theme"
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency"
import { usePermissions } from "hooks/usePermissions"
export const USERS_LINK = `/users?filter=${encodeURIComponent("status:active")}`
@@ -194,6 +195,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
const latencies = proxyContextValue.proxyLatencies
const isLoadingLatencies = Object.keys(latencies).length === 0
const isLoading = proxyContextValue.isLoading || isLoadingLatencies
const permissions = usePermissions()
if (isLoading) {
return (
@@ -280,15 +282,25 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
</MenuItem>
))}
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
{Boolean(permissions.editWorkspaceProxies) && (
<MenuItem
sx={{ fontSize: 14 }}
onClick={() => {
navigate("settings/deployment/workspace-proxies")
}}
>
Proxy settings
</MenuItem>
)}
<MenuItem
sx={{ fontSize: 14 }}
onClick={() => {
navigate("/settings/workspace-proxies")
onClick={(e) => {
// Stop the menu from closing
e.stopPropagation()
// Refresh the latencies.
refreshLatencies()
}}
>
Proxy settings
</MenuItem>
<MenuItem sx={{ fontSize: 14 }} onClick={refreshLatencies}>
Refresh Latencies
</MenuItem>
</Menu>
@@ -9,8 +9,6 @@ import { NavLink } from "react-router-dom"
import { combineClasses } from "utils/combineClasses"
import AccountIcon from "@mui/icons-material/Person"
import SecurityIcon from "@mui/icons-material/LockOutlined"
import PublicIcon from "@mui/icons-material/Public"
import { useDashboard } from "components/Dashboard/DashboardProvider"
const SidebarNavItem: FC<
PropsWithChildren<{ href: string; icon: ReactNode }>
@@ -43,7 +41,6 @@ const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
export const Sidebar: React.FC<{ user: User }> = ({ user }) => {
const styles = useStyles()
const dashboard = useDashboard()
return (
<nav className={styles.sidebar}>
@@ -79,14 +76,6 @@ export const Sidebar: React.FC<{ user: User }> = ({ user }) => {
>
Tokens
</SidebarNavItem>
{dashboard.experiments.includes("moons") && (
<SidebarNavItem
href="workspace-proxies"
icon={<SidebarNavItemIcon icon={PublicIcon} />}
>
Workspace Proxy
</SidebarNavItem>
)}
</nav>
)
}
+8
View File
@@ -360,6 +360,14 @@ describe("ProxyContextSelection", () => {
}),
)
}),
rest.get("/api/v2/workspaceproxies", async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
regions: regions,
}),
)
}),
)
TestingComponent()
+28 -8
View File
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query"
import { getWorkspaceProxies } from "api/api"
import { Region } from "api/typesGenerated"
import { getWorkspaceProxies, getWorkspaceProxyRegions } from "api/api"
import { Region, WorkspaceProxy } from "api/typesGenerated"
import { useDashboard } from "components/Dashboard/DashboardProvider"
import {
createContext,
@@ -12,6 +12,7 @@ import {
useState,
} from "react"
import { ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"
import { usePermissions } from "hooks/usePermissions"
export interface ProxyContextValue {
// proxy is **always** the workspace proxy that should be used.
@@ -36,7 +37,12 @@ export interface ProxyContextValue {
// proxies is the list of proxies returned by coderd. This is fetched async.
// isFetched, isLoading, and error are used to track the state of the async call.
proxies?: Region[]
//
// Region[] is returned if the user is a non-admin.
// WorkspaceProxy[] is returned if the user is an admin. WorkspaceProxy extends Region with
// more information about the proxy and the status. More information includes the error message if
// the proxy is unhealthy.
proxies?: Region[] | WorkspaceProxy[]
// isFetched is true when the 'proxies' api call is complete.
isFetched: boolean
isLoading: boolean
@@ -103,12 +109,26 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
if (regions) {
const rawContent = regions.getAttribute("content")
try {
return JSON.parse(rawContent as string)
const obj = JSON.parse(rawContent as string)
if ("regions" in obj) {
return obj.regions as Region[]
}
return obj as Region[]
} catch (ex) {
// Ignore this and fetch as normal!
}
}
})
const permissions = usePermissions()
const query = async (): Promise<Region[]> => {
const endpoint = permissions.editWorkspaceProxies
? getWorkspaceProxies
: getWorkspaceProxyRegions
const resp = await endpoint()
return resp.regions
}
const {
data: proxiesResp,
error: proxiesError,
@@ -116,7 +136,7 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
isFetched: proxiesFetched,
} = useQuery({
queryKey,
queryFn: getWorkspaceProxies,
queryFn: query,
staleTime: initialData ? Infinity : undefined,
initialData,
})
@@ -133,7 +153,7 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
setUserSavedProxy(loadUserSelectedProxy())
setProxy(
getPreferredProxy(
proxiesResp?.regions ?? [],
proxiesResp ?? [],
loadUserSelectedProxy(),
proxyLatencies,
// Do not auto select based on latencies, as inconsistent latencies can cause this
@@ -161,9 +181,9 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
: {
// If the experiment is disabled, then call 'getPreferredProxy' with the regions from
// the api call. The default behavior is to use the `primary` proxy.
...getPreferredProxy(proxiesResp?.regions || []),
...getPreferredProxy(proxiesResp || []),
},
proxies: proxiesResp?.regions,
proxies: proxiesResp,
isLoading: proxiesLoading,
isFetched: proxiesFetched,
error: proxiesError,
+10 -8
View File
@@ -1,4 +1,4 @@
import { Region, RegionsResponse } from "api/typesGenerated"
import { Region } from "api/typesGenerated"
import { useEffect, useReducer, useState } from "react"
import PerformanceObserver from "@fastly/performance-observer-polyfill"
import axios from "axios"
@@ -33,11 +33,11 @@ const proxyLatenciesReducer = (
}
export const useProxyLatency = (
proxies?: RegionsResponse,
proxies?: Region[],
): {
// Refetch can be called to refetch the proxy latencies.
// Until the new values are loaded, the old values will still be used.
refetch: () => void
refetch: () => Date
proxyLatencies: Record<string, ProxyLatencyReport>
} => {
// maxStoredLatencies is the maximum number of latencies to store per proxy in local storage.
@@ -62,7 +62,9 @@ export const useProxyLatency = (
new Date().toISOString(),
)
const refetch = () => {
setLatestFetchRequest(new Date().toISOString())
const d = new Date()
setLatestFetchRequest(d.toISOString())
return d
}
// Only run latency updates when the proxies change.
@@ -74,7 +76,7 @@ export const useProxyLatency = (
// proxyMap is a map of the proxy path_app_url to the proxy object.
// This is for the observer to know which requests are important to
// record.
const proxyChecks = proxies.regions.reduce((acc, proxy) => {
const proxyChecks = proxies.reduce((acc, proxy) => {
// Only run the latency check on healthy proxies.
if (!proxy.healthy) {
return acc
@@ -216,7 +218,7 @@ const updateStoredLatencies = (action: ProxyLatencyAction): void => {
// garbageCollectStoredLatencies will remove any latencies that are older then 1 week or latencies of proxies
// that no longer exist. This is intended to keep the size of local storage down.
const garbageCollectStoredLatencies = (
regions: RegionsResponse,
regions: Region[],
maxStored: number,
): void => {
const latencies = loadStoredLatencies()
@@ -228,12 +230,12 @@ const garbageCollectStoredLatencies = (
const cleanupLatencies = (
stored: Record<string, ProxyLatencyReport[]>,
regions: RegionsResponse,
regions: Region[],
now: Date,
maxStored: number,
): Record<string, ProxyLatencyReport[]> => {
Object.keys(stored).forEach((proxyID) => {
if (!regions.regions.find((region) => region.id === proxyID)) {
if (!regions.find((region) => region.id === proxyID)) {
delete stored[proxyID]
return
}
@@ -2,16 +2,13 @@ import { FC, PropsWithChildren } from "react"
import { Section } from "components/SettingsLayout/Section"
import { WorkspaceProxyView } from "./WorkspaceProxyView"
import makeStyles from "@mui/styles/makeStyles"
import { displayError } from "components/GlobalSnackbar/utils"
import { useProxy } from "contexts/ProxyContext"
export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
const styles = useStyles()
const description =
"Workspace proxies are used to reduce the latency of connections to your workspaces." +
"To get the best experience, choose the workspace proxy that is closest to you." +
"This selection only affects browser connections to your workspace."
"Workspace proxies are used to reduce the latency of connections to your workspaces."
const {
proxyLatencies,
@@ -20,7 +17,6 @@ export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
isFetched: proxiesFetched,
isLoading: proxiesLoading,
proxy,
setProxy,
} = useProxy()
return (
@@ -37,14 +33,6 @@ export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
hasLoaded={proxiesFetched}
getWorkspaceProxiesError={proxiesError}
preferredProxy={proxy.proxy}
onSelect={(proxy) => {
if (!proxy.healthy) {
displayError("Please select a healthy workspace proxy.")
return
}
setProxy(proxy)
}}
/>
</Section>
)
@@ -1,83 +1,95 @@
import { Region } from "api/typesGenerated"
import { Region, WorkspaceProxy } from "api/typesGenerated"
import { AvatarData } from "components/AvatarData/AvatarData"
import { Avatar } from "components/Avatar/Avatar"
import { useClickableTableRow } from "hooks/useClickableTableRow"
import TableCell from "@mui/material/TableCell"
import TableRow from "@mui/material/TableRow"
import { FC } from "react"
import {
HealthyBadge,
NotHealthyBadge,
NotReachableBadge,
NotRegisteredBadge,
} from "components/DeploySettingsLayout/Badges"
import { makeStyles } from "@mui/styles"
import { combineClasses } from "utils/combineClasses"
import { ProxyLatencyReport } from "contexts/useProxyLatency"
import { getLatencyColor } from "utils/latency"
import { alpha } from "@mui/material/styles"
export const ProxyRow: FC<{
latency?: ProxyLatencyReport
proxy: Region
onSelectRegion: (proxy: Region) => void
preferred: boolean
}> = ({ proxy, onSelectRegion, preferred, latency }) => {
const styles = useStyles()
const clickable = useClickableTableRow(() => {
onSelectRegion(proxy)
})
}> = ({ proxy, latency }) => {
// If we have a more specific proxy status, use that.
// All users can see healthy/unhealthy, some can see more.
let statusBadge = <ProxyStatus proxy={proxy} />
if ("status" in proxy) {
statusBadge = <DetailedProxyStatus proxy={proxy as WorkspaceProxy} />
}
return (
<TableRow
key={proxy.name}
data-testid={`${proxy.name}`}
{...clickable}
// Make sure to include our classname here.
className={combineClasses({
[clickable.className]: true,
[styles.preferredrow]: preferred,
})}
>
<TableCell>
<AvatarData
title={
proxy.display_name && proxy.display_name.length > 0
? proxy.display_name
: proxy.name
}
avatar={
proxy.icon_url !== "" && (
<Avatar
size="sm"
src={proxy.icon_url}
variant="square"
fitImage
/>
)
}
/>
</TableCell>
<>
<TableRow key={proxy.name} data-testid={`${proxy.name}`}>
<TableCell>
<AvatarData
title={
proxy.display_name && proxy.display_name.length > 0
? proxy.display_name
: proxy.name
}
avatar={
proxy.icon_url !== "" && (
<Avatar
size="sm"
src={proxy.icon_url}
variant="square"
fitImage
/>
)
}
/>
</TableCell>
<TableCell sx={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
<TableCell sx={{ fontSize: 14 }}>
<ProxyStatus proxy={proxy} />
</TableCell>
<TableCell
sx={{
fontSize: 14,
textAlign: "right",
color: (theme) =>
latency
? getLatencyColor(theme, latency.latencyMS)
: theme.palette.text.secondary,
}}
>
{latency ? `${latency.latencyMS.toFixed(0)} ms` : "Not available"}
</TableCell>
</TableRow>
<TableCell sx={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
<TableCell sx={{ fontSize: 14 }}>{statusBadge}</TableCell>
<TableCell
sx={{
fontSize: 14,
textAlign: "right",
color: (theme) =>
latency
? getLatencyColor(theme, latency.latencyMS)
: theme.palette.text.secondary,
}}
>
{latency ? `${latency.latencyMS.toFixed(0)} ms` : "Not available"}
</TableCell>
</TableRow>
</>
)
}
// DetailedProxyStatus allows a more precise status to be displayed.
const DetailedProxyStatus: FC<{
proxy: WorkspaceProxy
}> = ({ proxy }) => {
if (!proxy.status) {
// If the status is null/undefined/not provided, just go with the boolean "healthy" value.
return <ProxyStatus proxy={proxy} />
}
switch (proxy.status.status) {
case "ok":
return <HealthyBadge />
case "unhealthy":
return <NotHealthyBadge />
case "unreachable":
return <NotReachableBadge />
case "unregistered":
return <NotRegisteredBadge />
default:
return <NotHealthyBadge />
}
}
// ProxyStatus will only show "healthy" or "not healthy" status.
const ProxyStatus: FC<{
proxy: Region
}> = ({ proxy }) => {
@@ -88,14 +100,3 @@ const ProxyStatus: FC<{
return icon
}
const useStyles = makeStyles((theme) => ({
preferredrow: {
backgroundColor: alpha(
theme.palette.primary.main,
theme.palette.action.hoverOpacity,
),
outline: `1px solid ${theme.palette.primary.main}`,
outlineOffset: "-1px",
},
}))
@@ -20,7 +20,6 @@ export interface WorkspaceProxyViewProps {
getWorkspaceProxiesError?: Error | unknown
isLoading: boolean
hasLoaded: boolean
onSelect: (proxy: Region) => void
preferredProxy?: Region
selectProxyError?: Error | unknown
}
@@ -33,9 +32,7 @@ export const WorkspaceProxyView: FC<
getWorkspaceProxiesError,
isLoading,
hasLoaded,
onSelect,
selectProxyError,
preferredProxy,
}) => {
return (
<Stack>
@@ -69,10 +66,6 @@ export const WorkspaceProxyView: FC<
latency={proxyLatencies?.[proxy.id]}
key={proxy.id}
proxy={proxy}
onSelectRegion={onSelect}
preferred={
preferredProxy ? proxy.id === preferredProxy.id : false
}
/>
))}
</Cond>
@@ -30,9 +30,6 @@ PrimarySelected.args = {
proxies: MockWorkspaceProxies,
proxyLatencies: MockProxyLatencies,
preferredProxy: MockPrimaryWorkspaceProxy,
onSelect: () => {
return Promise.resolve()
},
}
export const Example = Template.bind({})
@@ -42,9 +39,6 @@ Example.args = {
proxies: MockWorkspaceProxies,
proxyLatencies: MockProxyLatencies,
preferredProxy: MockHealthyWildWorkspaceProxy,
onSelect: () => {
return Promise.resolve()
},
}
export const Loading = Template.bind({})
+37 -4
View File
@@ -71,7 +71,7 @@ export const MockTokens: TypesGen.APIKeyWithOwner[] = [
},
]
export const MockPrimaryWorkspaceProxy: TypesGen.Region = {
export const MockPrimaryWorkspaceProxy: TypesGen.WorkspaceProxy = {
id: "4aa23000-526a-481f-a007-0f20b98b1e12",
name: "primary",
display_name: "Default",
@@ -79,9 +79,16 @@ export const MockPrimaryWorkspaceProxy: TypesGen.Region = {
healthy: true,
path_app_url: "https://coder.com",
wildcard_hostname: "*.coder.com",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted: false,
status: {
status: "ok",
checked_at: new Date().toISOString(),
},
}
export const MockHealthyWildWorkspaceProxy: TypesGen.Region = {
export const MockHealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = {
id: "5e2c1ab7-479b-41a9-92ce-aa85625de52c",
name: "haswildcard",
display_name: "Subdomain Supported",
@@ -89,9 +96,16 @@ export const MockHealthyWildWorkspaceProxy: TypesGen.Region = {
healthy: true,
path_app_url: "https://external.com",
wildcard_hostname: "*.external.com",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted: false,
status: {
status: "ok",
checked_at: new Date().toISOString(),
},
}
export const MockUnhealthyWildWorkspaceProxy: TypesGen.Region = {
export const MockUnhealthyWildWorkspaceProxy: TypesGen.WorkspaceProxy = {
id: "8444931c-0247-4171-842a-569d9f9cbadb",
name: "unhealthy",
display_name: "Unhealthy",
@@ -99,9 +113,20 @@ export const MockUnhealthyWildWorkspaceProxy: TypesGen.Region = {
healthy: false,
path_app_url: "https://unhealthy.coder.com",
wildcard_hostname: "*unhealthy..coder.com",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted: false,
status: {
status: "unhealthy",
report: {
errors: ["This workspace proxy is manually marked as unhealthy."],
warnings: ["This is a manual warning for this workspace proxy."],
},
checked_at: new Date().toISOString(),
},
}
export const MockWorkspaceProxies: TypesGen.Region[] = [
export const MockWorkspaceProxies: TypesGen.WorkspaceProxy[] = [
MockPrimaryWorkspaceProxy,
MockHealthyWildWorkspaceProxy,
MockUnhealthyWildWorkspaceProxy,
@@ -113,6 +138,13 @@ export const MockWorkspaceProxies: TypesGen.Region[] = [
healthy: true,
path_app_url: "https://cowboy.coder.com",
wildcard_hostname: "",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
deleted: false,
status: {
status: "ok",
checked_at: new Date().toISOString(),
},
},
]
@@ -1625,6 +1657,7 @@ export const MockPermissions: Permissions = {
viewUpdateCheck: true,
viewDeploymentStats: true,
viewGitAuthConfig: true,
editWorkspaceProxies: true,
}
export const MockDeploymentConfig: Types.DeploymentConfig = {
+8
View File
@@ -24,6 +24,14 @@ export const handlers = [
}),
)
}),
rest.get("/api/v2/workspaceproxies", async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
regions: M.MockWorkspaceProxies,
}),
)
}),
// build info
rest.get("/api/v2/buildinfo", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockBuildInfo))
+7
View File
@@ -19,6 +19,7 @@ export const checks = {
viewUpdateCheck: "viewUpdateCheck",
viewGitAuthConfig: "viewGitAuthConfig",
viewDeploymentStats: "viewDeploymentStats",
editWorkspaceProxies: "editWorkspaceProxies",
} as const
export const permissionsToCheck = {
@@ -88,6 +89,12 @@ export const permissionsToCheck = {
},
action: "read",
},
[checks.editWorkspaceProxies]: {
object: {
resource_type: "workspace_proxy",
},
action: "create",
},
} as const
export type Permissions = Record<keyof typeof permissionsToCheck, boolean>