mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-02 04:51:29 +02:00
remove unused lang icons, disable text selection
This commit is contained in:
@@ -89,6 +89,8 @@ export default function SessionAboutToExpireDialog() {
|
||||
}, [busy, t]);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const username = await ProfilesSvc.Username();
|
||||
const active = await ProfilesSvc.GetActive();
|
||||
@@ -96,12 +98,16 @@ export default function SessionAboutToExpireDialog() {
|
||||
profileName: active.profileName || "default",
|
||||
username,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("logout from session-about-to-expire failed", e);
|
||||
} finally {
|
||||
WindowManager.CloseSessionAboutToExpire().catch(console.error);
|
||||
} catch (e) {
|
||||
await Dialogs.Error({
|
||||
Title: t("sessionAboutToExpire.logoutFailedTitle"),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, []);
|
||||
}, [busy, t]);
|
||||
|
||||
return (
|
||||
<ConfirmDialog ref={contentRef}>
|
||||
@@ -129,6 +135,7 @@ export default function SessionAboutToExpireDialog() {
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
|
||||
@@ -37,7 +37,13 @@ export default function SessionExpiredDialog() {
|
||||
</div>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant={"primary"} size={"md"} className={"w-full"} onClick={signIn}>
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={signIn}
|
||||
>
|
||||
{t("sessionExpired.signIn")}
|
||||
</Button>
|
||||
<Button variant={"secondary"} size={"md"} className={"w-full"} onClick={later}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import { Dialogs, Events } from "@wailsio/runtime";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Connection } from "@bindings/services";
|
||||
import { Button } from "@/components/Button";
|
||||
@@ -11,6 +11,7 @@ import { DialogDescription } from "@/components/DialogDescription";
|
||||
import { DialogHeading } from "@/components/DialogHeading";
|
||||
import { SquareIcon } from "@/components/SquareIcon";
|
||||
import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow";
|
||||
import { formatErrorMessage } from "@/lib/errors";
|
||||
|
||||
const EVENT_CANCEL = "browser-login:cancel";
|
||||
const WINDOW_WIDTH = 360;
|
||||
@@ -21,19 +22,29 @@ export default function WaitingForBrowserDialog() {
|
||||
const uri = params.get("uri") ?? "";
|
||||
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH);
|
||||
|
||||
const reportOpenFailure = useCallback(
|
||||
(e: unknown) => {
|
||||
void Dialogs.Error({
|
||||
Title: t("browserLogin.openFailedTitle"),
|
||||
Message: formatErrorMessage(e),
|
||||
});
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// Open the system browser only after the dialog has mounted (which
|
||||
// means useAutoSizeWindow has called Window.Show). startLogin used to
|
||||
// fire OpenURL itself but the browser typically beat React's mount
|
||||
// and landed on top of the still-hidden NetBird popup.
|
||||
useEffect(() => {
|
||||
if (!uri) return;
|
||||
Connection.OpenURL(uri).catch(console.error);
|
||||
}, [uri]);
|
||||
Connection.OpenURL(uri).catch(reportOpenFailure);
|
||||
}, [uri, reportOpenFailure]);
|
||||
|
||||
const tryAgain = useCallback(() => {
|
||||
if (!uri) return;
|
||||
Connection.OpenURL(uri).catch(console.error);
|
||||
}, [uri]);
|
||||
Connection.OpenURL(uri).catch(reportOpenFailure);
|
||||
}, [uri, reportOpenFailure]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
void Events.Emit(EVENT_CANCEL);
|
||||
@@ -67,6 +78,7 @@ export default function WaitingForBrowserDialog() {
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"secondary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
|
||||
@@ -122,6 +122,7 @@ export default function InstallProgressDialog() {
|
||||
{isError && (
|
||||
<DialogActions>
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"secondary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function SettingsAbout() {
|
||||
<img src={netbirdFull} alt={"NetBird"} className={"h-7 w-auto"} />
|
||||
<div className={"flex flex-col items-center gap-0.5 text-center"}>
|
||||
<p
|
||||
className={"text-sm font-semibold text-nb-gray-100 cursor-default select-none"}
|
||||
className={"text-sm font-semibold text-nb-gray-100 cursor-text select-text"}
|
||||
onClick={handleVersionClick}
|
||||
>
|
||||
{daemonVersion === "development" ? (
|
||||
@@ -48,7 +48,7 @@ export function SettingsAbout() {
|
||||
t("settings.about.client", { version: daemonVersion })
|
||||
)}
|
||||
</p>
|
||||
<p className={"text-sm text-nb-gray-300"}>
|
||||
<p className={"text-sm text-nb-gray-300 cursor-text select-text"}>
|
||||
{t("settings.about.gui", { version: guiVersion })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { System } from "@wailsio/runtime";
|
||||
import Button from "@/components/Button";
|
||||
import { HelpText } from "@/components/HelpText";
|
||||
import { Input } from "@/components/Input";
|
||||
@@ -7,6 +8,25 @@ import { Label } from "@/components/Label";
|
||||
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
|
||||
// macOS: the Darwin utun control socket parses the digits after "utun" as the
|
||||
// unit number, so the daemon (and the CLI's parseInterfaceName in
|
||||
// client/cmd/up.go) only accepts utun<N>.
|
||||
// Linux/Windows: no daemon-side validation; the Linux kernel caps names at
|
||||
// IFNAMSIZ-1 = 15 chars and the safe charset across both is [A-Za-z0-9._-].
|
||||
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";
|
||||
const PORT_MIN = 1;
|
||||
const PORT_MAX = 65535;
|
||||
// Mirrors client/iface/iface.go MinMTU / MaxMTU. 576 is the IPv4 "every host
|
||||
// must accept" datagram size from RFC 791 — safe floor when IPv6 is off; for
|
||||
// IPv6 the daemon still needs 1280 on the path (RFC 8200), but that is not
|
||||
// the validator's job to enforce.
|
||||
const MTU_MIN = 576;
|
||||
const MTU_MAX = 8192;
|
||||
|
||||
export function SettingsAdvanced() {
|
||||
const { t } = useTranslation();
|
||||
const { config, saveFields } = useSettings();
|
||||
@@ -19,6 +39,32 @@ export function SettingsAdvanced() {
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
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 hasErrors = Object.keys(errors).length > 0;
|
||||
const hasChanges =
|
||||
values.interfaceName !== config.interfaceName ||
|
||||
values.wireguardPort !== config.wireguardPort ||
|
||||
@@ -26,7 +72,7 @@ export function SettingsAdvanced() {
|
||||
values.preSharedKey !== config.preSharedKey;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!hasChanges || saving) return;
|
||||
if (!hasChanges || saving || hasErrors) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await saveFields(values);
|
||||
@@ -41,6 +87,7 @@ export function SettingsAdvanced() {
|
||||
<Input
|
||||
label={t("settings.advanced.interfaceName.label")}
|
||||
value={values.interfaceName}
|
||||
error={errors.interfaceName}
|
||||
onChange={(e) =>
|
||||
setValues((v) => ({ ...v, interfaceName: e.target.value }))
|
||||
}
|
||||
@@ -49,7 +96,10 @@ export function SettingsAdvanced() {
|
||||
<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,
|
||||
@@ -60,7 +110,10 @@ export function SettingsAdvanced() {
|
||||
<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) }))
|
||||
}
|
||||
@@ -91,7 +144,7 @@ export function SettingsAdvanced() {
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
disabled={!hasChanges || saving}
|
||||
disabled={!hasChanges || saving || hasErrors}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t("common.saveChanges")}
|
||||
|
||||
@@ -199,7 +199,7 @@ const ProfileRow = ({ profile, isActive, onDeregister, onDelete }: ProfileRowPro
|
||||
/>
|
||||
<div className={"flex flex-col min-w-0 flex-1 leading-tight"}>
|
||||
<div className={"flex items-center gap-2 min-w-0"}>
|
||||
<span className={"truncate font-medium text-nb-gray-100"}>
|
||||
<span className={"truncate font-medium text-nb-gray-100 select-text cursor-text"}>
|
||||
{profile.name}
|
||||
</span>
|
||||
{isActive && <Badge>{t("settings.profiles.active")}</Badge>}
|
||||
@@ -231,7 +231,7 @@ const TruncatedEmail = ({ email }: { email: string }) => {
|
||||
}, [email]);
|
||||
|
||||
const span = (
|
||||
<span ref={ref} className={"text-xs text-nb-gray-300 truncate mt-0.5"}>
|
||||
<span ref={ref} className={"text-xs text-nb-gray-300 truncate mt-0.5 select-text cursor-text"}>
|
||||
{email}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { CircleCheckBig, FolderOpen, Loader2 } from "lucide-react";
|
||||
import { Debug as DebugSvc } from "@bindings/services";
|
||||
import type { DebugBundleResult } from "@bindings/services/models.js";
|
||||
import { Button } from "@/components/Button";
|
||||
import { DialogActions } from "@/components/DialogActions";
|
||||
import { DialogDescription } from "@/components/DialogDescription";
|
||||
import { DialogHeading } from "@/components/DialogHeading";
|
||||
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
|
||||
import HelpText from "@/components/HelpText.tsx";
|
||||
import { Input } from "@/components/Input";
|
||||
import { Label } from "@/components/Label";
|
||||
import { StatusPanel } from "@/components/StatusPanel";
|
||||
import { SquareIcon } from "@/components/SquareIcon";
|
||||
import { cn } from "@/lib/cn";
|
||||
import type { DebugStage } from "@/modules/debug-bundle/useDebugBundle.ts";
|
||||
import { useDebugBundleContext } from "@/modules/debug-bundle/useDebugBundleContext.ts";
|
||||
@@ -112,22 +115,47 @@ export function SettingsTroubleshooting() {
|
||||
);
|
||||
}
|
||||
|
||||
function CenteredPanel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"absolute inset-0 flex flex-col items-center justify-center gap-5 p-8 text-center"
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressSection({ stage, onCancel }: { stage: DebugStage; onCancel: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const cancelling = stage.kind === "cancelling";
|
||||
return (
|
||||
<StatusPanel
|
||||
variant={"loading"}
|
||||
title={stageLabel(stage, t)}
|
||||
description={t("settings.troubleshooting.progress.description")}
|
||||
actions={
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onCancel} disabled={cancelling}>
|
||||
{cancelling
|
||||
? t("settings.troubleshooting.cancelling")
|
||||
: t("common.cancel")}
|
||||
<CenteredPanel>
|
||||
<SquareIcon icon={Loader2} className={"[&_svg]:animate-spin"} />
|
||||
|
||||
<div className={"flex flex-col items-center gap-2 max-w-xs"}>
|
||||
<DialogHeading className={"text-balance"}>
|
||||
{stageLabel(stage, t)}
|
||||
</DialogHeading>
|
||||
<DialogDescription>
|
||||
{t("settings.troubleshooting.progress.description")}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
<DialogActions className={"max-w-[220px]"}>
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"secondary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={onCancel}
|
||||
disabled={cancelling}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</DialogActions>
|
||||
</CenteredPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,39 +176,23 @@ function DoneResult({
|
||||
void DebugSvc.RevealFile(result.path).catch(() => {});
|
||||
};
|
||||
return (
|
||||
<StatusPanel
|
||||
variant={"success"}
|
||||
title={
|
||||
showKey
|
||||
? t("settings.troubleshooting.done.uploadedTitle")
|
||||
: t("settings.troubleshooting.done.savedTitle")
|
||||
}
|
||||
description={
|
||||
showKey
|
||||
? t("settings.troubleshooting.done.uploadedDescription")
|
||||
: t("settings.troubleshooting.done.savedDescription")
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button variant={"secondary"} size={"xs"} onClick={onClose}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
{showKey ? (
|
||||
<Button variant={"primary"} size={"xs"} copy={result.uploadedKey}>
|
||||
{t("settings.troubleshooting.done.copyKey")}
|
||||
</Button>
|
||||
) : (
|
||||
result.path && (
|
||||
<Button variant={"primary"} size={"xs"} onClick={onRevealPath}>
|
||||
<FolderOpen size={12} />
|
||||
{t("settings.troubleshooting.done.openFolder")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={"w-full max-w-xs mx-auto flex flex-col gap-3"}>
|
||||
<CenteredPanel>
|
||||
<SquareIcon icon={CircleCheckBig} className={"[&_svg]:text-green-500"} />
|
||||
|
||||
<div className={"flex flex-col items-center gap-2 max-w-xs"}>
|
||||
<DialogHeading className={"text-balance"}>
|
||||
{showKey
|
||||
? t("settings.troubleshooting.done.uploadedTitle")
|
||||
: t("settings.troubleshooting.done.savedTitle")}
|
||||
</DialogHeading>
|
||||
<DialogDescription>
|
||||
{showKey
|
||||
? t("settings.troubleshooting.done.uploadedDescription")
|
||||
: t("settings.troubleshooting.done.savedDescription")}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
<div className={"w-full max-w-xs flex flex-col gap-3"}>
|
||||
{showKey && <Input value={result.uploadedKey} readOnly copy />}
|
||||
|
||||
{result.path && !showKey && (
|
||||
@@ -214,7 +226,42 @@ function DoneResult({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StatusPanel>
|
||||
|
||||
<DialogActions className={"max-w-[220px]"}>
|
||||
{showKey ? (
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
copy={result.uploadedKey}
|
||||
>
|
||||
{t("settings.troubleshooting.done.copyKey")}
|
||||
</Button>
|
||||
) : (
|
||||
result.path && (
|
||||
<Button
|
||||
autoFocus
|
||||
variant={"primary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={onRevealPath}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
{t("settings.troubleshooting.done.openFolder")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"md"}
|
||||
className={"w-full"}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</CenteredPanel>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Dialogs } from "@wailsio/runtime";
|
||||
import i18next from "@/lib/i18n";
|
||||
import { useSettings } from "@/modules/settings/SettingsContext.tsx";
|
||||
@@ -45,6 +45,11 @@ export function useManagementUrl() {
|
||||
const [url, setUrl] = useState(
|
||||
config.managementUrl === CLOUD_MANAGEMENT_URL ? "" : config.managementUrl,
|
||||
);
|
||||
// Guard against double-showing the cloud-switch confirmation when the
|
||||
// user toggles the segmented control multiple times before the prior
|
||||
// Dialogs.Warning promise resolves. Without it each click queues a
|
||||
// fresh native dialog and the user sees them stack up.
|
||||
const switchConfirmOpenRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
setModeState(modeFromUrl(config.managementUrl));
|
||||
@@ -61,6 +66,8 @@ export function useManagementUrl() {
|
||||
// Switching from a self-hosted management server to NetBird Cloud
|
||||
// re-points the client at a different deployment and forces a
|
||||
// reconnect/re-login. Confirm before applying.
|
||||
if (switchConfirmOpenRef.current) return;
|
||||
switchConfirmOpenRef.current = true;
|
||||
const cancelLabel = i18next.t("common.cancel");
|
||||
const confirmLabel = i18next.t("settings.general.management.switchCloudConfirm");
|
||||
void Dialogs.Warning({
|
||||
@@ -70,11 +77,15 @@ export function useManagementUrl() {
|
||||
{ Label: cancelLabel, IsCancel: true, IsDefault: true },
|
||||
{ Label: confirmLabel },
|
||||
],
|
||||
}).then((result) => {
|
||||
if (result !== confirmLabel) return;
|
||||
setModeState(ManagementMode.Cloud);
|
||||
void saveField("managementUrl", CLOUD_MANAGEMENT_URL);
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
if (result !== confirmLabel) return;
|
||||
setModeState(ManagementMode.Cloud);
|
||||
void saveField("managementUrl", CLOUD_MANAGEMENT_URL);
|
||||
})
|
||||
.finally(() => {
|
||||
switchConfirmOpenRef.current = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setModeState(next);
|
||||
|
||||
Reference in New Issue
Block a user