From d6ecda59a10f4f76af2b414635201864d222f427 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 4 Jun 2025 17:50:18 +0200 Subject: [PATCH] feat(gui-client): introduce "General" settings page (#9381) This PR introduces "General" settings for the GUI client. The "Settings" menu item in the GUI is split into two sub-sections. The menu item is collapsible but open by default. |General|Advanced| |---|---| |![](https://github.com/user-attachments/assets/190cd23a-7ff6-4097-9eb5-a4ccf4a9c4a0)|![](https://github.com/user-attachments/assets/d538b749-9fe0-4995-84fc-b5c88132ede6)| "Connect on start" and "Account slug" can both be MDM managed. The autostart functionality is implemented via the Windows Registry. --------- Co-authored-by: Jamil Bou Kheir --- ...tingsPage.tsx => AdvancedSettingsPage.tsx} | 46 ++---- .../src-frontend/components/App.tsx | 85 ++++++++--- .../components/GeneralSettingsPage.tsx | 139 ++++++++++++++++++ .../src-frontend/components/ManagedInput.tsx | 51 +++++++ .../components/ManagedTextInput.tsx | 0 .../generated/GeneralSettingsViewModel.ts | 8 + rust/gui-client/src-frontend/main.tsx | 9 ++ rust/gui-client/src-tauri/src/controller.rs | 91 ++++++++++-- rust/gui-client/src-tauri/src/gui.rs | 19 ++- .../src-tauri/src/gui/os_windows.rs | 38 ++++- rust/gui-client/src-tauri/src/settings.rs | 46 ++++++ rust/gui-client/src-tauri/src/view.rs | 41 +++++- website/src/components/Changelog/GUI.tsx | 4 + 13 files changed, 500 insertions(+), 77 deletions(-) rename rust/gui-client/src-frontend/components/{SettingsPage.tsx => AdvancedSettingsPage.tsx} (74%) create mode 100644 rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx create mode 100644 rust/gui-client/src-frontend/components/ManagedInput.tsx create mode 100644 rust/gui-client/src-frontend/components/ManagedTextInput.tsx create mode 100644 rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts diff --git a/rust/gui-client/src-frontend/components/SettingsPage.tsx b/rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx similarity index 74% rename from rust/gui-client/src-frontend/components/SettingsPage.tsx rename to rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx index a5d42aed8..730e54e56 100644 --- a/rust/gui-client/src-frontend/components/SettingsPage.tsx +++ b/rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx @@ -1,26 +1,21 @@ import React, { useEffect, useState } from "react"; -import { - Button, - TextInput, - Label, - TextInputProps, - Tooltip, -} from "flowbite-react"; -import { AdvancedSettingsViewModel as Settings } from "../generated/AdvancedSettingsViewModel"; +import { Button, Label } from "flowbite-react"; +import { AdvancedSettingsViewModel } from "../generated/AdvancedSettingsViewModel"; +import { ManagedTextInput } from "./ManagedInput"; -interface SettingsPageProps { - settings: Settings | null; - saveSettings: (settings: Settings) => void; +interface Props { + settings: AdvancedSettingsViewModel | null; + saveSettings: (settings: AdvancedSettingsViewModel) => void; resetSettings: () => void; } -export default function SettingsPage({ +export default function AdvancedSettingsPage({ settings, saveSettings, resetSettings, -}: SettingsPageProps) { +}: Props) { // Local settings can be edited without affecting the global state. - const [localSettings, setLocalSettings] = useState( + const [localSettings, setLocalSettings] = useState( settings ?? { api_url: "", api_url_is_managed: false, @@ -45,9 +40,9 @@ export default function SettingsPage({ }, [settings]); return ( -
+
-

Advanced Settings

+

Advanced Settings

@@ -61,7 +56,7 @@ export default function SettingsPage({ e.preventDefault(); saveSettings(localSettings); }} - className="max-w mx-auto flex flex-col gap-2" + className="max-w flex flex-col gap-2" >

); } - -function ManagedTextInput(props: TextInputProps & { managed: boolean }) { - let { managed, ...inputProps } = props; - - if (managed) { - return ( - - - - ); - } else { - return ; - } -} diff --git a/rust/gui-client/src-frontend/components/App.tsx b/rust/gui-client/src-frontend/components/App.tsx index 441087e08..bd2df2693 100644 --- a/rust/gui-client/src-frontend/components/App.tsx +++ b/rust/gui-client/src-frontend/components/App.tsx @@ -1,34 +1,41 @@ import { + Bars3Icon, CogIcon, DocumentMagnifyingGlassIcon, HomeIcon, InformationCircleIcon, SwatchIcon, + WrenchScrewdriverIcon, } from "@heroicons/react/24/solid"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { Sidebar, + SidebarCollapse, SidebarItem, SidebarItemGroup, SidebarItems, } from "flowbite-react"; import React, { useEffect, useState } from "react"; import { NavLink, Route, Routes } from "react-router"; -import { AdvancedSettingsViewModel as Settings } from "../generated/AdvancedSettingsViewModel"; +import { AdvancedSettingsViewModel } from "../generated/AdvancedSettingsViewModel"; import { FileCount } from "../generated/FileCount"; import { Session } from "../generated/Session"; -import initSentry from "../initSentry"; import About from "./AboutPage"; +import AdvancedSettingsPage from "./AdvancedSettingsPage"; import ColorPalette from "./ColorPalettePage"; import Diagnostics from "./DiagnosticsPage"; +import GeneralSettingsPage from "./GeneralSettingsPage"; import Overview from "./OverviewPage"; -import SettingsPage from "./SettingsPage"; +import { GeneralSettingsViewModel } from "../generated/GeneralSettingsViewModel"; export default function App() { let [session, setSession] = useState(null); let [logCount, setLogCount] = useState(null); - let [settings, setSettings] = useState(null); + let [generalSettings, setGeneralSettings] = + useState(null); + let [advancedSettings, setAdvancedSettings] = + useState(null); useEffect(() => { const signedInUnlisten = listen("signed_in", (e) => { @@ -41,14 +48,24 @@ export default function App() { console.log("signed_out"); setSession(null); }); - const settingsChangedUnlisten = listen( - "settings_changed", + const generalSettingsChangedUnlisten = listen( + "general_settings_changed", (e) => { - let settings = e.payload; + let generalSettings = e.payload; - console.log("settings_changed", { settings }); - setSettings(settings); - initSentry(settings.api_url); + console.log("general_settings_changed", { settings: generalSettings }); + setGeneralSettings(generalSettings); + } + ); + const advancedSettingsChangedUnlisten = listen( + "advanced_settings_changed", + (e) => { + let advancedSettings = e.payload; + + console.log("advanced_settings_changed", { + settings: advancedSettings, + }); + setAdvancedSettings(advancedSettings); } ); const logsRecountedUnlisten = listen("logs_recounted", (e) => { @@ -63,7 +80,8 @@ export default function App() { return () => { signedInUnlisten.then((unlistenFn) => unlistenFn()); signedOutUnlisten.then((unlistenFn) => unlistenFn()); - settingsChangedUnlisten.then((unlistenFn) => unlistenFn()); + generalSettingsChangedUnlisten.then((unlistenFn) => unlistenFn()); + advancedSettingsChangedUnlisten.then((unlistenFn) => unlistenFn()); logsRecountedUnlisten.then((unlistenFn) => unlistenFn()); }; }, []); @@ -85,13 +103,26 @@ export default function App() { )} - - {({ isActive }) => ( - - Settings - - )} - + + + {({ isActive }) => ( + + General + + )} + + + {({ isActive }) => ( + + Advanced + + )} + + {({ isActive }) => ( + invoke("apply_general_settings", { settings }) + } + resetSettings={() => invoke("reset_general_settings")} + /> + } + /> + invoke("apply_advanced_settings", { settings }) } diff --git a/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx b/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx new file mode 100644 index 000000000..ce0ebce96 --- /dev/null +++ b/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useState } from "react"; +import { Button, Label, ToggleSwitch } from "flowbite-react"; +import { GeneralSettingsViewModel } from "../generated/GeneralSettingsViewModel"; +import { ManagedToggleSwitch, ManagedTextInput } from "./ManagedInput"; + +interface Props { + settings: GeneralSettingsViewModel | null; + saveSettings: (settings: GeneralSettingsViewModel) => void; + resetSettings: () => void; +} + +export default function GeneralSettingsPage({ + settings, + saveSettings, + resetSettings, +}: Props) { + // Local settings can be edited without affecting the global state. + const [localSettings, setLocalSettings] = useState( + settings ?? { + start_minimized: true, + account_slug: "", + connect_on_start: false, + start_on_login: false, + account_slug_is_managed: false, + connect_on_start_is_managed: false, + } + ); + + useEffect(() => { + setLocalSettings( + settings ?? { + start_minimized: true, + account_slug: "", + connect_on_start: false, + start_on_login: false, + account_slug_is_managed: false, + connect_on_start_is_managed: false, + } + ); + }, [settings]); + + return ( +
+
+

General settings

+
+ +
{ + e.preventDefault(); + saveSettings(localSettings); + }} + className="max-w flex flex-col gap-2" + > +
+ + + setLocalSettings({ + ...localSettings, + account_slug: e.target.value, + }) + } + /> +
+ +
+
+ + + setLocalSettings({ + ...localSettings, + start_minimized: e, + }) + } + /> +
+ +
+ + + setLocalSettings({ + ...localSettings, + start_on_login: e, + }) + } + /> +
+ +
+ + + setLocalSettings({ + ...localSettings, + connect_on_start: e, + }) + } + /> +
+
+ +
+ + +
+
+
+ ); +} diff --git a/rust/gui-client/src-frontend/components/ManagedInput.tsx b/rust/gui-client/src-frontend/components/ManagedInput.tsx new file mode 100644 index 000000000..95a9ccd37 --- /dev/null +++ b/rust/gui-client/src-frontend/components/ManagedInput.tsx @@ -0,0 +1,51 @@ +import { + TextInputProps, + Tooltip, + TextInput, + ToggleSwitch, + ToggleSwitchProps, +} from "flowbite-react"; +import React, { PropsWithChildren } from "react"; + +export function ManagedTextInput(props: TextInputProps & { managed: boolean }) { + let { managed, ...inputProps } = props; + + if (managed) { + return ( + + + + ); + } else { + return ; + } +} + +export function ManagedToggleSwitch( + props: ToggleSwitchProps & { managed: boolean } +) { + let { managed, ...toggleSwitchProps } = props; + + if (managed) { + return ( + + + + ); + } else { + return ; + } +} + +function ManagedTooltip(props: PropsWithChildren) { + let { children } = props; + + return ( + + {children} + + ); +} diff --git a/rust/gui-client/src-frontend/components/ManagedTextInput.tsx b/rust/gui-client/src-frontend/components/ManagedTextInput.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts b/rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts new file mode 100644 index 000000000..f8944495a --- /dev/null +++ b/rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts @@ -0,0 +1,8 @@ +export interface GeneralSettingsViewModel { + start_minimized: boolean; + start_on_login: boolean; + connect_on_start: boolean; + connect_on_start_is_managed: boolean; + account_slug: string; + account_slug_is_managed: boolean; +} diff --git a/rust/gui-client/src-frontend/main.tsx b/rust/gui-client/src-frontend/main.tsx index 16d755e9b..6db09a61c 100644 --- a/rust/gui-client/src-frontend/main.tsx +++ b/rust/gui-client/src-frontend/main.tsx @@ -25,6 +25,15 @@ const customTheme = createTheme({ }, }, }, + toggleSwitch: { + toggle: { + checked: { + color: { + default: "bg-accent-500", + }, + }, + }, + }, }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement, { diff --git a/rust/gui-client/src-tauri/src/controller.rs b/rust/gui-client/src-tauri/src/controller.rs index cbbe00529..ce0aa135b 100644 --- a/rust/gui-client/src-tauri/src/controller.rs +++ b/rust/gui-client/src-tauri/src/controller.rs @@ -6,6 +6,7 @@ use crate::{ service, settings::{self, AdvancedSettings, GeneralSettings, MdmSettings}, updates, uptime, + view::GeneralSettingsForm, }; use anyhow::{Context, Result, anyhow, bail}; use connlib_model::ResourceView; @@ -69,6 +70,7 @@ pub trait GuiIntegration { fn notify_settings_changed( &self, mdm_settings: MdmSettings, + general_settings: GeneralSettings, advanced_settings: AdvancedSettings, ) -> Result<()>; fn notify_logs_recounted(&self, file_count: &FileCount) -> Result<()>; @@ -86,14 +88,16 @@ pub trait GuiIntegration { fn show_settings_page( &self, mdm_settings: MdmSettings, + general_settings: GeneralSettings, settings: AdvancedSettings, ) -> Result<()>; fn show_about_page(&self) -> Result<()>; } pub enum ControllerRequest { - /// The GUI wants us to use these settings in-memory, they've already been saved to disk - ApplySettings(Box), + ApplyAdvancedSettings(Box), + ApplyGeneralSettings(Box), + ResetGeneralSettings, /// Clear the GUI's logs and await the Tunnel service to clear its logs ClearLogs(oneshot::Sender>), /// The same as the arguments to `client::logging::export_logs_to` @@ -277,7 +281,7 @@ impl Controller { .context("Failed to load token from disk during app start")? { // For backwards-compatibility prior to MDM-config, also call `start_session` if not configured. - if self.mdm_settings.connect_on_start.is_none_or(|c| c) { + if self.connect_on_start().is_none_or(|c| c) { self.start_session(token).await?; } } else { @@ -286,7 +290,7 @@ impl Controller { self.refresh_system_tray_menu(); - if !ran_before::get().await? { + if !ran_before::get().await? || !self.general_settings.start_minimized { self.integration.show_overview_page(self.auth.session())?; } @@ -433,6 +437,10 @@ impl Controller { let session = self.auth.session().context("Missing session")?; self.integration.notify_signed_in(session)?; + self.general_settings.account_slug = Some(session.account_slug.clone()); + settings::save_general(&self.general_settings).await?; + self.notify_settings_changed()?; + self.refresh_system_tray_menu(); Ok(()) @@ -478,7 +486,7 @@ impl Controller { use ControllerRequest::*; match req { - ApplySettings(settings) => { + ApplyAdvancedSettings(settings) => { self.log_filter_reloader .reload(&settings.log_filter) .context("Couldn't reload log filter")?; @@ -495,10 +503,7 @@ impl Controller { .await?; // Notify GUI that settings have changed - self.integration.notify_settings_changed( - self.mdm_settings.clone(), - self.advanced_settings.clone(), - )?; + self.notify_settings_changed()?; tracing::debug!("Applied new settings. Log level will take effect immediately."); @@ -507,6 +512,28 @@ impl Controller { self.integration.show_notification("Settings saved", "")? } + ApplyGeneralSettings(settings) => { + let account_slug = settings.account_slug.trim(); + + self.apply_general_settings(GeneralSettings { + start_minimized: settings.start_minimized, + start_on_login: Some(settings.start_on_login), + connect_on_start: Some(settings.connect_on_start), + account_slug: (!account_slug.is_empty()).then_some(account_slug.to_owned()), + ..self.general_settings.clone() + }) + .await?; + } + ResetGeneralSettings => { + self.apply_general_settings(GeneralSettings { + start_minimized: true, + start_on_login: None, + connect_on_start: None, + account_slug: None, + ..self.general_settings.clone() + }) + .await?; + } ClearLogs(completion_tx) => { if self.clear_logs_callback.is_some() { tracing::error!( @@ -531,12 +558,14 @@ impl Controller { Fail(Failure::Panic) => panic!("Test panic"), SignIn | SystemTrayMenu(system_tray::Event::SignIn) => { let auth_url = self.auth_url().clone(); + let account_slug = self.account_slug().map(|a| a.to_owned()); + let req = self .auth .start_sign_in() .context("Couldn't start sign-in flow")?; - let url = req.to_url(&auth_url, self.mdm_settings.account_slug.as_deref()); + let url = req.to_url(&auth_url, account_slug.as_deref()); self.refresh_system_tray_menu(); self.integration .open_url(url.expose_secret()) @@ -595,6 +624,7 @@ impl Controller { system_tray::Window::About => self.integration.show_about_page()?, system_tray::Window::Settings => self.integration.show_settings_page( self.mdm_settings.clone(), + self.general_settings.clone(), self.advanced_settings.clone(), )?, }; @@ -630,10 +660,7 @@ impl Controller { .context("Couldn't open update page")?; } UpdateState => { - self.integration.notify_settings_changed( - self.mdm_settings.clone(), - self.advanced_settings.clone(), - )?; + self.notify_settings_changed()?; match self.auth.session() { Some(session) => self.integration.notify_signed_in(session)?, None => self.integration.notify_signed_out()?, @@ -646,6 +673,19 @@ impl Controller { Ok(()) } + async fn apply_general_settings(&mut self, settings: GeneralSettings) -> Result<()> { + self.general_settings = settings; + + settings::save_general(&self.general_settings).await?; + + gui::set_autostart(self.general_settings.start_on_login.is_some_and(|v| v)).await?; + + self.notify_settings_changed()?; + self.integration.show_notification("Settings saved", "")?; + + Ok(()) + } + async fn handle_service_ipc_msg(&mut self, msg: service::ServerMsg) -> Result> { match msg { service::ServerMsg::ClearedLogs(result) => { @@ -953,6 +993,16 @@ impl Controller { .context("Failed to send IPC message") } + fn notify_settings_changed(&mut self) -> Result<()> { + self.integration.notify_settings_changed( + self.mdm_settings.clone(), + self.general_settings.clone(), + self.advanced_settings.clone(), + )?; + + Ok(()) + } + fn auth_url(&self) -> &Url { self.mdm_settings .auth_url @@ -966,6 +1016,19 @@ impl Controller { .as_ref() .unwrap_or(&self.advanced_settings.api_url) } + + fn account_slug(&self) -> Option<&str> { + self.mdm_settings + .account_slug + .as_deref() + .or(self.general_settings.account_slug.as_deref()) + } + + fn connect_on_start(&self) -> Option { + self.mdm_settings + .connect_on_start + .or(self.general_settings.connect_on_start) + } } async fn new_dns_notifier() -> Result>> { diff --git a/rust/gui-client/src-tauri/src/gui.rs b/rust/gui-client/src-tauri/src/gui.rs index 5aa81eec9..76352f4f6 100644 --- a/rust/gui-client/src-tauri/src/gui.rs +++ b/rust/gui-client/src-tauri/src/gui.rs @@ -10,7 +10,8 @@ use crate::{ ipc::{self, ClientRead, ClientWrite, SocketId}, logging::FileCount, settings::{ - self, AdvancedSettings, AdvancedSettingsLegacy, AdvancedSettingsViewModel, MdmSettings, + self, AdvancedSettings, AdvancedSettingsLegacy, AdvancedSettingsViewModel, GeneralSettings, + GeneralSettingsViewModel, MdmSettings, }, updates, }; @@ -101,14 +102,21 @@ impl GuiIntegration for TauriIntegration { fn notify_settings_changed( &self, mdm_settings: MdmSettings, + general_settings: GeneralSettings, advanced_settings: AdvancedSettings, ) -> Result<()> { self.app .emit( - "settings_changed", + "general_settings_changed", + GeneralSettingsViewModel::new(mdm_settings.clone(), general_settings), + ) + .context("Failed to send `general_settings_changed` event")?; + self.app + .emit( + "advanced_settings_changed", AdvancedSettingsViewModel::new(mdm_settings, advanced_settings), ) - .context("Failed to send `settings_changed` event")?; + .context("Failed to send `advanced_settings_changed` event")?; Ok(()) } @@ -174,10 +182,11 @@ impl GuiIntegration for TauriIntegration { fn show_settings_page( &self, mdm_settings: MdmSettings, + general_settings: GeneralSettings, advanced_settings: AdvancedSettings, ) -> Result<()> { - self.notify_settings_changed(mdm_settings, advanced_settings)?; // Ensure settings are up to date in GUI. - self.navigate("settings")?; + self.notify_settings_changed(mdm_settings, general_settings, advanced_settings)?; // Ensure settings are up to date in GUI. + self.navigate("general-settings")?; self.set_window_visible(true)?; Ok(()) diff --git a/rust/gui-client/src-tauri/src/gui/os_windows.rs b/rust/gui-client/src-tauri/src/gui/os_windows.rs index 44a598485..c5c379fd9 100644 --- a/rust/gui-client/src-tauri/src/gui/os_windows.rs +++ b/rust/gui-client/src-tauri/src/gui/os_windows.rs @@ -2,10 +2,44 @@ use super::{ControllerRequest, CtlrTx}; use anyhow::{Context, Result}; use firezone_bin_shared::BUNDLE_ID; use firezone_logging::err_with_src; +use std::env; use tauri::AppHandle; +use winreg::RegKey; +use winreg::enums::*; -pub async fn set_autostart(_enabled: bool) -> Result<()> { - todo!() +pub async fn set_autostart(enabled: bool) -> Result<()> { + // Get path to the current executable + let exec_path = env::current_exe().context("Failed to get current executable path")?; + let exec_path_str = format!("\"{}\"", exec_path.to_string_lossy()); + + // Open the registry key for autostart configuration + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let run_key = hkcu + .open_subkey_with_flags( + r"Software\Microsoft\Windows\CurrentVersion\Run", + KEY_READ | KEY_WRITE, + ) + .context("Failed to open registry key for autostart")?; + + if enabled { + // Add the application to autostart + run_key + .set_value("Firezone", &exec_path_str) + .context("Failed to add application to autostart registry")?; + + tracing::debug!("Added application to autostart: {}", exec_path_str); + } else { + // Remove the application from autostart + match run_key.delete_value("Firezone") { + Ok(_) => tracing::debug!("Removed application from autostart"), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => { + return Err(e).context("Failed to remove application from autostart registry"); + } + } + } + + Ok(()) } /// Since clickable notifications don't work on Linux yet, the update text diff --git a/rust/gui-client/src-tauri/src/settings.rs b/rust/gui-client/src-tauri/src/settings.rs index 195a7c56f..f48b013c0 100644 --- a/rust/gui-client/src-tauri/src/settings.rs +++ b/rust/gui-client/src-tauri/src/settings.rs @@ -64,6 +64,48 @@ pub struct GeneralSettings { pub favorite_resources: HashSet, #[serde(default)] pub internet_resource_enabled: Option, + #[serde(default = "start_minimized_default")] + pub start_minimized: bool, + #[serde(default)] + pub start_on_login: Option, + #[serde(default)] + pub connect_on_start: Option, + #[serde(default)] + pub account_slug: Option, +} + +fn start_minimized_default() -> bool { + true +} + +#[tslink::tslink(target = "./gui-client/src-frontend/generated/GeneralSettingsViewModel.ts")] +#[derive(Clone, Serialize)] +pub struct GeneralSettingsViewModel { + pub start_minimized: bool, + pub start_on_login: bool, + pub connect_on_start: bool, + pub connect_on_start_is_managed: bool, + pub account_slug: String, + pub account_slug_is_managed: bool, +} + +impl GeneralSettingsViewModel { + pub fn new(mdm_settings: MdmSettings, general_settings: GeneralSettings) -> Self { + Self { + connect_on_start_is_managed: mdm_settings.connect_on_start.is_some(), + account_slug_is_managed: mdm_settings.account_slug.is_some(), + start_minimized: general_settings.start_minimized, + start_on_login: general_settings.start_on_login.is_some_and(|v| v), + connect_on_start: mdm_settings + .connect_on_start + .or(general_settings.connect_on_start) + .is_some_and(|v| v), + account_slug: mdm_settings + .account_slug + .or(general_settings.account_slug) + .unwrap_or_default(), + } + } } #[tslink::tslink(target = "./gui-client/src-frontend/generated/AdvancedSettingsViewModel.ts")] @@ -171,6 +213,10 @@ pub async fn migrate_legacy_settings( let general = GeneralSettings { favorite_resources: legacy.favorite_resources, internet_resource_enabled: legacy.internet_resource_enabled, + start_minimized: true, + start_on_login: None, + connect_on_start: None, + account_slug: None, }; if let Err(e) = save_general(&general).await { diff --git a/rust/gui-client/src-tauri/src/view.rs b/rust/gui-client/src-tauri/src/view.rs index 1a9e214bf..23487c551 100644 --- a/rust/gui-client/src-tauri/src/view.rs +++ b/rust/gui-client/src-tauri/src/view.rs @@ -12,12 +12,22 @@ use crate::{ settings::AdvancedSettings, }; +#[derive(Clone, serde::Deserialize)] +pub struct GeneralSettingsForm { + pub start_minimized: bool, + pub start_on_login: bool, + pub connect_on_start: bool, + pub account_slug: String, +} + pub fn generate_handler() -> impl Fn(Invoke) -> bool + Send + Sync + 'static { tauri::generate_handler![ clear_logs, export_logs, apply_advanced_settings, reset_advanced_settings, + apply_general_settings, + reset_general_settings, sign_in, sign_out, update_state, @@ -48,6 +58,24 @@ async fn export_logs(app: tauri::AppHandle, managed: tauri::State<'_, Managed>) Ok(()) } +#[tauri::command] +async fn apply_general_settings( + managed: tauri::State<'_, Managed>, + settings: GeneralSettingsForm, +) -> Result<()> { + if managed.inner().inject_faults { + tokio::time::sleep(Duration::from_secs(2)).await; + } + + managed + .ctlr_tx + .send(ControllerRequest::ApplyGeneralSettings(Box::new(settings))) + .await + .context("Failed to send `ApplyGeneralSettings` command")?; + + Ok(()) +} + #[tauri::command] async fn apply_advanced_settings( managed: tauri::State<'_, Managed>, @@ -59,7 +87,7 @@ async fn apply_advanced_settings( managed .ctlr_tx - .send(ControllerRequest::ApplySettings(Box::new(settings))) + .send(ControllerRequest::ApplyAdvancedSettings(Box::new(settings))) .await .context("Failed to send `ApplySettings` command")?; @@ -73,6 +101,17 @@ async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<( Ok(()) } +#[tauri::command] +async fn reset_general_settings(managed: tauri::State<'_, Managed>) -> Result<()> { + managed + .ctlr_tx + .send(ControllerRequest::ResetGeneralSettings) + .await + .context("Failed to send `ResetGeneralSettings` command")?; + + Ok(()) +} + /// Pops up the "Save File" dialog fn show_export_dialog(app: &tauri::AppHandle, ctlr_tx: CtlrTx) -> Result<()> { let now = chrono::Local::now(); diff --git a/website/src/components/Changelog/GUI.tsx b/website/src/components/Changelog/GUI.tsx index 8688aabc2..53cb531a7 100644 --- a/website/src/components/Changelog/GUI.tsx +++ b/website/src/components/Changelog/GUI.tsx @@ -37,6 +37,10 @@ export default function GUI({ os }: { os: OS }) { Fixes an issue where Firezone could not start if the operating system refused our request to increase the UDP socket buffer sizes. + + Introduces "General" settings, allowing the user to manage autostart + behaviour as well as the to-be-used account slug. +