diff --git a/.tool-versions b/.tool-versions index a63a8d922..8455a93b9 100644 --- a/.tool-versions +++ b/.tool-versions @@ -10,3 +10,6 @@ shfmt 3.9.0 # Used to lint Bash scripts shellcheck 0.9.0 + +# GUI client +pnpm 10.13.1 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index c27cef18d..418b29e0f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1454,15 +1454,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cookie" version = "0.18.1" @@ -1852,7 +1843,7 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -2460,6 +2451,8 @@ dependencies = [ "serde", "serde_json", "serde_variant", + "specta", + "specta-typescript", "strum", "subtle", "tauri", @@ -2469,6 +2462,7 @@ dependencies = [ "tauri-plugin-opener", "tauri-plugin-shell", "tauri-runtime", + "tauri-specta", "tauri-utils", "tauri-winrt-notification", "tempfile", @@ -2480,7 +2474,6 @@ dependencies = [ "tracing-journald", "tracing-log", "tracing-subscriber", - "tslink", "url", "uuid", "windows", @@ -6934,6 +6927,51 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "paste", + "specta-macros", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta", + "specta-serde", + "thiserror 1.0.69", +] + [[package]] name = "spin" version = "0.9.8" @@ -7295,6 +7333,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -7543,6 +7582,34 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.12", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "tauri-utils" version = "2.5.0" @@ -8225,23 +8292,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tslink" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944e85e01601d4e4b3a8b0a127c3812aef1457cdf8161c6bc8be9956af047718" -dependencies = [ - "convert_case 0.6.0", - "lazy_static", - "proc-macro2", - "quote", - "serde", - "syn 2.0.104", - "thiserror 1.0.69", - "toml 0.8.22", - "uuid", -] - [[package]] name = "tun" version = "0.1.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f8297e2cd..cca6f9f5a 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -153,6 +153,8 @@ smoltcp = { version = "0.12", default-features = false } snownet = { path = "connlib/snownet" } socket-factory = { path = "connlib/socket-factory" } socket2 = { version = "0.5" } +specta = "=2.0.0-rc.22" +specta-typescript = "0.0.9" static_assertions = "1.1.0" str0m = { version = "0.9.0", default-features = false, features = ["sha1"] } strum = { version = "0.27.1", features = ["derive"] } @@ -170,6 +172,7 @@ tauri-plugin-notification = "2.3.0" tauri-plugin-opener = "2.4.0" tauri-plugin-shell = "2.3.0" tauri-runtime = "2.5.0" +tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } tauri-utils = "2.2.0" tempfile = "3.20.0" test-case = "3.3.1" diff --git a/rust/deny.toml b/rust/deny.toml index 4eac1219f..0eef5fcfe 100644 --- a/rust/deny.toml +++ b/rust/deny.toml @@ -229,7 +229,6 @@ deny = [ skip = [ "base64", "bitflags", - "convert_case", "core-foundation", "core-graphics", "core-graphics-types", diff --git a/rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx b/rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx index 0b398aca1..d04e139c5 100644 --- a/rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx +++ b/rust/gui-client/src-frontend/components/AdvancedSettingsPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useId, useState } from "react"; import { Button, Label } from "flowbite-react"; -import { AdvancedSettingsViewModel } from "../generated/AdvancedSettingsViewModel"; import { ManagedTextInput } from "./ManagedInput"; +import { AdvancedSettingsViewModel } from "../generated/bindings"; interface Props { settings: AdvancedSettingsViewModel | null; diff --git a/rust/gui-client/src-frontend/components/App.tsx b/rust/gui-client/src-frontend/components/App.tsx index 5f6f19881..6f1cb7da6 100644 --- a/rust/gui-client/src-frontend/components/App.tsx +++ b/rust/gui-client/src-frontend/components/App.tsx @@ -7,8 +7,6 @@ import { SwatchIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/solid"; -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; import { Sidebar, SidebarCollapse, @@ -17,9 +15,6 @@ import { } from "flowbite-react"; import React, { useEffect, useState } from "react"; import { Route, Routes } from "react-router"; -import { AdvancedSettingsViewModel } from "../generated/AdvancedSettingsViewModel"; -import { FileCount } from "../generated/FileCount"; -import { SessionViewModel } from "../generated/SessionViewModel"; import About from "./AboutPage"; import AdvancedSettingsPage from "./AdvancedSettingsPage"; import ReactRouterSidebarItem from "./ReactRouterSidebarItem"; @@ -27,7 +22,14 @@ import ColorPalette from "./ColorPalettePage"; import Diagnostics from "./DiagnosticsPage"; import GeneralSettingsPage from "./GeneralSettingsPage"; import Overview from "./OverviewPage"; -import { GeneralSettingsViewModel } from "../generated/GeneralSettingsViewModel"; +import { + AdvancedSettingsViewModel, + commands, + events, + FileCount, + GeneralSettingsViewModel, + SessionViewModel, +} from "../generated/bindings"; export default function App() { const [session, setSession] = useState(null); @@ -38,14 +40,13 @@ export default function App() { useState(null); useEffect(() => { - const sessionChanged = listen("session_changed", (e) => { + const sessionChangedUnlisten = events.sessionChanged.listen((e) => { const session = e.payload; console.log("session_changed", { session }); setSession(session); }); - const generalSettingsChangedUnlisten = listen( - "general_settings_changed", + const generalSettingsChangedUnlisten = events.generalSettingsChanged.listen( (e) => { const generalSettings = e.payload; @@ -53,28 +54,26 @@ export default function App() { setGeneralSettings(generalSettings); } ); - const advancedSettingsChangedUnlisten = listen( - "advanced_settings_changed", - (e) => { + const advancedSettingsChangedUnlisten = + events.advancedSettingsChanged.listen((e) => { const advancedSettings = e.payload; console.log("advanced_settings_changed", { settings: advancedSettings, }); setAdvancedSettings(advancedSettings); - } - ); - const logsRecountedUnlisten = listen("logs_recounted", (e) => { + }); + const logsRecountedUnlisten = events.logsRecounted.listen((e) => { const file_count = e.payload; console.log("logs_recounted", { file_count }); setLogCount(file_count); }); - invoke("update_state"); // Let the backend know that we (re)-initialised + commands.updateState(); // Let the backend know that we (re)-initialised return () => { - sessionChanged.then((unlistenFn) => unlistenFn()); + sessionChangedUnlisten.then((unlistenFn) => unlistenFn()); generalSettingsChangedUnlisten.then((unlistenFn) => unlistenFn()); advancedSettingsChangedUnlisten.then((unlistenFn) => unlistenFn()); logsRecountedUnlisten.then((unlistenFn) => unlistenFn()); @@ -131,8 +130,8 @@ export default function App() { element={ invoke("sign_in")} - signOut={() => invoke("sign_out")} + signIn={commands.signIn} + signOut={commands.signOut} /> } /> @@ -141,10 +140,8 @@ export default function App() { element={ - invoke("apply_general_settings", { settings }) - } - resetSettings={() => invoke("reset_general_settings")} + saveSettings={commands.applyGeneralSettings} + resetSettings={commands.resetGeneralSettings} /> } /> @@ -153,10 +150,8 @@ export default function App() { element={ - invoke("apply_advanced_settings", { settings }) - } - resetSettings={() => invoke("reset_advanced_settings")} + saveSettings={commands.applyAdvancedSettings} + resetSettings={commands.resetAdvancedSettings} /> } /> @@ -165,13 +160,8 @@ export default function App() { element={ invoke("export_logs")} - clearLogs={async () => { - await invoke("clear_logs"); - const logCount = await invoke("count_logs"); - - setLogCount(logCount); - }} + exportLogs={commands.exportLogs} + clearLogs={commands.clearLogs} /> } /> diff --git a/rust/gui-client/src-frontend/components/DiagnosticsPage.tsx b/rust/gui-client/src-frontend/components/DiagnosticsPage.tsx index 8cbb106eb..424a3fd9c 100644 --- a/rust/gui-client/src-frontend/components/DiagnosticsPage.tsx +++ b/rust/gui-client/src-frontend/components/DiagnosticsPage.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ShareIcon, TrashIcon } from "@heroicons/react/16/solid"; import { Button } from "flowbite-react"; -import { FileCount } from "../generated/FileCount"; +import { FileCount } from "../generated/bindings"; interface DiagnosticsPageProps { logCount: FileCount | null; @@ -17,7 +17,7 @@ export default function Diagnostics({ const bytes = logCount?.bytes ?? 0; const files = logCount?.files ?? 0; - const megabytes = Math.round(bytes / 100000) / 10; + const megabytes = Math.round(Number(bytes / 100000)) / 10; return (
diff --git a/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx b/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx index 58aad9318..9e2cad341 100644 --- a/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx +++ b/rust/gui-client/src-frontend/components/GeneralSettingsPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useId, useState } from "react"; import { Button, Label, ToggleSwitch } from "flowbite-react"; -import { GeneralSettingsViewModel } from "../generated/GeneralSettingsViewModel"; import { ManagedToggleSwitch, ManagedTextInput } from "./ManagedInput"; +import { GeneralSettingsViewModel } from "../generated/bindings"; interface Props { settings: GeneralSettingsViewModel | null; diff --git a/rust/gui-client/src-frontend/components/OverviewPage.tsx b/rust/gui-client/src-frontend/components/OverviewPage.tsx index 67789d377..370ecb5ee 100644 --- a/rust/gui-client/src-frontend/components/OverviewPage.tsx +++ b/rust/gui-client/src-frontend/components/OverviewPage.tsx @@ -1,7 +1,7 @@ import React from "react"; import logo from "../logo.png"; -import { SessionViewModel } from "../generated/SessionViewModel"; import { Button, Spinner } from "flowbite-react"; +import { SessionViewModel } from "../generated/bindings"; interface OverviewPageProps { session: SessionViewModel | null; diff --git a/rust/gui-client/src-frontend/generated/AdvancedSettingsViewModel.ts b/rust/gui-client/src-frontend/generated/AdvancedSettingsViewModel.ts deleted file mode 100644 index e422a8297..000000000 --- a/rust/gui-client/src-frontend/generated/AdvancedSettingsViewModel.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface AdvancedSettingsViewModel { - auth_url: string; - auth_url_is_managed: boolean; - api_url: string; - api_url_is_managed: boolean; - log_filter: string; - log_filter_is_managed: boolean; -} diff --git a/rust/gui-client/src-frontend/generated/FileCount.ts b/rust/gui-client/src-frontend/generated/FileCount.ts deleted file mode 100644 index fc979b927..000000000 --- a/rust/gui-client/src-frontend/generated/FileCount.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface FileCount { - bytes: number; - files: number; -} diff --git a/rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts b/rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts deleted file mode 100644 index 3d282f81d..000000000 --- a/rust/gui-client/src-frontend/generated/GeneralSettingsViewModel.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface GeneralSettingsViewModel { - start_minimized: boolean; - start_on_login: boolean; - connect_on_start: boolean; - connect_on_start_is_managed: boolean; - account_slug: string; - account_slug_is_managed: boolean; -} diff --git a/rust/gui-client/src-frontend/generated/README.md b/rust/gui-client/src-frontend/generated/README.md index 6dc2d256b..4a6096259 100644 --- a/rust/gui-client/src-frontend/generated/README.md +++ b/rust/gui-client/src-frontend/generated/README.md @@ -1,4 +1,4 @@ # Generated TypeScript interfaces -The interfaces in this directory are automatically generated by `tslink`. -To regenerate them, run `TSLINK_BUILD=true cargo build`. +The interfaces in this directory are automatically generated by `specta`. +To regenerate them, run the Firezone GUI client as a debug build, i.e. `cargo run --bin firezone-gui-client`. diff --git a/rust/gui-client/src-frontend/generated/SessionViewModel.ts b/rust/gui-client/src-frontend/generated/SessionViewModel.ts deleted file mode 100644 index 06d7da608..000000000 --- a/rust/gui-client/src-frontend/generated/SessionViewModel.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type SessionViewModel = - { - SignedIn: { - account_slug: string; - actor_name: string - } - } | - "Loading" | - "SignedOut"; diff --git a/rust/gui-client/src-frontend/generated/bindings.ts b/rust/gui-client/src-frontend/generated/bindings.ts new file mode 100644 index 000000000..53cfc313a --- /dev/null +++ b/rust/gui-client/src-frontend/generated/bindings.ts @@ -0,0 +1,175 @@ +/* eslint-disable */ +/* tslint:disable */ + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async clearLogs() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("clear_logs") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async exportLogs() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("export_logs") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async applyAdvancedSettings(settings: AdvancedSettings) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("apply_advanced_settings", { settings }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async resetAdvancedSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("reset_advanced_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async applyGeneralSettings(settings: GeneralSettingsForm) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("apply_general_settings", { settings }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async resetGeneralSettings() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("reset_general_settings") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async signIn() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sign_in") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async signOut() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("sign_out") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async updateState() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("update_state") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + +export const events = __makeEvents__<{ +advancedSettingsChanged: AdvancedSettingsChanged, +generalSettingsChanged: GeneralSettingsChanged, +logsRecounted: LogsRecounted, +sessionChanged: SessionChanged +}>({ +advancedSettingsChanged: "advanced-settings-changed", +generalSettingsChanged: "general-settings-changed", +logsRecounted: "logs-recounted", +sessionChanged: "session-changed" +}) + +/** user-defined constants **/ + + + +/** user-defined types **/ + +export type AdvancedSettings = { auth_url: string; api_url: string; log_filter: string } +export type AdvancedSettingsChanged = AdvancedSettingsViewModel +export type AdvancedSettingsViewModel = { auth_url: string; auth_url_is_managed: boolean; api_url: string; api_url_is_managed: boolean; log_filter: string; log_filter_is_managed: boolean } +export type Error = string +export type FileCount = { bytes: number; files: number } +export type GeneralSettingsChanged = GeneralSettingsViewModel +export type GeneralSettingsForm = { start_minimized: boolean; start_on_login: boolean; connect_on_start: boolean; account_slug: string } +export type 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 } +export type LogsRecounted = FileCount +export type SessionChanged = SessionViewModel +export type SessionViewModel = { SignedIn: { account_slug: string; actor_name: string } } | "Loading" | "SignedOut" + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/rust/gui-client/src-tauri/Cargo.toml b/rust/gui-client/src-tauri/Cargo.toml index 1658e8a44..8407f937f 100644 --- a/rust/gui-client/src-tauri/Cargo.toml +++ b/rust/gui-client/src-tauri/Cargo.toml @@ -49,6 +49,8 @@ semver = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_variant = { workspace = true } +specta = { workspace = true, features = ["url"] } +specta-typescript = { workspace = true } strum = { workspace = true } subtle = { workspace = true } tauri = { workspace = true, features = ["tray-icon", "image-png"] } @@ -57,6 +59,7 @@ tauri-plugin-notification = { workspace = true } tauri-plugin-opener = { workspace = true } tauri-plugin-shell = { workspace = true } tauri-runtime = { workspace = true } +tauri-specta = { workspace = true } tauri-utils = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["signal", "time", "macros", "rt", "rt-multi-thread"] } @@ -65,7 +68,6 @@ tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } tracing-log = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } -tslink = "0.4.2" url = { workspace = true } uuid = { workspace = true, features = ["v4"] } zip = { workspace = true, features = ["deflate", "time"] } diff --git a/rust/gui-client/src-tauri/src/gui.rs b/rust/gui-client/src-tauri/src/gui.rs index 8163e3551..757f6198f 100644 --- a/rust/gui-client/src-tauri/src/gui.rs +++ b/rust/gui-client/src-tauri/src/gui.rs @@ -13,13 +13,17 @@ use crate::{ GeneralSettingsViewModel, MdmSettings, }, updates, - view::SessionViewModel, + view::{ + AdvancedSettingsChanged, GeneralSettingsChanged, LogsRecounted, SessionChanged, + SessionViewModel, + }, }; use anyhow::{Context, Result, bail}; use firezone_logging::err_with_src; use futures::SinkExt as _; use std::time::Duration; -use tauri::{Emitter, Manager}; +use tauri::Manager; +use tauri_specta::Event; use tokio::{runtime::Runtime, sync::mpsc}; use tokio_stream::StreamExt; use tracing::instrument; @@ -103,9 +107,9 @@ impl Drop for TauriIntegration { impl GuiIntegration for TauriIntegration { fn notify_session_changed(&self, session: &SessionViewModel) -> Result<()> { - self.app - .emit("session_changed", session) - .context("Failed to send `session_changed` event") + SessionChanged(session.clone()) + .emit(&self.app) + .context("Failed to emit `session_changed` event") } fn notify_settings_changed( @@ -114,26 +118,27 @@ impl GuiIntegration for TauriIntegration { general_settings: GeneralSettings, advanced_settings: AdvancedSettings, ) -> Result<()> { - self.app - .emit( - "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 `advanced_settings_changed` event")?; + GeneralSettingsChanged(GeneralSettingsViewModel::new( + mdm_settings.clone(), + general_settings, + )) + .emit(&self.app) + .context("Failed to emit `general_settings_changed` event")?; + + AdvancedSettingsChanged(AdvancedSettingsViewModel::new( + mdm_settings, + advanced_settings, + )) + .emit(&self.app) + .context("Failed to emit `advanced_settings_changed` event")?; Ok(()) } fn notify_logs_recounted(&self, file_count: &FileCount) -> Result<()> { - self.app - .emit("logs_recounted", file_count) - .context("Failed to send `logs_recounted` event")?; + LogsRecounted(file_count.clone()) + .emit(&self.app) + .context("Failed to emit `logs_recounted` event")?; Ok(()) } @@ -360,6 +365,46 @@ pub fn run( anyhow::Ok(ctrl_task) }); + let tauri_specta_builder = tauri_specta::Builder::::new() + .events(tauri_specta::collect_events![ + crate::view::SessionChanged, + crate::view::GeneralSettingsChanged, + crate::view::AdvancedSettingsChanged, + crate::view::LogsRecounted, + ]) + .commands(tauri_specta::collect_commands![ + crate::view::clear_logs, + crate::view::export_logs, + crate::view::apply_advanced_settings, + crate::view::reset_advanced_settings, + crate::view::apply_general_settings, + crate::view::reset_general_settings, + crate::view::sign_in, + crate::view::sign_out, + crate::view::update_state, + ]) + .typ::(); + + #[cfg(debug_assertions)] + { + let bindings_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../src-frontend/generated/bindings.ts") + .canonicalize() + .context("Failed to create absolute path to bindings file")?; + + tracing::debug!(path = %bindings_path.display(), "Exporting TypeScript bindings"); + + tauri_specta_builder + .export( + specta_typescript::Typescript::default() + .bigint(specta_typescript::BigIntExportBehavior::Number) + .header("/* eslint-disable */\n/* tslint:disable */\n") + .formatter(specta_typescript::formatter::prettier), + bindings_path, + ) + .context("Failed to export TypeScript bindings")?; + } + tauri::Builder::default() .manage(Managed { req_tx, @@ -377,7 +422,12 @@ pub fn run( api.prevent_close(); } }) - .invoke_handler(crate::view::generate_handler()) + .invoke_handler(tauri_specta_builder.invoke_handler()) + .setup(move |app| { + tauri_specta_builder.mount_events(app); + + Ok(()) + }) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_shell::init()) diff --git a/rust/gui-client/src-tauri/src/logging.rs b/rust/gui-client/src-tauri/src/logging.rs index 9cfa5a80a..40ea9f864 100644 --- a/rust/gui-client/src-tauri/src/logging.rs +++ b/rust/gui-client/src-tauri/src/logging.rs @@ -204,8 +204,7 @@ fn system_layer() -> Result { Ok(tracing_subscriber::layer::Identity::new()) } -#[tslink::tslink(target = "./gui-client/src-frontend/generated/FileCount.ts")] -#[derive(Clone, Default, Serialize)] +#[derive(Clone, Default, Serialize, specta::Type)] pub struct FileCount { bytes: u64, files: u64, diff --git a/rust/gui-client/src-tauri/src/settings.rs b/rust/gui-client/src-tauri/src/settings.rs index bf97cc18e..d34144f82 100644 --- a/rust/gui-client/src-tauri/src/settings.rs +++ b/rust/gui-client/src-tauri/src/settings.rs @@ -52,7 +52,7 @@ pub struct AdvancedSettingsLegacy { pub log_filter: String, } -#[derive(Clone, Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize, specta::Type)] pub struct AdvancedSettings { pub auth_url: Url, pub api_url: Url, @@ -79,8 +79,7 @@ fn start_minimized_default() -> bool { true } -#[tslink::tslink(target = "./gui-client/src-frontend/generated/GeneralSettingsViewModel.ts")] -#[derive(Clone, Serialize)] +#[derive(Clone, Serialize, specta::Type)] pub struct GeneralSettingsViewModel { pub start_minimized: bool, pub start_on_login: bool, @@ -109,8 +108,7 @@ impl GeneralSettingsViewModel { } } -#[tslink::tslink(target = "./gui-client/src-frontend/generated/AdvancedSettingsViewModel.ts")] -#[derive(Clone, Serialize)] +#[derive(Clone, Serialize, specta::Type)] pub struct AdvancedSettingsViewModel { pub auth_url: String, pub auth_url_is_managed: bool, diff --git a/rust/gui-client/src-tauri/src/view.rs b/rust/gui-client/src-tauri/src/view.rs index d8283f830..8ebdaff0a 100644 --- a/rust/gui-client/src-tauri/src/view.rs +++ b/rust/gui-client/src-tauri/src/view.rs @@ -3,12 +3,16 @@ use std::{path::PathBuf, time::Duration}; use anyhow::Context as _; use firezone_logging::err_with_src; use serde::Serialize; -use tauri::{Wry, ipc::Invoke}; use tauri_plugin_dialog::DialogExt as _; -use crate::{controller::ControllerRequest, gui::Managed, settings::AdvancedSettings}; +use crate::{ + controller::ControllerRequest, + gui::Managed, + logging::FileCount, + settings::{AdvancedSettings, AdvancedSettingsViewModel, GeneralSettingsViewModel}, +}; -#[derive(Clone, serde::Deserialize)] +#[derive(Clone, serde::Deserialize, specta::Type)] pub struct GeneralSettingsForm { pub start_minimized: bool, pub start_on_login: bool, @@ -16,8 +20,7 @@ pub struct GeneralSettingsForm { pub account_slug: String, } -#[tslink::tslink(target = "./gui-client/src-frontend/generated/SessionViewModel.ts")] -#[derive(Clone, serde::Serialize)] +#[derive(Clone, serde::Serialize, specta::Type)] pub enum SessionViewModel { SignedIn { account_slug: String, @@ -27,22 +30,21 @@ pub enum SessionViewModel { SignedOut, } -pub fn generate_handler() -> impl Fn(Invoke) -> bool + Send + Sync + 'static { - tauri::generate_handler![ - clear_logs, - export_logs, - apply_advanced_settings, - reset_advanced_settings, - apply_general_settings, - reset_general_settings, - sign_in, - sign_out, - update_state, - ] -} +#[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] -async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<()> { +#[specta::specta] +pub async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<()> { let (tx, rx) = tokio::sync::oneshot::channel(); managed @@ -57,14 +59,16 @@ async fn clear_logs(managed: tauri::State<'_, Managed>) -> Result<()> { } #[tauri::command] -async fn export_logs(app: tauri::AppHandle, managed: tauri::State<'_, Managed>) -> Result<()> { +#[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] -async fn apply_general_settings( +#[specta::specta] +pub async fn apply_general_settings( managed: tauri::State<'_, Managed>, settings: GeneralSettingsForm, ) -> Result<()> { @@ -80,7 +84,8 @@ async fn apply_general_settings( } #[tauri::command] -async fn apply_advanced_settings( +#[specta::specta] +pub async fn apply_advanced_settings( managed: tauri::State<'_, Managed>, settings: AdvancedSettings, ) -> Result<()> { @@ -96,14 +101,16 @@ async fn apply_advanced_settings( } #[tauri::command] -async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<()> { +#[specta::specta] +pub async fn reset_advanced_settings(managed: tauri::State<'_, Managed>) -> Result<()> { apply_advanced_settings(managed, AdvancedSettings::default()).await?; Ok(()) } #[tauri::command] -async fn reset_general_settings(managed: tauri::State<'_, Managed>) -> Result<()> { +#[specta::specta] +pub async fn reset_general_settings(managed: tauri::State<'_, Managed>) -> Result<()> { managed .send_request(ControllerRequest::ResetGeneralSettings) .await?; @@ -148,21 +155,24 @@ fn show_export_dialog(app: &tauri::AppHandle, managed: Managed) -> Result<()> { } #[tauri::command] -async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<()> { +#[specta::specta] +pub async fn sign_in(managed: tauri::State<'_, Managed>) -> Result<()> { managed.send_request(ControllerRequest::SignIn).await?; Ok(()) } #[tauri::command] -async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<()> { +#[specta::specta] +pub async fn sign_out(managed: tauri::State<'_, Managed>) -> Result<()> { managed.send_request(ControllerRequest::SignOut).await?; Ok(()) } #[tauri::command] -async fn update_state(managed: tauri::State<'_, Managed>) -> Result<()> { +#[specta::specta] +pub async fn update_state(managed: tauri::State<'_, Managed>) -> Result<()> { managed.send_request(ControllerRequest::UpdateState).await?; Ok(()) @@ -170,20 +180,11 @@ async fn update_state(managed: tauri::State<'_, Managed>) -> Result<()> { type Result = std::result::Result; -#[derive(Debug)] -struct Error(anyhow::Error); - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - serializer.serialize_str(&format!("{:#}", self.0)) - } -} +#[derive(Debug, specta::Type, Serialize)] +pub struct Error(String); impl From for Error { - fn from(value: anyhow::Error) -> Self { - Self(value) + fn from(error: anyhow::Error) -> Self { + Self(format!("{error:#}")) } }