+ );
+}
diff --git a/rust/gui-client/src-frontend/components/OverviewPage.tsx b/rust/gui-client/src-frontend/components/OverviewPage.tsx
new file mode 100644
index 000000000..3e93e44eb
--- /dev/null
+++ b/rust/gui-client/src-frontend/components/OverviewPage.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+import logo from "../logo.png";
+import { Session } from "./App";
+import { Button } from "flowbite-react";
+
+interface OverviewPageProps {
+ session: Session | null;
+ signOut: () => void;
+ signIn: () => void;
+}
+
+export default function Overview({
+ session,
+ signOut,
+ signIn,
+}: OverviewPageProps) {
+ return (
+
+
+
+
Firezone
+
+ {!session ? (
+
+
+
+ You can sign in by clicking the Firezone icon in the taskbar or by
+ clicking 'Sign in' below.
+
+
+
+ Firezone will continue running after this window is closed.
+
+ It is always available from the taskbar.
+
+
+
+ ) : (
+
+
+
+ You are currently signed into
+
+ {session.account_slug}
+
+ as
+
+ {session.actor_name}
+
+ .
+ Click the Firezone icon in the taskbar to see the list of
+ Resources.
+
+
+
+ Firezone will continue running in the taskbar after this window is
+ closed.
+
+ WARNING: These settings are intended for internal
+ debug purposes only. Changing these is not supported
+ and will disrupt access to your resources.
+
+
+
+
+ );
+}
+
+function ManagedTextInput(props: TextInputProps & { managed: boolean }) {
+ let { managed, ...inputProps } = props;
+
+ if (managed) {
+ return
+
+
+ } else {
+ return
+ }
+}
diff --git a/rust/gui-client/src-frontend/custom.d.ts b/rust/gui-client/src-frontend/custom.d.ts
new file mode 100644
index 000000000..66b245666
--- /dev/null
+++ b/rust/gui-client/src-frontend/custom.d.ts
@@ -0,0 +1,4 @@
+declare module "*.png" {
+ const value: string;
+ export default value;
+}
diff --git a/rust/gui-client/src/logo.png b/rust/gui-client/src-frontend/logo.png
similarity index 100%
rename from rust/gui-client/src/logo.png
rename to rust/gui-client/src-frontend/logo.png
diff --git a/rust/gui-client/src/input.css b/rust/gui-client/src-frontend/main.css
similarity index 86%
rename from rust/gui-client/src/input.css
rename to rust/gui-client/src-frontend/main.css
index a3661beb9..4ca6549a9 100644
--- a/rust/gui-client/src/input.css
+++ b/rust/gui-client/src-frontend/main.css
@@ -1,13 +1,12 @@
@import "tailwindcss";
-/*
- * This file serves as the main entry point for your Tailwind CSS installation.
- * Generate the CSS from this with `pnpm tailwindcss -i src/input.css -o src/output.css`
- * This has been added to the build script in package.json for convenience.
- */
-
+@plugin "flowbite-react/plugin/tailwindcss";
@plugin 'flowbite/plugin';
+@source "../.flowbite-react/class-list.json";
+
+@custom-variant dark (&:where(.dark, .dark *));
+
@theme {
--color-primary-50: #fff9f5;
--color-primary-100: #fff1e5;
@@ -43,6 +42,8 @@
--color-neutral-700: #766a60;
--color-neutral-800: #4c3e33;
--color-neutral-900: #1b140e;
+
+ --default-font-family: "Source Sans 3";
}
/*
diff --git a/rust/gui-client/src-frontend/main.tsx b/rust/gui-client/src-frontend/main.tsx
new file mode 100644
index 000000000..8298c1d8b
--- /dev/null
+++ b/rust/gui-client/src-frontend/main.tsx
@@ -0,0 +1,36 @@
+import React, { StrictMode } from "react";
+import ReactDOM from "react-dom/client";
+import App from "./components/App";
+import { BrowserRouter } from "react-router";
+import { createTheme, ThemeProvider } from "flowbite-react";
+
+const customTheme = createTheme({
+ sidebar: {
+ root: { inner: "rounded-none bg-white" }
+ },
+ button: {
+ color: {
+ default: "bg-accent-450 hover:bg-accent-700 text-white",
+ alternative: "text-neutral-900 border border-neutral-200 hover:bg-neutral-300 hover:text-neutral-900",
+ },
+ },
+ textInput: {
+ field: {
+ input: {
+ colors: {
+ gray: "focus:ring-accent-500 focus:border-accent-500"
+ }
+ }
+ }
+ }
+});
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+
+
+
+
+
+
+
+);
diff --git a/rust/gui-client/src-frontend/vite-env.d.ts b/rust/gui-client/src-frontend/vite-env.d.ts
new file mode 100644
index 000000000..6ce68385f
--- /dev/null
+++ b/rust/gui-client/src-frontend/vite-env.d.ts
@@ -0,0 +1,4 @@
+///
+
+declare const __APP_VERSION__: string;
+declare const __GIT_VERSION__: string;
diff --git a/rust/gui-client/src-tauri/src/about.rs b/rust/gui-client/src-tauri/src/about.rs
deleted file mode 100644
index dc88641c5..000000000
--- a/rust/gui-client/src-tauri/src/about.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-//! Everything related to the About window
-
-#[tauri::command]
-pub(crate) fn get_cargo_version() -> String {
- env!("CARGO_PKG_VERSION").to_string()
-}
-
-#[tauri::command]
-pub(crate) fn get_git_version() -> String {
- option_env!("GITHUB_SHA").unwrap_or("unknown").to_owned()
-}
-
-#[cfg(test)]
-mod tests {
- #[test]
- fn version() {
- let cargo = super::get_cargo_version();
-
- assert!(cargo != "Unknown", "{}", cargo);
- assert!(cargo.starts_with("1."));
- assert!(cargo.len() >= 2, "{}", cargo);
- }
-}
diff --git a/rust/gui-client/src-tauri/src/clear_logs.rs b/rust/gui-client/src-tauri/src/clear_logs.rs
deleted file mode 100644
index 071fde458..000000000
--- a/rust/gui-client/src-tauri/src/clear_logs.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use anyhow::{Context, Result};
-use std::{io::ErrorKind::NotFound, path::Path};
-
-/// Deletes all `.log` files in `path`.
-pub async fn clear_logs(path: &Path) -> Result<()> {
- let mut dir = match tokio::fs::read_dir(path).await {
- Ok(x) => x,
- Err(error) => {
- if matches!(error.kind(), NotFound) {
- // In smoke tests, the Tunnel service runs in debug mode, so it won't write any logs to disk. If the Tunnel service's log dir doesn't exist, we shouldn't crash, it's correct to simply not delete the non-existent files
- return Ok(());
- }
- // But any other error like permissions errors, should bubble.
- return Err(error.into());
- }
- };
-
- let mut result = Ok(());
-
- // If we can't delete some files due to permission errors, just keep going
- // and delete as much as we can, then return the most recent error
- while let Some(entry) = dir
- .next_entry()
- .await
- .context("Failed to read next dir entry")?
- {
- if entry
- .file_name()
- .to_str()
- .is_none_or(|name| !name.ends_with("log"))
- {
- continue;
- }
-
- if let Err(e) = tokio::fs::remove_file(entry.path()).await {
- result = Err(e);
- }
- }
-
- result.context("Failed to delete at least one file")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[tokio::test]
- async fn only_deletes_log_files() {
- let dir = tempfile::tempdir().unwrap();
-
- std::fs::write(dir.path().join("first.log"), "log file 1").unwrap();
- std::fs::write(dir.path().join("second.log"), "log file 1").unwrap();
- std::fs::write(dir.path().join("not_a_logfile.tmp"), "something important").unwrap();
-
- clear_logs(dir.path()).await.unwrap();
-
- assert_eq!(
- std::fs::read_to_string(dir.path().join("not_a_logfile.tmp")).unwrap(),
- "something important"
- );
- }
-}
diff --git a/rust/gui-client/src-tauri/src/controller.rs b/rust/gui-client/src-tauri/src/controller.rs
index 124ce85bd..ec9113925 100644
--- a/rust/gui-client/src-tauri/src/controller.rs
+++ b/rust/gui-client/src-tauri/src/controller.rs
@@ -2,7 +2,8 @@ use crate::{
auth, deep_link,
gui::{self, system_tray},
ipc::{self, SocketId},
- logging, service,
+ logging::{self, FileCount},
+ service,
settings::{self, AdvancedSettings, GeneralSettings, MdmSettings},
updates, uptime,
};
@@ -63,12 +64,6 @@ pub struct Controller {
}
pub trait GuiIntegration {
- fn set_welcome_window_visible(
- &self,
- visible: bool,
- current_session: Option<&auth::Session>,
- ) -> Result<()>;
-
fn notify_signed_in(&self, session: &auth::Session) -> Result<()>;
fn notify_signed_out(&self) -> Result<()>;
fn notify_settings_changed(
@@ -76,6 +71,7 @@ pub trait GuiIntegration {
mdm_settings: MdmSettings,
advanced_settings: AdvancedSettings,
) -> Result<()>;
+ fn notify_logs_recounted(&self, file_count: &FileCount) -> Result<()>;
/// Also opens non-URLs
fn open_url>(&self, url: P) -> Result<()>;
@@ -85,12 +81,14 @@ pub trait GuiIntegration {
fn show_notification(&self, title: &str, body: &str) -> Result<()>;
fn show_update_notification(&self, ctlr_tx: CtlrTx, title: &str, url: url::Url) -> Result<()>;
- fn show_settings_window(
+ fn set_window_visible(&self, visible: bool) -> Result<()>;
+ fn show_overview_page(&self, current_session: Option<&auth::Session>) -> Result<()>;
+ fn show_settings_page(
&self,
mdm_settings: MdmSettings,
- advanced_settings: AdvancedSettings,
+ settings: AdvancedSettings,
) -> Result<()>;
- fn show_about_window(&self) -> Result<()>;
+ fn show_about_page(&self) -> Result<()>;
}
pub enum ControllerRequest {
@@ -106,6 +104,7 @@ pub enum ControllerRequest {
Fail(Failure),
SignIn,
SignOut,
+ UpdateState,
SystemTrayMenu(system_tray::Event),
UpdateNotificationClicked(Url),
}
@@ -288,8 +287,7 @@ impl Controller {
self.refresh_system_tray_menu();
if !ran_before::get().await? {
- self.integration
- .set_welcome_window_visible(true, self.auth.session())?;
+ self.integration.show_overview_page(self.auth.session())?;
}
while let Some(tick) = self.tick().await {
@@ -506,6 +504,8 @@ impl Controller {
// Refresh the menu in case the favorites were reset.
self.refresh_system_tray_menu();
+
+ self.integration.show_notification("Settings saved", "")?
}
ClearLogs(completion_tx) => {
if self.clear_logs_callback.is_some() {
@@ -541,8 +541,7 @@ impl Controller {
self.integration
.open_url(url.expose_secret())
.context("Couldn't open auth page")?;
- self.integration
- .set_welcome_window_visible(false, self.auth.session())?;
+ self.integration.set_window_visible(false)?;
}
SystemTrayMenu(system_tray::Event::AddFavorite(resource_id)) => {
self.general_settings.favorite_resources.insert(resource_id);
@@ -593,8 +592,8 @@ impl Controller {
}
SystemTrayMenu(system_tray::Event::ShowWindow(window)) => {
match window {
- system_tray::Window::About => self.integration.show_about_window()?,
- system_tray::Window::Settings => self.integration.show_settings_window(
+ system_tray::Window::About => self.integration.show_about_page()?,
+ system_tray::Window::Settings => self.integration.show_settings_page(
self.mdm_settings.clone(),
self.advanced_settings.clone(),
)?,
@@ -630,6 +629,19 @@ impl Controller {
.open_url(&download_url)
.context("Couldn't open update page")?;
}
+ UpdateState => {
+ self.integration.notify_settings_changed(
+ self.mdm_settings.clone(),
+ self.advanced_settings.clone(),
+ )?;
+ match self.auth.session() {
+ Some(session) => self.integration.notify_signed_in(session)?,
+ None => self.integration.notify_signed_out()?,
+ };
+
+ let file_count = logging::count_logs().await?;
+ self.integration.notify_logs_recounted(&file_count)?;
+ }
}
Ok(())
}
@@ -644,6 +656,9 @@ impl Controller {
};
tx.send(result)
.map_err(|_| anyhow!("Couldn't send `ClearLogs` result to Tauri task"))?;
+
+ let file_count = logging::count_logs().await?;
+ self.integration.notify_logs_recounted(&file_count)?;
}
service::ServerMsg::ConnectResult(result) => {
self.handle_connect_result(result).await?;
@@ -738,8 +753,7 @@ impl Controller {
}
},
gui::ClientMsg::NewInstance => {
- self.integration
- .set_welcome_window_visible(true, self.auth.session())?;
+ self.integration.show_overview_page(self.auth.session())?;
}
}
@@ -943,7 +957,7 @@ impl Controller {
self.mdm_settings
.auth_url
.as_ref()
- .unwrap_or(&self.advanced_settings.auth_base_url)
+ .unwrap_or(&self.advanced_settings.auth_url)
}
fn api_url(&self) -> &Url {
diff --git a/rust/gui-client/src-tauri/src/gui.rs b/rust/gui-client/src-tauri/src/gui.rs
index 237af4b48..95b8a2276 100644
--- a/rust/gui-client/src-tauri/src/gui.rs
+++ b/rust/gui-client/src-tauri/src/gui.rs
@@ -4,11 +4,11 @@
//! The real macOS Client is in `swift/apple`
use crate::{
- about, auth,
+ auth,
controller::{Controller, ControllerRequest, CtlrTx, Failure, GuiIntegration},
deep_link,
ipc::{self, ClientRead, ClientWrite, SocketId},
- logging,
+ logging::FileCount,
settings::{
self, AdvancedSettings, AdvancedSettingsLegacy, AdvancedSettingsViewModel, MdmSettings,
},
@@ -55,17 +55,19 @@ struct TauriIntegration {
}
impl TauriIntegration {
- fn show_window(&self, label: &str) -> Result<()> {
- let win = self
- .app
- .get_webview_window(label)
- .with_context(|| format!("Couldn't get handle to `{label}` window"))?;
+ fn main_window(&self) -> Result {
+ self.app
+ .get_webview_window("main")
+ .context("Couldn't get handle to window")
+ }
- // Needed to bring shown windows to the front
- // `request_user_attention` and `set_focus` don't work, at least on Linux
- win.hide()?;
- // Needed to show windows that are completely hidden
- win.show()?;
+ fn navigate(&self, path: &str) -> Result<()> {
+ let window = self.main_window()?;
+
+ let mut url = window.url()?;
+ url.set_path(path);
+
+ window.navigate(url)?;
Ok(())
}
@@ -80,32 +82,6 @@ impl Drop for TauriIntegration {
}
impl GuiIntegration for TauriIntegration {
- fn set_welcome_window_visible(
- &self,
- visible: bool,
- current_session: Option<&auth::Session>,
- ) -> Result<()> {
- let win = self
- .app
- .get_webview_window("welcome")
- .context("Couldn't get handle to Welcome window")?;
-
- if visible {
- win.show().context("Couldn't show Welcome window")?;
- win.set_focus().context("Failed to focus window")?;
- } else {
- win.hide().context("Couldn't hide Welcome window")?;
- }
-
- // Ensure state in frontend is up-to-date.
- match current_session {
- Some(session) => self.notify_signed_in(session)?,
- None => self.notify_signed_out()?,
- };
-
- Ok(())
- }
-
fn notify_signed_in(&self, session: &auth::Session) -> Result<()> {
self.app
.emit("signed_in", session)
@@ -137,6 +113,14 @@ impl GuiIntegration for TauriIntegration {
Ok(())
}
+ fn notify_logs_recounted(&self, file_count: &FileCount) -> Result<()> {
+ self.app
+ .emit("logs_recounted", file_count)
+ .context("Failed to send `logs_recounted` event")?;
+
+ Ok(())
+ }
+
fn open_url>(&self, url: P) -> Result<()> {
tauri_plugin_opener::open_url(url, Option::<&str>::None)?;
@@ -159,19 +143,51 @@ impl GuiIntegration for TauriIntegration {
os::show_update_notification(&self.app, ctlr_tx, title, url)
}
- fn show_settings_window(
- &self,
- mdm_settings: MdmSettings,
- advanced_settings: AdvancedSettings,
- ) -> Result<()> {
- self.show_window("settings")?;
- self.notify_settings_changed(mdm_settings, advanced_settings)?; // Ensure settings are up to date in GUI.
+ fn set_window_visible(&self, visible: bool) -> Result<()> {
+ let win = self.main_window()?;
+
+ if visible {
+ // Needed to bring shown windows to the front
+ // `request_user_attention` and `set_focus` don't work, at least on Linux
+ win.hide()?;
+ // Needed to show windows that are completely hidden
+ win.show()?;
+ } else {
+ win.hide().context("Couldn't hide window")?;
+ }
Ok(())
}
- fn show_about_window(&self) -> Result<()> {
- self.show_window("about")
+ fn show_overview_page(&self, current_session: Option<&auth::Session>) -> Result<()> {
+ // Ensure state in frontend is up-to-date.
+ match current_session {
+ Some(session) => self.notify_signed_in(session)?,
+ None => self.notify_signed_out()?,
+ };
+ self.navigate("overview")?;
+ self.set_window_visible(true)?;
+
+ Ok(())
+ }
+
+ fn show_settings_page(
+ &self,
+ mdm_settings: MdmSettings,
+ advanced_settings: AdvancedSettings,
+ ) -> Result<()> {
+ self.notify_settings_changed(mdm_settings, advanced_settings)?; // Ensure settings are up to date in GUI.
+ self.navigate("settings")?;
+ self.set_window_visible(true)?;
+
+ Ok(())
+ }
+
+ fn show_about_page(&self) -> Result<()> {
+ self.navigate("about")?;
+ self.set_window_visible(true)?;
+
+ Ok(())
}
}
@@ -265,17 +281,7 @@ pub fn run(
api.prevent_close();
}
})
- .invoke_handler(tauri::generate_handler![
- about::get_cargo_version,
- about::get_git_version,
- logging::clear_logs,
- logging::count_logs,
- logging::export_logs,
- settings::apply_advanced_settings,
- settings::reset_advanced_settings,
- crate::welcome::sign_in,
- crate::welcome::sign_out,
- ])
+ .invoke_handler(crate::view::generate_handler())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_shell::init())
diff --git a/rust/gui-client/src-tauri/src/lib.rs b/rust/gui-client/src-tauri/src/lib.rs
index 9deb320a1..8b6443295 100644
--- a/rust/gui-client/src-tauri/src/lib.rs
+++ b/rust/gui-client/src-tauri/src/lib.rs
@@ -1,10 +1,8 @@
#![cfg_attr(test, allow(clippy::unwrap_used))]
-mod about;
-mod clear_logs;
mod updates;
mod uptime;
-mod welcome;
+mod view;
// TODO: See how many of these we can make private.
pub mod auth;
@@ -17,8 +15,6 @@ pub mod logging;
pub mod service;
pub mod settings;
-pub use clear_logs::clear_logs;
-
/// The Sentry "release" we are part of.
///
/// Tunnel service and GUI client are always bundled into a single release.
diff --git a/rust/gui-client/src-tauri/src/logging.rs b/rust/gui-client/src-tauri/src/logging.rs
index b7886f669..4c3e8715a 100644
--- a/rust/gui-client/src-tauri/src/logging.rs
+++ b/rust/gui-client/src-tauri/src/logging.rs
@@ -1,89 +1,17 @@
//! Everything for logging to files, zipping up the files for export, and counting the files
-use crate::gui::Managed;
use anyhow::{Context as _, Result, bail};
use firezone_bin_shared::known_dirs;
-use firezone_logging::{FilterReloadHandle, err_with_src};
+use firezone_logging::FilterReloadHandle;
use serde::Serialize;
use std::{
fs,
io::{self, ErrorKind::NotFound},
path::{Path, PathBuf},
};
-use tauri_plugin_dialog::DialogExt as _;
use tokio::task::spawn_blocking;
use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt};
-use super::controller::{ControllerRequest, CtlrTx};
-
-#[tauri::command]
-pub(crate) async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<(), String> {
- let (tx, rx) = tokio::sync::oneshot::channel();
- if let Err(error) = managed.ctlr_tx.send(ControllerRequest::ClearLogs(tx)).await {
- // Tauri will only log errors to the JS console for us, so log this ourselves.
- tracing::error!(
- "Error while asking `Controller` to clear logs: {}",
- err_with_src(&error)
- );
- return Err(error.to_string());
- }
- if let Err(error) = rx.await {
- tracing::error!(
- "Error while awaiting log-clearing operation: {}",
- err_with_src(&error)
- );
- return Err(error.to_string());
- }
- Ok(())
-}
-
-#[tauri::command]
-pub(crate) async fn export_logs(
- app: tauri::AppHandle,
- managed: tauri::State<'_, Managed>,
-) -> Result<(), String> {
- show_export_dialog(&app, managed.ctlr_tx.clone()).map_err(|e| e.to_string())
-}
-
-#[tauri::command]
-pub(crate) async fn count_logs() -> Result {
- count_logs_imp().await.map_err(|e| e.to_string())
-}
-
-/// Pops up the "Save File" dialog
-fn show_export_dialog(app: &tauri::AppHandle, ctlr_tx: CtlrTx) -> Result<()> {
- let now = chrono::Local::now();
- let datetime_string = now.format("%Y_%m_%d-%H-%M");
- let stem = PathBuf::from(format!("firezone_logs_{datetime_string}"));
- let filename = stem.with_extension("zip");
- let Some(filename) = filename.to_str() else {
- bail!("zip filename isn't valid Unicode");
- };
-
- tauri_plugin_dialog::FileDialogBuilder::new(app.dialog().clone())
- .add_filter("Zip", &["zip"])
- .set_file_name(filename)
- .save_file(move |file_path| {
- let Some(file_path) = file_path else {
- return;
- };
-
- let path = match file_path.clone().into_path() {
- Ok(path) => path,
- Err(e) => {
- tracing::warn!(%file_path, "Invalid file path: {}", err_with_src(&e));
- return;
- }
- };
-
- // blocking_send here because we're in a sync callback within Tauri somewhere
- if let Err(e) = ctlr_tx.blocking_send(ControllerRequest::ExportLogs { path, stem }) {
- tracing::warn!("Failed to send `ExportLogs` command: {e}");
- }
- });
- Ok(())
-}
-
/// If you don't store `Handles` in a variable, the file logger handle will drop immediately,
/// resulting in empty log files.
#[must_use]
@@ -282,15 +210,51 @@ pub struct FileCount {
files: u64,
}
-/// Delete all files in the logs directory.
-///
-/// This includes the current log file, so we won't write any more logs to disk
-/// until the file rolls over or the app restarts.
-///
-/// If we get an error while removing a file, we still try to remove all other
-/// files, then we return the most recent error.
pub async fn clear_gui_logs() -> Result<()> {
- crate::clear_logs(&known_dirs::logs().context("Can't compute GUI log dir")?).await
+ clear_logs(&known_dirs::logs().context("Can't compute GUI log dir")?).await
+}
+
+pub async fn clear_service_logs() -> Result<()> {
+ clear_logs(&known_dirs::tunnel_service_logs().context("Can't compute service logs dir")?).await
+}
+
+/// Deletes all `.log` files in `path`.
+async fn clear_logs(path: &Path) -> Result<()> {
+ let mut dir = match tokio::fs::read_dir(path).await {
+ Ok(x) => x,
+ Err(error) => {
+ if matches!(error.kind(), NotFound) {
+ // In smoke tests, the Tunnel service runs in debug mode, so it won't write any logs to disk. If the Tunnel service's log dir doesn't exist, we shouldn't crash, it's correct to simply not delete the non-existent files
+ return Ok(());
+ }
+ // But any other error like permissions errors, should bubble.
+ return Err(error.into());
+ }
+ };
+
+ let mut result = Ok(());
+
+ // If we can't delete some files due to permission errors, just keep going
+ // and delete as much as we can, then return the most recent error
+ while let Some(entry) = dir
+ .next_entry()
+ .await
+ .context("Failed to read next dir entry")?
+ {
+ if entry
+ .file_name()
+ .to_str()
+ .is_none_or(|name| !name.ends_with("log") && name != "latest")
+ {
+ continue;
+ }
+
+ if let Err(e) = tokio::fs::remove_file(entry.path()).await {
+ result = Err(e);
+ }
+ }
+
+ result.context("Failed to delete at least one file")
}
/// Exports logs to a zip file
@@ -361,7 +325,7 @@ fn add_dir_to_zip(
}
/// Count log files and their sizes
-async fn count_logs_imp() -> Result {
+pub async fn count_logs() -> Result {
// I spent about 5 minutes on this and couldn't get it to work with `Stream`
let mut total_count = FileCount::default();
for log_path in log_paths()? {
@@ -373,7 +337,12 @@ async fn count_logs_imp() -> Result {
}
async fn count_one_dir(path: &Path) -> Result {
- let mut dir = tokio::fs::read_dir(path).await?;
+ let mut dir = match tokio::fs::read_dir(path).await {
+ Ok(dir) => dir,
+ Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(FileCount::default()),
+ Err(e) => return Err(anyhow::Error::new(e)),
+ };
+
let mut file_count = FileCount::default();
while let Some(entry) = dir.next_entry().await? {
@@ -398,3 +367,36 @@ fn log_paths() -> Result> {
},
])
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn only_deletes_log_files() {
+ let dir = tempfile::tempdir().unwrap();
+
+ std::fs::write(dir.path().join("first.log"), "log file 1").unwrap();
+ std::fs::write(dir.path().join("second.log"), "log file 1").unwrap();
+ std::fs::write(dir.path().join("not_a_logfile.tmp"), "something important").unwrap();
+
+ clear_logs(dir.path()).await.unwrap();
+
+ assert_eq!(
+ std::fs::read_to_string(dir.path().join("not_a_logfile.tmp")).unwrap(),
+ "something important"
+ );
+ }
+
+ #[tokio::test]
+ async fn non_existing_path_is_empty() {
+ let dir = tempfile::tempdir().unwrap();
+ let path = dir.path().to_owned();
+ drop(dir);
+
+ let file_count = count_one_dir(path.as_path()).await.unwrap();
+
+ assert_eq!(file_count.bytes, 0);
+ assert_eq!(file_count.files, 0);
+ }
+}
diff --git a/rust/gui-client/src-tauri/src/service.rs b/rust/gui-client/src-tauri/src/service.rs
index cd177f82c..046ea10ca 100644
--- a/rust/gui-client/src-tauri/src/service.rs
+++ b/rust/gui-client/src-tauri/src/service.rs
@@ -1,4 +1,7 @@
-use crate::ipc::{self, SocketId};
+use crate::{
+ ipc::{self, SocketId},
+ logging,
+};
use anyhow::{Context as _, Result, bail};
use atomicwrites::{AtomicFile, OverwriteBehavior};
use backoff::ExponentialBackoffBuilder;
@@ -362,11 +365,7 @@ impl<'a> Handler<'a> {
async fn handle_ipc_msg(&mut self, msg: ClientMsg) -> Result<()> {
match msg {
ClientMsg::ClearLogs => {
- let result = crate::clear_logs(
- &firezone_bin_shared::known_dirs::tunnel_service_logs()
- .context("Can't compute logs dir")?,
- )
- .await;
+ let result = logging::clear_service_logs().await;
self.send_ipc(ServerMsg::ClearedLogs(result.map_err(|e| e.to_string())))
.await?
}
@@ -528,7 +527,7 @@ impl<'a> Handler<'a> {
}
pub fn run_debug(dns_control: DnsControlMethod) -> Result<()> {
- let log_filter_reloader = crate::logging::setup_stdout()?;
+ let log_filter_reloader = logging::setup_stdout()?;
tracing::info!(
arch = std::env::consts::ARCH,
version = env!("CARGO_PKG_VERSION"),
@@ -555,7 +554,7 @@ pub fn run_smoke_test() -> Result<()> {
use anyhow::{Context as _, bail};
use firezone_bin_shared::{DnsController, device_id};
- let log_filter_reloader = crate::logging::setup_stdout()?;
+ let log_filter_reloader = logging::setup_stdout()?;
if !elevation_check()? {
bail!("Tunnel service failed its elevation check, try running as admin / root");
}
diff --git a/rust/gui-client/src-tauri/src/settings.rs b/rust/gui-client/src-tauri/src/settings.rs
index f0e64875a..911ca7886 100644
--- a/rust/gui-client/src-tauri/src/settings.rs
+++ b/rust/gui-client/src-tauri/src/settings.rs
@@ -1,18 +1,14 @@
//! Everything related to the Settings window, including
//! advanced settings and code for manipulating diagnostic logs.
-use crate::gui::Managed;
use anyhow::{Context as _, Result};
use connlib_model::ResourceId;
use firezone_bin_shared::known_dirs;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
-use std::time::Duration;
use std::{collections::HashSet, path::PathBuf};
use url::Url;
-use super::controller::ControllerRequest;
-
#[cfg(target_os = "linux")]
#[path = "settings/linux.rs"]
pub(crate) mod mdm;
@@ -27,33 +23,6 @@ pub(crate) mod mdm;
pub use mdm::load_mdm_settings;
-#[tauri::command]
-pub(crate) async fn apply_advanced_settings(
- managed: tauri::State<'_, Managed>,
- settings: AdvancedSettings,
-) -> Result<(), String> {
- if managed.inner().inject_faults {
- tokio::time::sleep(Duration::from_secs(2)).await;
- }
-
- managed
- .ctlr_tx
- .send(ControllerRequest::ApplySettings(Box::new(settings)))
- .await
- .map_err(|e| e.to_string())?;
-
- Ok(())
-}
-
-#[tauri::command]
-pub(crate) async fn reset_advanced_settings(
- managed: tauri::State<'_, Managed>,
-) -> Result<(), String> {
- apply_advanced_settings(managed, AdvancedSettings::default()).await?;
-
- Ok(())
-}
-
/// Defines all configuration options settable via MDM policies.
///
/// Configuring Firezone via MDM is optional, therefore all of these are [`Option`]s.
@@ -84,7 +53,7 @@ pub struct AdvancedSettingsLegacy {
#[derive(Clone, Deserialize, Serialize)]
pub struct AdvancedSettings {
- pub auth_base_url: Url,
+ pub auth_url: Url,
pub api_url: Url,
pub log_filter: String,
}
@@ -114,9 +83,7 @@ impl AdvancedSettingsViewModel {
api_url_is_managed: mdm_settings.api_url.is_some(),
log_filter_is_managed: mdm_settings.log_filter.is_some(),
- auth_url: mdm_settings
- .auth_url
- .unwrap_or(advanced_settings.auth_base_url),
+ auth_url: mdm_settings.auth_url.unwrap_or(advanced_settings.auth_url),
api_url: mdm_settings.api_url.unwrap_or(advanced_settings.api_url),
log_filter: mdm_settings
.log_filter
@@ -160,7 +127,7 @@ impl GeneralSettings {
impl Default for AdvancedSettings {
fn default() -> Self {
Self {
- auth_base_url: Url::parse(defaults::AUTH_BASE_URL).expect("static URL is a valid URL"),
+ auth_url: Url::parse(defaults::AUTH_BASE_URL).expect("static URL is a valid URL"),
api_url: Url::parse(defaults::API_URL).expect("static URL is a valid URL"),
log_filter: defaults::LOG_FILTER.to_string(),
}
@@ -185,7 +152,7 @@ pub async fn migrate_legacy_settings(
let general_settings = load_general_settings();
let advanced = AdvancedSettings {
- auth_base_url: legacy.auth_base_url,
+ auth_url: legacy.auth_base_url,
api_url: legacy.api_url,
log_filter: legacy.log_filter,
};
@@ -278,7 +245,7 @@ mod tests {
"log_filter": "info"
}"#;
- let actual = serde_json::from_str::(s).unwrap();
+ let actual = serde_json::from_str::(s).unwrap();
// Apparently the trailing slash here matters
assert_eq!(actual.auth_base_url.to_string(), "https://example.com/");
assert_eq!(actual.api_url.to_string(), "wss://example.com/");
diff --git a/rust/gui-client/src-tauri/src/view.rs b/rust/gui-client/src-tauri/src/view.rs
new file mode 100644
index 000000000..31cb96e15
--- /dev/null
+++ b/rust/gui-client/src-tauri/src/view.rs
@@ -0,0 +1,148 @@
+use std::{path::PathBuf, time::Duration};
+
+use anyhow::{Context as _, Result, bail};
+use firezone_logging::err_with_src;
+use tauri::{Wry, ipc::Invoke};
+use tauri_plugin_dialog::DialogExt as _;
+
+use crate::{
+ controller::{ControllerRequest, CtlrTx},
+ gui::Managed,
+ settings::AdvancedSettings,
+};
+
+pub fn generate_handler() -> impl Fn(Invoke) -> bool + Send + Sync + 'static {
+ tauri::generate_handler![
+ clear_logs,
+ export_logs,
+ apply_advanced_settings,
+ reset_advanced_settings,
+ sign_in,
+ sign_out,
+ update_state,
+ ]
+}
+
+#[tauri::command]
+async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<(), String> {
+ let (tx, rx) = tokio::sync::oneshot::channel();
+ if let Err(error) = managed.ctlr_tx.send(ControllerRequest::ClearLogs(tx)).await {
+ // Tauri will only log errors to the JS console for us, so log this ourselves.
+ tracing::error!(
+ "Error while asking `Controller` to clear logs: {}",
+ err_with_src(&error)
+ );
+ return Err(error.to_string());
+ }
+ if let Err(error) = rx.await {
+ tracing::error!(
+ "Error while awaiting log-clearing operation: {}",
+ err_with_src(&error)
+ );
+ return Err(error.to_string());
+ }
+ Ok(())
+}
+
+#[tauri::command]
+async fn export_logs(
+ app: tauri::AppHandle,
+ managed: tauri::State<'_, Managed>,
+) -> Result<(), String> {
+ show_export_dialog(&app, managed.ctlr_tx.clone()).map_err(|e| e.to_string())
+}
+
+#[tauri::command]
+async fn apply_advanced_settings(
+ managed: tauri::State<'_, Managed>,
+ settings: AdvancedSettings,
+) -> Result<(), String> {
+ if managed.inner().inject_faults {
+ tokio::time::sleep(Duration::from_secs(2)).await;
+ }
+
+ managed
+ .ctlr_tx
+ .send(ControllerRequest::ApplySettings(Box::new(settings)))
+ .await
+ .map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[tauri::command]
+async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<(), String> {
+ apply_advanced_settings(managed, AdvancedSettings::default()).await?;
+
+ Ok(())
+}
+
+/// Pops up the "Save File" dialog
+fn show_export_dialog(app: &tauri::AppHandle, ctlr_tx: CtlrTx) -> Result<()> {
+ let now = chrono::Local::now();
+ let datetime_string = now.format("%Y_%m_%d-%H-%M");
+ let stem = PathBuf::from(format!("firezone_logs_{datetime_string}"));
+ let filename = stem.with_extension("zip");
+ let Some(filename) = filename.to_str() else {
+ bail!("zip filename isn't valid Unicode");
+ };
+
+ tauri_plugin_dialog::FileDialogBuilder::new(app.dialog().clone())
+ .add_filter("Zip", &["zip"])
+ .set_file_name(filename)
+ .save_file(move |file_path| {
+ let Some(file_path) = file_path else {
+ return;
+ };
+
+ let path = match file_path.clone().into_path() {
+ Ok(path) => path,
+ Err(e) => {
+ tracing::warn!(%file_path, "Invalid file path: {}", err_with_src(&e));
+ return;
+ }
+ };
+
+ // blocking_send here because we're in a sync callback within Tauri somewhere
+ if let Err(e) = ctlr_tx.blocking_send(ControllerRequest::ExportLogs { path, stem }) {
+ tracing::warn!("Failed to send `ExportLogs` command: {e}");
+ }
+ });
+ Ok(())
+}
+
+#[tauri::command]
+async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<(), String> {
+ managed
+ .ctlr_tx
+ .send(ControllerRequest::SignIn)
+ .await
+ .context("Failed to send `SignIn` command")
+ .map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[tauri::command]
+async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<(), String> {
+ managed
+ .ctlr_tx
+ .send(ControllerRequest::SignOut)
+ .await
+ .context("Failed to send `SignOut` command")
+ .map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[tauri::command]
+async fn update_state(managed: tauri::State<'_, Managed>) -> Result<(), String> {
+ managed
+ .ctlr_tx
+ .send(ControllerRequest::UpdateState)
+ .await
+ .context("Failed to send `UpdateState` command")
+ .map_err(|e| e.to_string())?;
+
+ Ok(())
+}
diff --git a/rust/gui-client/src-tauri/src/welcome.rs b/rust/gui-client/src-tauri/src/welcome.rs
deleted file mode 100644
index 4e9683988..000000000
--- a/rust/gui-client/src-tauri/src/welcome.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-//! Everything related to the Welcome window
-
-use crate::gui::Managed;
-use anyhow::Context;
-
-use super::controller::ControllerRequest;
-
-#[tauri::command]
-pub(crate) async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<(), String> {
- managed
- .ctlr_tx
- .send(ControllerRequest::SignIn)
- .await
- .context("Failed to send `SignIn` command")
- .map_err(|e| e.to_string())?;
-
- Ok(())
-}
-
-#[tauri::command]
-pub(crate) async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<(), String> {
- managed
- .ctlr_tx
- .send(ControllerRequest::SignOut)
- .await
- .context("Failed to send `SignOut` command")
- .map_err(|e| e.to_string())?;
-
- Ok(())
-}
diff --git a/rust/gui-client/src-tauri/tauri.conf.json b/rust/gui-client/src-tauri/tauri.conf.json
index c75c329ea..5caf89ddd 100644
--- a/rust/gui-client/src-tauri/tauri.conf.json
+++ b/rust/gui-client/src-tauri/tauri.conf.json
@@ -64,33 +64,13 @@
},
"windows": [
{
- "label": "about",
- "title": "About Firezone",
- "url": "src/about.html",
+ "label": "main",
+ "title": "Firezone",
+ "url": "src-frontend/index.html",
"fullscreen": false,
"resizable": false,
- "width": 400,
- "height": 300,
- "visible": false
- },
- {
- "label": "settings",
- "title": "Settings",
- "url": "src/settings.html",
- "fullscreen": false,
- "resizable": true,
- "width": 640,
- "height": 480,
- "visible": false
- },
- {
- "label": "welcome",
- "title": "Welcome",
- "url": "src/welcome.html",
- "fullscreen": false,
- "resizable": false,
- "width": 800,
- "height": 450,
+ "width": 900,
+ "height": 500,
"visible": false
}
]
diff --git a/rust/gui-client/src/about.html b/rust/gui-client/src/about.html
deleted file mode 100644
index e0b8817ac..000000000
--- a/rust/gui-client/src/about.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
- About Firezone
-
-
-
-
-
- WARNING: These settings are intended for internal
- debug purposes only. Changing these is not
- supported and will disrupt access to your resources.
-