feat(site): improve DERP health page readability (#22984)

## Why

The DERP health page displayed raw field names like
`MappingVariesByDestIP`, `PMP`, `PCP`, `HairPinning` with no context.
Users without deep networking knowledge had no way to understand what
these flags meant or why they mattered. This change makes the page
self-documenting.

## What

- DERPPage (`/health/derp`)
- Replace flat pill row with four logically grouped tables:
**Connectivity**, **IPv6 Support**, **NAT Traversal**, **Port Mapping**.
  - Rename section from "Flags" to "Network Checks".
- Surface `CaptivePortal` flag (previously missing from the UI
entirely).
- Invert display of `MappingVariesByDestIP` and `CaptivePortal` so green
always means good.
- Handle `null` boolean fields (e.g. UPnP, PMP, PCP) with a distinct
"not checked" neutral icon.

- DERPRegionPage (`/health/derp/regions/:regionId`)
- Replace per-node `BooleanPill` row with a table showing **Exchange
Messages**, **Direct HTTP Upgrade**, **STUN Enabled**, and **STUN
Reachable** per node.
- Invert `uses_websocket` display as "Direct HTTP Upgrade" (green when
websocket is not needed).
- Surface **STUN Enabled** and **STUN Reachable** per node (data was
returned by the API but never rendered).
- Add null guards for `region` and `node` (remove `!` non-null
assertions).
- Convert all emotion/MUI styles to Tailwind classes; remove
`reportStyles` object and `useTheme` import.

- Content.tsx (shared)
- Adds an exported `StatusIcon` component with three states: `true`
(green check), `false` (red minus), `null` (neutral help icon).
This commit is contained in:
Cian Johnston
2026-03-16 09:14:24 +00:00
committed by GitHub
parent 703b974757
commit ae8bed4d8e
3 changed files with 256 additions and 138 deletions
+16
View File
@@ -4,6 +4,7 @@ import type { HealthCode, HealthSeverity } from "api/typesGenerated";
import {
CircleAlertIcon,
CircleCheckIcon,
CircleHelpIcon,
CircleMinusIcon,
} from "lucide-react";
import {
@@ -183,6 +184,21 @@ export const Pill: React.FC<PillProps> = ({ icon, children, ...divProps }) => {
);
};
interface StatusIconProps {
value: boolean | null;
}
export const StatusIcon: FC<StatusIconProps> = ({ value }) => {
if (value === null) {
return <CircleHelpIcon className="size-icon-sm text-content-disabled" />;
}
return value ? (
<CircleCheckIcon className="size-icon-sm text-content-success" />
) : (
<CircleMinusIcon className="size-icon-sm text-content-destructive" />
);
};
type BooleanPillProps = Omit<ComponentProps<typeof Pill>, "icon" | "value"> & {
value: boolean | null;
};
+133 -34
View File
@@ -6,12 +6,12 @@ import type {
} from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { Button } from "components/Button/Button";
import { Table, TableBody, TableCell, TableRow } from "components/Table/Table";
import { MapPinIcon } from "lucide-react";
import type { FC } from "react";
import { Link, useOutletContext } from "react-router";
import { pageTitle } from "utils/page";
import {
BooleanPill,
Header,
HeaderTitle,
HealthMessageDocsLink,
@@ -19,29 +19,105 @@ import {
Logs,
Main,
SectionLabel,
StatusIcon,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { healthyColor } from "./healthyColor";
const flags: BooleanKeys<NetcheckReport>[] = [
"UDP",
"IPv6",
"IPv4",
"IPv6CanSend",
"IPv4CanSend",
"OSHasIPv6",
"ICMPv4",
"MappingVariesByDestIP",
"HairPinning",
"UPnP",
"PMP",
"PCP",
];
type BooleanKeys<T> = {
[K in keyof T]: T[K] extends boolean | null ? K : never;
}[keyof T];
interface FlagInfo {
label: string;
description: string;
invert?: boolean;
}
const flagDescriptions: Record<BooleanKeys<NetcheckReport>, FlagInfo> = {
UDP: {
label: "UDP",
description: "Whether a UDP STUN round trip completed successfully.",
},
IPv6: {
label: "IPv6",
description: "Whether an IPv6 STUN round trip completed successfully.",
},
IPv4: {
label: "IPv4",
description: "Whether an IPv4 STUN round trip completed successfully.",
},
IPv6CanSend: {
label: "IPv6 Send",
description: "Whether this server can send IPv6 packets.",
},
IPv4CanSend: {
label: "IPv4 Send",
description: "Whether this server can send IPv4 packets.",
},
OSHasIPv6: {
label: "OS IPv6 Support",
description: "Whether the operating system supports IPv6.",
},
ICMPv4: {
label: "ICMP Ping",
description: "Whether an ICMPv4 round trip completed successfully.",
},
MappingVariesByDestIP: {
label: "No Symmetric NAT",
description:
"Whether STUN results are consistent across destinations. Symmetric NAT may degrade peer-to-peer connectivity.",
invert: true,
},
HairPinning: {
label: "NAT Hairpinning",
description:
"Whether the router supports communication between local devices through the public IP address.",
},
UPnP: {
label: "UPnP",
description: "Whether Universal Plug and Play was detected on the LAN.",
},
PMP: {
label: "NAT-PMP",
description: "Whether NAT Port Mapping Protocol was detected on the LAN.",
},
PCP: {
label: "PCP",
description: "Whether Port Control Protocol was detected on the LAN.",
},
CaptivePortal: {
label: "No Captive Portal",
description:
"Whether HTTP traffic is free from captive portal interception.",
invert: true,
},
};
interface FlagGroup {
title: string;
flags: BooleanKeys<NetcheckReport>[];
}
const flagGroups: FlagGroup[] = [
{
title: "Connectivity",
flags: ["UDP", "IPv4", "IPv6", "ICMPv4", "CaptivePortal"],
},
{
title: "IPv6 Support",
flags: ["OSHasIPv6", "IPv4CanSend", "IPv6CanSend"],
},
{
title: "NAT Traversal",
flags: ["MappingVariesByDestIP", "HairPinning"],
},
{
title: "Port Mapping",
flags: ["UPnP", "PMP", "PCP"],
},
];
const DERPPage: FC = () => {
const { derp } = useOutletContext<HealthcheckReport>();
const { netcheck, regions, netcheck_logs: logs } = derp;
@@ -75,19 +151,44 @@ const DERPPage: FC = () => {
})}
<section>
<SectionLabel>Flags</SectionLabel>
<div css={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
{flags.map((flag) => (
<BooleanPill key={flag} value={safeNetcheck[flag]}>
{flag}
</BooleanPill>
))}
</div>
<SectionLabel>Network Checks</SectionLabel>
{flagGroups.map((group) => (
<div key={group.title} className="mb-6">
<h5 className="text-xs uppercase tracking-wide text-content-secondary m-0 mb-2">
{group.title}
</h5>
<Table>
<TableBody>
{group.flags.map((flag) => (
<TableRow key={flag}>
<TableCell className="w-8">
<StatusIcon
value={
safeNetcheck[flag] === null
? null
: flagDescriptions[flag].invert
? !safeNetcheck[flag]
: safeNetcheck[flag]
}
/>
</TableCell>
<TableCell className="font-medium whitespace-nowrap w-36">
{flagDescriptions[flag].label}
</TableCell>
<TableCell className="text-content-secondary">
{flagDescriptions[flag].description}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
))}
</section>
<section>
<SectionLabel>Regions</SectionLabel>
<div css={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
<div className="flex flex-wrap gap-3">
{Object.values(regions ?? {})
.filter((region) => {
// Values can technically be null
@@ -100,9 +201,12 @@ const DERPPage: FC = () => {
return 0;
})
.map(({ severity, region }) => {
if (!region) {
return null;
}
return (
<Button variant="outline" key={region!.RegionID} asChild>
<Link to={`/health/derp/regions/${region!.RegionID}`}>
<Button variant="outline" key={region.RegionID} asChild>
<Link to={`/health/derp/regions/${region.RegionID}`}>
<MapPinIcon
style={{
color: healthyColor(
@@ -111,23 +215,18 @@ const DERPPage: FC = () => {
),
}}
/>
{region!.RegionName}
{region.RegionName}
</Link>
</Button>
);
})}
</div>
</section>
<section>
<SectionLabel>Logs</SectionLabel>
<Logs
lines={logs}
css={(theme) => ({
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
color: theme.palette.text.secondary,
})}
className="rounded-lg border border-solid border-border text-content-secondary"
/>
</section>
</Main>
+107 -104
View File
@@ -1,4 +1,3 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import type {
DERPNodeReport,
DERPRegionReport,
@@ -6,6 +5,7 @@ import type {
HealthSeverity,
} from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { Table, TableBody, TableCell, TableRow } from "components/Table/Table";
import {
Tooltip,
TooltipContent,
@@ -26,10 +26,16 @@ import {
Logs,
Main,
Pill,
StatusIcon,
} from "./Content";
interface NodeCheckRow {
label: string;
description: string;
value: boolean | null;
}
const DERPRegionPage: FC = () => {
const theme = useTheme();
const healthStatus = useOutletContext<HealthcheckReport>();
const params = useParams() as { regionId: string };
const regionId = Number(params.regionId);
@@ -40,37 +46,26 @@ const DERPRegionPage: FC = () => {
severity,
} = healthStatus.derp.regions[regionId] as DERPRegionReport;
if (!region) {
return null;
}
return (
<>
<title>{pageTitle(region!.RegionName, "Health")}</title>
<title>{pageTitle(region.RegionName, "Health")}</title>
<Header>
<hgroup>
<Link
css={{
fontSize: 12,
textDecoration: "none",
color: theme.palette.text.secondary,
fontWeight: 500,
display: "inline-flex",
alignItems: "center",
"&:hover": {
color: theme.palette.text.primary,
},
marginBottom: 8,
lineHeight: "1.2",
}}
className="text-xs no-underline text-content-secondary font-medium inline-flex items-center hover:text-content-primary mb-2 leading-tight"
to="/health/derp"
>
<ChevronLeftIcon
className="size-icon-xs"
css={{ verticalAlign: "middle", marginRight: 8 }}
/>
<ChevronLeftIcon className="size-icon-xs align-middle mr-2" />
Back to DERP
</Link>
<HeaderTitle>
<HealthyDot severity={severity as HealthSeverity} />
{region!.RegionName}
{region.RegionName}
</HeaderTitle>
</hgroup>
</Header>
@@ -90,11 +85,11 @@ const DERPRegionPage: FC = () => {
})}
<section>
<div css={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
<div className="flex flex-wrap gap-3">
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<HashIcon className="size-icon-sm" />}>
{region!.RegionID}
{region.RegionID}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Region ID</TooltipContent>
@@ -102,70 +97,119 @@ const DERPRegionPage: FC = () => {
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<CodeIcon className="size-icon-sm" />}>
{region!.RegionCode}
{region.RegionCode}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Region Code</TooltipContent>
</Tooltip>
<BooleanPill value={region!.EmbeddedRelay}>
Embedded Relay
</BooleanPill>
<Tooltip>
<TooltipTrigger asChild>
<BooleanPill value={region.EmbeddedRelay}>
Embedded Relay
</BooleanPill>
</TooltipTrigger>
<TooltipContent side="bottom">
Whether this region uses a relay server embedded in the Coder
deployment.
</TooltipContent>
</Tooltip>
</div>
</section>
{reports.map((report) => {
report = report as DERPNodeReport; // Can technically be null
{reports.map((rawReport) => {
if (!rawReport) {
return null;
}
const report = rawReport as DERPNodeReport;
const { node, client_logs: logs } = report;
if (!node) {
return null;
}
const latencyColor = getLatencyColor(report.round_trip_ping_ms);
const latencyBackground = getLatencyColor(
report.round_trip_ping_ms,
"background",
);
const checks: NodeCheckRow[] = [
{
label: "Exchange Messages",
description:
"Whether DERP clients can relay messages through this node.",
value: report.can_exchange_messages,
},
{
label: "Direct HTTP Upgrade",
description:
"Whether the connection used a direct HTTP upgrade instead of falling back to WebSocket. Fallback may indicate the DERP upgrade header is being blocked.",
value: !report.uses_websocket,
},
{
label: "STUN Enabled",
description: "Whether STUN is enabled on this node.",
value: report.stun.Enabled,
},
{
label: "STUN Reachable",
description:
"Whether this node responded to a STUN request successfully.",
value: report.stun.CanSTUN,
},
];
return (
<section
key={node!.HostName}
css={{
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
fontSize: 14,
}}
key={node.HostName}
className="border border-solid border-border rounded-lg overflow-hidden text-sm"
>
<header css={reportStyles.header}>
<header className="p-6 flex justify-between items-center">
<div>
<h4 css={reportStyles.title}>{node!.HostName}</h4>
<div css={reportStyles.ports}>
<span>DERP Port: {node!.DERPPort ?? "None"}</span>
<span>STUN Port: {node!.STUNPort ?? "None"}</span>
<h4 className="font-medium m-0 leading-none">
{node.HostName}
</h4>
<div className="flex items-center gap-2 text-content-secondary text-xs leading-tight mt-2">
<span>DERP Port: {node.DERPPort ?? "None"}</span>
<span>STUN Port: {node.STUNPort ?? "None"}</span>
</div>
</div>
<div css={reportStyles.pills}>
<Tooltip>
<TooltipTrigger asChild>
<Pill
className={latencyColor}
icon={<StatusCircle background={latencyBackground} />}
>
{report.round_trip_ping_ms}ms
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">
Round trip ping
</TooltipContent>
</Tooltip>
<BooleanPill value={report.can_exchange_messages}>
Exchange Messages
</BooleanPill>
<BooleanPill value={report.uses_websocket}>
Websocket
</BooleanPill>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Pill
className={latencyColor}
icon={<StatusCircle background={latencyBackground} />}
>
{report.round_trip_ping_ms}ms
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Round trip ping</TooltipContent>
</Tooltip>
</header>
<Logs lines={logs?.flat() ?? []} css={reportStyles.logs} />
<Table>
<TableBody className="[&>tr>td:first-of-type]:border-l-0 [&>tr>td:last-child]:border-r-0 [&>tr:last-child>td]:border-b-0 [&>tr>td]:!rounded-none">
{checks.map((check) => (
<TableRow key={check.label}>
<TableCell className="w-8">
<StatusIcon value={check.value} />
</TableCell>
<TableCell className="font-medium whitespace-nowrap w-40">
{check.label}
</TableCell>
<TableCell className="text-content-secondary">
{check.description}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Logs
lines={logs?.flat() ?? []}
className="border-0 border-t border-solid border-border"
/>
{report.client_errs.length > 0 && (
<Logs
lines={report.client_errs.flat()}
css={[reportStyles.logs, reportStyles.clientErrors]}
className="border-0 border-t border-solid border-border bg-surface-destructive text-content-destructive"
/>
)}
</section>
@@ -186,45 +230,4 @@ const StatusCircle: FC<StatusCircleProps> = ({ background }) => {
);
};
const reportStyles = {
header: {
padding: 24,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
title: {
fontWeight: 500,
margin: 0,
lineHeight: "1",
},
pills: {
display: "flex",
gap: 8,
alignItems: "center",
},
ports: (theme) => ({
display: "flex",
alignItems: "center",
gap: 8,
color: theme.palette.text.secondary,
fontSize: 12,
lineHeight: "1.2",
marginTop: 8,
}),
divider: (theme) => ({
height: 1,
backgroundColor: theme.palette.divider,
}),
logs: (theme) => ({
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
borderTop: `1px solid ${theme.palette.divider}`,
}),
clientErrors: (theme) => ({
background: theme.roles.error.background,
color: theme.roles.error.text,
}),
} satisfies Record<string, Interpolation<Theme>>;
export default DERPRegionPage;