This commit is contained in:
Eduard Gert
2026-06-12 18:00:28 +02:00
parent c4d4208ea6
commit 6cc6d68b37
18 changed files with 487 additions and 317 deletions
@@ -24,6 +24,7 @@ import { useClientVersion } from "@/contexts/ClientVersionContext";
import { cn } from "@/lib/cn";
import { formatShortcut, useKeyboardShortcut } from "@/hooks/useKeyboardShortcut";
import { useViewMode, type ViewMode } from "@/contexts/ViewModeContext";
import { useRestrictions } from "@/contexts/RestrictionsContext";
import { isWindows } from "@/lib/platform.ts";
const SETTINGS_SHORTCUT = { key: ",", cmd: true } as const;
@@ -33,6 +34,7 @@ export const MainHeader = () => {
const [menuOpen, setMenuOpen] = useState(false);
const { viewMode, setViewMode } = useViewMode();
const { updateAvailable } = useClientVersion();
const { mdm, features } = useRestrictions();
const openSettings = useCallback(() => {
setMenuOpen(false);
@@ -55,7 +57,9 @@ export const MainHeader = () => {
setViewMode(mode);
};
const profileSlot = <ProfileDropdown onManageProfiles={openManageProfiles} />;
const profileSlot = features.disableProfiles ? null : (
<ProfileDropdown onManageProfiles={openManageProfiles} />
);
const settingsSlot = (
<div className={"relative"}>
@@ -94,19 +98,23 @@ export const MainHeader = () => {
</DropdownMenuShortcut>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<ViewModeItem
icon={RectangleVertical}
label={t("header.menu.defaultView")}
selected={viewMode === "default"}
onSelect={() => selectMode("default")}
/>
<ViewModeItem
icon={PanelsRightBottom}
label={t("header.menu.advancedView")}
selected={viewMode === "advanced"}
onSelect={() => selectMode("advanced")}
/>
{!mdm.disableAdvancedView && (
<>
<DropdownMenuSeparator />
<ViewModeItem
icon={RectangleVertical}
label={t("header.menu.defaultView")}
selected={viewMode === "default"}
onSelect={() => selectMode("default")}
/>
<ViewModeItem
icon={PanelsRightBottom}
label={t("header.menu.advancedView")}
selected={viewMode === "advanced"}
onSelect={() => selectMode("advanced")}
/>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
{updateAvailable && (
@@ -6,12 +6,14 @@ import { Navigation } from "@/modules/main/advanced/Navigation.tsx";
import { cn } from "@/lib/cn";
import { NavSectionProvider, useNavSection } from "@/contexts/NavSectionContext";
import { ViewModeProvider, useViewMode } from "@/contexts/ViewModeContext";
import { useEffect } from "react";
import { NotConnectedState } from "@/components/empty-state/NotConnectedState";
import { useStatus } from "@/contexts/StatusContext";
import { Peers } from "@/modules/main/advanced/peers/Peers";
import { Networks } from "@/modules/main/advanced/networks/Networks";
import { NetworksProvider } from "@/contexts/NetworksContext";
import { PeerDetailProvider, usePeerDetail } from "@/contexts/PeerDetailContext";
import { useRestrictions } from "@/contexts/RestrictionsContext";
import { PeerDetailPanel } from "@/modules/main/advanced/peers/PeerDetailPanel";
import { isWindows } from "@/lib/platform.ts";
@@ -29,7 +31,16 @@ export const MainPage = () => {
};
const MainBody = () => {
const { viewMode } = useViewMode();
const { viewMode, setViewMode } = useViewMode();
const { mdm, features } = useRestrictions();
// Force flip the view if mdm changed it
useEffect(() => {
if (mdm.disableAdvancedView && viewMode === "advanced") {
setViewMode("default");
}
}, [mdm.disableAdvancedView, viewMode, setViewMode]);
const isAdvanced = viewMode === "advanced";
return (
@@ -43,9 +54,11 @@ const MainBody = () => {
)}
>
<MainConnectionStatusSwitch />
<div className={"absolute left-5 right-5 bottom-5 wails-no-draggable"}>
<MainExitNodeSwitcher />
</div>
{!features.disableNetworks && (
<div className={"absolute left-5 right-5 bottom-5 wails-no-draggable"}>
<MainExitNodeSwitcher />
</div>
)}
</div>
{isAdvanced && (
<NavSectionProvider>
@@ -4,6 +4,8 @@ import { Layers3Icon, LucideProps, MonitorSmartphoneIcon } from "lucide-react";
import { cn } from "@/lib/cn";
import { useNavSection, type NavSection } from "@/contexts/NavSectionContext";
import { useStatus } from "@/contexts/StatusContext";
import { useRestrictions } from "@/contexts/RestrictionsContext";
import { useEffect } from "react";
type TabEntry = {
value: NavSection;
@@ -15,20 +17,30 @@ export const Navigation = () => {
const { t } = useTranslation();
const { section, setSection } = useNavSection();
const { status } = useStatus();
const { features } = useRestrictions();
const isConnected = status?.status === "Connected";
// Reset back to peers tab if mdm or feature flag flipped it
useEffect(() => {
if (features.disableNetworks && section === "networks") {
setSection("peers");
}
}, [features.disableNetworks, section, setSection]);
const tabs: TabEntry[] = [
{
value: "peers",
label: t("nav.peers.title"),
icon: MonitorSmartphoneIcon,
},
{
];
if (!features.disableNetworks) {
tabs.push({
value: "networks",
label: t("nav.resources.title"),
icon: Layers3Icon,
},
];
});
}
return (
<div className={"wails-no-draggable shrink-0 flex items-stretch "}>
@@ -14,6 +14,7 @@ import {
isValidManagementUrl,
normalizeManagementUrl,
} from "@/hooks/useManagementUrl";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
type Props = {
open: boolean;
@@ -31,6 +32,8 @@ const sanitizeProfileInput = (value: string): string =>
export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) => {
const { t } = useTranslation();
const { mdm } = useRestrictions();
const managedManagementUrl = mdm.managementURL;
const [name, setName] = useState("");
const [nameError, setNameError] = useState<string | null>(null);
const nameRef = useRef<HTMLInputElement>(null);
@@ -70,6 +73,12 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
return;
}
if (managedManagementUrl) {
onCreate(sanitized, managedManagementUrl);
onOpenChange(false);
return;
}
if (mode === ManagementMode.Cloud) {
onCreate(sanitized, CLOUD_MANAGEMENT_URL);
onOpenChange(false);
@@ -145,35 +154,41 @@ export const ProfileCreationModal = ({ open, onOpenChange, onCreate }: Props) =>
/>
</div>
<div className="flex flex-col gap-2">
<div className={"pl-1"}>
<Label as={"div"} className={"mb-0.5"}>
{t("settings.general.management.label")}
</Label>
<HelpText margin={false}>
{t("profile.dialog.managementHelp")}
</HelpText>
</div>
<div className="flex flex-col gap-3">
<ManagementServerSwitch value={mode} onChange={setMode} fullWidth />
{mode === ManagementMode.SelfHosted && (
<Input
ref={urlRef}
autoFocus
placeholder={t(
"settings.general.management.urlPlaceholder",
)}
value={url}
onChange={(e) => setUrl(e.target.value)}
error={urlInputError}
warning={urlInputWarning}
spellCheck={false}
autoComplete="off"
autoCapitalize="off"
{!managedManagementUrl && (
<div className="flex flex-col gap-2">
<div className={"pl-1"}>
<Label as={"div"} className={"mb-0.5"}>
{t("settings.general.management.label")}
</Label>
<HelpText margin={false}>
{t("profile.dialog.managementHelp")}
</HelpText>
</div>
<div className="flex flex-col gap-3">
<ManagementServerSwitch
value={mode}
onChange={setMode}
fullWidth
/>
)}
{mode === ManagementMode.SelfHosted && (
<Input
ref={urlRef}
autoFocus
placeholder={t(
"settings.general.management.urlPlaceholder",
)}
value={url}
onChange={(e) => setUrl(e.target.value)}
error={urlInputError}
warning={urlInputWarning}
spellCheck={false}
autoComplete="off"
autoCapitalize="off"
/>
)}
</div>
</div>
</div>
)}
<DialogActions className={"flex-row items-center justify-end gap-2.5 pt-2"}>
<Button
@@ -7,6 +7,7 @@ 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();
@@ -14,9 +15,11 @@ 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;
@@ -24,15 +27,14 @@ const MTU_MAX = 8192;
export function SettingsAdvanced() {
const { t } = useTranslation();
const { config, saveFields } = useSettings();
const { mdm } = useRestrictions();
const [values, setValues] = useState({
interfaceName: config.interfaceName,
wireguardPort: config.wireguardPort,
mtu: config.mtu,
});
// PSK is write-only from the UI: the daemon returns only preSharedKeySet,
// never the value. Empty means "leave unchanged"; a typed value is sent on
// save. Reset on every config reload (e.g. after a successful save).
const [psk, setPsk] = useState("");
const [saving, setSaving] = useState(false);
@@ -66,18 +68,22 @@ export function SettingsAdvanced() {
return out;
}, [values.interfaceName, values.wireguardPort, values.mtu, t]);
const hasErrors = Object.keys(errors).length > 0;
const filteredErrors = mdm.wireguardPort ? { ...errors, wireguardPort: undefined } : errors;
const hasErrors = Object.values(filteredErrors).some((v) => v !== undefined);
const hasChanges =
values.interfaceName !== config.interfaceName ||
values.wireguardPort !== config.wireguardPort ||
(!mdm.wireguardPort && values.wireguardPort !== config.wireguardPort) ||
values.mtu !== config.mtu ||
psk !== "";
(!mdm.preSharedKey && psk !== "");
const handleSave = async () => {
if (!hasChanges || saving || hasErrors) return;
setSaving(true);
try {
await saveFields(values, psk ? { preSharedKey: psk } : undefined);
const partial: typeof values = { ...values };
if (mdm.wireguardPort) partial.wireguardPort = config.wireguardPort;
const pskOpts = !mdm.preSharedKey && psk ? { preSharedKey: psk } : undefined;
await saveFields(partial, pskOpts);
} finally {
setSaving(false);
}
@@ -92,24 +98,28 @@ export function SettingsAdvanced() {
error={errors.interfaceName}
onChange={(e) => setValues((v) => ({ ...v, interfaceName: e.target.value }))}
/>
<div className={"grid grid-cols-2 gap-4"}>
<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>
<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"}
@@ -122,23 +132,25 @@ export function SettingsAdvanced() {
</div>
</SectionGroup>
<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={psk !== ""}
placeholder={
config.preSharedKeySet
? t("settings.advanced.psk.configured")
: "kQv0qF3oQpJYdgD5mC9hL7sB2xZ8nT4eU6wY1aR3jK0="
}
value={psk}
onChange={(e) => setPsk(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={psk !== ""}
placeholder={
config.preSharedKeySet
? t("settings.advanced.psk.configured")
: "kQv0qF3oQpJYdgD5mC9hL7sB2xZ8nT4eU6wY1aR3jK0="
}
value={psk}
onChange={(e) => setPsk(e.target.value)}
/>
</div>
</SectionGroup>
)}
<SettingsBottomBar>
<Button
@@ -10,6 +10,7 @@ import { useAutostartSetting, useSettings } from "@/contexts/SettingsContext.tsx
import { ManagementServerSwitch } from "@/components/ManagementServerSwitch.tsx";
import { ManagementMode, useManagementUrl } from "@/hooks/useManagementUrl.ts";
import { LanguagePicker } from "@/components/LanguagePicker.tsx";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
export function SettingsGeneral() {
const { t } = useTranslation();
@@ -17,6 +18,7 @@ export function SettingsGeneral() {
const { autostart, setAutostartEnabled } = useAutostartSetting();
const { mode, setMode, setUrl, displayUrl, showError, canSave, save, checking, unreachable } =
useManagementUrl();
const { mdm } = useRestrictions();
const inputRef = useRef<HTMLInputElement>(null);
const prevMode = useRef(mode);
@@ -37,12 +39,14 @@ export function SettingsGeneral() {
label={t("settings.general.notifications.label")}
helpText={t("settings.general.notifications.help")}
/>
<FancyToggleSwitch
value={!config.disableAutoConnect}
onChange={(v) => setField("disableAutoConnect", !v)}
label={t("settings.general.connectOnStartup.label")}
helpText={t("settings.general.connectOnStartup.help")}
/>
{!mdm.disableAutoConnect && (
<FancyToggleSwitch
value={!config.disableAutoConnect}
onChange={(v) => setField("disableAutoConnect", !v)}
label={t("settings.general.connectOnStartup.label")}
helpText={t("settings.general.connectOnStartup.help")}
/>
)}
{(autostart === null || autostart.supported) && (
<FancyToggleSwitch
value={autostart?.enabled ?? false}
@@ -54,46 +58,48 @@ export function SettingsGeneral() {
)}
</SectionGroup>
<SectionGroup title={t("settings.general.section.connection")}>
<div>
<div className={"flex items-start gap-3"}>
<div className={"flex-1 min-w-0"}>
<Label as={"div"}>{t("settings.general.management.label")}</Label>
<HelpText>{t("settings.general.management.help")}</HelpText>
{!mdm.managementURL && (
<SectionGroup title={t("settings.general.section.connection")}>
<div>
<div className={"flex items-start gap-3"}>
<div className={"flex-1 min-w-0"}>
<Label as={"div"}>{t("settings.general.management.label")}</Label>
<HelpText>{t("settings.general.management.help")}</HelpText>
</div>
<ManagementServerSwitch value={mode} onChange={setMode} />
</div>
<ManagementServerSwitch value={mode} onChange={setMode} />
{mode === ManagementMode.SelfHosted && (
<div className={"flex items-start gap-3 mt-2"}>
<Input
ref={inputRef}
value={displayUrl}
onChange={(e) => setUrl(e.target.value)}
placeholder={t("settings.general.management.urlPlaceholder")}
error={
showError
? t("settings.general.management.urlError")
: undefined
}
warning={
unreachable
? t("settings.general.management.urlUnreachable")
: undefined
}
/>
<Button
variant={"primary"}
size={"md"}
disabled={!canSave}
loading={checking}
onClick={() => save()}
>
{t("common.save")}
</Button>
</div>
)}
</div>
{mode === ManagementMode.SelfHosted && (
<div className={"flex items-start gap-3 mt-2"}>
<Input
ref={inputRef}
value={displayUrl}
onChange={(e) => setUrl(e.target.value)}
placeholder={t("settings.general.management.urlPlaceholder")}
error={
showError
? t("settings.general.management.urlError")
: undefined
}
warning={
unreachable
? t("settings.general.management.urlUnreachable")
: undefined
}
/>
<Button
variant={"primary"}
size={"md"}
disabled={!canSave}
loading={checking}
onClick={() => save()}
>
{t("common.save")}
</Button>
</div>
)}
</div>
</SectionGroup>
</SectionGroup>
)}
</>
);
}
@@ -3,6 +3,7 @@ import { Tooltip } from "@/components/Tooltip.tsx";
import { VerticalTabs } from "@/components/VerticalTabs.tsx";
import { UpdateBadge } from "@/modules/auto-update/UpdateBadge.tsx";
import { useClientVersion } from "@/contexts/ClientVersionContext.tsx";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
import {
BoltIcon,
InfoIcon,
@@ -17,6 +18,7 @@ import {
export const SettingsNavigation = () => {
const { t } = useTranslation();
const { updateAvailable } = useClientVersion();
const { mdm, features } = useRestrictions();
const aboutAdornment = updateAvailable ? (
<Tooltip content={t("settings.tabs.updateAvailable")} side={"right"}>
@@ -27,36 +29,44 @@ export const SettingsNavigation = () => {
return (
<div className={"flex flex-col w-52 shrink-0 items-center select-none"}>
<VerticalTabs.List>
<VerticalTabs.Trigger
value={"general"}
icon={SlidersHorizontalIcon}
title={t("settings.tabs.general")}
/>
<VerticalTabs.Trigger
value={"network"}
icon={NetworkIcon}
title={t("settings.tabs.network")}
/>
<VerticalTabs.Trigger
value={"security"}
icon={ShieldIcon}
title={t("settings.tabs.security")}
/>
<VerticalTabs.Trigger
value={"profiles"}
icon={UserCircleIcon}
title={t("settings.tabs.profiles")}
/>
<VerticalTabs.Trigger
value={"ssh"}
icon={SquareTerminalIcon}
title={t("settings.tabs.ssh")}
/>
<VerticalTabs.Trigger
value={"advanced"}
icon={BoltIcon}
title={t("settings.tabs.advanced")}
/>
{!features.disableUpdateSettings && (
<>
<VerticalTabs.Trigger
value={"general"}
icon={SlidersHorizontalIcon}
title={t("settings.tabs.general")}
/>
<VerticalTabs.Trigger
value={"network"}
icon={NetworkIcon}
title={t("settings.tabs.network")}
/>
<VerticalTabs.Trigger
value={"security"}
icon={ShieldIcon}
title={t("settings.tabs.security")}
/>
{!features.disableProfiles && (
<VerticalTabs.Trigger
value={"profiles"}
icon={UserCircleIcon}
title={t("settings.tabs.profiles")}
/>
)}
{!mdm.allowServerSSH && (
<VerticalTabs.Trigger
value={"ssh"}
icon={SquareTerminalIcon}
title={t("settings.tabs.ssh")}
/>
)}
<VerticalTabs.Trigger
value={"advanced"}
icon={BoltIcon}
title={t("settings.tabs.advanced")}
/>
</>
)}
<VerticalTabs.Trigger
value={"troubleshooting"}
icon={LifeBuoyIcon}
@@ -2,10 +2,12 @@ import { useTranslation } from "react-i18next";
import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch";
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
import { useSettings } from "@/contexts/SettingsContext.tsx";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
export function SettingsNetwork() {
const { t } = useTranslation();
const { config, setField } = useSettings();
const { mdm } = useRestrictions();
return (
<>
@@ -31,18 +33,22 @@ export function SettingsNetwork() {
label={t("settings.network.dns.label")}
helpText={t("settings.network.dns.help")}
/>
<FancyToggleSwitch
value={!config.disableClientRoutes}
onChange={(v) => setField("disableClientRoutes", !v)}
label={t("settings.network.clientRoutes.label")}
helpText={t("settings.network.clientRoutes.help")}
/>
<FancyToggleSwitch
value={!config.disableServerRoutes}
onChange={(v) => setField("disableServerRoutes", !v)}
label={t("settings.network.serverRoutes.label")}
helpText={t("settings.network.serverRoutes.help")}
/>
{!mdm.disableClientRoutes && (
<FancyToggleSwitch
value={!config.disableClientRoutes}
onChange={(v) => setField("disableClientRoutes", !v)}
label={t("settings.network.clientRoutes.label")}
helpText={t("settings.network.clientRoutes.help")}
/>
)}
{!mdm.disableServerRoutes && (
<FancyToggleSwitch
value={!config.disableServerRoutes}
onChange={(v) => setField("disableServerRoutes", !v)}
label={t("settings.network.serverRoutes.label")}
helpText={t("settings.network.serverRoutes.help")}
/>
)}
<FancyToggleSwitch
value={!config.disableIpv6}
onChange={(v) => setField("disableIpv6", !v)}
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import { useLocation } from "react-router-dom";
import { Events } from "@wailsio/runtime";
import * as ScrollArea from "@radix-ui/react-scroll-area";
@@ -16,13 +16,54 @@ import { SettingsSSH } from "@/modules/settings/SettingsSSH.tsx";
import { SettingsAdvanced } from "@/modules/settings/SettingsAdvanced.tsx";
import { SettingsTroubleshooting } from "@/modules/settings/SettingsTroubleshooting.tsx";
import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
const EVENT_SETTINGS_OPEN = "netbird:settings:open";
const enum Tab {
General = "general",
Network = "network",
Security = "security",
Profiles = "profiles",
SSH = "ssh",
Advanced = "advanced",
Troubleshooting = "troubleshooting",
About = "about",
}
const TAB_CONTENT: Record<Tab, ReactNode> = {
[Tab.General]: <SettingsGeneral />,
[Tab.Network]: <SettingsNetwork />,
[Tab.Security]: <SettingsSecurity />,
[Tab.Profiles]: <ProfilesTab />,
[Tab.SSH]: <SettingsSSH />,
[Tab.Advanced]: <SettingsAdvanced />,
[Tab.Troubleshooting]: <SettingsTroubleshooting />,
[Tab.About]: <SettingsAbout />,
};
export const SettingsPage = () => {
const location = useLocation();
const navState = location.state as { tab?: string } | null;
const [active, setActive] = useState(() => navState?.tab ?? "general");
const { mdm, features } = useRestrictions();
const visibleTabs = useMemo<Tab[]>(() => {
const editable = !features.disableUpdateSettings;
const visibility: Record<Tab, boolean> = {
[Tab.General]: editable,
[Tab.Network]: editable,
[Tab.Security]: editable,
[Tab.Profiles]: editable && !features.disableProfiles,
[Tab.SSH]: editable && !mdm.allowServerSSH,
[Tab.Advanced]: editable,
[Tab.Troubleshooting]: true,
[Tab.About]: true,
};
return (Object.keys(visibility) as Tab[]).filter((t) => visibility[t]);
}, [features.disableUpdateSettings, features.disableProfiles, mdm.allowServerSSH]);
const defaultTab = visibleTabs[0];
const [active, setActive] = useState<string>(() => navState?.tab ?? defaultTab);
useEffect(() => {
if (navState?.tab) setActive(navState.tab);
@@ -30,9 +71,14 @@ export const SettingsPage = () => {
useEffect(() => {
return Events.On(EVENT_SETTINGS_OPEN, (e: { data: string }) => {
setActive(e.data || "general");
setActive(e.data || defaultTab);
});
}, []);
}, [defaultTab]);
// Reset active tab if it got disabled by any feature flag or mdm restrictions
useEffect(() => {
if (!visibleTabs.includes(active as Tab)) setActive(defaultTab);
}, [visibleTabs, active, defaultTab]);
return (
<>
@@ -53,30 +99,11 @@ export const SettingsPage = () => {
>
<ScrollArea.Viewport className={"h-full w-full"}>
<div className={"py-6 px-7"}>
<VerticalTabs.Content value={"general"}>
<SettingsGeneral />
</VerticalTabs.Content>
<VerticalTabs.Content value={"network"}>
<SettingsNetwork />
</VerticalTabs.Content>
<VerticalTabs.Content value={"security"}>
<SettingsSecurity />
</VerticalTabs.Content>
<VerticalTabs.Content value={"profiles"}>
<ProfilesTab />
</VerticalTabs.Content>
<VerticalTabs.Content value={"ssh"}>
<SettingsSSH />
</VerticalTabs.Content>
<VerticalTabs.Content value={"advanced"}>
<SettingsAdvanced />
</VerticalTabs.Content>
<VerticalTabs.Content value={"troubleshooting"}>
<SettingsTroubleshooting />
</VerticalTabs.Content>
<VerticalTabs.Content value={"about"}>
<SettingsAbout />
</VerticalTabs.Content>
{visibleTabs.map((tab) => (
<VerticalTabs.Content key={tab} value={tab}>
{TAB_CONTENT[tab]}
</VerticalTabs.Content>
))}
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
@@ -2,19 +2,25 @@ import { useTranslation } from "react-i18next";
import FancyToggleSwitch from "@/components/switches/FancyToggleSwitch";
import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
import { useSettings } from "@/contexts/SettingsContext.tsx";
import { useRestrictions } from "@/contexts/RestrictionsContext.tsx";
export function SettingsSecurity() {
const { t } = useTranslation();
const { config, setField } = useSettings();
const { mdm } = useRestrictions();
const showEncryptionSection = !(mdm.rosenpassEnabled && mdm.rosenpassPermissive);
return (
<>
<SectionGroup title={t("settings.security.section.firewall")}>
<FancyToggleSwitch
value={config.blockInbound}
onChange={(v) => setField("blockInbound", v)}
label={t("settings.security.blockInbound.label")}
helpText={t("settings.security.blockInbound.help")}
/>
{!mdm.blockInbound && (
<FancyToggleSwitch
value={config.blockInbound}
onChange={(v) => setField("blockInbound", v)}
label={t("settings.security.blockInbound.label")}
helpText={t("settings.security.blockInbound.help")}
/>
)}
<FancyToggleSwitch
value={config.blockLanAccess}
onChange={(v) => setField("blockLanAccess", v)}
@@ -23,24 +29,30 @@ export function SettingsSecurity() {
/>
</SectionGroup>
<SectionGroup title={t("settings.security.section.encryption")}>
<FancyToggleSwitch
value={config.rosenpassEnabled}
onChange={(v) => {
setField("rosenpassEnabled", v);
if (!v) setField("rosenpassPermissive", false);
}}
label={t("settings.security.rosenpass.label")}
helpText={t("settings.security.rosenpass.help")}
/>
<FancyToggleSwitch
value={config.rosenpassPermissive}
onChange={(v) => setField("rosenpassPermissive", v)}
label={t("settings.security.rosenpassPermissive.label")}
helpText={t("settings.security.rosenpassPermissive.help")}
disabled={!config.rosenpassEnabled}
/>
</SectionGroup>
{showEncryptionSection && (
<SectionGroup title={t("settings.security.section.encryption")}>
{!mdm.rosenpassEnabled && (
<FancyToggleSwitch
value={config.rosenpassEnabled}
onChange={(v) => {
setField("rosenpassEnabled", v);
if (!v) setField("rosenpassPermissive", false);
}}
label={t("settings.security.rosenpass.label")}
helpText={t("settings.security.rosenpass.help")}
/>
)}
{!mdm.rosenpassPermissive && (
<FancyToggleSwitch
value={config.rosenpassPermissive}
onChange={(v) => setField("rosenpassPermissive", v)}
label={t("settings.security.rosenpassPermissive.label")}
helpText={t("settings.security.rosenpassPermissive.help")}
disabled={!config.rosenpassEnabled}
/>
)}
</SectionGroup>
)}
</>
);
}
@@ -5,7 +5,7 @@ import {
Settings as SettingsSvc,
WindowManager,
} from "@bindings/services";
import { SetConfigParams } from "@bindings/services/models.js";
import { Restrictions, SetConfigParams } from "@bindings/services/models.js";
import { ConfirmDialog } from "@/components/dialog/ConfirmDialog";
import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow";
import { errorDialog, formatErrorMessage } from "@/lib/errors";
@@ -22,7 +22,9 @@ function shouldShowManagementStep(
activeProfile: string,
email: string,
managementUrl: string,
managedManagementUrl: string,
): boolean {
if (managedManagementUrl) return false;
if (activeProfile !== "default") return false;
if (email.trim() !== "") return false;
return isNetbirdCloud(managementUrl);
@@ -50,9 +52,10 @@ export default function WelcomeDialog() {
ProfilesSvc.GetActive(),
]);
const profileName = active.profileName || "default";
const [config, list] = await Promise.all([
const [config, list, restrictions] = await Promise.all([
SettingsSvc.GetConfig({ profileName, username }),
ProfilesSvc.List(username),
SettingsSvc.GetRestrictions().catch(() => new Restrictions()),
]);
const profile = list.find((p) => p.name === profileName);
const email = profile?.email ?? "";
@@ -65,6 +68,7 @@ export default function WelcomeDialog() {
profileName,
email,
config.managementUrl,
restrictions.mdm.managementURL,
),
});
} catch (e) {