mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Workspace Proxy picker show latency to each proxy (#7486)
* chore: Add cors to workspace proxies to allow for latency checks * Add latency check to wsproxy Use performance API timings. - Fix cors and timing headers - Accept custom headers
This commit is contained in:
@@ -806,6 +806,17 @@ func New(options *Options) *API {
|
||||
return []string{}
|
||||
})
|
||||
r.NotFound(cspMW(compressHandler(http.HandlerFunc(api.siteHandler.ServeHTTP))).ServeHTTP)
|
||||
|
||||
// This must be before all middleware to improve the response time.
|
||||
// So make a new router, and mount the old one as the root.
|
||||
rootRouter := chi.NewRouter()
|
||||
// This is the only route we add before all the middleware.
|
||||
// We want to time the latency of the request, so any middleware will
|
||||
// interfere with that timing.
|
||||
rootRouter.Get("/latency-check", LatencyCheck(api.AccessURL))
|
||||
rootRouter.Mount("/", r)
|
||||
api.RootHandler = rootRouter
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,15 @@ func TestDERPLatencyCheck(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestFastLatencyCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
res, err := client.Request(context.Background(), http.MethodGet, "/latency-check", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func LatencyCheck(allowedOrigins ...*url.URL) http.HandlerFunc {
|
||||
allowed := make([]string, 0, len(allowedOrigins))
|
||||
for _, origin := range allowedOrigins {
|
||||
// Allow the origin without a path
|
||||
tmp := *origin
|
||||
tmp.Path = ""
|
||||
allowed = append(allowed, strings.TrimSuffix(origin.String(), "/"))
|
||||
}
|
||||
origins := strings.Join(allowed, ",")
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Allowing timing information to be shared. This allows the browser
|
||||
// to exclude TLS handshake timing.
|
||||
rw.Header().Set("Timing-Allow-Origin", origins)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,7 @@ func TestRegions(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("GoingAway", func(t *testing.T) {
|
||||
t.Skip("This is flakey in CI because it relies on internal go routine timing. Should refactor.")
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/buildinfo"
|
||||
"github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
@@ -186,6 +187,21 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
|
||||
SecureAuthCookie: opts.SecureAuthCookie,
|
||||
}
|
||||
|
||||
// The primary coderd dashboard needs to make some GET requests to
|
||||
// the workspace proxies to check latency.
|
||||
corsMW := cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{
|
||||
// Allow the dashboard to make requests to the proxy for latency
|
||||
// checks.
|
||||
opts.DashboardURL.String(),
|
||||
},
|
||||
// Only allow GET requests for latency checks.
|
||||
AllowedMethods: []string{http.MethodOptions, http.MethodGet},
|
||||
AllowedHeaders: []string{"Accept", "Content-Type", "X-LATENCY-CHECK", "X-CSRF-TOKEN"},
|
||||
// Do not send any cookies
|
||||
AllowCredentials: false,
|
||||
})
|
||||
|
||||
// Routes
|
||||
apiRateLimiter := httpmw.RateLimit(opts.APIRateLimit, time.Minute)
|
||||
// Persistent middlewares to all routes
|
||||
@@ -198,20 +214,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
|
||||
httpmw.ExtractRealIP(s.Options.RealIPConfig),
|
||||
httpmw.Logger(s.Logger),
|
||||
httpmw.Prometheus(s.PrometheusRegistry),
|
||||
// The primary coderd dashboard needs to make some GET requests to
|
||||
// the workspace proxies to check latency.
|
||||
cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{
|
||||
// Allow the dashboard to make requests to the proxy for latency
|
||||
// checks.
|
||||
opts.DashboardURL.String(),
|
||||
},
|
||||
// Only allow GET requests for latency checks.
|
||||
AllowedMethods: []string{http.MethodGet},
|
||||
AllowedHeaders: []string{"Accept", "Content-Type"},
|
||||
// Do not send any cookies
|
||||
AllowCredentials: false,
|
||||
}),
|
||||
corsMW,
|
||||
|
||||
// HandleSubdomain is a middleware that handles all requests to the
|
||||
// subdomain-based workspace apps.
|
||||
@@ -260,6 +263,13 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
|
||||
})
|
||||
})
|
||||
|
||||
// See coderd/coderd.go for why we need this.
|
||||
rootRouter := chi.NewRouter()
|
||||
// Make sure to add the cors middleware to the latency check route.
|
||||
rootRouter.Get("/latency-check", corsMW(coderd.LatencyCheck(s.DashboardURL, s.AppServer.AccessURL)).ServeHTTP)
|
||||
rootRouter.Mount("/", r)
|
||||
s.Handler = rootRouter
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,33 @@ import "jest-location-mock"
|
||||
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"
|
||||
|
||||
jestFetchMock.enableMocks()
|
||||
|
||||
// useProxyLatency does some http requests to determine latency.
|
||||
// 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) => {
|
||||
if (!proxies) {
|
||||
return {} as Record<string, ProxyLatencyReport>
|
||||
}
|
||||
|
||||
return proxies.regions.reduce((acc, proxy) => {
|
||||
acc[proxy.id] = {
|
||||
accurate: true,
|
||||
// Return a constant latency of 8ms.
|
||||
// If you make this random it could break stories.
|
||||
latencyMS: 8,
|
||||
at: new Date(),
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, ProxyLatencyReport>)
|
||||
},
|
||||
}))
|
||||
|
||||
global.TextEncoder = TextEncoder
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||
global.TextDecoder = TextDecoder as any
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "1.0.5",
|
||||
"@emoji-mart/react": "1.0.1",
|
||||
"@fastly/performance-observer-polyfill": "^2.0.0",
|
||||
"@emotion/react": "^11.10.8",
|
||||
"@emotion/styled": "^11.10.8",
|
||||
"@fontsource/ibm-plex-mono": "4.5.10",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MockWorkspace,
|
||||
MockWorkspaceAgent,
|
||||
MockWorkspaceApp,
|
||||
MockProxyLatencies,
|
||||
} from "testHelpers/entities"
|
||||
import { AppLink, AppLinkProps } from "./AppLink"
|
||||
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
|
||||
@@ -17,6 +18,7 @@ export default {
|
||||
const Template: Story<AppLinkProps> = (args) => (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: getPreferredProxy(MockWorkspaceProxies, MockPrimaryWorkspaceProxy),
|
||||
proxies: MockWorkspaceProxies,
|
||||
isLoading: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState, FC } from "react"
|
||||
import { makeStyles, useTheme } from "@mui/styles"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
import {
|
||||
HelpTooltipText,
|
||||
HelpPopover,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
} from "components/Tooltips/HelpTooltip"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { WorkspaceAgent, DERPRegion } from "api/typesGenerated"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
import { getLatencyColor } from "utils/colors"
|
||||
|
||||
const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
|
||||
// Find the right latency to display
|
||||
@@ -22,17 +23,9 @@ const getDisplayLatency = (theme: Theme, agent: WorkspaceAgent) => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Get the color
|
||||
let color = theme.palette.success.light
|
||||
if (latency.latency_ms >= 150 && latency.latency_ms < 300) {
|
||||
color = theme.palette.warning.light
|
||||
} else if (latency.latency_ms >= 300) {
|
||||
color = theme.palette.error.light
|
||||
}
|
||||
|
||||
return {
|
||||
...latency,
|
||||
color,
|
||||
color: getLatencyColor(theme, latency.latency_ms),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MockWorkspaceAgentStartTimeout,
|
||||
MockWorkspaceAgentTimeout,
|
||||
MockWorkspaceApp,
|
||||
MockProxyLatencies,
|
||||
} from "testHelpers/entities"
|
||||
import { AgentRow, AgentRowProps } from "./AgentRow"
|
||||
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
|
||||
@@ -56,6 +57,7 @@ const TemplateFC = (
|
||||
return (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: getPreferredProxy(proxies, selectedProxy),
|
||||
proxies: proxies,
|
||||
isLoading: false,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { MockWorkspace, MockWorkspaceResource } from "testHelpers/entities"
|
||||
import {
|
||||
MockProxyLatencies,
|
||||
MockWorkspace,
|
||||
MockWorkspaceResource,
|
||||
} from "testHelpers/entities"
|
||||
import { AgentRow } from "./AgentRow"
|
||||
import { ResourceCard, ResourceCardProps } from "./ResourceCard"
|
||||
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
|
||||
@@ -18,6 +22,7 @@ Example.args = {
|
||||
agentRow: (agent) => (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: getPreferredProxy([], undefined),
|
||||
proxies: [],
|
||||
isLoading: false,
|
||||
@@ -84,6 +89,7 @@ BunchOfMetadata.args = {
|
||||
agentRow: (agent) => (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: getPreferredProxy([], undefined),
|
||||
proxies: [],
|
||||
isLoading: false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace"
|
||||
import { withReactContext } from "storybook-react-context"
|
||||
import EventSource from "eventsourcemock"
|
||||
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
|
||||
import { MockProxyLatencies } from "../../testHelpers/entities"
|
||||
|
||||
export default {
|
||||
title: "components/Workspace",
|
||||
@@ -26,6 +27,7 @@ export default {
|
||||
const Template: Story<WorkspaceProps> = (args) => (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: getPreferredProxy([], undefined),
|
||||
proxies: [],
|
||||
isLoading: false,
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
useContext,
|
||||
useState,
|
||||
} from "react"
|
||||
import { ProxyLatencyReport, useProxyLatency } from "./useProxyLatency"
|
||||
|
||||
interface ProxyContextValue {
|
||||
proxy: PreferredProxy
|
||||
proxies?: Region[]
|
||||
proxyLatencies?: Record<string, ProxyLatencyReport>
|
||||
// isfetched is true when the proxy api call is complete.
|
||||
isFetched: boolean
|
||||
// isLoading is true if the proxy is in the process of being fetched.
|
||||
@@ -72,6 +74,10 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
},
|
||||
})
|
||||
|
||||
// Everytime we get a new proxiesResponse, update the latency check
|
||||
// to each workspace proxy.
|
||||
const proxyLatencies = useProxyLatency(proxiesResp)
|
||||
|
||||
const setAndSaveProxy = (
|
||||
selectedProxy?: Region,
|
||||
// By default the proxies come from the api call above.
|
||||
@@ -95,6 +101,7 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: proxyLatencies,
|
||||
proxy: experimentEnabled
|
||||
? proxy
|
||||
: {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Region, RegionsResponse } from "api/typesGenerated"
|
||||
import { useEffect, useReducer } from "react"
|
||||
import PerformanceObserver from "@fastly/performance-observer-polyfill"
|
||||
import axios from "axios"
|
||||
import { generateRandomString } from "utils/random"
|
||||
|
||||
export interface ProxyLatencyReport {
|
||||
// accurate identifies if the latency was calculated using the
|
||||
// PerformanceResourceTiming API. If this is false, then the
|
||||
// latency is calculated using the total duration of the request
|
||||
// and will be off by a good margin.
|
||||
accurate: boolean
|
||||
latencyMS: number
|
||||
// at is when the latency was recorded.
|
||||
at: Date
|
||||
}
|
||||
|
||||
interface ProxyLatencyAction {
|
||||
proxyID: string
|
||||
report: ProxyLatencyReport
|
||||
}
|
||||
|
||||
const proxyLatenciesReducer = (
|
||||
state: Record<string, ProxyLatencyReport>,
|
||||
action: ProxyLatencyAction,
|
||||
): Record<string, ProxyLatencyReport> => {
|
||||
// Just overwrite any existing latency.
|
||||
state[action.proxyID] = action.report
|
||||
return state
|
||||
}
|
||||
|
||||
export const useProxyLatency = (
|
||||
proxies?: RegionsResponse,
|
||||
): Record<string, ProxyLatencyReport> => {
|
||||
const [proxyLatencies, dispatchProxyLatencies] = useReducer(
|
||||
proxyLatenciesReducer,
|
||||
{},
|
||||
)
|
||||
|
||||
// Only run latency updates when the proxies change.
|
||||
useEffect(() => {
|
||||
if (!proxies) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
// Only run the latency check on healthy proxies.
|
||||
if (!proxy.healthy) {
|
||||
return acc
|
||||
}
|
||||
|
||||
// Add a random query param to the url to make sure we don't get a cached response.
|
||||
// This is important in case there is some caching layer between us and the proxy.
|
||||
const url = new URL(
|
||||
`/latency-check?cache_bust=${generateRandomString(6)}`,
|
||||
proxy.path_app_url,
|
||||
)
|
||||
acc[url.toString()] = proxy
|
||||
return acc
|
||||
}, {} as Record<string, Region>)
|
||||
|
||||
// dispatchProxyLatenciesGuarded will assign the latency to the proxy
|
||||
// via the reducer. But it will only do so if the performance entry is
|
||||
// a resource entry that we care about.
|
||||
const dispatchProxyLatenciesGuarded = (entry: PerformanceEntry): void => {
|
||||
if (entry.entryType !== "resource") {
|
||||
// We should never get these, but just in case.
|
||||
return
|
||||
}
|
||||
|
||||
// The entry.name is the url of the request.
|
||||
const check = proxyChecks[entry.name]
|
||||
if (!check) {
|
||||
// This is not a proxy request, so ignore it.
|
||||
return
|
||||
}
|
||||
|
||||
// These docs are super useful.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Resource_timing
|
||||
let latencyMS = 0
|
||||
let accurate = false
|
||||
if (
|
||||
"requestStart" in entry &&
|
||||
(entry as PerformanceResourceTiming).requestStart !== 0
|
||||
) {
|
||||
// This is the preferred logic to get the latency.
|
||||
const timingEntry = entry as PerformanceResourceTiming
|
||||
latencyMS = timingEntry.responseStart - timingEntry.requestStart
|
||||
accurate = true
|
||||
} else {
|
||||
// This is the total duration of the request and will be off by a good margin.
|
||||
// This is a fallback if the better timing is not available.
|
||||
// eslint-disable-next-line no-console -- We can remove this when we display the "accurate" bool on the UI
|
||||
console.log(
|
||||
`Using fallback latency calculation for "${entry.name}". Latency will be incorrect and larger then actual.`,
|
||||
)
|
||||
latencyMS = entry.duration
|
||||
}
|
||||
dispatchProxyLatencies({
|
||||
proxyID: check.id,
|
||||
report: {
|
||||
latencyMS,
|
||||
accurate,
|
||||
at: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Start a new performance observer to record of all the requests
|
||||
// to the proxies.
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
// If we get entries via this callback, then dispatch the events to the latency reducer.
|
||||
list.getEntries().forEach((entry) => {
|
||||
dispatchProxyLatenciesGuarded(entry)
|
||||
})
|
||||
})
|
||||
|
||||
// The resource requests include xmlhttp requests.
|
||||
observer.observe({ entryTypes: ["resource"] })
|
||||
|
||||
const proxyRequests = Object.keys(proxyChecks).map((latencyURL) => {
|
||||
return axios.get(latencyURL, {
|
||||
withCredentials: false,
|
||||
// Must add a custom header to make the request not a "simple request"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
|
||||
headers: { "X-LATENCY-CHECK": "true" },
|
||||
})
|
||||
})
|
||||
|
||||
// When all the proxy requests finish
|
||||
Promise.all(proxyRequests)
|
||||
// TODO: If there is an error on any request, we might want to store some indicator of that?
|
||||
.finally(() => {
|
||||
// takeRecords will return any entries that were not called via the callback yet.
|
||||
// We want to call this before we disconnect the observer to make sure we get all the
|
||||
// proxy requests recorded.
|
||||
observer.takeRecords().forEach((entry) => {
|
||||
dispatchProxyLatenciesGuarded(entry)
|
||||
})
|
||||
// At this point, we can be confident that all the proxy requests have been recorded
|
||||
// via the performance observer. So we can disconnect the observer.
|
||||
observer.disconnect()
|
||||
})
|
||||
}, [proxies])
|
||||
|
||||
return proxyLatencies
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import WS from "jest-websocket-mock"
|
||||
import { rest } from "msw"
|
||||
import {
|
||||
MockPrimaryWorkspaceProxy,
|
||||
MockProxyLatencies,
|
||||
MockWorkspace,
|
||||
MockWorkspaceAgent,
|
||||
MockWorkspaceProxies,
|
||||
@@ -43,6 +44,7 @@ const renderTerminal = () => {
|
||||
element={
|
||||
<ProxyContext.Provider
|
||||
value={{
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
proxy: {
|
||||
selectedProxy: MockPrimaryWorkspaceProxy,
|
||||
preferredPathAppURL: "",
|
||||
|
||||
@@ -14,6 +14,7 @@ export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
|
||||
"This selection only affects browser connections to your workspace."
|
||||
|
||||
const {
|
||||
proxyLatencies,
|
||||
proxies,
|
||||
error: proxiesError,
|
||||
isFetched: proxiesFetched,
|
||||
@@ -30,6 +31,7 @@ export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
|
||||
layout="fluid"
|
||||
>
|
||||
<WorkspaceProxyView
|
||||
proxyLatencies={proxyLatencies}
|
||||
proxies={proxies}
|
||||
isLoading={proxiesLoading}
|
||||
hasLoaded={proxiesFetched}
|
||||
|
||||
@@ -9,15 +9,19 @@ import {
|
||||
HealthyBadge,
|
||||
NotHealthyBadge,
|
||||
} from "components/DeploySettingsLayout/Badges"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { makeStyles, useTheme } from "@mui/styles"
|
||||
import { combineClasses } from "utils/combineClasses"
|
||||
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
||||
import { getLatencyColor } from "utils/colors"
|
||||
|
||||
export const ProxyRow: FC<{
|
||||
latency?: ProxyLatencyReport
|
||||
proxy: Region
|
||||
onSelectRegion: (proxy: Region) => void
|
||||
preferred: boolean
|
||||
}> = ({ proxy, onSelectRegion, preferred }) => {
|
||||
}> = ({ proxy, onSelectRegion, preferred, latency }) => {
|
||||
const styles = useStyles()
|
||||
const theme = useTheme()
|
||||
|
||||
const clickable = useClickableTableRow(() => {
|
||||
onSelectRegion(proxy)
|
||||
@@ -53,6 +57,15 @@ export const ProxyRow: FC<{
|
||||
<TableCell>
|
||||
<ProxyStatus proxy={proxy} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
style={{
|
||||
color: latency ? getLatencyColor(theme, latency.latencyMS) : "",
|
||||
}}
|
||||
>
|
||||
{latency ? `${latency.latencyMS.toFixed(1)} ms` : "?"}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import { FC } from "react"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { Region } from "api/typesGenerated"
|
||||
import { ProxyRow } from "./WorkspaceProxyRow"
|
||||
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
||||
|
||||
export interface WorkspaceProxyViewProps {
|
||||
proxies?: Region[]
|
||||
proxyLatencies?: Record<string, ProxyLatencyReport>
|
||||
getWorkspaceProxiesError?: Error | unknown
|
||||
isLoading: boolean
|
||||
hasLoaded: boolean
|
||||
@@ -27,6 +29,7 @@ export const WorkspaceProxyView: FC<
|
||||
React.PropsWithChildren<WorkspaceProxyViewProps>
|
||||
> = ({
|
||||
proxies,
|
||||
proxyLatencies,
|
||||
getWorkspaceProxiesError,
|
||||
isLoading,
|
||||
hasLoaded,
|
||||
@@ -49,6 +52,7 @@ export const WorkspaceProxyView: FC<
|
||||
<TableCell width="40%">Proxy</TableCell>
|
||||
<TableCell width="30%">URL</TableCell>
|
||||
<TableCell width="10%">Status</TableCell>
|
||||
<TableCell width="20%">Latency</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -62,6 +66,7 @@ export const WorkspaceProxyView: FC<
|
||||
<Cond>
|
||||
{proxies?.map((proxy) => (
|
||||
<ProxyRow
|
||||
latency={proxyLatencies?.[proxy.id]}
|
||||
key={proxy.id}
|
||||
proxy={proxy}
|
||||
onSelectRegion={onSelect}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MockWorkspaceProxies,
|
||||
MockPrimaryWorkspaceProxy,
|
||||
MockHealthyWildWorkspaceProxy,
|
||||
MockProxyLatencies,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
WorkspaceProxyView,
|
||||
@@ -27,6 +28,7 @@ PrimarySelected.args = {
|
||||
isLoading: false,
|
||||
hasLoaded: true,
|
||||
proxies: MockWorkspaceProxies,
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
preferredProxy: MockPrimaryWorkspaceProxy,
|
||||
onSelect: () => {
|
||||
return Promise.resolve()
|
||||
@@ -38,6 +40,7 @@ Example.args = {
|
||||
isLoading: false,
|
||||
hasLoaded: true,
|
||||
proxies: MockWorkspaceProxies,
|
||||
proxyLatencies: MockProxyLatencies,
|
||||
preferredProxy: MockHealthyWildWorkspaceProxy,
|
||||
onSelect: () => {
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -7,6 +7,7 @@ import range from "lodash/range"
|
||||
import { Permissions } from "xServices/auth/authXService"
|
||||
import { TemplateVersionFiles } from "utils/templateVersion"
|
||||
import { FileTree } from "utils/filetree"
|
||||
import { ProxyLatencyReport } from "contexts/useProxyLatency"
|
||||
|
||||
export const MockOrganization: TypesGen.Organization = {
|
||||
id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0",
|
||||
@@ -113,6 +114,34 @@ export const MockWorkspaceProxies: TypesGen.Region[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const MockProxyLatencies: Record<string, ProxyLatencyReport> = {
|
||||
...MockWorkspaceProxies.reduce((acc, proxy) => {
|
||||
if (!proxy.healthy) {
|
||||
return acc
|
||||
}
|
||||
acc[proxy.id] = {
|
||||
// Make one of them inaccurate.
|
||||
accurate: proxy.id !== "26e84c16-db24-4636-a62d-aa1a4232b858",
|
||||
// This is a deterministic way to generate a latency to for each proxy.
|
||||
// It will be the same for each run as long as the IDs don't change.
|
||||
latencyMS:
|
||||
(Number(
|
||||
Array.from(proxy.id).reduce(
|
||||
// Multiply each char code by some large prime number to increase the
|
||||
// size of the number and allow use to get some decimal points.
|
||||
(acc, char) => acc + char.charCodeAt(0) * 37,
|
||||
0,
|
||||
),
|
||||
) /
|
||||
// Cap at 250ms
|
||||
100) %
|
||||
250,
|
||||
at: new Date(),
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, ProxyLatencyReport>),
|
||||
}
|
||||
|
||||
export const MockBuildInfo: TypesGen.BuildInfoResponse = {
|
||||
external_url: "file:///mock-url",
|
||||
version: "v99.999.9999+c9cdf14",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Theme } from "@mui/material/styles"
|
||||
|
||||
// Used to convert our theme colors to Hex since monaco theme only support hex colors
|
||||
// From https://www.jameslmilner.com/posts/converting-rgb-hex-hsl-colors/
|
||||
export function hslToHex(hsl: string): string {
|
||||
@@ -21,3 +23,15 @@ export function hslToHex(hsl: string): string {
|
||||
}
|
||||
return `#${f(0)}${f(8)}${f(4)}`
|
||||
}
|
||||
|
||||
// getLatencyColor is the text color to use for a given latency
|
||||
// in milliseconds.
|
||||
export const getLatencyColor = (theme: Theme, latency: number) => {
|
||||
let color = theme.palette.success.light
|
||||
if (latency >= 150 && latency < 300) {
|
||||
color = theme.palette.warning.light
|
||||
} else if (latency >= 300) {
|
||||
color = theme.palette.error.light
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
+8
-1
@@ -1458,6 +1458,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4"
|
||||
integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==
|
||||
|
||||
"@fastly/performance-observer-polyfill@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@fastly/performance-observer-polyfill/-/performance-observer-polyfill-2.0.0.tgz#fb697180f92019119d8c55d20216adce6436f941"
|
||||
integrity sha512-cQC4E6ReYY4Vud+eCJSCr1N0dSz+fk7xJlLiSgPFDHbnFLZo5DenazoersMt9D8JkEhl9Z5ZwJ/8apcjSrdb8Q==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
"@fontsource/ibm-plex-mono@4.5.10":
|
||||
version "4.5.10"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/ibm-plex-mono/-/ibm-plex-mono-4.5.10.tgz#25d004646853bf46b3787341300662fe61f8ad78"
|
||||
@@ -11715,7 +11722,7 @@ tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.3:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0:
|
||||
tslib@^2, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
|
||||
|
||||
Reference in New Issue
Block a user