mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 23:59:08 +02:00
[management,client] 0.75.0 release with new desktop UI (#6473)
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com>
This commit is contained in:
co-authored by
Zoltan Papp
Eduard Gert
braginini
Pascal Fischer
riccardom
parent
c9d387bd0d
commit
91acb8147c
@@ -0,0 +1,179 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { System } from "@wailsio/runtime";
|
||||
import Button from "@/components/buttons/Button";
|
||||
import { HelpText } from "@/components/typography/HelpText";
|
||||
import { Input } from "@/components/inputs/Input";
|
||||
import { Label } from "@/components/typography/Label";
|
||||
import { SectionGroup, SettingsBottomBar } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/contexts/SettingsContext.tsx";
|
||||
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
|
||||
|
||||
// macOS daemon/CLI only accept utun<N> (Darwin parses digits as the utun unit); Linux caps at IFNAMSIZ-1 = 15 chars.
|
||||
const IS_MAC = System.IsMac();
|
||||
const INTERFACE_NAME_RE = IS_MAC ? /^utun\d+$/ : /^[A-Za-z0-9._-]{1,15}$/;
|
||||
const INTERFACE_NAME_ERROR_KEY = IS_MAC
|
||||
? "settings.advanced.interfaceName.errorMac"
|
||||
: "settings.advanced.interfaceName.error";
|
||||
|
||||
// Port 0 lets the daemon pick a random free port.
|
||||
const PORT_MIN = 0;
|
||||
const PORT_MAX = 65535;
|
||||
|
||||
// Mirrors client/iface/iface.go MinMTU / MaxMTU.
|
||||
const MTU_MIN = 576;
|
||||
const MTU_MAX = 8192;
|
||||
|
||||
const PSK_MASK = "**********";
|
||||
|
||||
export function SettingsAdvanced() {
|
||||
const { t } = useTranslation();
|
||||
const { config, saveFields } = useSettings();
|
||||
const { mdm } = useRestrictions();
|
||||
|
||||
const initialPsk = config.preSharedKeySet ? PSK_MASK : "";
|
||||
|
||||
const [values, setValues] = useState({
|
||||
interfaceName: config.interfaceName,
|
||||
wireguardPort: config.wireguardPort,
|
||||
mtu: config.mtu,
|
||||
});
|
||||
|
||||
const [pskInputValue, setPskInputValue] = useState(initialPsk);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setValues({
|
||||
interfaceName: config.interfaceName,
|
||||
wireguardPort: config.wireguardPort,
|
||||
mtu: config.mtu,
|
||||
});
|
||||
setPskInputValue(config.preSharedKeySet ? PSK_MASK : "");
|
||||
}, [config.interfaceName, config.wireguardPort, config.mtu, config.preSharedKeySet]);
|
||||
|
||||
const errors = useMemo(() => {
|
||||
const out: { interfaceName?: string; wireguardPort?: string; mtu?: string } = {};
|
||||
if (!INTERFACE_NAME_RE.test(values.interfaceName)) {
|
||||
out.interfaceName = t(INTERFACE_NAME_ERROR_KEY);
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(values.wireguardPort) ||
|
||||
values.wireguardPort < PORT_MIN ||
|
||||
values.wireguardPort > PORT_MAX
|
||||
) {
|
||||
out.wireguardPort = t("settings.advanced.port.error", {
|
||||
min: PORT_MIN,
|
||||
max: PORT_MAX,
|
||||
});
|
||||
}
|
||||
if (!Number.isInteger(values.mtu) || values.mtu < MTU_MIN || values.mtu > MTU_MAX) {
|
||||
out.mtu = t("settings.advanced.mtu.error", { min: MTU_MIN, max: MTU_MAX });
|
||||
}
|
||||
return out;
|
||||
}, [values.interfaceName, values.wireguardPort, values.mtu, t]);
|
||||
|
||||
const filteredErrors = mdm.wireguardPort ? { ...errors, wireguardPort: undefined } : errors;
|
||||
const hasErrors = Object.values(filteredErrors).some((v) => v !== undefined);
|
||||
const pskChanged = pskInputValue !== initialPsk;
|
||||
const hasChanges =
|
||||
values.interfaceName !== config.interfaceName ||
|
||||
(!mdm.wireguardPort && values.wireguardPort !== config.wireguardPort) ||
|
||||
values.mtu !== config.mtu ||
|
||||
(!mdm.preSharedKey && pskChanged);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!hasChanges || saving || hasErrors) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const partial: typeof values = { ...values };
|
||||
if (mdm.wireguardPort) partial.wireguardPort = config.wireguardPort;
|
||||
|
||||
const pskEdited = !mdm.preSharedKey && pskChanged && pskInputValue !== PSK_MASK;
|
||||
const pskOpts = pskEdited ? { preSharedKey: pskInputValue } : undefined;
|
||||
await saveFields(partial, pskOpts);
|
||||
if (pskEdited) setPskInputValue(pskInputValue === "" ? "" : PSK_MASK);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionGroup title={t("settings.advanced.section.interface")}>
|
||||
<Input
|
||||
label={t("settings.advanced.interfaceName.label")}
|
||||
value={values.interfaceName}
|
||||
error={errors.interfaceName}
|
||||
onChange={(e) => setValues((v) => ({ ...v, interfaceName: e.target.value }))}
|
||||
spellCheck={false}
|
||||
autoComplete={"off"}
|
||||
autoCorrect={"off"}
|
||||
autoCapitalize={"off"}
|
||||
/>
|
||||
<div className={mdm.wireguardPort ? "" : "grid grid-cols-2 gap-4"}>
|
||||
{!mdm.wireguardPort && (
|
||||
<div>
|
||||
<Input
|
||||
label={t("settings.advanced.port.label")}
|
||||
type={"number"}
|
||||
min={PORT_MIN}
|
||||
max={PORT_MAX}
|
||||
value={values.wireguardPort}
|
||||
error={errors.wireguardPort}
|
||||
onChange={(e) =>
|
||||
setValues((v) => ({
|
||||
...v,
|
||||
wireguardPort: Number(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<HelpText className={"mt-1.5"}>
|
||||
{t("settings.advanced.port.help")}
|
||||
</HelpText>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
label={t("settings.advanced.mtu.label")}
|
||||
type={"number"}
|
||||
min={MTU_MIN}
|
||||
max={MTU_MAX}
|
||||
value={values.mtu}
|
||||
error={errors.mtu}
|
||||
onChange={(e) => setValues((v) => ({ ...v, mtu: Number(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
</SectionGroup>
|
||||
|
||||
{!mdm.preSharedKey && (
|
||||
<SectionGroup title={t("settings.advanced.section.security")}>
|
||||
<div>
|
||||
<Label as={"div"}>{t("settings.advanced.psk.label")}</Label>
|
||||
<HelpText>{t("settings.advanced.psk.help")}</HelpText>
|
||||
<Input
|
||||
type={"password"}
|
||||
showPasswordToggle={pskInputValue !== PSK_MASK}
|
||||
placeholder={"kQv0qF3oQpJYdgD5mC9hL7sB2xZ8nT4eU6wY1aR3jK0="}
|
||||
value={pskInputValue}
|
||||
onChange={(e) => setPskInputValue(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete={"new-password"}
|
||||
autoCorrect={"off"}
|
||||
autoCapitalize={"off"}
|
||||
/>
|
||||
</div>
|
||||
</SectionGroup>
|
||||
)}
|
||||
|
||||
<SettingsBottomBar>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
disabled={!hasChanges || saving || hasErrors}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t("common.saveChanges")}
|
||||
</Button>
|
||||
</SettingsBottomBar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user