mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-15 13:19:56 +00:00
254 lines
9.0 KiB
TypeScript
254 lines
9.0 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { FolderOpen } from "lucide-react";
|
|
import { Debug as DebugSvc } from "@bindings/services";
|
|
import type { DebugBundleResult } from "@bindings/services/models.js";
|
|
import { Button } from "@/components/Button";
|
|
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
|
import HelpText from "@/components/HelpText.tsx";
|
|
import { Input } from "@/components/Input";
|
|
import { Label } from "@/components/Label";
|
|
import { StatusPanel } from "@/components/StatusPanel";
|
|
import { cn } from "@/lib/cn";
|
|
import type { DebugStage } from "@/modules/debug-bundle/useDebugBundle.ts";
|
|
import { useDebugBundleContext } from "@/modules/debug-bundle/useDebugBundleContext.ts";
|
|
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
|
|
|
export function SettingsTroubleshooting() {
|
|
const {
|
|
anonymize,
|
|
setAnonymize,
|
|
systemInfo,
|
|
setSystemInfo,
|
|
upload,
|
|
setUpload,
|
|
trace,
|
|
setTrace,
|
|
traceMinutes,
|
|
setTraceMinutes,
|
|
run,
|
|
stage,
|
|
cancel,
|
|
reset,
|
|
} = useDebugBundleContext();
|
|
|
|
if (stage.kind === "done") {
|
|
return (
|
|
<DoneResult
|
|
result={stage.result}
|
|
uploaded={stage.uploadAttempted}
|
|
onClose={reset}
|
|
/>
|
|
);
|
|
}
|
|
if (stage.kind !== "idle") {
|
|
return <ProgressSection stage={stage} onCancel={cancel} />;
|
|
}
|
|
|
|
return (
|
|
<SectionGroup title={"Debug bundle"}>
|
|
<HelpText className={"-mt-2 mb-2"}>
|
|
A debug bundle helps NetBird support investigate connection problems. <br /> It's a
|
|
.zip file with logs, system details and debug information from your device.
|
|
</HelpText>
|
|
|
|
<FancyToggleSwitch
|
|
value={anonymize}
|
|
onChange={setAnonymize}
|
|
label={"Anonymize Sensitive Information"}
|
|
helpText={"Hides public IP addresses and non-NetBird domains from logs."}
|
|
/>
|
|
<FancyToggleSwitch
|
|
value={systemInfo}
|
|
onChange={setSystemInfo}
|
|
label={"Include System Information"}
|
|
helpText={"Include OS, kernel, network interfaces, and routing tables."}
|
|
/>
|
|
<FancyToggleSwitch
|
|
value={upload}
|
|
onChange={setUpload}
|
|
label={"Upload Bundle to NetBird Servers"}
|
|
helpText={
|
|
"Securely uploads the bundle and returns an upload key. Share the key with NetBird support over GitHub or Slack instead of attaching the file directly."
|
|
}
|
|
/>
|
|
<FancyToggleSwitch
|
|
value={trace}
|
|
onChange={setTrace}
|
|
label={"Capture Trace Logs"}
|
|
helpText={
|
|
"Raises logging to TRACE and cycles NetBird up and down to capture connection logs. The previous level is restored after the bundle is built."
|
|
}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-6 justify-between",
|
|
!trace && "opacity-50 pointer-events-none",
|
|
)}
|
|
>
|
|
<div className={"flex-1 max-w-md"}>
|
|
<Label as={"div"}>Capture Duration</Label>
|
|
<HelpText margin={false}>
|
|
How long to capture trace logs before generating the bundle.
|
|
</HelpText>
|
|
</div>
|
|
<div className={"w-40 shrink-0"}>
|
|
<Input
|
|
type={"number"}
|
|
min={1}
|
|
max={30}
|
|
value={traceMinutes}
|
|
onChange={(e) =>
|
|
setTraceMinutes(Math.max(1, Math.min(30, Number(e.target.value) || 1)))
|
|
}
|
|
customSuffix={"Minute(s)"}
|
|
disabled={!trace}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<BottomBar>
|
|
<Button variant={"primary"} size={"md"} onClick={run}>
|
|
Create Bundle
|
|
</Button>
|
|
</BottomBar>
|
|
</SectionGroup>
|
|
);
|
|
}
|
|
|
|
function ProgressSection({ stage, onCancel }: { stage: DebugStage; onCancel: () => void }) {
|
|
const cancelling = stage.kind === "cancelling";
|
|
return (
|
|
<StatusPanel
|
|
variant={"loading"}
|
|
title={stageLabel(stage)}
|
|
description={
|
|
"Collecting logs, system details, and connection state. This usually takes a moment — keep this window open until it completes."
|
|
}
|
|
actions={
|
|
<Button variant={"secondary"} size={"xs"} onClick={onCancel} disabled={cancelling}>
|
|
{cancelling ? "Cancelling…" : "Cancel"}
|
|
</Button>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function DoneResult({
|
|
result,
|
|
uploaded,
|
|
onClose,
|
|
}: {
|
|
result: DebugBundleResult;
|
|
uploaded: boolean;
|
|
onClose: () => void;
|
|
}) {
|
|
const showKey = uploaded && Boolean(result.uploadedKey);
|
|
const uploadFailed = uploaded && !result.uploadedKey;
|
|
const onRevealPath = () => {
|
|
if (!result.path) return;
|
|
void DebugSvc.RevealFile(result.path).catch(() => {});
|
|
};
|
|
return (
|
|
<StatusPanel
|
|
variant={"success"}
|
|
title={showKey ? "Debug bundle successfully uploaded!" : "Bundle saved"}
|
|
description={
|
|
showKey
|
|
? "Share the upload key below with NetBird support. A local copy was also saved on your device."
|
|
: "Your debug bundle has been saved locally."
|
|
}
|
|
actions={
|
|
<>
|
|
<Button variant={"secondary"} size={"xs"} onClick={onClose}>
|
|
Close
|
|
</Button>
|
|
{showKey ? (
|
|
<Button variant={"primary"} size={"xs"} copy={result.uploadedKey}>
|
|
Copy Key
|
|
</Button>
|
|
) : (
|
|
result.path && (
|
|
<Button variant={"primary"} size={"xs"} onClick={onRevealPath}>
|
|
<FolderOpen size={12} />
|
|
Open Folder
|
|
</Button>
|
|
)
|
|
)}
|
|
</>
|
|
}
|
|
>
|
|
<div className={"w-full max-w-xs mx-auto flex flex-col gap-3"}>
|
|
{showKey && <Input value={result.uploadedKey} readOnly copy />}
|
|
|
|
{result.path && !showKey && (
|
|
<Input
|
|
value={result.path}
|
|
readOnly
|
|
customSuffix={
|
|
<button
|
|
type={"button"}
|
|
onClick={onRevealPath}
|
|
className={"pointer-events-auto hover:text-white transition-all"}
|
|
aria-label={"Open file location"}
|
|
>
|
|
<FolderOpen size={16} />
|
|
</button>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{uploadFailed && (
|
|
<div
|
|
className={
|
|
"rounded-md border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300"
|
|
}
|
|
>
|
|
Upload failed
|
|
{result.uploadFailureReason ? `: ${result.uploadFailureReason}` : "."} The
|
|
bundle is still saved locally.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</StatusPanel>
|
|
);
|
|
}
|
|
|
|
function BottomBar({ children }: { children: ReactNode }) {
|
|
return (
|
|
<div className={"absolute bottom-0 left-0 w-full"}>
|
|
<div
|
|
className={
|
|
"w-full flex justify-end gap-3 px-8 py-5 border-t border-nb-gray-900 bg-nb-gray-935"
|
|
}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const stageLabel = (stage: DebugStage): string => {
|
|
switch (stage.kind) {
|
|
case "preparing-trace":
|
|
return "Switching to trace logging…";
|
|
case "reconnecting":
|
|
return "Reconnecting NetBird…";
|
|
case "capturing": {
|
|
const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
return `Capturing logs — ${fmt(
|
|
stage.totalSec - stage.remainingSec,
|
|
)} / ${fmt(stage.totalSec)}`;
|
|
}
|
|
case "restoring-level":
|
|
return "Restoring previous log level…";
|
|
case "bundling":
|
|
return "Generating debug bundle…";
|
|
case "uploading":
|
|
return "Uploading to NetBird…";
|
|
case "cancelling":
|
|
return "Cancelling…";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|