diff --git a/site/src/pages/HealthPage/Content.tsx b/site/src/pages/HealthPage/Content.tsx index a4a833f805..7179a98fce 100644 --- a/site/src/pages/HealthPage/Content.tsx +++ b/site/src/pages/HealthPage/Content.tsx @@ -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 = ({ icon, children, ...divProps }) => { ); }; +interface StatusIconProps { + value: boolean | null; +} + +export const StatusIcon: FC = ({ value }) => { + if (value === null) { + return ; + } + return value ? ( + + ) : ( + + ); +}; + type BooleanPillProps = Omit, "icon" | "value"> & { value: boolean | null; }; diff --git a/site/src/pages/HealthPage/DERPPage.tsx b/site/src/pages/HealthPage/DERPPage.tsx index 36e0e922a7..fade74cc66 100644 --- a/site/src/pages/HealthPage/DERPPage.tsx +++ b/site/src/pages/HealthPage/DERPPage.tsx @@ -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[] = [ - "UDP", - "IPv6", - "IPv4", - "IPv6CanSend", - "IPv4CanSend", - "OSHasIPv6", - "ICMPv4", - "MappingVariesByDestIP", - "HairPinning", - "UPnP", - "PMP", - "PCP", -]; - type BooleanKeys = { [K in keyof T]: T[K] extends boolean | null ? K : never; }[keyof T]; +interface FlagInfo { + label: string; + description: string; + invert?: boolean; +} + +const flagDescriptions: Record, 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[]; +} + +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(); const { netcheck, regions, netcheck_logs: logs } = derp; @@ -75,19 +151,44 @@ const DERPPage: FC = () => { })}
- Flags -
- {flags.map((flag) => ( - - {flag} - - ))} -
+ Network Checks + {flagGroups.map((group) => ( +
+
+ {group.title} +
+ + + {group.flags.map((flag) => ( + + + + + + {flagDescriptions[flag].label} + + + {flagDescriptions[flag].description} + + + ))} + +
+
+ ))}
Regions -
+
{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 ( - ); })}
-
Logs ({ - borderRadius: 8, - border: `1px solid ${theme.palette.divider}`, - color: theme.palette.text.secondary, - })} + className="rounded-lg border border-solid border-border text-content-secondary" />
diff --git a/site/src/pages/HealthPage/DERPRegionPage.tsx b/site/src/pages/HealthPage/DERPRegionPage.tsx index af51b794f1..e47844045c 100644 --- a/site/src/pages/HealthPage/DERPRegionPage.tsx +++ b/site/src/pages/HealthPage/DERPRegionPage.tsx @@ -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(); 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 ( <> - {pageTitle(region!.RegionName, "Health")} + {pageTitle(region.RegionName, "Health")}
- + Back to DERP - {region!.RegionName} + {region.RegionName}
@@ -90,11 +85,11 @@ const DERPRegionPage: FC = () => { })}
-
+
}> - {region!.RegionID} + {region.RegionID} Region ID @@ -102,70 +97,119 @@ const DERPRegionPage: FC = () => { }> - {region!.RegionCode} + {region.RegionCode} Region Code - - Embedded Relay - + + + + Embedded Relay + + + + Whether this region uses a relay server embedded in the Coder + deployment. + +
- {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 (
-
+
-

{node!.HostName}

-
- DERP Port: {node!.DERPPort ?? "None"} - STUN Port: {node!.STUNPort ?? "None"} +

+ {node.HostName} +

+
+ DERP Port: {node.DERPPort ?? "None"} + STUN Port: {node.STUNPort ?? "None"}
-
- - - } - > - {report.round_trip_ping_ms}ms - - - - Round trip ping - - - - Exchange Messages - - - Websocket - -
+ + + } + > + {report.round_trip_ping_ms}ms + + + Round trip ping +
- + + + + {checks.map((check) => ( + + + + + + {check.label} + + + {check.description} + + + ))} + +
+ {report.client_errs.length > 0 && ( )}
@@ -186,45 +230,4 @@ const StatusCircle: FC = ({ 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>; - export default DERPRegionPage;