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 <jamilbk@users.noreply.github.com>
This commit is contained in:
Thomas Eizinger
2025-06-04 17:50:18 +02:00
committed by GitHub
parent 6ef079357c
commit d6ecda59a1
13 changed files with 500 additions and 77 deletions

View File

@@ -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<Settings>(
const [localSettings, setLocalSettings] = useState<AdvancedSettingsViewModel>(
settings ?? {
api_url: "",
api_url_is_managed: false,
@@ -45,9 +40,9 @@ export default function SettingsPage({
}, [settings]);
return (
<div className="container mx-auto p-4">
<div className="container p-4">
<div className="pb-2">
<h2 className="text-xl font-semibold mb-4">Advanced Settings</h2>
<h2 className="text-xl font-semibold">Advanced Settings</h2>
</div>
<p className="text-neutral-900 mb-6">
@@ -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"
>
<div>
<Label className="text-neutral-600" htmlFor="auth-base-url-input">
@@ -130,20 +125,3 @@ export default function SettingsPage({
</div>
);
}
function ManagedTextInput(props: TextInputProps & { managed: boolean }) {
let { managed, ...inputProps } = props;
if (managed) {
return (
<Tooltip
content="This setting is managed by your organisation."
clearTheme={{ target: true }}
>
<TextInput {...inputProps} disabled={true} />
</Tooltip>
);
} else {
return <TextInput {...inputProps} />;
}
}

View File

@@ -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<Session | null>(null);
let [logCount, setLogCount] = useState<FileCount | null>(null);
let [settings, setSettings] = useState<Settings | null>(null);
let [generalSettings, setGeneralSettings] =
useState<GeneralSettingsViewModel | null>(null);
let [advancedSettings, setAdvancedSettings] =
useState<AdvancedSettingsViewModel | null>(null);
useEffect(() => {
const signedInUnlisten = listen<Session>("signed_in", (e) => {
@@ -41,14 +48,24 @@ export default function App() {
console.log("signed_out");
setSession(null);
});
const settingsChangedUnlisten = listen<Settings>(
"settings_changed",
const generalSettingsChangedUnlisten = listen<GeneralSettingsViewModel>(
"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<AdvancedSettingsViewModel>(
"advanced_settings_changed",
(e) => {
let advancedSettings = e.payload;
console.log("advanced_settings_changed", {
settings: advancedSettings,
});
setAdvancedSettings(advancedSettings);
}
);
const logsRecountedUnlisten = listen<FileCount>("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() {
</SidebarItem>
)}
</NavLink>
<NavLink to="/settings">
{({ isActive }) => (
<SidebarItem active={isActive} icon={CogIcon} as="div">
Settings
</SidebarItem>
)}
</NavLink>
<SidebarCollapse label="Settings" open={true} icon={Bars3Icon}>
<NavLink to="/general-settings">
{({ isActive }) => (
<SidebarItem active={isActive} icon={CogIcon} as="div">
General
</SidebarItem>
)}
</NavLink>
<NavLink to="/advanced-settings">
{({ isActive }) => (
<SidebarItem
active={isActive}
icon={WrenchScrewdriverIcon}
as="div"
>
Advanced
</SidebarItem>
)}
</NavLink>
</SidebarCollapse>
<NavLink to="/diagnostics">
{({ isActive }) => (
<SidebarItem
@@ -141,10 +172,22 @@ export default function App() {
}
/>
<Route
path="/settings"
path="/general-settings"
element={
<SettingsPage
settings={settings}
<GeneralSettingsPage
settings={generalSettings}
saveSettings={(settings) =>
invoke("apply_general_settings", { settings })
}
resetSettings={() => invoke("reset_general_settings")}
/>
}
/>
<Route
path="/advanced-settings"
element={
<AdvancedSettingsPage
settings={advancedSettings}
saveSettings={(settings) =>
invoke("apply_advanced_settings", { settings })
}

View File

@@ -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<GeneralSettingsViewModel>(
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 (
<div className="container p-4">
<div className="pb-2">
<h2 className="text-xl font-semibold">General settings</h2>
</div>
<form
onSubmit={(e) => {
e.preventDefault();
saveSettings(localSettings);
}}
className="max-w flex flex-col gap-2"
>
<div>
<Label className="text-neutral-600" htmlFor="account-slug-input">
Account slug
</Label>
<ManagedTextInput
name="account_slug"
id="account-slug-input"
managed={localSettings.account_slug_is_managed}
value={localSettings.account_slug}
onChange={(e) =>
setLocalSettings({
...localSettings,
account_slug: e.target.value,
})
}
/>
</div>
<div className="flex flex-col max-w-1/2 gap-4 mt-4">
<div className="flex justify-between items-center">
<Label className="text-neutral-600" htmlFor="start-minimized-input">
Start minimized
</Label>
<ToggleSwitch
name="start_minimized"
id="start-minimized-input"
checked={localSettings.start_minimized}
onChange={(e) =>
setLocalSettings({
...localSettings,
start_minimized: e,
})
}
/>
</div>
<div className="flex justify-between items-center">
<Label className="text-neutral-600" htmlFor="start-on-login-input">
Start on login
</Label>
<ToggleSwitch
name="start_on_login"
id="start-on-login-input"
checked={localSettings.start_on_login}
onChange={(e) =>
setLocalSettings({
...localSettings,
start_on_login: e,
})
}
/>
</div>
<div className="flex justify-between items-center">
<Label
className="text-neutral-600"
htmlFor="connect-on-start-input"
>
Connect on start
</Label>
<ManagedToggleSwitch
name="connect-on-start"
id="connect-on-start-input"
managed={localSettings.connect_on_start_is_managed}
checked={localSettings.connect_on_start}
onChange={(e) =>
setLocalSettings({
...localSettings,
connect_on_start: e,
})
}
/>
</div>
</div>
<div className="flex justify-end gap-4 mt-4">
<Button type="reset" onClick={resetSettings} color="alternative">
Reset to Defaults
</Button>
<Button type="submit">Apply</Button>
</div>
</form>
</div>
);
}

View File

@@ -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 (
<ManagedTooltip>
<TextInput {...inputProps} disabled={true} />
</ManagedTooltip>
);
} else {
return <TextInput {...inputProps} />;
}
}
export function ManagedToggleSwitch(
props: ToggleSwitchProps & { managed: boolean }
) {
let { managed, ...toggleSwitchProps } = props;
if (managed) {
return (
<ManagedTooltip>
<ToggleSwitch {...toggleSwitchProps} disabled={true} />
</ManagedTooltip>
);
} else {
return <ToggleSwitch {...toggleSwitchProps} />;
}
}
function ManagedTooltip(props: PropsWithChildren) {
let { children } = props;
return (
<Tooltip
content="This setting is managed by your organisation."
clearTheme={{ target: true }}
>
{children}
</Tooltip>
);
}

View File

@@ -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;
}

View File

@@ -25,6 +25,15 @@ const customTheme = createTheme({
},
},
},
toggleSwitch: {
toggle: {
checked: {
color: {
default: "bg-accent-500",
},
},
},
},
});
ReactDOM.createRoot(document.getElementById("root") as HTMLElement, {

View File

@@ -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<AdvancedSettings>),
ApplyAdvancedSettings(Box<AdvancedSettings>),
ApplyGeneralSettings(Box<GeneralSettingsForm>),
ResetGeneralSettings,
/// Clear the GUI's logs and await the Tunnel service to clear its logs
ClearLogs(oneshot::Sender<Result<(), String>>),
/// The same as the arguments to `client::logging::export_logs_to`
@@ -277,7 +281,7 @@ impl<I: GuiIntegration> Controller<I> {
.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<I: GuiIntegration> Controller<I> {
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<I: GuiIntegration> Controller<I> {
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<I: GuiIntegration> Controller<I> {
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<I: GuiIntegration> Controller<I> {
.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<I: GuiIntegration> Controller<I> {
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<I: GuiIntegration> Controller<I> {
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<I: GuiIntegration> Controller<I> {
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<I: GuiIntegration> Controller<I> {
.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<I: GuiIntegration> Controller<I> {
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<ControlFlow<()>> {
match msg {
service::ServerMsg::ClearedLogs(result) => {
@@ -953,6 +993,16 @@ impl<I: GuiIntegration> Controller<I> {
.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<I: GuiIntegration> Controller<I> {
.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<bool> {
self.mdm_settings
.connect_on_start
.or(self.general_settings.connect_on_start)
}
}
async fn new_dns_notifier() -> Result<impl Stream<Item = Result<()>>> {

View File

@@ -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(())

View File

@@ -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

View File

@@ -64,6 +64,48 @@ pub struct GeneralSettings {
pub favorite_resources: HashSet<ResourceId>,
#[serde(default)]
pub internet_resource_enabled: Option<bool>,
#[serde(default = "start_minimized_default")]
pub start_minimized: bool,
#[serde(default)]
pub start_on_login: Option<bool>,
#[serde(default)]
pub connect_on_start: Option<bool>,
#[serde(default)]
pub account_slug: Option<String>,
}
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 {

View File

@@ -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<Wry>) -> 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();

View File

@@ -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.
</ChangeItem>
<ChangeItem pull="9381">
Introduces "General" settings, allowing the user to manage autostart
behaviour as well as the to-be-used account slug.
</ChangeItem>
</Unreleased>
<Entry version="1.4.14" date={new Date("2025-05-21")}>
<ChangeItem pull="9147">