-
Notifications
You must be signed in to change notification settings - Fork 43
feat: add network check command with results webview #1002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
a56eccd
feat: add netcheck command with results webview
EhabY 8b22bca
test: cover netcheck parsing, panel, CLI args, and telemetry
EhabY ebb0997
feat: polish netcheck webview theming and responsive layout
EhabY 32e551e
refactor: split netcheck webview into modules and dedupe diagnostic c…
EhabY 315698e
refactor: simplify netcheck webview and share panel test coverage
EhabY 65646dc
refactor: tighten netcheck types, share webview CSS, add SDK drift guard
EhabY c71caab
fix: address netcheck webview review feedback
EhabY 4a11879
fix: surface region faults and dedupe JSON-open in netcheck
EhabY 6a4fbee
fix: surface unhealthy-node regions in the Issues list
EhabY 30bff35
fix: stop double-listing region warnings; share page header
EhabY b76e470
feat: link to network check from the slow-connection status bar
EhabY File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "name": "@repo/netcheck", | ||
| "version": "1.0.0", | ||
| "description": "Coder network check report webview", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "dev": "vite build --watch", | ||
| "typecheck": "tsc --noEmit" | ||
| }, | ||
| "dependencies": { | ||
| "@repo/shared": "workspace:*", | ||
| "@repo/webview-shared": "workspace:*" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/vscode-webview": "catalog:", | ||
| "typescript": "catalog:", | ||
| "vite": "catalog:" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { regionName } from "./regions"; | ||
|
|
||
| import type { NetcheckConnectivity, NetcheckReport } from "@repo/shared"; | ||
|
|
||
| /** Maps to a `tone-*` CSS class so color lives in the stylesheet, not here. */ | ||
| export type Tone = "good" | "bad" | "warn" | "neutral"; | ||
|
|
||
| export interface ConnectivityItem { | ||
| label: string; | ||
| value: string; | ||
| tone: Tone; | ||
| } | ||
|
|
||
| type Outcome = [value: string, tone: Tone]; | ||
|
|
||
| /** Connectivity facts derived from the embedded tailscale netcheck probe. */ | ||
| export function buildConnectivityItems( | ||
| report: NetcheckReport, | ||
| ): ConnectivityItem[] { | ||
| const probe = report.derp.netcheck; | ||
| if (!probe) { | ||
| return []; | ||
| } | ||
|
|
||
| // Tones: bad = real problem, warn = works but suboptimal, neutral = optional. | ||
| const items: ConnectivityItem[] = [ | ||
| boolItem("UDP", probe.UDP, { | ||
| true: ["Reachable", "good"], | ||
| false: ["Blocked", "bad"], | ||
| }), | ||
| boolItem("IPv4", probe.IPv4, { | ||
| true: ["Yes", "good"], | ||
| false: ["No", "warn"], | ||
| }), | ||
| boolItem("IPv6", probe.IPv6, { | ||
| true: ["Yes", "good"], | ||
| false: ["No", "neutral"], | ||
| }), | ||
| boolItem("NAT mapping", probe.MappingVariesByDestIP, { | ||
| true: ["Varies by destination (hard NAT)", "warn"], | ||
| false: ["Consistent (easy NAT)", "good"], | ||
| }), | ||
| boolItem("Hairpinning", probe.HairPinning, { | ||
| true: ["Supported", "good"], | ||
| false: ["Not supported", "neutral"], | ||
| }), | ||
| portMappingItem(probe), | ||
| ]; | ||
|
|
||
| const preferred = preferredRegionName(report); | ||
| if (preferred) { | ||
| items.push({ label: "Preferred relay", value: preferred, tone: "good" }); | ||
| } | ||
| return items; | ||
| } | ||
|
|
||
| /** Renders a boolean probe field; a missing value is a neutral "Unknown". */ | ||
| function boolItem( | ||
| label: string, | ||
| state: boolean | null | undefined, | ||
| cases: { true: Outcome; false: Outcome }, | ||
| ): ConnectivityItem { | ||
| if (typeof state !== "boolean") { | ||
| return { label, value: "Unknown", tone: "neutral" }; | ||
| } | ||
| const [value, tone] = state ? cases.true : cases.false; | ||
| return { label, value, tone }; | ||
| } | ||
|
|
||
| function portMappingItem(probe: NetcheckConnectivity): ConnectivityItem { | ||
| const fields = [ | ||
| [probe.UPnP, "UPnP"], | ||
| [probe.PMP, "NAT-PMP"], | ||
| [probe.PCP, "PCP"], | ||
| ] as const; | ||
| const detected = fields.filter(([on]) => on).map(([, name]) => name); | ||
| if (detected.length > 0) { | ||
| return { label: "Port mapping", value: detected.join(", "), tone: "good" }; | ||
| } | ||
| // A null field means "could not determine", so report "None detected" only | ||
| // once a protocol was actually probed. | ||
| const probed = fields.some(([on]) => typeof on === "boolean"); | ||
| return { | ||
| label: "Port mapping", | ||
| value: probed ? "None detected" : "Unknown", | ||
| tone: "neutral", | ||
| }; | ||
| } | ||
|
|
||
| function preferredRegionName(report: NetcheckReport): string | undefined { | ||
| const id = report.derp.netcheck?.PreferredDERP; | ||
| if (!id) { | ||
|
EhabY marked this conversation as resolved.
|
||
| return undefined; | ||
| } | ||
| return regionName(report.derp.regions[String(id)], id); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| declare module "*.css"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| export type TriState = "yes" | "no" | "unknown"; | ||
|
|
||
| const NANOS_PER_MS = 1_000_000; | ||
|
|
||
| /** Below this, show one decimal; at or above, round to whole ms. */ | ||
| const DECIMAL_PRECISION_BELOW_MS = 100; | ||
|
|
||
| export function nanosToMs(nanos: number): number { | ||
| return nanos / NANOS_PER_MS; | ||
| } | ||
|
|
||
| export function formatLatency(ms: number | undefined): string { | ||
| if (ms === undefined) { | ||
| return "—"; | ||
| } | ||
| if (ms < 1) { | ||
| return "<1 ms"; | ||
| } | ||
| if (ms < DECIMAL_PRECISION_BELOW_MS) { | ||
| return `${ms.toFixed(1)} ms`; | ||
| } | ||
| return `${Math.round(ms)} ms`; | ||
| } | ||
|
|
||
| /** Renders a STUN/relay capability result for a table cell. */ | ||
| export function formatTriState(value: TriState): string { | ||
| switch (value) { | ||
| case "yes": | ||
| return "Yes"; | ||
| case "no": | ||
| return "Failed"; | ||
| case "unknown": | ||
| return "—"; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import { regionName } from "./regions"; | ||
|
|
||
| import type { | ||
| NetcheckReport, | ||
| NetcheckSectionHealth, | ||
| NetcheckSeverity, | ||
| } from "@repo/shared"; | ||
|
|
||
| export interface Issue { | ||
| kind: "error" | "warning"; | ||
| code?: string; | ||
| message: string; | ||
| } | ||
|
|
||
| const SEVERITY_LABEL = { | ||
| ok: "Healthy", | ||
| warning: "Warning", | ||
| error: "Error", | ||
| } as const satisfies Record<NetcheckSeverity, string>; | ||
|
|
||
| const BANNER_TITLE = { | ||
| ok: "Network is healthy", | ||
| warning: "Network has warnings", | ||
| error: "Network problems detected", | ||
| } as const satisfies Record<NetcheckSeverity, string>; | ||
|
|
||
| const SECTION_STATUS = { | ||
| ok: "healthy", | ||
| warning: "warning", | ||
| error: "error", | ||
| } as const satisfies Record<NetcheckSeverity, string>; | ||
|
|
||
| export function severityLabel(severity: NetcheckSeverity): string { | ||
| return SEVERITY_LABEL[severity]; | ||
| } | ||
|
|
||
| export function bannerTitle(severity: NetcheckSeverity): string { | ||
| return BANNER_TITLE[severity]; | ||
| } | ||
|
|
||
| /** One-line status for a report section, e.g. "2 warnings" or "healthy". */ | ||
| export function sectionSummary(section: NetcheckSectionHealth): string { | ||
| const count = section.warnings.length; | ||
| if (section.severity === "warning" && count > 0) { | ||
| return `${count} warning${count === 1 ? "" : "s"}`; | ||
| } | ||
| return SECTION_STATUS[section.severity]; | ||
|
EhabY marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** Section errors first, then warnings, so the most severe issues lead. */ | ||
| export function collectIssues(report: NetcheckReport): Issue[] { | ||
| const errors: Issue[] = []; | ||
| const warnings: Issue[] = []; | ||
| const addSection = (section: NetcheckSectionHealth) => { | ||
| if (section.error) { | ||
| errors.push({ kind: "error", message: section.error }); | ||
| } | ||
| for (const warning of section.warnings) { | ||
| warnings.push({ | ||
| kind: "warning", | ||
| message: warning.message, | ||
| ...(warning.code ? { code: warning.code } : {}), | ||
| }); | ||
| } | ||
| }; | ||
| addSection(report.derp); | ||
|
EhabY marked this conversation as resolved.
|
||
| if (report.derp.netcheck_err) { | ||
| errors.push({ kind: "error", message: report.derp.netcheck_err }); | ||
| } | ||
| for (const [key, region] of Object.entries(report.derp.regions)) { | ||
| const name = regionName(region, Number(key)); | ||
| // Region warnings are already in the section list; list only errors. | ||
| if (region.error) { | ||
| errors.push({ kind: "error", message: `${name}: ${region.error}` }); | ||
| } else if (region.severity === "error") { | ||
| errors.push({ | ||
| kind: "error", | ||
| message: `${name}: a node failed its health check`, | ||
| }); | ||
| } | ||
| } | ||
| addSection(report.interfaces); | ||
| return [...errors, ...warnings]; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.