mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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| |---|---| ||| "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:
@@ -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} />;
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
139
rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx
Normal file
139
rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
51
rust/gui-client/src-frontend/components/ManagedInput.tsx
Normal file
51
rust/gui-client/src-frontend/components/ManagedInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -25,6 +25,15 @@ const customTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
toggleSwitch: {
|
||||
toggle: {
|
||||
checked: {
|
||||
color: {
|
||||
default: "bg-accent-500",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement, {
|
||||
|
||||
@@ -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<()>>> {
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user