mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
refactor(gui-client): replace tslink with tauri-specta (#10031)
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.
This commit is contained in:
@@ -10,3 +10,6 @@ shfmt 3.9.0
|
||||
|
||||
# Used to lint Bash scripts
|
||||
shellcheck 0.9.0
|
||||
|
||||
# GUI client
|
||||
pnpm 10.13.1
|
||||
|
||||
106
rust/Cargo.lock
generated
106
rust/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -229,7 +229,6 @@ deny = [
|
||||
skip = [
|
||||
"base64",
|
||||
"bitflags",
|
||||
"convert_case",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"core-graphics-types",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SessionViewModel | null>(null);
|
||||
@@ -38,14 +40,13 @@ export default function App() {
|
||||
useState<AdvancedSettingsViewModel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const sessionChanged = listen<SessionViewModel>("session_changed", (e) => {
|
||||
const sessionChangedUnlisten = events.sessionChanged.listen((e) => {
|
||||
const session = e.payload;
|
||||
|
||||
console.log("session_changed", { session });
|
||||
setSession(session);
|
||||
});
|
||||
const generalSettingsChangedUnlisten = listen<GeneralSettingsViewModel>(
|
||||
"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<AdvancedSettingsViewModel>(
|
||||
"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<FileCount>("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={
|
||||
<Overview
|
||||
session={session}
|
||||
signIn={() => invoke("sign_in")}
|
||||
signOut={() => invoke("sign_out")}
|
||||
signIn={commands.signIn}
|
||||
signOut={commands.signOut}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -141,10 +140,8 @@ export default function App() {
|
||||
element={
|
||||
<GeneralSettingsPage
|
||||
settings={generalSettings}
|
||||
saveSettings={(settings) =>
|
||||
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={
|
||||
<AdvancedSettingsPage
|
||||
settings={advancedSettings}
|
||||
saveSettings={(settings) =>
|
||||
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={
|
||||
<Diagnostics
|
||||
logCount={logCount}
|
||||
exportLogs={() => invoke("export_logs")}
|
||||
clearLogs={async () => {
|
||||
await invoke("clear_logs");
|
||||
const logCount = await invoke<FileCount>("count_logs");
|
||||
|
||||
setLogCount(logCount);
|
||||
}}
|
||||
exportLogs={commands.exportLogs}
|
||||
clearLogs={commands.clearLogs}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div className="container mx-auto p-4">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface FileCount {
|
||||
bytes: number;
|
||||
files: number;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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`.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export type SessionViewModel =
|
||||
{
|
||||
SignedIn: {
|
||||
account_slug: string;
|
||||
actor_name: string
|
||||
}
|
||||
} |
|
||||
"Loading" |
|
||||
"SignedOut";
|
||||
175
rust/gui-client/src-frontend/generated/bindings.ts
Normal file
175
rust/gui-client/src-frontend/generated/bindings.ts
Normal file
@@ -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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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<Result<null, Error>> {
|
||||
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__<T> = {
|
||||
listen: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
|
||||
once: (
|
||||
cb: TAURI_API_EVENT.EventCallback<T>,
|
||||
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
|
||||
emit: null extends T
|
||||
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
|
||||
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
|
||||
};
|
||||
|
||||
export type Result<T, E> =
|
||||
| { status: "ok"; data: T }
|
||||
| { status: "error"; error: E };
|
||||
|
||||
function __makeEvents__<T extends Record<string, any>>(
|
||||
mappings: Record<keyof T, string>,
|
||||
) {
|
||||
return new Proxy(
|
||||
{} as unknown as {
|
||||
[K in keyof T]: __EventObj__<T[K]> & {
|
||||
(handle: __WebviewWindow__): __EventObj__<T[K]>;
|
||||
};
|
||||
},
|
||||
{
|
||||
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__<any>) => {
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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::<tauri::Wry>::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::<crate::view::Error>();
|
||||
|
||||
#[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())
|
||||
|
||||
@@ -204,8 +204,7 @@ fn system_layer() -> Result<tracing_subscriber::layer::Identity> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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,
|
||||
]
|
||||
}
|
||||
#[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<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Error(anyhow::Error);
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&format!("{:#}", self.0))
|
||||
}
|
||||
}
|
||||
#[derive(Debug, specta::Type, Serialize)]
|
||||
pub struct Error(String);
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self(value)
|
||||
fn from(error: anyhow::Error) -> Self {
|
||||
Self(format!("{error:#}"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user