diff --git a/client/ui-wails/frontend/src/hooks/useStatus.ts b/client/ui-wails/frontend/src/hooks/useStatus.ts index 45a006907..2afee0413 100644 --- a/client/ui-wails/frontend/src/hooks/useStatus.ts +++ b/client/ui-wails/frontend/src/hooks/useStatus.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { Events } from "@wailsio/runtime"; -import { Peers } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Status } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +import { Peers } from "@bindings/services"; +import type { Status } from "@bindings/services/models.js"; const EVENT_STATUS = "netbird:status"; diff --git a/client/ui-wails/frontend/src/layouts/AppLayout.tsx b/client/ui-wails/frontend/src/layouts/AppLayout.tsx index be8ec1121..79ab3979f 100644 --- a/client/ui-wails/frontend/src/layouts/AppLayout.tsx +++ b/client/ui-wails/frontend/src/layouts/AppLayout.tsx @@ -1,13 +1,16 @@ import { Outlet } from "react-router-dom"; import { Header } from "@/layouts/Header.tsx"; import { AutoUpdate } from "@/modules/auto-update/AutoUpdate.tsx"; +import { ProfileProvider } from "@/modules/profile/ProfileContext.tsx"; export const AppLayout = () => { return ( -
-
- - -
+ +
+
+ + +
+
); }; diff --git a/client/ui-wails/frontend/src/modules/auto-update/AutoUpdate.tsx b/client/ui-wails/frontend/src/modules/auto-update/AutoUpdate.tsx index 9e829814a..178c5b643 100644 --- a/client/ui-wails/frontend/src/modules/auto-update/AutoUpdate.tsx +++ b/client/ui-wails/frontend/src/modules/auto-update/AutoUpdate.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Button } from "@/components/Button"; import { useStatus } from "@/hooks/useStatus"; -import { Update as UpdateSvc } from "../../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; +import { Update as UpdateSvc } from "@bindings/services"; export const AutoUpdate = () => { const { status } = useStatus(); diff --git a/client/ui-wails/frontend/src/modules/profile/ProfileContext.tsx b/client/ui-wails/frontend/src/modules/profile/ProfileContext.tsx new file mode 100644 index 000000000..064c1a4df --- /dev/null +++ b/client/ui-wails/frontend/src/modules/profile/ProfileContext.tsx @@ -0,0 +1,76 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; +import { Profiles as ProfilesSvc } from "@bindings/services"; + +type ProfileContextValue = { + username: string; + activeProfile: string; + loaded: boolean; + error: string | null; + refresh: () => Promise; + switchProfile: (name: string) => Promise; +}; + +const ProfileContext = createContext(null); + +export const useProfile = () => { + const ctx = useContext(ProfileContext); + if (!ctx) { + throw new Error("useProfile must be used inside ProfileProvider"); + } + return ctx; +}; + +export const ProfileProvider = ({ children }: { children: ReactNode }) => { + const [username, setUsername] = useState(""); + const [activeProfile, setActiveProfile] = useState(""); + const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + try { + const u = await ProfilesSvc.Username(); + const active = await ProfilesSvc.GetActive(); + setUsername(u); + setActiveProfile(active.profileName || "default"); + setError(null); + } catch (e) { + setError(String(e)); + } finally { + setLoaded(true); + } + }, []); + + useEffect(() => { + refresh(); + }, [refresh]); + + const switchProfile = useCallback( + async (name: string) => { + await ProfilesSvc.Switch({ profileName: name, username }); + setActiveProfile(name); + }, + [username], + ); + + return ( + + {children} + + ); +}; diff --git a/client/ui-wails/frontend/src/modules/settings/Settings.tsx b/client/ui-wails/frontend/src/modules/settings/Settings.tsx index 20f4f3ef2..dd49e51b8 100644 --- a/client/ui-wails/frontend/src/modules/settings/Settings.tsx +++ b/client/ui-wails/frontend/src/modules/settings/Settings.tsx @@ -1,120 +1,18 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { Browser } from "@wailsio/runtime"; -import { - Settings as SettingsSvc, - Profiles as ProfilesSvc, - Update as UpdateSvc, - Debug as DebugSvc, - Connection as ConnectionSvc, -} from "../../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { - Config, - DebugBundleResult, -} from "../../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; -import { Check, Copy, FolderOpen, Loader2 } from "lucide-react"; -import netbirdAppIcon from "@/assets/logos/netbird-app-icon.svg"; -import pkg from "../../../package.json"; -import { useStatus } from "@/hooks/useStatus"; -import { Button } from "@/components/Button"; -import FancyToggleSwitch from "@/components/FancyToggleSwitch"; -import { HelpText } from "@/components/HelpText"; -import { Input } from "@/components/Input"; -import { Label } from "@/components/Label"; -import { cn } from "@/lib/cn"; +import { useState } from "react"; import { MainRightSide } from "@/layouts/MainRightSide.tsx"; import { VerticalTabs } from "@/components/VerticalTabs.tsx"; import { SettingsNavigationTriggers } from "@/modules/settings/SettingsNavigationTriggers.tsx"; - -type Ctx = { - cfg: Config; - setField: (k: K, v: Config[K]) => void; -}; - -const SAVE_DEBOUNCE_MS = 400; - -const buildPayload = ( - cfg: Config, - profileName: string, - username: string, -) => ({ - profileName, - username, - managementUrl: cfg.managementUrl, - adminUrl: cfg.adminUrl, - interfaceName: cfg.interfaceName, - wireguardPort: cfg.wireguardPort, - mtu: cfg.mtu, - preSharedKey: cfg.preSharedKey, - disableAutoConnect: cfg.disableAutoConnect, - serverSshAllowed: cfg.serverSshAllowed, - rosenpassEnabled: cfg.rosenpassEnabled, - rosenpassPermissive: cfg.rosenpassPermissive, - disableNotifications: cfg.disableNotifications, - lazyConnectionEnabled: cfg.lazyConnectionEnabled, - blockInbound: cfg.blockInbound, - networkMonitor: cfg.networkMonitor, - disableClientRoutes: cfg.disableClientRoutes, - disableServerRoutes: cfg.disableServerRoutes, - disableDns: cfg.disableDns, - blockLanAccess: cfg.blockLanAccess, - enableSshRoot: cfg.enableSshRoot, - enableSshSftp: cfg.enableSshSftp, - enableSshLocalPortForwarding: cfg.enableSshLocalPortForwarding, - enableSshRemotePortForwarding: cfg.enableSshRemotePortForwarding, - disableSshAuth: cfg.disableSshAuth, - sshJwtCacheTtl: cfg.sshJwtCacheTtl, -}); +import { SettingsProvider } from "@/modules/settings/SettingsContext.tsx"; +import { SettingsGeneral } from "@/modules/settings/SettingsGeneral.tsx"; +import { SettingsNetwork } from "@/modules/settings/SettingsNetwork.tsx"; +import { SettingsSecurity } from "@/modules/settings/SettingsSecurity.tsx"; +import { SettingsSSH } from "@/modules/settings/SettingsSSH.tsx"; +import { SettingsAdvanced } from "@/modules/settings/SettingsAdvanced.tsx"; +import { SettingsTroubleshooting } from "@/modules/settings/SettingsTroubleshooting.tsx"; +import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx"; export const Settings = () => { const [active, setActive] = useState("general"); - const [username, setUsername] = useState(""); - const [profile, setProfile] = useState(""); - const [cfg, setCfg] = useState(null); - const [error, setError] = useState(null); - const dirtyRef = useRef(false); - - const load = useCallback(async () => { - try { - const u = await ProfilesSvc.Username(); - const activeProfile = await ProfilesSvc.GetActive(); - const profileName = activeProfile.profileName || "default"; - setUsername(u); - setProfile(profileName); - const c = await SettingsSvc.GetConfig({ - profileName, - username: u, - }); - setCfg(c); - setError(null); - } catch (e) { - setError(String(e)); - } - }, []); - - useEffect(() => { - load(); - }, [load]); - - const setField: Ctx["setField"] = (k, v) => { - dirtyRef.current = true; - setCfg((c) => (c ? { ...c, [k]: v } : c)); - }; - - const saveNow = useCallback(async () => { - if (!cfg) return; - try { - await SettingsSvc.SetConfig(buildPayload(cfg, profile, username)); - setError(null); - } catch (e) { - setError(String(e)); - } - }, [cfg, profile, username]); - - useEffect(() => { - if (!cfg || !dirtyRef.current) return; - const t = setTimeout(saveNow, SAVE_DEBOUNCE_MS); - return () => clearTimeout(t); - }, [cfg, saveNow]); return ( { onValueChange={setActive} className={"wails-draggable p-4"} > - + - {error && ( -

{error}

- )} -
- {!cfg ? ( -
- Loading… -
- ) : ( -
- - - - - - - - - - - - - - - - - - - - - -
- )} -
+ + + + + + + + + + + + + + + + + + + + + + +
); }; - -const SectionGroup = ({ - title, - children, -}: { - title: string; - children: React.ReactNode; -}) => ( -
-

- {title} -

-
{children}
-
-); - -function GeneralSection({ - cfg, - setField, - onSaveServer, -}: Ctx & { onSaveServer: () => void | Promise }) { - return ( - <> - - setField("disableAutoConnect", !v)} - label={"Connect on startup"} - helpText={ - "Automatically connect to NetBird when the app launches." - } - /> - setField("disableNotifications", !v)} - label={"Show notifications"} - helpText={ - "Show desktop notifications for connection events and updates." - } - /> - - - -
- - - The NetBird management server this client connects to. - Saving will reconnect to apply the new server. - -
-
- - setField("managementUrl", e.target.value) - } - /> -
- -
-
-
- - ); -} - -function NetworkSection({ cfg, setField }: Ctx) { - return ( - <> - - setField("lazyConnectionEnabled", v)} - label={"Lazy connections"} - helpText={ - "Only establish peer tunnels on first traffic instead of eagerly at startup." - } - /> - setField("networkMonitor", v)} - label={"Network monitor"} - helpText={ - "Reconnect automatically when the host network changes (Wi-Fi switch, VPN, sleep/wake)." - } - /> - - - - setField("disableDns", !v)} - label={"Enable DNS"} - helpText={ - "Apply NetBird-managed DNS settings to the host resolver." - } - /> - setField("disableClientRoutes", !v)} - label={"Enable client routes"} - helpText={ - "Accept routes advertised by other peers so this client can reach their networks." - } - /> - setField("disableServerRoutes", !v)} - label={"Enable server routes"} - helpText={ - "Advertise this host's local routes to other peers." - } - /> - - - ); -} - -function SecuritySection({ cfg, setField }: Ctx) { - return ( - <> - - setField("blockInbound", v)} - label={"Block inbound traffic"} - helpText={ - "Drop all unsolicited inbound traffic on the NetBird interface." - } - /> - setField("blockLanAccess", v)} - label={"Block LAN access"} - helpText={ - "Prevent peers from reaching this host's local network." - } - /> - - - - setField("rosenpassEnabled", v)} - label={"Quantum-resistant encryption"} - helpText={ - "Add a post-quantum key exchange (Rosenpass) on top of WireGuard." - } - > - setField("rosenpassPermissive", v)} - label={"Permissive mode"} - helpText={ - "Allow connections to peers without quantum-resistance support." - } - /> - - - - ); -} - -function SshSection({ cfg, setField }: Ctx) { - const sshOff = !cfg.serverSshAllowed; - return ( - <> - - setField("serverSshAllowed", v)} - label={"Allow SSH"} - helpText={ - "Run the NetBird SSH server on this host so other peers can connect to it." - } - /> - - - - setField("enableSshRoot", v)} - label={"Allow root login"} - helpText={ - "Permit incoming SSH sessions to authenticate as root." - } - disabled={sshOff} - /> - setField("enableSshSftp", v)} - label={"Enable SFTP"} - helpText={"Allow file transfers over the NetBird SSH server."} - disabled={sshOff} - /> - setField("enableSshLocalPortForwarding", v)} - label={"Local port forwarding"} - helpText={ - "Allow clients to forward local ports through this host." - } - disabled={sshOff} - /> - - setField("enableSshRemotePortForwarding", v) - } - label={"Remote port forwarding"} - helpText={ - "Allow clients to expose remote ports back through this host." - } - disabled={sshOff} - /> - - - - setField("disableSshAuth", v)} - label={"Disable SSH auth"} - helpText={ - "Skip JWT authentication for incoming SSH sessions. Insecure — diagnostics only." - } - disabled={sshOff} - /> -
-
- - - How long verified JWTs are cached before - re-validation. Shorter values increase load on the - management server; longer values delay revocation. - -
-
- - setField( - "sshJwtCacheTtl", - Number(e.target.value), - ) - } - customSuffix={ - s - } - disabled={sshOff} - /> -
-
-
- - ); -} - -function AdvancedSection({ cfg, setField }: Ctx) { - return ( - <> - -
- - - Optional WireGuard pre-shared key for an extra layer of - symmetric encryption. Must match the value configured - on every peer in the network. - - - setField("preSharedKey", e.target.value) - } - /> -
-
- - - setField("interfaceName", e.target.value)} - /> -
- - setField("wireguardPort", Number(e.target.value)) - } - /> - - setField("mtu", Number(e.target.value)) - } - /> -
-
- - ); -} - -const NETBIRD_UPLOAD_URL = "https://debug.netbird.io/upload"; -const TRACE_LOG_FILE_COUNT = 5; -const PLAIN_LOG_FILE_COUNT = 1; - -type Stage = - | { kind: "idle" } - | { kind: "preparing-trace" } - | { kind: "reconnecting" } - | { kind: "capturing"; remainingSec: number; totalSec: number } - | { kind: "restoring-level" } - | { kind: "bundling" } - | { kind: "uploading" } - | { kind: "done"; result: DebugBundleResult; uploadAttempted: boolean } - | { kind: "error"; message: string }; - -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -function TroubleshootingSection({ - profile, - username, -}: { - profile: string; - username: string; -}) { - const [anonymize, setAnonymize] = useState(true); - const [systemInfo, setSystemInfo] = useState(true); - const [upload, setUpload] = useState(false); - const [trace, setTrace] = useState(false); - const [traceMinutes, setTraceMinutes] = useState(3); - const [stage, setStage] = useState({ kind: "idle" }); - - const isRunning = - stage.kind !== "idle" && - stage.kind !== "done" && - stage.kind !== "error"; - - const reset = () => setStage({ kind: "idle" }); - - const run = async () => { - const uploadUrl = upload ? NETBIRD_UPLOAD_URL : ""; - try { - let originalLevel = "info"; - if (trace) { - setStage({ kind: "preparing-trace" }); - try { - const cur = await DebugSvc.GetLogLevel(); - if (cur?.level) originalLevel = cur.level; - } catch { - // best effort - } - await DebugSvc.SetLogLevel({ level: "trace" }); - - setStage({ kind: "reconnecting" }); - try { - await ConnectionSvc.Down(); - } catch { - // already down - } - await ConnectionSvc.Up({ profileName: profile, username }); - - const totalSec = Math.max( - 1, - Math.min(30, traceMinutes), - ) * 60; - for (let remaining = totalSec; remaining > 0; remaining--) { - setStage({ - kind: "capturing", - remainingSec: remaining, - totalSec, - }); - await sleep(1000); - } - - setStage({ kind: "restoring-level" }); - try { - await DebugSvc.SetLogLevel({ level: originalLevel }); - } catch { - // restore is best-effort - } - } - - setStage({ kind: "bundling" }); - const logFileCount = trace - ? TRACE_LOG_FILE_COUNT - : PLAIN_LOG_FILE_COUNT; - - if (uploadUrl) setStage({ kind: "uploading" }); - const result = await DebugSvc.Bundle({ - anonymize, - systemInfo, - uploadUrl, - logFileCount, - }); - setStage({ - kind: "done", - result, - uploadAttempted: Boolean(uploadUrl), - }); - } catch (e) { - setStage({ kind: "error", message: String(e) }); - } - }; - - return ( - -

- A debug bundle helps NetBird support investigate connection - problems. It's a zip file with logs and system details from - this device. -

- - - - - -
- -
- - setTraceMinutes( - Math.max( - 1, - Math.min( - 30, - Number(e.target.value) || 1, - ), - ), - ) - } - customSuffix={ - min - } - disabled={isRunning} - /> -
-
-
- -
- - {stage.kind === "error" && ( - - )} -
- - -
- ); -} - -function BundleStatus({ stage }: { stage: Stage }) { - if (stage.kind === "idle") return null; - - if ( - stage.kind === "preparing-trace" || - stage.kind === "reconnecting" || - stage.kind === "capturing" || - stage.kind === "restoring-level" || - stage.kind === "bundling" || - stage.kind === "uploading" - ) { - return ( -
- -

- {stageLabel(stage)} -

-
- ); - } - - if (stage.kind === "error") { - return ( -
- {stage.message} -
- ); - } - - return ; -} - -function stageLabel(stage: Stage): 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 "Building bundle…"; - case "uploading": - return "Uploading to NetBird…"; - default: - return ""; - } -} - -function BundleResult({ - result, - uploaded, -}: { - result: DebugBundleResult; - uploaded: boolean; -}) { - const uploadFailed = uploaded && !result.uploadedKey; - return ( -
- {uploaded && result.uploadedKey && ( -
-

- Bundle uploaded -

-

- Share this key with NetBird support so they can find - your bundle. -

- -
- )} - - {uploadFailed && ( -
- Upload failed - {result.uploadFailureReason - ? `: ${result.uploadFailureReason}` - : "."}{" "} - The bundle is still saved locally. -
- )} - - {result.path && ( -
-

- {uploaded && result.uploadedKey - ? "A local copy was also saved at:" - : "Bundle saved to:"} -

- -

- You may need admin privileges to open this file. -

-
- )} -
- ); -} - -function CopyableValue({ - value, - mono = false, - large = false, -}: { - value: string; - mono?: boolean; - large?: boolean; -}) { - const [copied, setCopied] = useState(false); - const onCopy = async () => { - try { - await navigator.clipboard.writeText(value); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - } catch { - // ignore - } - }; - const onReveal = () => { - void Browser.OpenURL(`file://${value}`).catch(() => {}); - }; - return ( -
- - {value} - - - {value.startsWith("/") || value.match(/^[A-Za-z]:\\/) ? ( - - ) : null} -
- ); -} - -const LEGAL_LINKS: { label: string; url: string }[] = [ - { label: "Imprint", url: "https://netbird.io/imprint" }, - { label: "Privacy", url: "https://netbird.io/privacy" }, - { label: "CLA", url: "https://netbird.io/cla" }, - { label: "Terms of Service", url: "https://netbird.io/terms" }, -]; - -function openUrl(url: string) { - void Browser.OpenURL(url).catch(() => window.open(url, "_blank")); -} - -function AboutSection() { - const { status } = useStatus(); - const guiVersion = pkg.version; - const daemonVersion = status?.daemonVersion ?? "—"; - - const updateVersion = (status?.events ?? []) - .map((e) => e.metadata?.["new_version_available"]) - .find((v): v is string => Boolean(v)); - - const triggerUpdate = () => { - UpdateSvc.Trigger().catch(() => {}); - }; - - return ( -
-
- {"NetBird"} -
-

NetBird

-
-
GUI v{guiVersion}
-
Client v{daemonVersion}
-
-
-
- - {updateVersion && ( -
-
-

- Version {updateVersion} is available. -

- -
- -
- )} - -

- © {new Date().getFullYear()} NetBird. All Rights Reserved. -

-
- {LEGAL_LINKS.map((link, i) => ( - - {i > 0 && ( - - · - - )} - - - ))} -
-
- ); -} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsAbout.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsAbout.tsx new file mode 100644 index 000000000..4274312c9 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsAbout.tsx @@ -0,0 +1,113 @@ +import { Browser } from "@wailsio/runtime"; +import { Update as UpdateSvc } from "@bindings/services"; +import netbirdAppIcon from "@/assets/logos/netbird-app-icon.svg"; +import pkg from "../../../package.json"; +import { useStatus } from "@/hooks/useStatus"; +import { Button } from "@/components/Button"; + +const LEGAL_LINKS: { label: string; url: string }[] = [ + { label: "Imprint", url: "https://netbird.io/imprint" }, + { label: "Privacy", url: "https://netbird.io/privacy" }, + { label: "CLA", url: "https://netbird.io/cla" }, + { label: "Terms of Service", url: "https://netbird.io/terms" }, +]; + +function openUrl(url: string) { + void Browser.OpenURL(url).catch(() => window.open(url, "_blank")); +} + +export function SettingsAbout() { + const { status } = useStatus(); + const guiVersion = pkg.version; + const daemonVersion = status?.daemonVersion ?? "—"; + + const updateVersion = (status?.events ?? []) + .map((e) => e.metadata?.["new_version_available"]) + .find((v): v is string => Boolean(v)); + + const triggerUpdate = () => { + UpdateSvc.Trigger().catch(() => {}); + }; + + return ( +
+
+ {"NetBird"} +
+

NetBird

+
+
GUI v{guiVersion}
+
Client v{daemonVersion}
+
+
+
+ + {updateVersion && ( +
+
+

+ Version {updateVersion} is available. +

+ +
+ +
+ )} + +

+ © {new Date().getFullYear()} NetBird. All Rights Reserved. +

+
+ {LEGAL_LINKS.map((link, i) => ( + + {i > 0 && ( + + · + + )} + + + ))} +
+
+ ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsAdvanced.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsAdvanced.tsx new file mode 100644 index 000000000..86fb1bae1 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsAdvanced.tsx @@ -0,0 +1,57 @@ +import { HelpText } from "@/components/HelpText"; +import { Input } from "@/components/Input"; +import { Label } from "@/components/Label"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/modules/settings/SettingsContext.tsx"; + +export function SettingsAdvanced() { + const { config, setField } = useSettings(); + return ( + <> + +
+ + + Optional WireGuard pre-shared key for an extra layer of + symmetric encryption. Must match the value configured + on every peer in the network. + + + setField("preSharedKey", e.target.value) + } + /> +
+
+ + + setField("interfaceName", e.target.value)} + /> +
+ + setField("wireguardPort", Number(e.target.value)) + } + /> + + setField("mtu", Number(e.target.value)) + } + /> +
+
+ + ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsContext.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsContext.tsx new file mode 100644 index 000000000..db9bfa68a --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsContext.tsx @@ -0,0 +1,127 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import { Settings as SettingsSvc } from "@bindings/services"; +import type { Config } from "@bindings/services/models.js"; +import { useProfile } from "@/modules/profile/ProfileContext.tsx"; + +const SAVE_DEBOUNCE_MS = 400; + +type SettingsContextValue = { + config: Config; + setField: (k: K, v: Config[K]) => void; + saveNow: () => Promise; +}; + +const SettingsContext = createContext(null); + +export const useSettings = () => { + const ctx = useContext(SettingsContext); + if (!ctx) { + throw new Error("useSettings must be used inside SettingsProvider"); + } + return ctx; +}; + +const useSettingsState = () => { + const { username, activeProfile, loaded: profileLoaded } = useProfile(); + const [config, setConfig] = useState(null); + const [error, setError] = useState(null); + const saveTimer = useRef | null>(null); + + useEffect(() => { + if (!profileLoaded || !activeProfile) return; + (async () => { + try { + const c = await SettingsSvc.GetConfig({ + profileName: activeProfile, + username, + }); + setConfig(c); + setError(null); + } catch (e) { + setError(String(e)); + } + })(); + }, [profileLoaded, activeProfile, username]); + + useEffect( + () => () => { + if (saveTimer.current) clearTimeout(saveTimer.current); + }, + [], + ); + + const save = useCallback( + async (next: Config) => { + try { + await SettingsSvc.SetConfig({ + ...next, + profileName: activeProfile, + username, + }); + setError(null); + } catch (e) { + setError(String(e)); + } + }, + [activeProfile, username], + ); + + const setField = useCallback( + (k: K, v: Config[K]) => { + setConfig((c) => { + if (!c) return c; + const next = { ...c, [k]: v }; + if (saveTimer.current) clearTimeout(saveTimer.current); + saveTimer.current = setTimeout(() => { + void save(next); + }, SAVE_DEBOUNCE_MS); + return next; + }); + }, + [save], + ); + + const saveNow = useCallback(async () => { + if (!config) return; + if (saveTimer.current) { + clearTimeout(saveTimer.current); + saveTimer.current = null; + } + await save(config); + }, [config, save]); + + return { config, error, setField, saveNow }; +}; + +export const SettingsProvider = ({ children }: { children: ReactNode }) => { + const { config, error, setField, saveNow } = useSettingsState(); + + return ( + <> + {error && ( +

{error}

+ )} +
+ {!config ? ( +
+ Loading… +
+ ) : ( + +
{children}
+
+ )} +
+ + ); +}; diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsGeneral.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsGeneral.tsx new file mode 100644 index 000000000..e0ae9e8e9 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsGeneral.tsx @@ -0,0 +1,60 @@ +import { Button } from "@/components/Button"; +import FancyToggleSwitch from "@/components/FancyToggleSwitch"; +import { HelpText } from "@/components/HelpText"; +import { Input } from "@/components/Input"; +import { Label } from "@/components/Label"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/modules/settings/SettingsContext.tsx"; + +export function SettingsGeneral() { + const { config, setField, saveNow } = useSettings(); + return ( + <> + + setField("disableAutoConnect", !v)} + label={"Connect on startup"} + helpText={ + "Automatically connect to NetBird when the app launches." + } + /> + setField("disableNotifications", !v)} + label={"Show notifications"} + helpText={ + "Show desktop notifications for connection events and updates." + } + /> + + + +
+ + + The NetBird management server this client connects to. + Saving will reconnect to apply the new server. + +
+
+ + setField("managementUrl", e.target.value) + } + /> +
+ +
+
+
+ + ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsNavigationTriggers.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsNavigationTriggers.tsx index 195512fc5..269be8827 100644 --- a/client/ui-wails/frontend/src/modules/settings/SettingsNavigationTriggers.tsx +++ b/client/ui-wails/frontend/src/modules/settings/SettingsNavigationTriggers.tsx @@ -11,7 +11,7 @@ import { export const SettingsNavigationTriggers = () => { return ( -
+
+ + setField("lazyConnectionEnabled", v)} + label={"Lazy connections"} + helpText={ + "Only establish peer tunnels on first traffic instead of eagerly at startup." + } + /> + setField("networkMonitor", v)} + label={"Network monitor"} + helpText={ + "Reconnect automatically when the host network changes (Wi-Fi switch, VPN, sleep/wake)." + } + /> + + + + setField("disableDns", !v)} + label={"Enable DNS"} + helpText={ + "Apply NetBird-managed DNS settings to the host resolver." + } + /> + setField("disableClientRoutes", !v)} + label={"Enable client routes"} + helpText={ + "Accept routes advertised by other peers so this client can reach their networks." + } + /> + setField("disableServerRoutes", !v)} + label={"Enable server routes"} + helpText={ + "Advertise this host's local routes to other peers." + } + /> + + + ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsSSH.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsSSH.tsx new file mode 100644 index 000000000..95cd593ff --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsSSH.tsx @@ -0,0 +1,108 @@ +import FancyToggleSwitch from "@/components/FancyToggleSwitch"; +import { HelpText } from "@/components/HelpText"; +import { Input } from "@/components/Input"; +import { Label } from "@/components/Label"; +import { cn } from "@/lib/cn"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/modules/settings/SettingsContext.tsx"; + +export function SettingsSSH() { + const { config, setField } = useSettings(); + const sshOff = !config.serverSshAllowed; + return ( + <> + + setField("serverSshAllowed", v)} + label={"Allow SSH"} + helpText={ + "Run the NetBird SSH server on this host so other peers can connect to it." + } + /> + + + + setField("enableSshRoot", v)} + label={"Allow root login"} + helpText={ + "Permit incoming SSH sessions to authenticate as root." + } + disabled={sshOff} + /> + setField("enableSshSftp", v)} + label={"Enable SFTP"} + helpText={"Allow file transfers over the NetBird SSH server."} + disabled={sshOff} + /> + setField("enableSshLocalPortForwarding", v)} + label={"Local port forwarding"} + helpText={ + "Allow clients to forward local ports through this host." + } + disabled={sshOff} + /> + + setField("enableSshRemotePortForwarding", v) + } + label={"Remote port forwarding"} + helpText={ + "Allow clients to expose remote ports back through this host." + } + disabled={sshOff} + /> + + + + setField("disableSshAuth", v)} + label={"Disable SSH auth"} + helpText={ + "Skip JWT authentication for incoming SSH sessions. Insecure — diagnostics only." + } + disabled={sshOff} + /> +
+
+ + + How long verified JWTs are cached before + re-validation. Shorter values increase load on the + management server; longer values delay revocation. + +
+
+ + setField( + "sshJwtCacheTtl", + Number(e.target.value), + ) + } + customSuffix={ + s + } + disabled={sshOff} + /> +
+
+
+ + ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsSection.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsSection.tsx new file mode 100644 index 000000000..c961af55f --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsSection.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from "react"; + +export const SectionGroup = ({ + title, + children, +}: { + title: string; + children: ReactNode; +}) => ( +
+

+ {title} +

+
{children}
+
+); diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsSecurity.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsSecurity.tsx new file mode 100644 index 000000000..14930ec76 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsSecurity.tsx @@ -0,0 +1,49 @@ +import FancyToggleSwitch from "@/components/FancyToggleSwitch"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { useSettings } from "@/modules/settings/SettingsContext.tsx"; + +export function SettingsSecurity() { + const { config, setField } = useSettings(); + return ( + <> + + setField("blockInbound", v)} + label={"Block inbound traffic"} + helpText={ + "Drop all unsolicited inbound traffic on the NetBird interface." + } + /> + setField("blockLanAccess", v)} + label={"Block LAN access"} + helpText={ + "Prevent peers from reaching this host's local network." + } + /> + + + + setField("rosenpassEnabled", v)} + label={"Quantum-resistant encryption"} + helpText={ + "Add a post-quantum key exchange (Rosenpass) on top of WireGuard." + } + > + setField("rosenpassPermissive", v)} + label={"Permissive mode"} + helpText={ + "Allow connections to peers without quantum-resistance support." + } + /> + + + + ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/SettingsTroubleshooting.tsx b/client/ui-wails/frontend/src/modules/settings/SettingsTroubleshooting.tsx new file mode 100644 index 000000000..25e9b18a9 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/SettingsTroubleshooting.tsx @@ -0,0 +1,320 @@ +import { useState } from "react"; +import { Browser } from "@wailsio/runtime"; +import { Check, Copy, FolderOpen, Loader2 } from "lucide-react"; +import type { DebugBundleResult } from "@bindings/services/models.js"; +import { Button } from "@/components/Button"; +import FancyToggleSwitch from "@/components/FancyToggleSwitch"; +import { Input } from "@/components/Input"; +import { Label } from "@/components/Label"; +import { cn } from "@/lib/cn"; +import { SectionGroup } from "@/modules/settings/SettingsSection.tsx"; +import { + useDebugBundle, + type DebugStage, +} from "@/modules/settings/useDebugBundle.ts"; + +export function SettingsTroubleshooting() { + const bundle = useDebugBundle(); + const { + anonymize, + setAnonymize, + systemInfo, + setSystemInfo, + upload, + setUpload, + trace, + setTrace, + traceMinutes, + setTraceMinutes, + stage, + isRunning, + run, + reset, + } = bundle; + + return ( + +

+ A debug bundle helps NetBird support investigate connection + problems. It's a zip file with logs and system details from + this device. +

+ + + + + +
+ +
+ + setTraceMinutes( + Math.max( + 1, + Math.min( + 30, + Number(e.target.value) || 1, + ), + ), + ) + } + customSuffix={ + min + } + disabled={isRunning} + /> +
+
+
+ +
+ + {stage.kind === "error" && ( + + )} +
+ + +
+ ); +} + +function BundleStatus({ stage }: { stage: DebugStage }) { + if (stage.kind === "idle") return null; + + if ( + stage.kind === "preparing-trace" || + stage.kind === "reconnecting" || + stage.kind === "capturing" || + stage.kind === "restoring-level" || + stage.kind === "bundling" || + stage.kind === "uploading" + ) { + return ( +
+ +

+ {stageLabel(stage)} +

+
+ ); + } + + if (stage.kind === "error") { + return ( +
+ {stage.message} +
+ ); + } + + return ; +} + +function 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 "Building bundle…"; + case "uploading": + return "Uploading to NetBird…"; + default: + return ""; + } +} + +function BundleResult({ + result, + uploaded, +}: { + result: DebugBundleResult; + uploaded: boolean; +}) { + const uploadFailed = uploaded && !result.uploadedKey; + return ( +
+ {uploaded && result.uploadedKey && ( +
+

+ Bundle uploaded +

+

+ Share this key with NetBird support so they can find + your bundle. +

+ +
+ )} + + {uploadFailed && ( +
+ Upload failed + {result.uploadFailureReason + ? `: ${result.uploadFailureReason}` + : "."}{" "} + The bundle is still saved locally. +
+ )} + + {result.path && ( +
+

+ {uploaded && result.uploadedKey + ? "A local copy was also saved at:" + : "Bundle saved to:"} +

+ +

+ You may need admin privileges to open this file. +

+
+ )} +
+ ); +} + +function CopyableValue({ + value, + mono = false, + large = false, +}: { + value: string; + mono?: boolean; + large?: boolean; +}) { + const [copied, setCopied] = useState(false); + const onCopy = async () => { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + // ignore + } + }; + const onReveal = () => { + void Browser.OpenURL(`file://${value}`).catch(() => {}); + }; + return ( +
+ + {value} + + + {value.startsWith("/") || value.match(/^[A-Za-z]:\\/) ? ( + + ) : null} +
+ ); +} diff --git a/client/ui-wails/frontend/src/modules/settings/useDebugBundle.ts b/client/ui-wails/frontend/src/modules/settings/useDebugBundle.ts new file mode 100644 index 000000000..a5ffc93ec --- /dev/null +++ b/client/ui-wails/frontend/src/modules/settings/useDebugBundle.ts @@ -0,0 +1,124 @@ +import { useState } from "react"; +import { + Connection as ConnectionSvc, + Debug as DebugSvc, +} from "@bindings/services"; +import type { DebugBundleResult } from "@bindings/services/models.js"; +import { useProfile } from "@/modules/profile/ProfileContext.tsx"; + +const NETBIRD_UPLOAD_URL = "https://debug.netbird.io/upload"; +const TRACE_LOG_FILE_COUNT = 5; +const PLAIN_LOG_FILE_COUNT = 1; + +export type DebugStage = + | { kind: "idle" } + | { kind: "preparing-trace" } + | { kind: "reconnecting" } + | { kind: "capturing"; remainingSec: number; totalSec: number } + | { kind: "restoring-level" } + | { kind: "bundling" } + | { kind: "uploading" } + | { kind: "done"; result: DebugBundleResult; uploadAttempted: boolean } + | { kind: "error"; message: string }; + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export const useDebugBundle = () => { + const { activeProfile, username } = useProfile(); + const [anonymize, setAnonymize] = useState(true); + const [systemInfo, setSystemInfo] = useState(true); + const [upload, setUpload] = useState(false); + const [trace, setTrace] = useState(false); + const [traceMinutes, setTraceMinutes] = useState(3); + const [stage, setStage] = useState({ kind: "idle" }); + + const isRunning = + stage.kind !== "idle" && + stage.kind !== "done" && + stage.kind !== "error"; + + const reset = () => setStage({ kind: "idle" }); + + const run = async () => { + const uploadUrl = upload ? NETBIRD_UPLOAD_URL : ""; + try { + let originalLevel = "info"; + if (trace) { + setStage({ kind: "preparing-trace" }); + try { + const cur = await DebugSvc.GetLogLevel(); + if (cur?.level) originalLevel = cur.level; + } catch { + // best effort + } + await DebugSvc.SetLogLevel({ level: "trace" }); + + setStage({ kind: "reconnecting" }); + try { + await ConnectionSvc.Down(); + } catch { + // already down + } + await ConnectionSvc.Up({ + profileName: activeProfile, + username, + }); + + const totalSec = + Math.max(1, Math.min(30, traceMinutes)) * 60; + for (let remaining = totalSec; remaining > 0; remaining--) { + setStage({ + kind: "capturing", + remainingSec: remaining, + totalSec, + }); + await sleep(1000); + } + + setStage({ kind: "restoring-level" }); + try { + await DebugSvc.SetLogLevel({ level: originalLevel }); + } catch { + // restore is best-effort + } + } + + setStage({ kind: "bundling" }); + const logFileCount = trace + ? TRACE_LOG_FILE_COUNT + : PLAIN_LOG_FILE_COUNT; + + if (uploadUrl) setStage({ kind: "uploading" }); + const result = await DebugSvc.Bundle({ + anonymize, + systemInfo, + uploadUrl, + logFileCount, + }); + setStage({ + kind: "done", + result, + uploadAttempted: Boolean(uploadUrl), + }); + } catch (e) { + setStage({ kind: "error", message: String(e) }); + } + }; + + return { + anonymize, + setAnonymize, + systemInfo, + setSystemInfo, + upload, + setUpload, + trace, + setTrace, + traceMinutes, + setTraceMinutes, + stage, + isRunning, + run, + reset, + }; +}; diff --git a/client/ui-wails/frontend/src/screens/Debug.tsx b/client/ui-wails/frontend/src/screens/Debug.tsx index 929e4325f..bff36c245 100644 --- a/client/ui-wails/frontend/src/screens/Debug.tsx +++ b/client/ui-wails/frontend/src/screens/Debug.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { Debug as DebugSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { DebugBundleResult } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +import { Debug as DebugSvc } from "@bindings/services"; +import type { DebugBundleResult } from "@bindings/services/models.js"; import { Button } from "../components/Button"; import { Input } from "../components/Input"; import { Switch } from "../components/Switch"; diff --git a/client/ui-wails/frontend/src/screens/Networks.tsx b/client/ui-wails/frontend/src/screens/Networks.tsx index ea3bd055e..0c64f506b 100644 --- a/client/ui-wails/frontend/src/screens/Networks.tsx +++ b/client/ui-wails/frontend/src/screens/Networks.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { RefreshCw } from "lucide-react"; -import { Networks as NetworksSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Network } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +import { Networks as NetworksSvc } from "@bindings/services"; +import type { Network } from "@bindings/services/models.js"; import { Button } from "../components/Button"; import { Tabs } from "../components/Tabs"; diff --git a/client/ui-wails/frontend/src/screens/Peers.tsx b/client/ui-wails/frontend/src/screens/Peers.tsx index f1522ca87..327b6ac38 100644 --- a/client/ui-wails/frontend/src/screens/Peers.tsx +++ b/client/ui-wails/frontend/src/screens/Peers.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import { ChevronDown, ChevronRight, Network, ShieldCheck, Zap } from "lucide-react"; import { useStatus } from "../hooks/useStatus"; -import type { PeerStatus } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +import type { PeerStatus } from "@bindings/services/models.js"; import { Card } from "../components/Card"; import { Input } from "../components/Input"; import { cn } from "../lib/cn"; diff --git a/client/ui-wails/frontend/src/screens/Profiles.tsx b/client/ui-wails/frontend/src/screens/Profiles.tsx index 3a1035afa..5c9204480 100644 --- a/client/ui-wails/frontend/src/screens/Profiles.tsx +++ b/client/ui-wails/frontend/src/screens/Profiles.tsx @@ -3,23 +3,23 @@ import { Plus, RefreshCw } from "lucide-react"; import { Profiles as ProfilesSvc, Connection, -} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Profile } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +} from "@bindings/services"; +import type { Profile } from "@bindings/services/models.js"; import { Button } from "../components/Button"; import { Input } from "../components/Input"; import { Card } from "../components/Card"; +import { useProfile } from "@/modules/profile/ProfileContext.tsx"; export default function Profiles() { - const [username, setUsername] = useState(""); + const { username, loaded, refresh: refreshProfile, switchProfile } = useProfile(); const [profiles, setProfiles] = useState([]); const [error, setError] = useState(null); const [adding, setAdding] = useState(false); const refresh = useCallback(async () => { + if (!username) return; try { - const u = username || (await ProfilesSvc.Username()); - if (!username) setUsername(u); - const list = await ProfilesSvc.List(u); + const list = await ProfilesSvc.List(username); setProfiles(list); setError(null); } catch (e) { @@ -28,12 +28,12 @@ export default function Profiles() { }, [username]); useEffect(() => { - refresh(); - }, [refresh]); + if (loaded) refresh(); + }, [loaded, refresh]); const select = async (name: string) => { try { - await ProfilesSvc.Switch({ profileName: name, username }); + await switchProfile(name); await Connection.Up({ profileName: name, username }); await refresh(); } catch (e) { @@ -54,6 +54,7 @@ export default function Profiles() { if (name === "default") return; try { await ProfilesSvc.Remove({ profileName: name, username }); + await refreshProfile(); await refresh(); } catch (e) { setError(String(e)); diff --git a/client/ui-wails/frontend/src/screens/QuickActions.tsx b/client/ui-wails/frontend/src/screens/QuickActions.tsx index 749b4bf8a..1f867b154 100644 --- a/client/ui-wails/frontend/src/screens/QuickActions.tsx +++ b/client/ui-wails/frontend/src/screens/QuickActions.tsx @@ -1,6 +1,6 @@ import { CheckCircle2, Circle, Loader2, Power } from "lucide-react"; import { useStatus } from "../hooks/useStatus"; -import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; +import { Connection } from "@bindings/services"; import { Button } from "../components/Button"; import { cn } from "../lib/cn"; diff --git a/client/ui-wails/frontend/src/screens/Settings.tsx b/client/ui-wails/frontend/src/screens/Settings.tsx index 3781611b6..f4729e3d7 100644 --- a/client/ui-wails/frontend/src/screens/Settings.tsx +++ b/client/ui-wails/frontend/src/screens/Settings.tsx @@ -2,8 +2,8 @@ import { useCallback, useEffect, useState } from "react"; import { Settings as SettingsSvc, Profiles as ProfilesSvc, -} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { Config } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +} from "@bindings/services"; +import type { Config } from "@bindings/services/models.js"; import { Button } from "../components/Button"; import { Input } from "../components/Input"; import { Switch } from "../components/Switch"; diff --git a/client/ui-wails/frontend/src/screens/Status.tsx b/client/ui-wails/frontend/src/screens/Status.tsx index a666f755c..e844a1b8d 100644 --- a/client/ui-wails/frontend/src/screens/Status.tsx +++ b/client/ui-wails/frontend/src/screens/Status.tsx @@ -1,7 +1,7 @@ import { CheckCircle2, Circle, Loader2, AlertTriangle, Power } from "lucide-react"; import { useStatus } from "../hooks/useStatus"; -import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; -import type { SystemEvent } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; +import { Connection } from "@bindings/services"; +import type { SystemEvent } from "@bindings/services/models.js"; import { Button } from "../components/Button"; import { Card } from "../components/Card"; import { cn } from "../lib/cn"; diff --git a/client/ui-wails/frontend/src/screens/Update.tsx b/client/ui-wails/frontend/src/screens/Update.tsx index 04d9eb245..ed7473503 100644 --- a/client/ui-wails/frontend/src/screens/Update.tsx +++ b/client/ui-wails/frontend/src/screens/Update.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { Loader2 } from "lucide-react"; -import { Update as UpdateSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; +import { Update as UpdateSvc } from "@bindings/services"; const TIMEOUT_MS = 15 * 60 * 1000; diff --git a/client/ui-wails/frontend/tsconfig.json b/client/ui-wails/frontend/tsconfig.json index ad912d829..8b9974133 100644 --- a/client/ui-wails/frontend/tsconfig.json +++ b/client/ui-wails/frontend/tsconfig.json @@ -22,7 +22,8 @@ "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "@bindings/*": ["bindings/github.com/netbirdio/netbird/client/ui-wails/*"] } }, "include": ["src", "bindings"], diff --git a/client/ui-wails/frontend/vite.config.ts b/client/ui-wails/frontend/vite.config.ts index 2df5ee457..02b41be2c 100644 --- a/client/ui-wails/frontend/vite.config.ts +++ b/client/ui-wails/frontend/vite.config.ts @@ -8,6 +8,10 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), + "@bindings": path.resolve( + __dirname, + "./bindings/github.com/netbirdio/netbird/client/ui-wails", + ), }, }, plugins: [react(), wails("./bindings")],