mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-03-22 01:41:57 +00:00
Despite still being in development, the `tauri-specta` project already proves to be quite useful. It allows us to generate TypeScript bindings for our commands and events, creating a type-safe contract between the frontend and the backend. For example, this ensures that the TypeScript code calls a command actually with the required parameters and thus avoids runtime failures. Similarly, the frontend can listen on type-safe events without having to use any magic strings.
191 lines
5.0 KiB
Rust
191 lines
5.0 KiB
Rust
use std::{path::PathBuf, time::Duration};
|
|
|
|
use anyhow::Context as _;
|
|
use firezone_logging::err_with_src;
|
|
use serde::Serialize;
|
|
use tauri_plugin_dialog::DialogExt as _;
|
|
|
|
use crate::{
|
|
controller::ControllerRequest,
|
|
gui::Managed,
|
|
logging::FileCount,
|
|
settings::{AdvancedSettings, AdvancedSettingsViewModel, GeneralSettingsViewModel},
|
|
};
|
|
|
|
#[derive(Clone, serde::Deserialize, specta::Type)]
|
|
pub struct GeneralSettingsForm {
|
|
pub start_minimized: bool,
|
|
pub start_on_login: bool,
|
|
pub connect_on_start: bool,
|
|
pub account_slug: String,
|
|
}
|
|
|
|
#[derive(Clone, serde::Serialize, specta::Type)]
|
|
pub enum SessionViewModel {
|
|
SignedIn {
|
|
account_slug: String,
|
|
actor_name: String,
|
|
},
|
|
Loading,
|
|
SignedOut,
|
|
}
|
|
|
|
#[derive(Clone, serde::Serialize, specta::Type, tauri_specta::Event)]
|
|
pub struct SessionChanged(pub SessionViewModel);
|
|
|
|
#[derive(Clone, serde::Serialize, specta::Type, tauri_specta::Event)]
|
|
pub struct GeneralSettingsChanged(pub GeneralSettingsViewModel);
|
|
|
|
#[derive(Clone, serde::Serialize, specta::Type, tauri_specta::Event)]
|
|
pub struct AdvancedSettingsChanged(pub AdvancedSettingsViewModel);
|
|
|
|
#[derive(Clone, serde::Serialize, specta::Type, tauri_specta::Event)]
|
|
pub struct LogsRecounted(pub FileCount);
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
|
managed
|
|
.send_request(ControllerRequest::ClearLogs(tx))
|
|
.await?;
|
|
|
|
rx.await
|
|
.context("Failed to await `ClearLogs` result")?
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn export_logs(app: tauri::AppHandle, managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
show_export_dialog(&app, managed.inner().clone())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub 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
|
|
.send_request(ControllerRequest::ApplyGeneralSettings(Box::new(settings)))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn apply_advanced_settings(
|
|
managed: tauri::State<'_, Managed>,
|
|
settings: AdvancedSettings,
|
|
) -> Result<()> {
|
|
if managed.inner().inject_faults {
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
}
|
|
|
|
managed
|
|
.send_request(ControllerRequest::ApplyAdvancedSettings(Box::new(settings)))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
apply_advanced_settings(managed, AdvancedSettings::default()).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn reset_general_settings(managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
managed
|
|
.send_request(ControllerRequest::ResetGeneralSettings)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Pops up the "Save File" dialog
|
|
fn show_export_dialog(app: &tauri::AppHandle, managed: Managed) -> 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 filename = filename
|
|
.to_str()
|
|
.context("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) =
|
|
managed.blocking_send_request(ControllerRequest::ExportLogs { path, stem })
|
|
{
|
|
tracing::warn!("{e:#}");
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
managed.send_request(ControllerRequest::SignIn).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
managed.send_request(ControllerRequest::SignOut).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn update_state(managed: tauri::State<'_, Managed>) -> Result<()> {
|
|
managed.send_request(ControllerRequest::UpdateState).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
type Result<T> = std::result::Result<T, Error>;
|
|
|
|
#[derive(Debug, specta::Type, Serialize)]
|
|
pub struct Error(String);
|
|
|
|
impl From<anyhow::Error> for Error {
|
|
fn from(error: anyhow::Error) -> Self {
|
|
Self(format!("{error:#}"))
|
|
}
|
|
}
|