mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-12 11:49:55 +00:00
wip
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-visually-hidden": "^1.2.4",
|
||||
"@wailsio/runtime": "latest",
|
||||
"chroma-js": "^3.2.0",
|
||||
|
||||
42
client/ui-wails/frontend/pnpm-lock.yaml
generated
42
client/ui-wails/frontend/pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ dependencies:
|
||||
'@radix-ui/react-scroll-area':
|
||||
specifier: ^1.2.10
|
||||
version: 1.2.10(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-switch':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-visually-hidden':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -1102,6 +1105,32 @@ packages:
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1)
|
||||
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1)
|
||||
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1)
|
||||
'@types/react': 18.3.28
|
||||
'@types/react-dom': 18.3.7(@types/react@18.3.28)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1):
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
@@ -1171,6 +1200,19 @@ packages:
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-previous@1.1.1(@types/react@18.3.28)(react@18.3.1):
|
||||
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 18.3.28
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-use-rect@1.1.1(@types/react@18.3.28)(react@18.3.1):
|
||||
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -1,69 +1,129 @@
|
||||
1. General
|
||||
# Settings — Tabs & Controls
|
||||
|
||||
The "old tray" toggles + notifications. This is what 90% of users come to Settings for.
|
||||
Each row has a title and short description. Booleans default to **toggle switch**; pick another control only when noted.
|
||||
|
||||
- Connect on startup — disableAutoConnect (inverted)
|
||||
- Allow SSH — serverSshAllowed (master switch; the SSH tab is the detail)
|
||||
- Quantum-resistance — rosenpassEnabled
|
||||
- Nested when on: Permissive mode — rosenpassPermissive
|
||||
- Lazy connections — lazyConnectionEnabled
|
||||
- Block inbound — blockInbound
|
||||
- Show notifications — disableNotifications (inverted)
|
||||
Tab order: **General · Network · SSH · Troubleshooting · About**.
|
||||
|
||||
▎ Note: blockInbound is technically a firewall behavior, but Stage 1 explicitly groups it with the tray-replacement toggles. Keep it here.
|
||||
---
|
||||
|
||||
2. Connection
|
||||
## 1. General
|
||||
|
||||
Identity + how the wire is established. The "what server am I talking to and how" tab.
|
||||
App behavior + how the client connects.
|
||||
|
||||
- Management URL — managementUrl
|
||||
- Pre-shared key — preSharedKey (password input, toggle reveal)
|
||||
- Advanced (collapsed by default)
|
||||
- Admin URL — adminUrl
|
||||
- Interface name — interfaceName
|
||||
- WireGuard port — wireguardPort
|
||||
- MTU — mtu
|
||||
### Startup
|
||||
|
||||
3. Network
|
||||
- **Connect on startup** — `disableAutoConnect` (inverted) · *toggle switch*
|
||||
- Automatically connect to NetBird when the app launches.
|
||||
- **Show notifications** — `disableNotifications` (inverted) · *toggle switch*
|
||||
- Show desktop notifications for connection events and updates.
|
||||
|
||||
Routing / DNS / LAN behavior — i.e. what the daemon does to the host network.
|
||||
### Connection
|
||||
|
||||
- Network monitor — networkMonitor
|
||||
- Disable DNS — disableDns
|
||||
- Disable client routes — disableClientRoutes
|
||||
- Disable server routes — disableServerRoutes
|
||||
- Block LAN access — blockLanAccess
|
||||
- **Management URL** — `managementUrl` · *text input*
|
||||
- The NetBird management server this client connects to.
|
||||
- **Admin URL** — `adminUrl` · *text input*
|
||||
- Web dashboard URL used by "Open Admin Panel".
|
||||
- **Pre-shared key** — `preSharedKey` · *password input with reveal toggle*
|
||||
- Optional WireGuard pre-shared key for an extra layer of symmetric encryption.
|
||||
|
||||
4. SSH
|
||||
### Interface
|
||||
|
||||
Detailed SSH server config. Greyed out with an inline notice ("Enable Allow SSH in General to configure") when serverSshAllowed is off.
|
||||
- **Interface name** — `interfaceName` · *text input*
|
||||
- Name of the WireGuard network interface created on this host.
|
||||
- **WireGuard port** — `wireguardPort` · *number input*
|
||||
- Local UDP port the WireGuard interface listens on.
|
||||
- **MTU** — `mtu` · *number input*
|
||||
- Maximum transmission unit for the WireGuard interface.
|
||||
|
||||
- SSH root login — enableSshRoot
|
||||
- SFTP — enableSshSftp
|
||||
- Local port forwarding — enableSshLocalPortForwarding
|
||||
- Remote port forwarding — enableSshRemotePortForwarding
|
||||
- Advanced (collapsed)
|
||||
- Disable SSH auth — disableSshAuth
|
||||
- JWT cache TTL — sshJwtCacheTtl
|
||||
---
|
||||
|
||||
5. Diagnostics
|
||||
## 2. Network
|
||||
|
||||
Everything you reach for when something is wrong. Mixes config (log level) with actions (bundle creation) deliberately — they're used together.
|
||||
Routing, DNS, firewall, and encryption — everything the daemon does on the wire and to the host network.
|
||||
|
||||
- Log level — Debug / Info / Warn / Error (dropdown)
|
||||
- Log file path — read-only, with Copy + Reveal in Finder/Explorer buttons (configFile / logFile from daemon)
|
||||
- Config file path — same pattern
|
||||
- Debug bundle (own section)
|
||||
- Anonymize toggle
|
||||
- Include system info toggle
|
||||
- Upload on create toggle → reveals URL field when on
|
||||
- Create Bundle button → progress indicator → resulting path or upload URL displayed below
|
||||
### Routing & DNS
|
||||
|
||||
6. About
|
||||
- **Lazy connections** — `lazyConnectionEnabled` · *toggle switch*
|
||||
- Only establish peer tunnels on first traffic instead of eagerly at startup.
|
||||
- **Network monitor** — `networkMonitor` · *toggle switch*
|
||||
- Reconnect automatically when the host network changes (Wi-Fi switch, VPN, sleep/wake).
|
||||
- **Enable DNS** — `disableDns` (inverted) · *toggle switch*
|
||||
- Apply NetBird-managed DNS settings to the host resolver.
|
||||
- **Enable client routes** — `disableClientRoutes` (inverted) · *toggle switch*
|
||||
- Accept routes advertised by other peers so this client can reach their networks.
|
||||
- **Enable server routes** — `disableServerRoutes` (inverted) · *toggle switch*
|
||||
- Advertise this host's local routes to other peers.
|
||||
|
||||
Version + update flow + identity reference.
|
||||
### Firewall
|
||||
|
||||
- **Block inbound traffic** — `blockInbound` · *toggle switch*
|
||||
- Drop all unsolicited inbound traffic on the NetBird interface.
|
||||
- **Block LAN access** — `blockLanAccess` · *toggle switch*
|
||||
- Prevent peers from reaching this host's local network.
|
||||
|
||||
### Encryption
|
||||
|
||||
- **Quantum-resistant encryption** — `rosenpassEnabled` · *toggle switch*
|
||||
- Add a post-quantum key exchange (Rosenpass) on top of WireGuard.
|
||||
- **Permissive mode** — `rosenpassPermissive` · *toggle switch* (nested, only when above is on)
|
||||
- Allow connections to peers without quantum-resistance support.
|
||||
|
||||
---
|
||||
|
||||
## 3. SSH
|
||||
|
||||
NetBird SSH server config. Master switch at the top; sub-toggles greyed out with an inline notice ("Enable Allow SSH to configure") when the master is off.
|
||||
|
||||
### Server
|
||||
|
||||
- **Allow SSH** — `serverSshAllowed` · *toggle switch* (master)
|
||||
- Run the NetBird SSH server on this host so other peers can connect to it.
|
||||
- **Allow root login** — `enableSshRoot` · *toggle switch*
|
||||
- Permit incoming SSH sessions to authenticate as `root`.
|
||||
- **Enable SFTP** — `enableSshSftp` · *toggle switch*
|
||||
- Allow file transfers over the NetBird SSH server.
|
||||
- **Local port forwarding** — `enableSshLocalPortForwarding` · *toggle switch*
|
||||
- Allow clients to forward local ports through this host.
|
||||
- **Remote port forwarding** — `enableSshRemotePortForwarding` · *toggle switch*
|
||||
- Allow clients to expose remote ports back through this host.
|
||||
|
||||
### Authentication
|
||||
|
||||
- **Disable SSH auth** — `disableSshAuth` · *toggle switch*
|
||||
- Skip JWT authentication for incoming SSH sessions. **Insecure — diagnostics only.**
|
||||
- **JWT cache TTL** — `sshJwtCacheTtl` · *number input (seconds)*
|
||||
- How long verified JWTs are cached before re-validation.
|
||||
|
||||
---
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
Everything you reach for when something is wrong. Config + actions deliberately mixed — they're used together.
|
||||
|
||||
### Logging
|
||||
|
||||
- **Log level** — *dropdown: Debug / Info / Warn / Error*
|
||||
- Verbosity of the daemon log. Raise to Debug when reproducing an issue.
|
||||
- **Log file path** — *read-only text + Copy + Reveal in Finder/Explorer*
|
||||
- **Config file path** — *read-only text + Copy + Reveal in Finder/Explorer*
|
||||
|
||||
### Debug bundle
|
||||
|
||||
- **Anonymize** — *toggle switch*
|
||||
- Strip IPs, hostnames, and peer names from the bundle before saving.
|
||||
- **Include system info** — *toggle switch*
|
||||
- Add OS, kernel, and network interface details to the bundle.
|
||||
- **Upload on create** — *toggle switch*
|
||||
- When on, reveals an upload URL field and uploads the bundle after creation.
|
||||
- **Create Bundle** — *button* → progress indicator → resulting path or upload URL.
|
||||
|
||||
---
|
||||
|
||||
## 5. About
|
||||
|
||||
Version, update flow, and identity reference.
|
||||
|
||||
- App version, daemon version
|
||||
- Check for Updates button → drives the auto-update flow (15-min timeout, success/error states)
|
||||
- Local peer info quick-reference (FQDN, IP) — same data the connection-state view shows
|
||||
- **Check for Updates** — *button* (drives auto-update flow; 15-min timeout, success/error states)
|
||||
- Local peer info quick-reference (FQDN, IP) — same data shown in the connection-state view
|
||||
- Links: docs, GitHub repo, license
|
||||
|
||||
@@ -5,35 +5,30 @@ import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||
import QuickActions from "@/screens/QuickActions.tsx";
|
||||
import LoginUrl from "@/screens/LoginUrl.tsx";
|
||||
import Update from "@/screens/Update.tsx";
|
||||
import Layout from "@/layout.tsx";
|
||||
import Peers from "@/screens/Peers.tsx";
|
||||
import Networks from "@/screens/Networks.tsx";
|
||||
import Profiles from "@/screens/Profiles.tsx";
|
||||
import Settings from "@/screens/Settings.tsx";
|
||||
import Debug from "@/screens/Debug.tsx";
|
||||
import {Main} from "@/screens/Main.tsx";
|
||||
import { AppLayout } from "@/layouts/AppLayout.tsx";
|
||||
import { Main } from "@/layouts/Main.tsx";
|
||||
import { Settings } from "@/modules/settings/Settings.tsx";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/login" element={<LoginUrl />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route path="peers" element={<Peers />} />
|
||||
<Route path="networks" element={<Networks />} />
|
||||
<Route path="profiles" element={<Profiles />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="debug" element={<Debug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SkeletonTheme>
|
||||
</React.StrictMode>,
|
||||
<React.StrictMode>
|
||||
<SkeletonTheme baseColor={"#25282d"} highlightColor={"#33373e"}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="/quick" element={<QuickActions />} />
|
||||
<Route path="/login" element={<LoginUrl />} />
|
||||
<Route path="/update" element={<Update />} />
|
||||
<Route element={<AppLayout />}>
|
||||
<Route index element={<Main />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={"/"} replace />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SkeletonTheme>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from "react";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
import { Label } from "@/components/Label";
|
||||
import { ToggleSwitch } from "@/components/ToggleSwitch";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
interface Props {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
helpText?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
dataCy?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
textWrapperClassName?: string;
|
||||
}
|
||||
|
||||
export default function FancyToggleSwitch({
|
||||
value,
|
||||
onChange,
|
||||
helpText,
|
||||
label,
|
||||
children,
|
||||
disabled = false,
|
||||
dataCy,
|
||||
className,
|
||||
labelClassName,
|
||||
textWrapperClassName = "max-w-sm",
|
||||
}: Readonly<Props>) {
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
onChange(!value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (disabled) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
handleToggle();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
role={"switch"}
|
||||
aria-checked={value}
|
||||
className={cn(
|
||||
"cursor-pointer transition-all duration-300 relative z-[1]",
|
||||
"inline-block text-left w-full",
|
||||
disabled && "opacity-50 pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex justify-between gap-10"}>
|
||||
<div className={cn(textWrapperClassName)}>
|
||||
<Label className={labelClassName}>{label}</Label>
|
||||
<HelpText margin={false}>{helpText}</HelpText>
|
||||
</div>
|
||||
<div className={"mt-2 pr-1"}>
|
||||
<ToggleSwitch
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
dataCy={dataCy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{children && value ? (
|
||||
<div className="mt-4" onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ type Props = HTMLMotionProps<"button"> & {
|
||||
description?: string;
|
||||
active?: boolean;
|
||||
iconSize?: number;
|
||||
iconBackground?: boolean;
|
||||
};
|
||||
|
||||
export const NavItem = forwardRef<HTMLButtonElement, Props>(
|
||||
@@ -19,6 +20,7 @@ export const NavItem = forwardRef<HTMLButtonElement, Props>(
|
||||
description,
|
||||
active = false,
|
||||
iconSize = 15,
|
||||
iconBackground = true,
|
||||
className,
|
||||
type = "button",
|
||||
...props
|
||||
@@ -40,21 +42,33 @@ export const NavItem = forwardRef<HTMLButtonElement, Props>(
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-md flex items-center justify-center shrink-0",
|
||||
"transition-colors duration-150",
|
||||
active ? "bg-nb-gray-800" : "bg-nb-gray-920",
|
||||
)}
|
||||
>
|
||||
{iconBackground ? (
|
||||
<div
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-md flex items-center justify-center shrink-0",
|
||||
"transition-colors duration-150",
|
||||
active ? "bg-nb-gray-800" : "bg-nb-gray-920",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
size={iconSize}
|
||||
className={cn(
|
||||
"transition-colors duration-150",
|
||||
active
|
||||
? "text-nb-gray-200"
|
||||
: "text-nb-gray-400",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Icon
|
||||
size={iconSize}
|
||||
className={cn(
|
||||
"transition-colors duration-150",
|
||||
"shrink-0 ml-2 transition-colors duration-150",
|
||||
active ? "text-nb-gray-200" : "text-nb-gray-400",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={"min-w-0"}>
|
||||
<h2
|
||||
className={cn(
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function PlaceholderHeader() {
|
||||
return (
|
||||
<div
|
||||
className="h-[36px] shrink-0 cursor-default wails-draggable"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -124,9 +124,10 @@ export const ProfileSelector = ({ email = "" }: Props) => {
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"h-7 w-7 flex items-center justify-center bg-nb-gray-900 rounded-md text-xs font-semibold"
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center justify-center bg-nb-gray-900 rounded-md text-xs font-semibold",
|
||||
email ? "h-7 w-7" : "h-6 w-6",
|
||||
)}
|
||||
style={{ color: initialColor }}
|
||||
>
|
||||
{initial}
|
||||
|
||||
71
client/ui-wails/frontend/src/components/ToggleSwitch.tsx
Normal file
71
client/ui-wails/frontend/src/components/ToggleSwitch.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
type SwitchVariants = VariantProps<typeof switchVariants>;
|
||||
|
||||
const switchVariants = cva("", {
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-[24px] w-[44px]",
|
||||
small: "h-[18px] w-[36px]",
|
||||
},
|
||||
variant: {
|
||||
default: [
|
||||
"dark:data-[state=checked]:bg-netbird dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200",
|
||||
],
|
||||
"red-green": [
|
||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||
],
|
||||
red: [
|
||||
"dark:data-[state=checked]:bg-red-600 dark:data-[state=unchecked]:bg-nb-gray-700",
|
||||
"data-[state=checked]:bg-red-500 data-[state=unchecked]:bg-red-200",
|
||||
],
|
||||
},
|
||||
"thumb-size": {
|
||||
default: "h-5 w-5 data-[state=checked]:translate-x-5",
|
||||
small: "h-[14px] w-[14px] data-[state=checked]:translate-x-[17px]",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ToggleSwitch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
|
||||
SwitchVariants & { dataCy?: string }
|
||||
>(
|
||||
(
|
||||
{ className, size = "default", variant = "default", dataCy, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950",
|
||||
className,
|
||||
switchVariants({ size, variant }),
|
||||
)}
|
||||
{...props}
|
||||
data-cy={dataCy}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick?.(e);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
switchVariants({ "thumb-size": size }),
|
||||
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
),
|
||||
);
|
||||
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { ToggleSwitch };
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import PlaceholderHeader from "@/components/PlaceholderHeader";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<PlaceholderHeader />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
client/ui-wails/frontend/src/layouts/AppLayout.tsx
Normal file
11
client/ui-wails/frontend/src/layouts/AppLayout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Header } from "@/layouts/Header.tsx";
|
||||
|
||||
export const AppLayout = () => {
|
||||
return (
|
||||
<div className={"flex h-full flex-col"}>
|
||||
<Header />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
|
||||
export const ConnectionStatus = () => {
|
||||
return (
|
||||
<div className={"flex flex-col items-center"}>
|
||||
<div className={"flex flex-col h-full items-center justify-center"}>
|
||||
<NetBirdConnectToggle state={ConnectionState.Connected} />
|
||||
<h1
|
||||
className={
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { ProfileSelector } from "@/components/ProfileSelector.tsx";
|
||||
import { IconButton } from "@/components/IconButton.tsx";
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
type Props = {
|
||||
settingsActive?: boolean;
|
||||
onSettingsClick?: () => void;
|
||||
};
|
||||
export const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const settingsActive = location.pathname.startsWith("/settings");
|
||||
|
||||
export const Header = ({ settingsActive = false, onSettingsClick }: Props) => {
|
||||
return (
|
||||
<div className={"w-full justify-between flex mb-12"}>
|
||||
<ProfileSelector email={"eduard@netbird.io"} />
|
||||
<div
|
||||
className={
|
||||
"pt-4 shrink-0 cursor-default wails-draggable flex items-center justify-end px-4 gap-3 bg-gradient-to-b from-nb-gray-800/15"
|
||||
}
|
||||
>
|
||||
<div className={"ml-20"}>
|
||||
<ProfileSelector email={"eduard@netbird.io"} />
|
||||
</div>
|
||||
<IconButton
|
||||
icon={SettingsIcon}
|
||||
onClick={onSettingsClick}
|
||||
onClick={() => navigate(settingsActive ? "/" : "/settings")}
|
||||
className={cn(
|
||||
settingsActive &&
|
||||
"bg-nb-gray-930 text-nb-gray-200 hover:text-nb-gray-200",
|
||||
|
||||
22
client/ui-wails/frontend/src/layouts/Main.tsx
Normal file
22
client/ui-wails/frontend/src/layouts/Main.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import { Navigation } from "@/layouts/Navigation.tsx";
|
||||
import { Peers } from "@/modules/peers/Peers.tsx";
|
||||
|
||||
export const Main = () => {
|
||||
return (
|
||||
<div className={"wails-draggable flex flex-1 min-h-0 p-4 gap-4"}>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col max-w-xs w-full shrink-0 items-center"
|
||||
}
|
||||
>
|
||||
<ConnectionStatus />
|
||||
<Navigation peersActive />
|
||||
</div>
|
||||
<MainRightSide>
|
||||
<Peers />
|
||||
</MainRightSide>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx";
|
||||
import { Header } from "@/layouts/Header.tsx";
|
||||
import { Navigation } from "@/layouts/Navigation.tsx";
|
||||
|
||||
export type MainModule = "peers" | "settings";
|
||||
|
||||
type Props = {
|
||||
active: MainModule;
|
||||
onChange: (module: MainModule) => void;
|
||||
};
|
||||
|
||||
export const MainLeftSide = ({ active, onChange }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={"flex flex-col max-w-xs w-full shrink-0 items-center"}
|
||||
>
|
||||
<Header
|
||||
settingsActive={active === "settings"}
|
||||
onSettingsClick={() =>
|
||||
onChange(active === "settings" ? "peers" : "settings")
|
||||
}
|
||||
/>
|
||||
<ConnectionStatus />
|
||||
<Navigation
|
||||
peersActive={active === "peers"}
|
||||
onPeersClick={() => {
|
||||
if (active !== "peers") onChange("peers");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
export const Navigation = ({ peersActive = false, onPeersClick }: Props) => {
|
||||
return (
|
||||
<nav className={"w-full flex flex-col gap-1 mt-8"}>
|
||||
<nav className={"w-full flex flex-col gap-1 mt-auto"}>
|
||||
<NavItem
|
||||
icon={MonitorSmartphoneIcon}
|
||||
title={"Peers"}
|
||||
|
||||
27
client/ui-wails/frontend/src/lib/MainModuleContext.tsx
Normal file
27
client/ui-wails/frontend/src/lib/MainModuleContext.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
export type MainModule = "peers" | "settings";
|
||||
|
||||
type Ctx = {
|
||||
active: MainModule;
|
||||
setActive: (m: MainModule) => void;
|
||||
};
|
||||
|
||||
const MainModuleContext = createContext<Ctx | null>(null);
|
||||
|
||||
export const MainModuleProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [active, setActive] = useState<MainModule>("peers");
|
||||
return (
|
||||
<MainModuleContext.Provider value={{ active, setActive }}>
|
||||
{children}
|
||||
</MainModuleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMainModule = () => {
|
||||
const ctx = useContext(MainModuleContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useMainModule must be used within MainModuleProvider");
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,7 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import {
|
||||
SettingsNavigation,
|
||||
SettingsSection,
|
||||
} from "@/modules/settings/SettingsNavigation.tsx";
|
||||
|
||||
export const Settings = () => {
|
||||
const [active, setActive] = useState<SettingsSection>("general");
|
||||
|
||||
return (
|
||||
<div className={"flex flex-col w-full h-full min-h-0 pt-4 px-4"}>
|
||||
<h2 className={"text-sm font-medium text-nb-gray-200"}>Settings</h2>
|
||||
<div className={"wails-draggable flex flex-1 min-h-0 p-4 gap-4"}>
|
||||
<div className={"flex flex-col w-52 shrink-0 items-center"}>
|
||||
<SettingsNavigation active={active} onChange={setActive} />
|
||||
</div>
|
||||
<MainRightSide>{null}</MainRightSide>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NavItem } from "@/components/NavItem.tsx";
|
||||
import {
|
||||
InfoIcon,
|
||||
LifeBuoyIcon,
|
||||
NetworkIcon,
|
||||
SlidersHorizontalIcon,
|
||||
TerminalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export type SettingsSection =
|
||||
| "general"
|
||||
| "network"
|
||||
| "ssh"
|
||||
| "troubleshooting"
|
||||
| "about";
|
||||
|
||||
type Props = {
|
||||
active: SettingsSection;
|
||||
onChange: (section: SettingsSection) => void;
|
||||
};
|
||||
|
||||
const ITEMS: {
|
||||
id: SettingsSection;
|
||||
icon: typeof SlidersHorizontalIcon;
|
||||
title: string;
|
||||
}[] = [
|
||||
{ id: "general", icon: SlidersHorizontalIcon, title: "General" },
|
||||
{ id: "network", icon: NetworkIcon, title: "Network" },
|
||||
{ id: "ssh", icon: TerminalIcon, title: "SSH" },
|
||||
{ id: "troubleshooting", icon: LifeBuoyIcon, title: "Troubleshooting" },
|
||||
{ id: "about", icon: InfoIcon, title: "About" },
|
||||
];
|
||||
|
||||
export const SettingsNavigation = ({ active, onChange }: Props) => {
|
||||
return (
|
||||
<nav className={"w-full flex flex-col gap-1"}>
|
||||
{ITEMS.map(({ id, icon, title }) => (
|
||||
<NavItem
|
||||
key={id}
|
||||
icon={icon}
|
||||
title={title}
|
||||
iconSize={14}
|
||||
iconBackground={false}
|
||||
className={"py-2.5"}
|
||||
active={active === id}
|
||||
onClick={() => {
|
||||
if (active !== id) onChange(id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { MainLeftSide, MainModule } from "@/layouts/MainLeftSide.tsx";
|
||||
import { MainRightSide } from "@/layouts/MainRightSide.tsx";
|
||||
import { Peers } from "@/modules/peers/Peers.tsx";
|
||||
import { Settings } from "@/modules/settings/Settings.tsx";
|
||||
|
||||
type Props = {
|
||||
|
||||
};
|
||||
export const Main = ({}: Props) => {
|
||||
const [active, setActive] = useState<MainModule>("peers");
|
||||
|
||||
return (
|
||||
<div className={"wails-draggable flex h-full p-4 gap-4 min-h-0"}>
|
||||
<MainLeftSide active={active} onChange={setActive} />
|
||||
|
||||
<MainRightSide>
|
||||
{active === "peers" ? <Peers /> : <Settings />}
|
||||
</MainRightSide>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
278
client/ui-wails/frontend/wails-go-api (1).md
Normal file
278
client/ui-wails/frontend/wails-go-api (1).md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Wails Go API surface for the React frontend
|
||||
|
||||
All bindings live under `frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/`. Import them as:
|
||||
|
||||
```ts
|
||||
import { Connection, Peers, Networks, Settings, Profiles, Debug, Update, Forwarding } from "./bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
import * as $models from "./bindings/github.com/netbirdio/netbird/client/ui-wails/services/models";
|
||||
```
|
||||
|
||||
Every method returns `$CancellablePromise<T>` (Wails3 wrapper around a Promise — call `.cancel()` to abort the underlying gRPC stream / call).
|
||||
|
||||
## Push events
|
||||
|
||||
Subscribe with the Wails event API: `import { Events } from "@wailsio/runtime"`.
|
||||
|
||||
| Event name | Payload type | Fires on |
|
||||
|---|---|---|
|
||||
| `netbird:status` | `Status` | Daemon connection-state change (Connected / Connecting / Disconnected / Idle), peer-list change, address change, management/signal flip. **Replaces polling**. |
|
||||
| `netbird:event` | `SystemEvent` | One push per daemon-emitted event (DNS/network/auth/connectivity/system). Drives toasts and the event log. |
|
||||
| `netbird:update:available` | `UpdateAvailable` | Daemon detected a new version. Show the update menu/banner. |
|
||||
| `netbird:update:progress` | `UpdateProgress` | `action:"show"` → open the update progress page; `action:"hide"` → close. |
|
||||
|
||||
Calling `Peers.Watch()` once at boot starts both backend stream loops; both self-restart with backoff on errors.
|
||||
|
||||
## Connection lifecycle — `Connection`
|
||||
|
||||
```ts
|
||||
Connection.Up(p: UpParams): Promise<void>
|
||||
Connection.Down(): Promise<void>
|
||||
Connection.Login(p: LoginParams): Promise<LoginResult>
|
||||
Connection.WaitSSOLogin(p: WaitSSOParams): Promise<string> // returns email/userInfo
|
||||
Connection.Logout(p: LogoutParams): Promise<void>
|
||||
```
|
||||
|
||||
- **Up flow**: call `Login` first; if `LoginResult.needsSsoLogin === true` open `verificationUriComplete` in the browser, then call `WaitSSOLogin` with `{ userCode: LoginResult.userCode, hostname: ... }`. Once that resolves call `Up`.
|
||||
- **Down flow**: just `Down()`. The daemon transitions to `Idle`.
|
||||
|
||||
```ts
|
||||
class LoginParams { profileName, username, managementUrl, setupKey, preSharedKey, hostname, hint: string }
|
||||
class LoginResult { needsSsoLogin: boolean; userCode, verificationUri, verificationUriComplete: string }
|
||||
class WaitSSOParams { userCode, hostname: string }
|
||||
class UpParams { profileName, username: string }
|
||||
class LogoutParams { profileName, username: string }
|
||||
```
|
||||
|
||||
## Status / peer list — `Peers`
|
||||
|
||||
```ts
|
||||
Peers.Get(): Promise<Status> // one-shot snapshot
|
||||
Peers.Watch(): Promise<void> // call once at boot to enable push events
|
||||
```
|
||||
|
||||
```ts
|
||||
class Status {
|
||||
status: string // "Idle" | "Connecting" | "Connected" | "SessionExpired" (see below)
|
||||
daemonVersion: string
|
||||
management: PeerLink
|
||||
signal: PeerLink
|
||||
local: LocalPeer
|
||||
peers: PeerStatus[]
|
||||
events: SystemEvent[]
|
||||
}
|
||||
|
||||
class PeerLink {
|
||||
url: string
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
class LocalPeer {
|
||||
ip, pubKey, fqdn: string
|
||||
networks: string[]
|
||||
}
|
||||
|
||||
class PeerStatus {
|
||||
ip, pubKey, fqdn: string
|
||||
connStatus: string // "Connected" | "Connecting" | "Idle"
|
||||
connStatusUpdateUnix: number // unix seconds
|
||||
relayed: boolean
|
||||
localIceCandidateType, remoteIceCandidateType: string
|
||||
localIceCandidateEndpoint, remoteIceCandidateEndpoint: string
|
||||
bytesRx, bytesTx: number
|
||||
latencyMs: number
|
||||
relayAddress: string // populated when relayed
|
||||
lastHandshakeUnix: number
|
||||
rosenpassEnabled: boolean
|
||||
networks: string[]
|
||||
}
|
||||
|
||||
class SystemEvent {
|
||||
id: string
|
||||
severity: string // "info" | "warning" | "error" | "critical"
|
||||
category: string // "network" | "dns" | "authentication" | "connectivity" | "system"
|
||||
message: string // technical / log message
|
||||
userMessage: string // human-friendly message — render this
|
||||
timestamp: number // unix seconds
|
||||
metadata: Record<string, string>
|
||||
}
|
||||
```
|
||||
|
||||
### Connection-state values
|
||||
|
||||
The `Status.status` field uses these literal strings (from the daemon):
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `"Idle"` | Disconnected — Up not invoked, or Down completed |
|
||||
| `"Connecting"` | Up in progress |
|
||||
| `"Connected"` | Tunnel up |
|
||||
| `"SessionExpired"` | SSO token expired — needs Login again |
|
||||
|
||||
(The Fyne UI also reads a synthetic `"Error"` label for some failed states; check `events` for details.)
|
||||
|
||||
### ICE candidate type values
|
||||
|
||||
`localIceCandidateType` / `remoteIceCandidateType` are pion/ICE strings: `"host"`, `"srflx"`, `"prflx"`, `"relay"`, or `""` while connecting.
|
||||
|
||||
## Networks — `Networks`
|
||||
|
||||
```ts
|
||||
Networks.List(): Promise<Network[]>
|
||||
Networks.Select(p: SelectNetworksParams): Promise<void>
|
||||
Networks.Deselect(p: SelectNetworksParams): Promise<void>
|
||||
```
|
||||
|
||||
```ts
|
||||
class Network {
|
||||
id, range: string // range is a CIDR
|
||||
selected: boolean
|
||||
domains: string[] // empty unless this is a domain network
|
||||
resolvedIps: Record<string, string[]> // domain -> IPs
|
||||
}
|
||||
|
||||
class SelectNetworksParams {
|
||||
networkIds: string[]
|
||||
append: boolean // false = replace selection, true = merge with existing
|
||||
all: boolean // true = ignore networkIds and target every network (Select-All / Deselect-All)
|
||||
}
|
||||
```
|
||||
|
||||
The Fyne UI's All / Overlapping / Exit-node tabs are filters over the same `List()` result:
|
||||
- **Exit-node**: `range === "0.0.0.0/0" || range === "::/0"`
|
||||
- **Overlapping**: client-side detection of CIDR overlap among `range` values
|
||||
- **All**: everything
|
||||
|
||||
## Forwarding / exposed services — `Forwarding`
|
||||
|
||||
```ts
|
||||
Forwarding.List(): Promise<ForwardingRule[]>
|
||||
```
|
||||
|
||||
```ts
|
||||
class ForwardingRule {
|
||||
protocol: string // "tcp" | "udp"
|
||||
destinationPort: PortInfo
|
||||
translatedAddress, translatedHostname: string
|
||||
translatedPort: PortInfo
|
||||
}
|
||||
|
||||
class PortInfo { // exactly one field is populated
|
||||
port?: number
|
||||
range?: PortRange
|
||||
}
|
||||
|
||||
class PortRange { start, end: number }
|
||||
```
|
||||
|
||||
## Profiles — `Profiles`
|
||||
|
||||
```ts
|
||||
Profiles.List(username: string): Promise<Profile[]>
|
||||
Profiles.GetActive(): Promise<ActiveProfile>
|
||||
Profiles.Switch(p: ProfileRef): Promise<void>
|
||||
Profiles.Add(p: ProfileRef): Promise<void>
|
||||
Profiles.Remove(p: ProfileRef): Promise<void>
|
||||
Profiles.Username(): Promise<string> // current OS username
|
||||
```
|
||||
|
||||
```ts
|
||||
class Profile { name: string; isActive: boolean }
|
||||
class ProfileRef { profileName, username: string }
|
||||
class ActiveProfile { profileName, username: string }
|
||||
```
|
||||
|
||||
## Settings / config — `Settings`
|
||||
|
||||
```ts
|
||||
Settings.GetConfig(p: ConfigParams): Promise<Config>
|
||||
Settings.SetConfig(p: SetConfigParams): Promise<void>
|
||||
Settings.GetFeatures(): Promise<Features>
|
||||
```
|
||||
|
||||
```ts
|
||||
class ConfigParams { profileName, username: string } // identifies which profile's config
|
||||
|
||||
class Config {
|
||||
managementUrl, adminUrl, configFile, logFile, preSharedKey: string
|
||||
interfaceName: string; wireguardPort, mtu: number
|
||||
disableAutoConnect, serverSshAllowed: boolean
|
||||
rosenpassEnabled, rosenpassPermissive: boolean
|
||||
disableNotifications, lazyConnectionEnabled, blockInbound: boolean
|
||||
networkMonitor, disableClientRoutes, disableServerRoutes: boolean
|
||||
disableDns, blockLanAccess: boolean
|
||||
enableSshRoot, enableSshSftp: boolean
|
||||
enableSshLocalPortForwarding, enableSshRemotePortForwarding: boolean
|
||||
disableSshAuth: boolean
|
||||
sshJwtCacheTtl: number
|
||||
}
|
||||
|
||||
class SetConfigParams {
|
||||
// identity (always required)
|
||||
profileName, username: string
|
||||
// any field below is optional — only the ones you set are pushed to the daemon
|
||||
managementUrl?, adminUrl?, ...
|
||||
// ... same shape as Config
|
||||
}
|
||||
|
||||
class Features {
|
||||
// feature flags from the daemon — hide UI sections when these are true
|
||||
disableProfiles, disableUpdateSettings, disableNetworks: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`SetConfig` is partial — supply only the fields you want to change, plus `profileName` + `username`. Booleans use Go pointer-presence under the hood; on the TS side undefined / missing means "leave as-is".
|
||||
|
||||
## Debug bundle / log level — `Debug`
|
||||
|
||||
```ts
|
||||
Debug.GetLogLevel(): Promise<LogLevel>
|
||||
Debug.SetLogLevel(lvl: LogLevel): Promise<void>
|
||||
Debug.Bundle(p: DebugBundleParams): Promise<DebugBundleResult>
|
||||
```
|
||||
|
||||
```ts
|
||||
class LogLevel { level: string } // "trace" | "debug" | "info" | "warning" | "error" | "panic"
|
||||
|
||||
class DebugBundleParams {
|
||||
anonymize: boolean
|
||||
systemInfo: boolean
|
||||
uploadUrl: string // empty string = no upload
|
||||
logFileCount: number // 0 = default
|
||||
}
|
||||
|
||||
class DebugBundleResult {
|
||||
path: string // local path of the generated bundle
|
||||
uploadedKey: string // populated when uploadUrl was set
|
||||
uploadFailureReason: string // populated on upload error
|
||||
}
|
||||
```
|
||||
|
||||
## Update flow — `Update`
|
||||
|
||||
```ts
|
||||
Update.Trigger(): Promise<UpdateResult> // start the install
|
||||
Update.GetInstallerResult(): Promise<UpdateResult> // poll the install outcome (long-running)
|
||||
```
|
||||
|
||||
```ts
|
||||
class UpdateResult { success: boolean; errorMsg: string }
|
||||
|
||||
class UpdateAvailable { // payload of "netbird:update:available"
|
||||
version: string
|
||||
enforced: boolean // true = management server requires it
|
||||
}
|
||||
|
||||
class UpdateProgress { // payload of "netbird:update:progress"
|
||||
action: string // "show" | "hide"
|
||||
version: string
|
||||
}
|
||||
```
|
||||
|
||||
Typical flow:
|
||||
1. Listen for `"netbird:update:available"` → show the "Update X.Y.Z" affordance.
|
||||
2. User clicks → call `Update.Trigger()`.
|
||||
3. The page that shows the install progress polls `GetInstallerResult()` (15-min timeout). On `success: true` the daemon will exit; the app should `app.Quit()` (or restart). On `success: false` show `errorMsg`.
|
||||
|
||||
## Toast notifications
|
||||
|
||||
The tray sends OS notifications via `application/services/notifications` automatically for `netbird:event` events that have `userMessage`. The frontend doesn't need to do anything for that; the data is also delivered via `netbird:event` if you want to render an in-window log.
|
||||
@@ -113,6 +113,7 @@ func main() {
|
||||
InvisibleTitleBarHeight: 38,
|
||||
Backdrop: application.MacBackdropTranslucent,
|
||||
TitleBar: application.MacTitleBarHiddenInset,
|
||||
CollectionBehavior: application.MacWindowCollectionBehaviorFullScreenNone,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user