mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-15 13:19:56 +00:00
refactor
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className={"relative flex h-full flex-col"}>
|
||||
<Header />
|
||||
<Outlet />
|
||||
<AutoUpdate />
|
||||
</div>
|
||||
<ProfileProvider>
|
||||
<div className={"relative flex h-full flex-col"}>
|
||||
<Header />
|
||||
<Outlet />
|
||||
<AutoUpdate />
|
||||
</div>
|
||||
</ProfileProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<void>;
|
||||
switchProfile: (name: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const ProfileContext = createContext<ProfileContextValue | null>(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<string | null>(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 (
|
||||
<ProfileContext.Provider
|
||||
value={{
|
||||
username,
|
||||
activeProfile,
|
||||
loaded,
|
||||
error,
|
||||
refresh,
|
||||
switchProfile,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ProfileContext.Provider>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
113
client/ui-wails/frontend/src/modules/settings/SettingsAbout.tsx
Normal file
113
client/ui-wails/frontend/src/modules/settings/SettingsAbout.tsx
Normal file
@@ -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 (
|
||||
<div className={"flex flex-col gap-5 max-w-2xl"}>
|
||||
<div className={"flex gap-6 items-center"}>
|
||||
<img
|
||||
src={netbirdAppIcon}
|
||||
alt={"NetBird"}
|
||||
className={
|
||||
"w-24 h-24 rounded-2xl shrink-0 border border-nb-gray-800"
|
||||
}
|
||||
/>
|
||||
<div className={"flex-1 min-w-0 flex flex-col gap-2"}>
|
||||
<h2 className={"text-2xl font-semibold"}>NetBird</h2>
|
||||
<div className={"text-sm text-nb-gray-300 space-y-0.5"}>
|
||||
<div>GUI v{guiVersion}</div>
|
||||
<div>Client v{daemonVersion}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{updateVersion && (
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-between gap-4 rounded-md border border-nb-gray-800 bg-nb-gray-920 px-4 py-3"
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<p className={"text-sm font-medium"}>
|
||||
Version {updateVersion} is available.
|
||||
</p>
|
||||
<button
|
||||
type={"button"}
|
||||
onClick={() =>
|
||||
openUrl(
|
||||
`https://github.com/netbirdio/netbird/releases/tag/v${updateVersion}`,
|
||||
)
|
||||
}
|
||||
className={"text-xs text-netbird hover:underline"}
|
||||
>
|
||||
What's new?
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
onClick={triggerUpdate}
|
||||
>
|
||||
Restart now
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className={"text-xs text-nb-gray-500"}>
|
||||
© {new Date().getFullYear()} NetBird. All Rights Reserved.
|
||||
</p>
|
||||
<div
|
||||
className={
|
||||
"flex flex-wrap gap-x-3 gap-y-1 text-xs text-nb-gray-400"
|
||||
}
|
||||
>
|
||||
{LEGAL_LINKS.map((link, i) => (
|
||||
<span key={link.url} className={"flex items-center"}>
|
||||
{i > 0 && (
|
||||
<span
|
||||
className={"mr-3 text-nb-gray-700"}
|
||||
aria-hidden
|
||||
>
|
||||
·
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type={"button"}
|
||||
onClick={() => openUrl(link.url)}
|
||||
className={"hover:text-nb-gray-200 transition"}
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<SectionGroup title={"Security"}>
|
||||
<div>
|
||||
<Label as={"div"}>Pre-shared key</Label>
|
||||
<HelpText>
|
||||
Optional WireGuard pre-shared key for an extra layer of
|
||||
symmetric encryption. Must match the value configured
|
||||
on every peer in the network.
|
||||
</HelpText>
|
||||
<Input
|
||||
type={"password"}
|
||||
showPasswordToggle
|
||||
value={config.preSharedKey}
|
||||
onChange={(e) =>
|
||||
setField("preSharedKey", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Interface"}>
|
||||
<Input
|
||||
label={"Name"}
|
||||
value={config.interfaceName}
|
||||
onChange={(e) => setField("interfaceName", e.target.value)}
|
||||
/>
|
||||
<div className={"grid grid-cols-2 gap-4"}>
|
||||
<Input
|
||||
label={"WireGuard Port"}
|
||||
type={"number"}
|
||||
value={config.wireguardPort}
|
||||
onChange={(e) =>
|
||||
setField("wireguardPort", Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label={"MTU"}
|
||||
type={"number"}
|
||||
value={config.mtu}
|
||||
onChange={(e) =>
|
||||
setField("mtu", Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SectionGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 extends keyof Config>(k: K, v: Config[K]) => void;
|
||||
saveNow: () => Promise<void>;
|
||||
};
|
||||
|
||||
const SettingsContext = createContext<SettingsContextValue | null>(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<Config | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const saveTimer = useRef<ReturnType<typeof setTimeout> | 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 extends keyof Config>(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 && (
|
||||
<p className={"px-6 py-2 text-sm text-red-500"}>{error}</p>
|
||||
)}
|
||||
<div className={"flex-1 min-h-0 overflow-y-auto"}>
|
||||
{!config ? (
|
||||
<div className={"p-6 text-sm text-nb-gray-500"}>
|
||||
Loading…
|
||||
</div>
|
||||
) : (
|
||||
<SettingsContext.Provider
|
||||
value={{ config, setField, saveNow }}
|
||||
>
|
||||
<div className={"px-6 py-5"}>{children}</div>
|
||||
</SettingsContext.Provider>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<SectionGroup title={"General"}>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableAutoConnect}
|
||||
onChange={(v) => setField("disableAutoConnect", !v)}
|
||||
label={"Connect on startup"}
|
||||
helpText={
|
||||
"Automatically connect to NetBird when the app launches."
|
||||
}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableNotifications}
|
||||
onChange={(v) => setField("disableNotifications", !v)}
|
||||
label={"Show notifications"}
|
||||
helpText={
|
||||
"Show desktop notifications for connection events and updates."
|
||||
}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Connection"}>
|
||||
<div>
|
||||
<Label as={"div"}>Management Server</Label>
|
||||
<HelpText>
|
||||
The NetBird management server this client connects to.
|
||||
Saving will reconnect to apply the new server.
|
||||
</HelpText>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<div className={"flex-1"}>
|
||||
<Input
|
||||
value={config.managementUrl}
|
||||
onChange={(e) =>
|
||||
setField("managementUrl", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
onClick={() => saveNow()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
|
||||
export const SettingsNavigationTriggers = () => {
|
||||
return (
|
||||
<div className={"flex flex-col w-52 shrink-0 items-center"}>
|
||||
<div className={"flex flex-col w-52 shrink-0 items-center select-none"}>
|
||||
<VerticalTabs.List>
|
||||
<VerticalTabs.Trigger
|
||||
value={"general"}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
export function SettingsNetwork() {
|
||||
const { config, setField } = useSettings();
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={"Connectivity"}>
|
||||
<FancyToggleSwitch
|
||||
value={config.lazyConnectionEnabled}
|
||||
onChange={(v) => setField("lazyConnectionEnabled", v)}
|
||||
label={"Lazy connections"}
|
||||
helpText={
|
||||
"Only establish peer tunnels on first traffic instead of eagerly at startup."
|
||||
}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.networkMonitor}
|
||||
onChange={(v) => setField("networkMonitor", v)}
|
||||
label={"Network monitor"}
|
||||
helpText={
|
||||
"Reconnect automatically when the host network changes (Wi-Fi switch, VPN, sleep/wake)."
|
||||
}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Routing & DNS"}>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableDns}
|
||||
onChange={(v) => setField("disableDns", !v)}
|
||||
label={"Enable DNS"}
|
||||
helpText={
|
||||
"Apply NetBird-managed DNS settings to the host resolver."
|
||||
}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableClientRoutes}
|
||||
onChange={(v) => setField("disableClientRoutes", !v)}
|
||||
label={"Enable client routes"}
|
||||
helpText={
|
||||
"Accept routes advertised by other peers so this client can reach their networks."
|
||||
}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={!config.disableServerRoutes}
|
||||
onChange={(v) => setField("disableServerRoutes", !v)}
|
||||
label={"Enable server routes"}
|
||||
helpText={
|
||||
"Advertise this host's local routes to other peers."
|
||||
}
|
||||
/>
|
||||
</SectionGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
108
client/ui-wails/frontend/src/modules/settings/SettingsSSH.tsx
Normal file
108
client/ui-wails/frontend/src/modules/settings/SettingsSSH.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<SectionGroup title={"Server"}>
|
||||
<FancyToggleSwitch
|
||||
value={config.serverSshAllowed}
|
||||
onChange={(v) => setField("serverSshAllowed", v)}
|
||||
label={"Allow SSH"}
|
||||
helpText={
|
||||
"Run the NetBird SSH server on this host so other peers can connect to it."
|
||||
}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Capabilities"}>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshRoot}
|
||||
onChange={(v) => setField("enableSshRoot", v)}
|
||||
label={"Allow root login"}
|
||||
helpText={
|
||||
"Permit incoming SSH sessions to authenticate as root."
|
||||
}
|
||||
disabled={sshOff}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshSftp}
|
||||
onChange={(v) => setField("enableSshSftp", v)}
|
||||
label={"Enable SFTP"}
|
||||
helpText={"Allow file transfers over the NetBird SSH server."}
|
||||
disabled={sshOff}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshLocalPortForwarding}
|
||||
onChange={(v) => setField("enableSshLocalPortForwarding", v)}
|
||||
label={"Local port forwarding"}
|
||||
helpText={
|
||||
"Allow clients to forward local ports through this host."
|
||||
}
|
||||
disabled={sshOff}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.enableSshRemotePortForwarding}
|
||||
onChange={(v) =>
|
||||
setField("enableSshRemotePortForwarding", v)
|
||||
}
|
||||
label={"Remote port forwarding"}
|
||||
helpText={
|
||||
"Allow clients to expose remote ports back through this host."
|
||||
}
|
||||
disabled={sshOff}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Authentication"}>
|
||||
<FancyToggleSwitch
|
||||
value={config.disableSshAuth}
|
||||
onChange={(v) => setField("disableSshAuth", v)}
|
||||
label={"Disable SSH auth"}
|
||||
helpText={
|
||||
"Skip JWT authentication for incoming SSH sessions. Insecure — diagnostics only."
|
||||
}
|
||||
disabled={sshOff}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-6",
|
||||
sshOff && "opacity-50 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div className={"flex-1 max-w-md"}>
|
||||
<Label as={"div"}>JWT cache TTL</Label>
|
||||
<HelpText margin={false}>
|
||||
How long verified JWTs are cached before
|
||||
re-validation. Shorter values increase load on the
|
||||
management server; longer values delay revocation.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-32 shrink-0"}>
|
||||
<Input
|
||||
type={"number"}
|
||||
value={config.sshJwtCacheTtl}
|
||||
onChange={(e) =>
|
||||
setField(
|
||||
"sshJwtCacheTtl",
|
||||
Number(e.target.value),
|
||||
)
|
||||
}
|
||||
customSuffix={
|
||||
<span className={"text-nb-gray-400"}>s</span>
|
||||
}
|
||||
disabled={sshOff}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SectionGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const SectionGroup = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<section className={"mb-8"}>
|
||||
<h2
|
||||
className={
|
||||
"text-xs uppercase tracking-wider text-nb-gray-400 mb-3 font-semibold"
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<div className={"flex flex-col gap-4"}>{children}</div>
|
||||
</section>
|
||||
);
|
||||
@@ -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 (
|
||||
<>
|
||||
<SectionGroup title={"Firewall"}>
|
||||
<FancyToggleSwitch
|
||||
value={config.blockInbound}
|
||||
onChange={(v) => setField("blockInbound", v)}
|
||||
label={"Block inbound traffic"}
|
||||
helpText={
|
||||
"Drop all unsolicited inbound traffic on the NetBird interface."
|
||||
}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={config.blockLanAccess}
|
||||
onChange={(v) => setField("blockLanAccess", v)}
|
||||
label={"Block LAN access"}
|
||||
helpText={
|
||||
"Prevent peers from reaching this host's local network."
|
||||
}
|
||||
/>
|
||||
</SectionGroup>
|
||||
|
||||
<SectionGroup title={"Encryption"}>
|
||||
<FancyToggleSwitch
|
||||
value={config.rosenpassEnabled}
|
||||
onChange={(v) => setField("rosenpassEnabled", v)}
|
||||
label={"Quantum-resistant encryption"}
|
||||
helpText={
|
||||
"Add a post-quantum key exchange (Rosenpass) on top of WireGuard."
|
||||
}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={config.rosenpassPermissive}
|
||||
onChange={(v) => setField("rosenpassPermissive", v)}
|
||||
label={"Permissive mode"}
|
||||
helpText={
|
||||
"Allow connections to peers without quantum-resistance support."
|
||||
}
|
||||
/>
|
||||
</FancyToggleSwitch>
|
||||
</SectionGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<SectionGroup title={"Debug bundle"}>
|
||||
<p className={"text-sm text-nb-gray-300 mb-2"}>
|
||||
A debug bundle helps NetBird support investigate connection
|
||||
problems. It's a zip file with logs and system details from
|
||||
this device.
|
||||
</p>
|
||||
|
||||
<FancyToggleSwitch
|
||||
value={anonymize}
|
||||
onChange={setAnonymize}
|
||||
label={"Anonymize personal data"}
|
||||
helpText={
|
||||
"Replace IPs, hostnames, and peer names before saving."
|
||||
}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={systemInfo}
|
||||
onChange={setSystemInfo}
|
||||
label={"Include system info"}
|
||||
helpText={
|
||||
"Include OS, kernel, network interfaces, and routing tables."
|
||||
}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={upload}
|
||||
onChange={setUpload}
|
||||
label={"Send to NetBird support"}
|
||||
helpText={
|
||||
"Uploads the bundle directly. You'll get a key to share with us."
|
||||
}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={trace}
|
||||
onChange={setTrace}
|
||||
label={"Capture detailed (trace) logs"}
|
||||
helpText={
|
||||
"Restart NetBird with extra logging for a few minutes, then create the bundle. NetBird will briefly disconnect."
|
||||
}
|
||||
disabled={isRunning}
|
||||
>
|
||||
<div className={"flex items-center gap-3 max-w-sm"}>
|
||||
<Label as={"div"} className={"!mb-0"}>
|
||||
Capture for
|
||||
</Label>
|
||||
<div className={"w-24"}>
|
||||
<Input
|
||||
type={"number"}
|
||||
min={1}
|
||||
max={30}
|
||||
value={traceMinutes}
|
||||
onChange={(e) =>
|
||||
setTraceMinutes(
|
||||
Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
30,
|
||||
Number(e.target.value) || 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
customSuffix={
|
||||
<span className={"text-nb-gray-400"}>min</span>
|
||||
}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FancyToggleSwitch>
|
||||
|
||||
<div className={"flex items-center gap-3 mt-2"}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
onClick={run}
|
||||
disabled={isRunning}
|
||||
>
|
||||
{isRunning ? "Creating bundle…" : "Create bundle"}
|
||||
</Button>
|
||||
{stage.kind === "error" && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"md"}
|
||||
onClick={reset}
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BundleStatus stage={stage} />
|
||||
</SectionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={
|
||||
"mt-4 flex items-center gap-3 rounded-md border border-nb-gray-800 bg-nb-gray-920 px-4 py-3"
|
||||
}
|
||||
>
|
||||
<Loader2
|
||||
className={"animate-spin text-netbird shrink-0"}
|
||||
size={18}
|
||||
/>
|
||||
<p className={"text-sm text-nb-gray-200"}>
|
||||
{stageLabel(stage)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (stage.kind === "error") {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"mt-4 rounded-md border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300"
|
||||
}
|
||||
>
|
||||
{stage.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BundleResult result={stage.result} uploaded={stage.uploadAttempted} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={"mt-4 flex flex-col gap-3"}>
|
||||
{uploaded && result.uploadedKey && (
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-nb-gray-800 bg-nb-gray-920 px-4 py-4"
|
||||
}
|
||||
>
|
||||
<p className={"text-sm font-medium mb-1"}>
|
||||
Bundle uploaded
|
||||
</p>
|
||||
<p className={"text-xs text-nb-gray-400 mb-3"}>
|
||||
Share this key with NetBird support so they can find
|
||||
your bundle.
|
||||
</p>
|
||||
<CopyableValue value={result.uploadedKey} mono large />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadFailed && (
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300"
|
||||
}
|
||||
>
|
||||
Upload failed
|
||||
{result.uploadFailureReason
|
||||
? `: ${result.uploadFailureReason}`
|
||||
: "."}{" "}
|
||||
The bundle is still saved locally.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.path && (
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-nb-gray-800 bg-nb-gray-920 px-4 py-3"
|
||||
}
|
||||
>
|
||||
<p className={"text-xs text-nb-gray-400 mb-2"}>
|
||||
{uploaded && result.uploadedKey
|
||||
? "A local copy was also saved at:"
|
||||
: "Bundle saved to:"}
|
||||
</p>
|
||||
<CopyableValue value={result.path} mono />
|
||||
<p className={"text-xs text-nb-gray-500 mt-2"}>
|
||||
You may need admin privileges to open this file.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<code
|
||||
className={cn(
|
||||
"flex-1 min-w-0 truncate rounded bg-nb-gray-900 px-3 py-2 border border-nb-gray-800",
|
||||
mono && "font-mono",
|
||||
large ? "text-sm" : "text-xs",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</code>
|
||||
<button
|
||||
type={"button"}
|
||||
onClick={onCopy}
|
||||
className={
|
||||
"p-2 rounded-md border border-nb-gray-800 text-nb-gray-300 hover:text-white hover:bg-nb-gray-900"
|
||||
}
|
||||
aria-label={"Copy"}
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
</button>
|
||||
{value.startsWith("/") || value.match(/^[A-Za-z]:\\/) ? (
|
||||
<button
|
||||
type={"button"}
|
||||
onClick={onReveal}
|
||||
className={
|
||||
"p-2 rounded-md border border-nb-gray-800 text-nb-gray-300 hover:text-white hover:bg-nb-gray-900"
|
||||
}
|
||||
aria-label={"Reveal"}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
client/ui-wails/frontend/src/modules/settings/useDebugBundle.ts
Normal file
124
client/ui-wails/frontend/src/modules/settings/useDebugBundle.ts
Normal file
@@ -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<DebugStage>({ 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,
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Profile[]>([]);
|
||||
const [error, setError] = useState<string | null>(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));
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["src/*"],
|
||||
"@bindings/*": ["bindings/github.com/netbirdio/netbird/client/ui-wails/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "bindings"],
|
||||
|
||||
@@ -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")],
|
||||
|
||||
Reference in New Issue
Block a user