From b058e66e32effe76a9b59a52c19d2b47e6daf0e7 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Mon, 29 Dec 2025 12:38:50 +0100 Subject: [PATCH] Add auto update setting (#519) --- src/interfaces/Account.ts | 1 + src/modules/settings/ClientSettingsTab.tsx | 139 ++++++++++++++++++++- 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src/interfaces/Account.ts b/src/interfaces/Account.ts index 88e1714..4ddb87f 100644 --- a/src/interfaces/Account.ts +++ b/src/interfaces/Account.ts @@ -22,6 +22,7 @@ export interface Account { dns_domain: string; network_range?: string; lazy_connection_enabled: boolean; + auto_update_version: string; }; onboarding?: AccountOnboarding; } diff --git a/src/modules/settings/ClientSettingsTab.tsx b/src/modules/settings/ClientSettingsTab.tsx index 010aae4..8d2aef9 100644 --- a/src/modules/settings/ClientSettingsTab.tsx +++ b/src/modules/settings/ClientSettingsTab.tsx @@ -1,16 +1,27 @@ import Breadcrumbs from "@components/Breadcrumbs"; +import Button from "@components/Button"; import FancyToggleSwitch from "@components/FancyToggleSwitch"; +import HelpText from "@components/HelpText"; import InlineLink from "@components/InlineLink"; +import { Input } from "@components/Input"; +import { Label } from "@components/Label"; import { notify } from "@components/Notification"; +import { + SelectDropdown, + SelectOption, +} from "@components/select/SelectDropdown"; +import { useHasChanges } from "@hooks/useHasChanges"; import * as Tabs from "@radix-ui/react-tabs"; import { useApiCall } from "@utils/api"; +import { validator } from "@utils/helpers"; import { ClockFadingIcon, ExternalLinkIcon, FlaskConicalIcon, MonitorSmartphoneIcon, + RefreshCcw, } from "lucide-react"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { useSWRConfig } from "swr"; import SettingsIcon from "@/assets/icons/SettingsIcon"; import { usePermissions } from "@/contexts/PermissionsProvider"; @@ -20,6 +31,21 @@ type Props = { account: Account; }; +const latestOrCustomVersion = [ + { + label: "Disabled", + value: "disabled", + }, + { + label: "Latest Version", + value: "latest", + }, + { + label: "Custom Version", + value: "custom", + }, +] as SelectOption[]; + export default function ClientSettingsTab({ account }: Readonly) { const { permission } = usePermissions(); @@ -30,6 +56,77 @@ export default function ClientSettingsTab({ account }: Readonly) { account.settings?.lazy_connection_enabled ?? false, ); + const autoUpdateSetting = account.settings?.auto_update_version; + const isAutoUpdateEnabled = + !!autoUpdateSetting && autoUpdateSetting !== "disabled"; + const isCustomVersion = validator.isValidVersion(autoUpdateSetting); + const [autoUpdateMethod, setAutoUpdateMethod] = useState( + isAutoUpdateEnabled ? (isCustomVersion ? "custom" : "latest") : "disabled", + ); + + const [autoUpdateCustomVersion, setAutoUpdateCustomVersion] = useState( + isCustomVersion ? autoUpdateSetting : "", + ); + + const { hasChanges, updateRef } = useHasChanges([ + autoUpdateMethod, + autoUpdateCustomVersion, + ]); + + const handleUpdateMethodChange = (value: string) => { + setAutoUpdateMethod(value); + if (value === "disabled" || value === "latest") { + setAutoUpdateCustomVersion(""); + } + }; + + const versionError = useMemo(() => { + const msg = "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1"; + if (autoUpdateCustomVersion == "") return ""; + if (autoUpdateCustomVersion == "-") return ""; + const validSemver = validator.isValidVersion(autoUpdateCustomVersion); + if (!validSemver) return msg; + return ""; + }, [autoUpdateCustomVersion]); + + const canSaveCustomVersion = + autoUpdateCustomVersion !== "" && + autoUpdateMethod === "custom" && + versionError === ""; + + const isSaveButtonDisabled = useMemo(() => { + return ( + !hasChanges || + !permission.settings.update || + (autoUpdateMethod === "custom" && !canSaveCustomVersion) + ); + }, [ + hasChanges, + permission.settings.update, + autoUpdateMethod, + canSaveCustomVersion, + ]); + + const saveChanges = async () => { + notify({ + title: "Client Settings", + description: `Client settings successfully updated.`, + promise: saveRequest + .put({ + id: account.id, + settings: { + ...account.settings, + auto_update_version: autoUpdateCustomVersion || autoUpdateMethod, + }, + }) + .then(() => { + mutate("/accounts"); + updateRef([autoUpdateMethod, autoUpdateCustomVersion]); + }), + loadingMessage: "Updating client settings...", + }); + }; + const toggleLazyConnection = async (toggle: boolean) => { notify({ title: "Lazy Connections", @@ -70,10 +167,48 @@ export default function ClientSettingsTab({ account }: Readonly) {

Clients

+
-
+
+ + + Select how NetBird clients handle automatic updates by choosing + the latest version, a custom version, or disabling updates + altogether. + +
+ + { + setAutoUpdateCustomVersion(v.target.value); + }} + /> +
+
+ +

Experimental