mirror of https://github.com/coder/coder.git
feat(site): add health warning and a health monitor page (#8844)
This commit is contained in:
parent
44f9b0228a
commit
cf35c0dfc5
|
@ -8024,7 +8024,8 @@ const docTemplate = `{
|
|||
"tailnet_pg_coordinator",
|
||||
"single_tailnet",
|
||||
"template_restart_requirement",
|
||||
"template_insights_page"
|
||||
"template_insights_page",
|
||||
"deployment_health_page"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExperimentMoons",
|
||||
|
@ -8032,7 +8033,8 @@ const docTemplate = `{
|
|||
"ExperimentTailnetPGCoordinator",
|
||||
"ExperimentSingleTailnet",
|
||||
"ExperimentTemplateRestartRequirement",
|
||||
"ExperimentTemplateInsightsPage"
|
||||
"ExperimentTemplateInsightsPage",
|
||||
"ExperimentDeploymentHealthPage"
|
||||
]
|
||||
},
|
||||
"codersdk.Feature": {
|
||||
|
|
|
@ -7185,7 +7185,8 @@
|
|||
"tailnet_pg_coordinator",
|
||||
"single_tailnet",
|
||||
"template_restart_requirement",
|
||||
"template_insights_page"
|
||||
"template_insights_page",
|
||||
"deployment_health_page"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExperimentMoons",
|
||||
|
@ -7193,7 +7194,8 @@
|
|||
"ExperimentTailnetPGCoordinator",
|
||||
"ExperimentSingleTailnet",
|
||||
"ExperimentTemplateRestartRequirement",
|
||||
"ExperimentTemplateInsightsPage"
|
||||
"ExperimentTemplateInsightsPage",
|
||||
"ExperimentDeploymentHealthPage"
|
||||
]
|
||||
},
|
||||
"codersdk.Feature": {
|
||||
|
|
|
@ -1871,6 +1871,9 @@ const (
|
|||
// Insights page
|
||||
ExperimentTemplateInsightsPage Experiment = "template_insights_page"
|
||||
|
||||
// Deployment health page
|
||||
ExperimentDeploymentHealthPage Experiment = "deployment_health_page"
|
||||
|
||||
// Add new experiments here!
|
||||
// ExperimentExample Experiment = "example"
|
||||
)
|
||||
|
@ -1881,6 +1884,7 @@ const (
|
|||
// not be included here and will be essentially hidden.
|
||||
var ExperimentsAll = Experiments{
|
||||
ExperimentTemplateInsightsPage,
|
||||
ExperimentDeploymentHealthPage,
|
||||
}
|
||||
|
||||
// Experiments is a list of experiments that are enabled for the deployment.
|
||||
|
|
|
@ -2677,6 +2677,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| `single_tailnet` |
|
||||
| `template_restart_requirement` |
|
||||
| `template_insights_page` |
|
||||
| `deployment_health_page` |
|
||||
|
||||
## codersdk.Feature
|
||||
|
||||
|
|
|
@ -183,6 +183,7 @@ const TemplateInsightsPage = lazy(
|
|||
() =>
|
||||
import("./pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage"),
|
||||
)
|
||||
const HealthPage = lazy(() => import("./pages/HealthPage/HealthPage"))
|
||||
|
||||
export const AppRouter: FC = () => {
|
||||
return (
|
||||
|
@ -197,6 +198,8 @@ export const AppRouter: FC = () => {
|
|||
<Route element={<DashboardLayout />}>
|
||||
<Route index element={<IndexPage />} />
|
||||
|
||||
<Route path="health" element={<HealthPage />} />
|
||||
|
||||
<Route path="gitauth/:provider" element={<GitAuthPage />} />
|
||||
|
||||
<Route path="workspaces" element={<WorkspacesPage />} />
|
||||
|
|
|
@ -1405,3 +1405,15 @@ export const getInsightsTemplate = async (
|
|||
const response = await axios.get(`/api/v2/insights/templates?${params}`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getHealth = () => {
|
||||
return axios.get<{
|
||||
healthy: boolean
|
||||
time: string
|
||||
coder_version: string
|
||||
derp: { healthy: boolean }
|
||||
access_url: { healthy: boolean }
|
||||
websocket: { healthy: boolean }
|
||||
database: { healthy: boolean }
|
||||
}>("/api/v2/debug/health")
|
||||
}
|
||||
|
|
|
@ -1563,6 +1563,7 @@ export const Entitlements: Entitlement[] = [
|
|||
|
||||
// From codersdk/deployment.go
|
||||
export type Experiment =
|
||||
| "deployment_health_page"
|
||||
| "moons"
|
||||
| "single_tailnet"
|
||||
| "tailnet_pg_coordinator"
|
||||
|
@ -1570,6 +1571,7 @@ export type Experiment =
|
|||
| "template_restart_requirement"
|
||||
| "workspace_actions"
|
||||
export const Experiments: Experiment[] = [
|
||||
"deployment_health_page",
|
||||
"moons",
|
||||
"single_tailnet",
|
||||
"tailnet_pg_coordinator",
|
||||
|
|
|
@ -16,6 +16,7 @@ import Box from "@mui/material/Box"
|
|||
import InfoOutlined from "@mui/icons-material/InfoOutlined"
|
||||
import Button from "@mui/material/Button"
|
||||
import { docs } from "utils/docs"
|
||||
import { HealthBanner } from "./HealthBanner"
|
||||
|
||||
export const DashboardLayout: FC = () => {
|
||||
const styles = useStyles()
|
||||
|
@ -30,6 +31,7 @@ export const DashboardLayout: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<HealthBanner />
|
||||
<ServiceBanner />
|
||||
{canViewDeployment && <LicenseBanner />}
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { Alert } from "components/Alert/Alert"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import Link from "@mui/material/Link"
|
||||
import { colors } from "theme/colors"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getHealth } from "api/api"
|
||||
import { useDashboard } from "./DashboardProvider"
|
||||
|
||||
export const HealthBanner = () => {
|
||||
const { data: healthStatus } = useQuery({
|
||||
queryKey: ["health"],
|
||||
queryFn: () => getHealth(),
|
||||
})
|
||||
const dashboard = useDashboard()
|
||||
const hasHealthIssues = healthStatus && !healthStatus.data.healthy
|
||||
|
||||
if (
|
||||
dashboard.experiments.includes("deployment_health_page") &&
|
||||
hasHealthIssues
|
||||
) {
|
||||
return (
|
||||
<Alert
|
||||
severity="error"
|
||||
variant="filled"
|
||||
sx={{
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
backgroundColor: colors.red[10],
|
||||
}}
|
||||
>
|
||||
We have detected problems with your Coder deployment. Please{" "}
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/health"
|
||||
sx={{ fontWeight: 600, color: "inherit" }}
|
||||
>
|
||||
inspect the health status
|
||||
</Link>
|
||||
.
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
|
@ -6,6 +6,7 @@ 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 MonitorHeartOutlined from "@mui/icons-material/MonitorHeartOutlined"
|
||||
import { GitIcon } from "components/Icons/GitIcon"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { ElementType, PropsWithChildren, ReactNode, FC } from "react"
|
||||
|
@ -93,6 +94,14 @@ export const Sidebar: React.FC = () => {
|
|||
>
|
||||
Security
|
||||
</SidebarNavItem>
|
||||
{dashboard.experiments.includes("deployment_health_page") && (
|
||||
<SidebarNavItem
|
||||
href="/health"
|
||||
icon={<SidebarNavItemIcon icon={MonitorHeartOutlined} />}
|
||||
>
|
||||
Health
|
||||
</SidebarNavItem>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { makeStyles } from "@mui/styles"
|
||||
import { FC, PropsWithChildren } from "react"
|
||||
import { combineClasses } from "utils/combineClasses"
|
||||
|
||||
export const FullWidthPageHeader: FC<PropsWithChildren> = ({ children }) => {
|
||||
export const FullWidthPageHeader: FC<
|
||||
PropsWithChildren & { sticky?: boolean }
|
||||
> = ({ children, sticky = true }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<header className={styles.header} data-testid="header">
|
||||
<header
|
||||
className={combineClasses([styles.header, sticky ? styles.sticky : ""])}
|
||||
data-testid="header"
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
)
|
||||
|
@ -35,8 +41,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(6),
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
|
||||
zIndex: 10,
|
||||
flexWrap: "wrap",
|
||||
|
||||
|
@ -48,6 +53,10 @@ const useStyles = makeStyles((theme) => ({
|
|||
flexDirection: "column",
|
||||
},
|
||||
},
|
||||
sticky: {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
},
|
||||
actions: {
|
||||
marginLeft: "auto",
|
||||
[theme.breakpoints.down("md")]: {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import Box from "@mui/material/Box"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { ComponentProps, FC, PropsWithChildren } from "react"
|
||||
import { combineClasses } from "utils/combineClasses"
|
||||
|
||||
export const Stats: FC<ComponentProps<"div">> = (props) => {
|
||||
export const Stats: FC<ComponentProps<typeof Box>> = (props) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<div
|
||||
<Box
|
||||
{...props}
|
||||
className={combineClasses([styles.stats, props.className])}
|
||||
/>
|
||||
|
@ -16,18 +17,18 @@ export const StatsItem: FC<
|
|||
{
|
||||
label: string
|
||||
value: string | number | JSX.Element
|
||||
} & ComponentProps<"div">
|
||||
} & ComponentProps<typeof Box>
|
||||
> = ({ label, value, ...divProps }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div
|
||||
<Box
|
||||
{...divProps}
|
||||
className={combineClasses([styles.statItem, divProps.className])}
|
||||
>
|
||||
<span className={styles.statsLabel}>{label}:</span>
|
||||
<span className={styles.statsValue}>{value}</span>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC } from "react"
|
||||
import { ComponentProps, FC } from "react"
|
||||
import Editor, { DiffEditor, loader } from "@monaco-editor/react"
|
||||
import * as monaco from "monaco-editor"
|
||||
import { useCoderTheme } from "./coderTheme"
|
||||
|
@ -9,8 +9,10 @@ loader.config({ monaco })
|
|||
export const SyntaxHighlighter: FC<{
|
||||
value: string
|
||||
language: string
|
||||
editorProps?: ComponentProps<typeof Editor> &
|
||||
ComponentProps<typeof DiffEditor>
|
||||
compareWith?: string
|
||||
}> = ({ value, compareWith, language }) => {
|
||||
}> = ({ value, compareWith, language, editorProps }) => {
|
||||
const styles = useStyles()
|
||||
const hasDiff = compareWith && value !== compareWith
|
||||
const coderTheme = useCoderTheme()
|
||||
|
@ -25,6 +27,7 @@ export const SyntaxHighlighter: FC<{
|
|||
renderSideBySide: true,
|
||||
readOnly: true,
|
||||
},
|
||||
...editorProps,
|
||||
}
|
||||
|
||||
if (coderTheme.isLoading) {
|
||||
|
@ -46,5 +49,6 @@ const useStyles = makeStyles((theme) => ({
|
|||
wrapper: {
|
||||
padding: theme.spacing(1, 0),
|
||||
background: theme.palette.background.paper,
|
||||
height: "100%",
|
||||
},
|
||||
}))
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import { HealthPageView } from "./HealthPage"
|
||||
import { MockHealth } from "testHelpers/entities"
|
||||
|
||||
const meta: Meta<typeof HealthPageView> = {
|
||||
title: "pages/HealthPageView",
|
||||
component: HealthPageView,
|
||||
args: {
|
||||
tab: {
|
||||
value: "derp",
|
||||
set: () => {},
|
||||
},
|
||||
healthStatus: MockHealth,
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof HealthPageView>
|
||||
|
||||
export const HealthPage: Story = {}
|
||||
|
||||
export const UnhealthPage: Story = {
|
||||
args: {
|
||||
healthStatus: {
|
||||
...MockHealth,
|
||||
healthy: false,
|
||||
derp: {
|
||||
...MockHealth.derp,
|
||||
healthy: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
import Box from "@mui/material/Box"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getHealth } from "api/api"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { useTab } from "hooks"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { pageTitle } from "utils/page"
|
||||
import { colors } from "theme/colors"
|
||||
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline"
|
||||
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import {
|
||||
FullWidthPageHeader,
|
||||
PageHeaderTitle,
|
||||
PageHeaderSubtitle,
|
||||
} from "components/PageHeader/FullWidthPageHeader"
|
||||
import { Stats, StatsItem } from "components/Stats/Stats"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import { createDayString } from "utils/createDayString"
|
||||
|
||||
const sections = {
|
||||
derp: "DERP",
|
||||
access_url: "Access URL",
|
||||
websocket: "Websocket",
|
||||
database: "Database",
|
||||
} as const
|
||||
|
||||
export default function HealthPage() {
|
||||
const tab = useTab("tab", "derp")
|
||||
const { data: healthStatus } = useQuery({
|
||||
queryKey: ["health"],
|
||||
queryFn: () => getHealth(),
|
||||
refetchInterval: 10_000,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Health")}</title>
|
||||
</Helmet>
|
||||
|
||||
{healthStatus ? (
|
||||
<HealthPageView healthStatus={healthStatus.data} tab={tab} />
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function HealthPageView({
|
||||
healthStatus,
|
||||
tab,
|
||||
}: {
|
||||
healthStatus: Awaited<ReturnType<typeof getHealth>>["data"]
|
||||
tab: ReturnType<typeof useTab>
|
||||
}) {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "calc(100vh - 62px - 36px)",
|
||||
overflow: "hidden",
|
||||
// Remove padding added from dashboard layout (.siteContent)
|
||||
marginBottom: "-48px",
|
||||
}}
|
||||
>
|
||||
<FullWidthPageHeader sticky={false}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
{healthStatus.healthy ? (
|
||||
<CheckCircleOutlined
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
color: (theme) => theme.palette.success.light,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ErrorOutline
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<PageHeaderTitle>
|
||||
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
{healthStatus.healthy
|
||||
? "All systems operational"
|
||||
: "Some issues have been detected"}
|
||||
</PageHeaderSubtitle>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stats aria-label="Deployment details" className={styles.stats}>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Last check"
|
||||
value={createDayString(healthStatus.time)}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Coder version"
|
||||
value={healthStatus.coder_version}
|
||||
/>
|
||||
</Stats>
|
||||
</FullWidthPageHeader>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "start",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: (theme) => theme.spacing(32),
|
||||
flexShrink: 0,
|
||||
borderRight: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
padding: (theme) => theme.spacing(1.5, 3),
|
||||
letterSpacing: "0.5px",
|
||||
}}
|
||||
>
|
||||
Health
|
||||
</Box>
|
||||
<Box component="nav">
|
||||
{Object.keys(sections)
|
||||
.sort()
|
||||
.map((key) => {
|
||||
const label = sections[key as keyof typeof sections]
|
||||
const isActive = tab.value === key
|
||||
const isHealthy =
|
||||
healthStatus[key as keyof typeof sections].healthy
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="button"
|
||||
key={key}
|
||||
onClick={() => {
|
||||
tab.set(key)
|
||||
}}
|
||||
sx={{
|
||||
background: isActive ? colors.gray[13] : "none",
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
textAlign: "left",
|
||||
height: 36,
|
||||
padding: (theme) => theme.spacing(0, 3),
|
||||
cursor: "pointer",
|
||||
pointerEvents: isActive ? "none" : "auto",
|
||||
color: (theme) =>
|
||||
isActive
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
background: (theme) => theme.palette.action.hover,
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isHealthy ? (
|
||||
<CheckCircleOutlined
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (theme) => theme.palette.success.light,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ErrorOutline
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{label}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
{/* 62px - navbar and 36px - the bottom bar */}
|
||||
<Box sx={{ height: "100%", overflowY: "auto", width: "100%" }}>
|
||||
<SyntaxHighlighter
|
||||
language="json"
|
||||
editorProps={{ height: "100%" }}
|
||||
value={JSON.stringify(
|
||||
healthStatus[tab.value as keyof typeof healthStatus],
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stats: {
|
||||
padding: 0,
|
||||
border: 0,
|
||||
gap: theme.spacing(6),
|
||||
rowGap: theme.spacing(3),
|
||||
flex: 1,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
|
||||
statsItem: {
|
||||
flexDirection: "column",
|
||||
gap: 0,
|
||||
padding: 0,
|
||||
|
||||
"& > span:first-of-type": {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
}))
|
|
@ -1862,3 +1862,422 @@ export const MockLicenseResponse: GetLicensesResponse[] = [
|
|||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const MockHealth = {
|
||||
time: "2023-08-01T16:51:03.29792825Z",
|
||||
healthy: true,
|
||||
failing_sections: null,
|
||||
derp: {
|
||||
healthy: true,
|
||||
regions: {
|
||||
"999": {
|
||||
healthy: true,
|
||||
region: {
|
||||
EmbeddedRelay: true,
|
||||
RegionID: 999,
|
||||
RegionCode: "coder",
|
||||
RegionName: "Council Bluffs, Iowa",
|
||||
Nodes: [
|
||||
{
|
||||
Name: "999stun0",
|
||||
RegionID: 999,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "999b",
|
||||
RegionID: 999,
|
||||
HostName: "dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
],
|
||||
},
|
||||
node_reports: [
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "999stun0",
|
||||
RegionID: 999,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: false,
|
||||
round_trip_ping: 0,
|
||||
uses_websocket: false,
|
||||
client_logs: [],
|
||||
client_errs: [],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: true,
|
||||
CanSTUN: true,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "999b",
|
||||
RegionID: 999,
|
||||
HostName: "dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: true,
|
||||
round_trip_ping: 7674330,
|
||||
uses_websocket: false,
|
||||
client_logs: [
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://dev.coder.com/derp",
|
||||
],
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://dev.coder.com/derp",
|
||||
],
|
||||
],
|
||||
client_errs: [[], []],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: false,
|
||||
CanSTUN: false,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
"10007": {
|
||||
healthy: true,
|
||||
region: {
|
||||
EmbeddedRelay: false,
|
||||
RegionID: 10007,
|
||||
RegionCode: "coder_sydney",
|
||||
RegionName: "sydney",
|
||||
Nodes: [
|
||||
{
|
||||
Name: "10007stun0",
|
||||
RegionID: 10007,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "10007a",
|
||||
RegionID: 10007,
|
||||
HostName: "sydney.dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
],
|
||||
},
|
||||
node_reports: [
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "10007stun0",
|
||||
RegionID: 10007,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: false,
|
||||
round_trip_ping: 0,
|
||||
uses_websocket: false,
|
||||
client_logs: [],
|
||||
client_errs: [],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: true,
|
||||
CanSTUN: true,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "10007a",
|
||||
RegionID: 10007,
|
||||
HostName: "sydney.dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: true,
|
||||
round_trip_ping: 170527034,
|
||||
uses_websocket: false,
|
||||
client_logs: [
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp",
|
||||
],
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://sydney.dev.coder.com/derp",
|
||||
],
|
||||
],
|
||||
client_errs: [[], []],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: false,
|
||||
CanSTUN: false,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
"10008": {
|
||||
healthy: true,
|
||||
region: {
|
||||
EmbeddedRelay: false,
|
||||
RegionID: 10008,
|
||||
RegionCode: "coder_europe-frankfurt",
|
||||
RegionName: "europe-frankfurt",
|
||||
Nodes: [
|
||||
{
|
||||
Name: "10008stun0",
|
||||
RegionID: 10008,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "10008a",
|
||||
RegionID: 10008,
|
||||
HostName: "europe.dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
],
|
||||
},
|
||||
node_reports: [
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "10008stun0",
|
||||
RegionID: 10008,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: false,
|
||||
round_trip_ping: 0,
|
||||
uses_websocket: false,
|
||||
client_logs: [],
|
||||
client_errs: [],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: true,
|
||||
CanSTUN: true,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "10008a",
|
||||
RegionID: 10008,
|
||||
HostName: "europe.dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: true,
|
||||
round_trip_ping: 111329690,
|
||||
uses_websocket: false,
|
||||
client_logs: [
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp",
|
||||
],
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://europe.dev.coder.com/derp",
|
||||
],
|
||||
],
|
||||
client_errs: [[], []],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: false,
|
||||
CanSTUN: false,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
"10009": {
|
||||
healthy: true,
|
||||
region: {
|
||||
EmbeddedRelay: false,
|
||||
RegionID: 10009,
|
||||
RegionCode: "coder_brazil-saopaulo",
|
||||
RegionName: "brazil-saopaulo",
|
||||
Nodes: [
|
||||
{
|
||||
Name: "10009stun0",
|
||||
RegionID: 10009,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "10009a",
|
||||
RegionID: 10009,
|
||||
HostName: "brazil.dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
],
|
||||
},
|
||||
node_reports: [
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "10009stun0",
|
||||
RegionID: 10009,
|
||||
HostName: "stun.l.google.com",
|
||||
STUNPort: 19302,
|
||||
STUNOnly: true,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: false,
|
||||
round_trip_ping: 0,
|
||||
uses_websocket: false,
|
||||
client_logs: [],
|
||||
client_errs: [],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: true,
|
||||
CanSTUN: true,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
healthy: true,
|
||||
node: {
|
||||
Name: "10009a",
|
||||
RegionID: 10009,
|
||||
HostName: "brazil.dev.coder.com",
|
||||
STUNPort: -1,
|
||||
DERPPort: 443,
|
||||
},
|
||||
node_info: {
|
||||
TokenBucketBytesPerSecond: 0,
|
||||
TokenBucketBytesBurst: 0,
|
||||
},
|
||||
can_exchange_messages: true,
|
||||
round_trip_ping: 138185506,
|
||||
uses_websocket: false,
|
||||
client_logs: [
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp",
|
||||
],
|
||||
[
|
||||
"derphttp.Client.Connect: connecting to https://brazil.dev.coder.com/derp",
|
||||
],
|
||||
],
|
||||
client_errs: [[], []],
|
||||
error: null,
|
||||
stun: {
|
||||
Enabled: false,
|
||||
CanSTUN: false,
|
||||
Error: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
netcheck: {
|
||||
UDP: true,
|
||||
IPv6: false,
|
||||
IPv4: true,
|
||||
IPv6CanSend: false,
|
||||
IPv4CanSend: true,
|
||||
OSHasIPv6: true,
|
||||
ICMPv4: false,
|
||||
MappingVariesByDestIP: false,
|
||||
HairPinning: null,
|
||||
UPnP: false,
|
||||
PMP: false,
|
||||
PCP: false,
|
||||
PreferredDERP: 999,
|
||||
RegionLatency: {
|
||||
"999": 1638180,
|
||||
"10007": 174853022,
|
||||
"10008": 112142029,
|
||||
"10009": 138855606,
|
||||
},
|
||||
RegionV4Latency: {
|
||||
"999": 1638180,
|
||||
"10007": 174853022,
|
||||
"10008": 112142029,
|
||||
"10009": 138855606,
|
||||
},
|
||||
RegionV6Latency: {},
|
||||
GlobalV4: "34.71.26.24:55368",
|
||||
GlobalV6: "",
|
||||
CaptivePortal: null,
|
||||
},
|
||||
netcheck_err: null,
|
||||
netcheck_logs: [
|
||||
"netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (9b07930007da49dd7df79bc7) in 1.791799ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (7397fec097f1d5b01364566b) in 1.791529ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (1fdaaa016ca386485f097f68) in 2.192899ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (2596fe60895fbd9542823a76) in 2.146459ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 10007stun0 from 34.71.26.24:55368 (19ec320f3b76e8b027b06d3e) in 2.139619ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 999stun0 from 34.71.26.24:55368 (a17973bc57c35e606c0f46f5) in 2.131089ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 10008stun0 from 34.71.26.24:55368 (c958e15209d139a6e410f13a) in 2.127549ms",
|
||||
"netcheck: netcheck.runProbe: got STUN response for 10009stun0 from 34.71.26.24:55368 (284a1b64dff22f40a3514524) in 2.107549ms",
|
||||
"netcheck: [v1] measureAllICMPLatency: listen ip4:icmp 0.0.0.0: socket: operation not permitted",
|
||||
"netcheck: [v1] report: udp=true v6=false v6os=true mapvarydest=false hair= portmap= v4a=34.71.26.24:55368 derp=999 derpdist=999v4:2ms,10007v4:175ms,10008v4:112ms,10009v4:139ms",
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
access_url: {
|
||||
access_url: "https://dev.coder.com",
|
||||
healthy: true,
|
||||
reachable: true,
|
||||
status_code: 200,
|
||||
healthz_response: "OK",
|
||||
error: null,
|
||||
},
|
||||
websocket: {
|
||||
healthy: true,
|
||||
response: {
|
||||
body: "",
|
||||
code: 101,
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
database: {
|
||||
healthy: true,
|
||||
reachable: true,
|
||||
latency: 92570,
|
||||
error: null,
|
||||
},
|
||||
coder_version: "v0.27.1-devel+c575292",
|
||||
}
|
||||
|
|
|
@ -387,4 +387,8 @@ export const handlers = [
|
|||
rest.get("/api/v2/workspaceagents/:agent/logs", (_, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockWorkspaceAgentLogs))
|
||||
}),
|
||||
|
||||
rest.get("/api/v2/debug/health", (_, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockHealth))
|
||||
}),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue