import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Events } from "@wailsio/runtime"; import { Connection, WindowManager } from "@bindings/services"; import { ToggleSwitch } from "@/components/switches/ToggleSwitch.tsx"; import { useStatus } from "@/contexts/StatusContext.tsx"; import { useProfile } from "@/contexts/ProfileContext.tsx"; import { cn } from "@/lib/cn.ts"; import { errorDialog, formatErrorMessage } from "@/lib/errors.ts"; import { startConnection, EVENT_BROWSER_LOGIN_CANCEL, EVENT_TRIGGER_LOGIN, } from "@/lib/connection.ts"; import { CopyToClipboard } from "@/components/CopyToClipboard"; import { TruncatedText } from "@/components/TruncatedText"; import { shortenDns } from "@/lib/formatters"; import { contentTop } from "@/components/empty-state/EmptyState"; import { Check as CheckIcon, ChevronDownIcon, Copy as CopyIcon } from "lucide-react"; import * as Popover from "@radix-ui/react-popover"; import netbirdFullLogo from "@/assets/logos/netbird-full.svg"; enum ConnectionState { Disconnected = "disconnected", Connecting = "connecting", Connected = "connected", Disconnecting = "disconnecting", } const STATUS_KEY: Record = { [ConnectionState.Disconnected]: "connect.status.disconnected", [ConnectionState.Connecting]: "connect.status.connecting", [ConnectionState.Connected]: "connect.status.connected", [ConnectionState.Disconnecting]: "connect.status.disconnecting", }; const NEEDS_LOGIN_STATES = new Set(["NeedsLogin", "SessionExpired", "LoginFailed"]); const FORCE_TOGGLE_DELAY_MS = 7000; const errorMessage = formatErrorMessage; export const MainConnectionStatusSwitch = () => { const { t } = useTranslation(); const { status, refresh } = useStatus(); const { activeProfile, username } = useProfile(); const daemonState = status?.status ?? "Idle"; const needsLogin = NEEDS_LOGIN_STATES.has(daemonState); const unreachable = daemonState === "DaemonUnavailable"; type Action = "connect" | "logging-in" | "disconnect" | null; const [action, setAction] = useState(null); const loginGuard = useRef(false); const driveLogin = useCallback(() => { if (loginGuard.current) return; loginGuard.current = true; setAction("logging-in"); void startConnection(() => { loginGuard.current = false; setAction(null); refresh().catch((err: unknown) => console.error("refresh after login failed", err)); }); }, [refresh]); const connState: ConnectionState = useMemo(() => { if (action === "disconnect" && daemonState === "Connected") { return ConnectionState.Disconnecting; } if ((action === "connect" || action === "logging-in") && daemonState !== "Connected") { return ConnectionState.Connecting; } switch (daemonState) { case "Connected": return ConnectionState.Connected; case "Connecting": return ConnectionState.Connecting; case "Idle": case "NeedsLogin": case "LoginFailed": case "SessionExpired": case "DaemonUnavailable": return ConnectionState.Disconnected; default: return ConnectionState.Disconnected; } }, [daemonState, action]); const connect = async () => { setAction("connect"); try { await Connection.Up({ profileName: activeProfile, username, }); await refresh(); } catch (e) { setAction(null); await refresh(); await errorDialog({ Title: t("connect.error.connectTitle"), Message: errorMessage(e), }); } }; const disconnect = async () => { setAction("disconnect"); try { await Connection.Down(); await refresh(); } catch (e) { setAction(null); await refresh(); await errorDialog({ Title: t("connect.error.disconnectTitle"), Message: errorMessage(e), }); } }; const sawConnectingRef = useRef(false); useEffect(() => { if (action === null) { sawConnectingRef.current = false; return; } if (daemonState === "Connecting") { sawConnectingRef.current = true; } if (action === "connect") { if (needsLogin) { driveLogin(); return; } if (daemonState === "Connected" || unreachable) { setAction(null); return; } if (sawConnectingRef.current && daemonState === "Idle") { setAction(null); } return; } if (action === "disconnect") { if (daemonState === "Idle" || daemonState === "Disconnected" || unreachable) { setAction(null); } } }, [action, daemonState, needsLogin, unreachable, driveLogin]); useEffect(() => { const off = Events.On(EVENT_TRIGGER_LOGIN, () => { driveLogin(); }); return () => off(); }, [driveLogin]); const handleSwitch = (next: boolean) => { if (unreachable) return; if (isTransitioning) { if (canForceCancel) void forceCancel(); return; } if (action !== null) return; if (needsLogin) { driveLogin(); return; } if (next && connState === ConnectionState.Disconnected) { void connect(); } else if (!next && connState === ConnectionState.Connected) { void disconnect(); } }; const isTransitioning = connState === ConnectionState.Connecting || connState === ConnectionState.Disconnecting; const isOn = connState === ConnectionState.Connected || connState === ConnectionState.Connecting; const [canForceCancel, setCanForceCancel] = useState(false); useEffect(() => { if (!isTransitioning) { setCanForceCancel(false); return; } const id = setTimeout(() => setCanForceCancel(true), FORCE_TOGGLE_DELAY_MS); return () => clearTimeout(id); }, [isTransitioning]); const forceCancel = async () => { if (action === "logging-in") { Events.Emit(EVENT_BROWSER_LOGIN_CANCEL).catch((err: unknown) => console.error("emit browser-login cancel failed", err), ); } WindowManager.CloseBrowserLogin().catch((err: unknown) => console.warn("close browser-login window failed", err), ); setAction("disconnect"); try { await Connection.Down(); await refresh(); } catch (e) { setAction(null); await refresh(); await errorDialog({ Title: t("connect.error.disconnectTitle"), Message: errorMessage(e), }); } }; const show = connState === ConnectionState.Connected; const fqdn = status?.local.fqdn || ""; const ip = status?.local.ip || ""; const ipv6 = status?.local.ipv6 || ""; return (
{"NetBird"}

{t(STATUS_KEY[connState])}

); }; const LocalIpLine = ({ ip, ipv6, show }: { ip: string; ipv6: string; show: boolean }) => { const [open, setOpen] = useState(false); const hasV6 = !!ipv6; if (!hasV6) { return ( {ip || " "} ); } return (
e.preventDefault()} className={cn( "z-50 min-w-64 max-w-[280px] overflow-hidden", "rounded-lg border border-nb-gray-900 bg-nb-gray-935", "p-1 shadow-lg outline-none text-nb-gray-200", "flex flex-col", )} >
); }; const IpRow = ({ value }: { value: string }) => { const [copied, setCopied] = useState(false); const handleClick = async () => { if (!value) return; try { await navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 500); } catch (e) { console.warn("copy IP to clipboard failed", e); } }; return ( ); };