diff --git a/rust/gui-client/src-frontend/components/App.tsx b/rust/gui-client/src-frontend/components/App.tsx index bd2df2693..db2ff51dc 100644 --- a/rust/gui-client/src-frontend/components/App.tsx +++ b/rust/gui-client/src-frontend/components/App.tsx @@ -20,7 +20,7 @@ import React, { useEffect, useState } from "react"; import { NavLink, Route, Routes } from "react-router"; import { AdvancedSettingsViewModel } from "../generated/AdvancedSettingsViewModel"; import { FileCount } from "../generated/FileCount"; -import { Session } from "../generated/Session"; +import { SessionViewModel } from "../generated/SessionViewModel"; import About from "./AboutPage"; import AdvancedSettingsPage from "./AdvancedSettingsPage"; import ColorPalette from "./ColorPalettePage"; @@ -30,7 +30,7 @@ import Overview from "./OverviewPage"; import { GeneralSettingsViewModel } from "../generated/GeneralSettingsViewModel"; export default function App() { - let [session, setSession] = useState(null); + let [session, setSession] = useState(null); let [logCount, setLogCount] = useState(null); let [generalSettings, setGeneralSettings] = useState(null); @@ -38,16 +38,12 @@ export default function App() { useState(null); useEffect(() => { - const signedInUnlisten = listen("signed_in", (e) => { + const sessionChanged = listen("session_changed", (e) => { let session = e.payload; - console.log("signed_in", { session }); + console.log("session_changed", { session }); setSession(session); }); - const signedOutUnlisten = listen("signed_out", (_e) => { - console.log("signed_out"); - setSession(null); - }); const generalSettingsChangedUnlisten = listen( "general_settings_changed", (e) => { @@ -78,8 +74,7 @@ export default function App() { invoke("update_state"); // Let the backend know that we (re)-initialised return () => { - signedInUnlisten.then((unlistenFn) => unlistenFn()); - signedOutUnlisten.then((unlistenFn) => unlistenFn()); + sessionChanged.then((unlistenFn) => unlistenFn()); generalSettingsChangedUnlisten.then((unlistenFn) => unlistenFn()); advancedSettingsChangedUnlisten.then((unlistenFn) => unlistenFn()); logsRecountedUnlisten.then((unlistenFn) => unlistenFn()); diff --git a/rust/gui-client/src-frontend/components/OverviewPage.tsx b/rust/gui-client/src-frontend/components/OverviewPage.tsx index 3e93e44eb..08289d557 100644 --- a/rust/gui-client/src-frontend/components/OverviewPage.tsx +++ b/rust/gui-client/src-frontend/components/OverviewPage.tsx @@ -1,68 +1,120 @@ import React from "react"; import logo from "../logo.png"; -import { Session } from "./App"; -import { Button } from "flowbite-react"; +import { SessionViewModel } from "../generated/SessionViewModel"; +import { Button, Spinner } from "flowbite-react"; interface OverviewPageProps { - session: Session | null; + session: SessionViewModel | null; signOut: () => void; signIn: () => void; } -export default function Overview({ - session, - signOut, - signIn, -}: OverviewPageProps) { +export default function Overview(props: OverviewPageProps) { return (
Firezone Logo

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. -

-
-
- )} + +
+ ); +} + +function Session(props: OverviewPageProps) { + if (!props.session) { + return ; + } + + switch (props.session) { + case "SignedOut": { + return ; + } + case "Loading": { + return ; + } + default: + let { account_slug, actor_name } = props.session.SignedIn; + + return ( + + ); + } +} + +interface SignedOutProps { + signIn: () => void; +} + +function SignedOut({ signIn }: SignedOutProps) { + return ( +
+
+

+ 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. +

+
+
+ ); +} + +interface SignedInProps { + accountSlug: string; + actorName: string; + signOut: () => void; +} + +function SignedIn({ actorName, accountSlug, signOut }: SignedInProps) { + return ( +
+
+

+ You are currently signed into  + + {accountSlug} + +  as  + + {actorName} + + .
+ 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. +

+
+
+ ); +} + +function Loading() { + return ( +
+
+ +

+ Firezone will continue running in the taskbar after this window is + closed. +

+
); } diff --git a/rust/gui-client/src-frontend/generated/Session.ts b/rust/gui-client/src-frontend/generated/Session.ts deleted file mode 100644 index aea319b37..000000000 --- a/rust/gui-client/src-frontend/generated/Session.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Session { - account_slug: string; - actor_name: string; -} diff --git a/rust/gui-client/src-frontend/generated/SessionViewModel.ts b/rust/gui-client/src-frontend/generated/SessionViewModel.ts new file mode 100644 index 000000000..06d7da608 --- /dev/null +++ b/rust/gui-client/src-frontend/generated/SessionViewModel.ts @@ -0,0 +1,9 @@ +export type SessionViewModel = + { + SignedIn: { + account_slug: string; + actor_name: string + } + } | + "Loading" | + "SignedOut"; diff --git a/rust/gui-client/src-frontend/main.tsx b/rust/gui-client/src-frontend/main.tsx index 6db09a61c..e00722bd3 100644 --- a/rust/gui-client/src-frontend/main.tsx +++ b/rust/gui-client/src-frontend/main.tsx @@ -34,6 +34,11 @@ const customTheme = createTheme({ }, }, }, + spinner: { + color: { + default: "fill-accent-500", + }, + }, }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement, { diff --git a/rust/gui-client/src-tauri/Cargo.toml b/rust/gui-client/src-tauri/Cargo.toml index 532acd466..42a458790 100644 --- a/rust/gui-client/src-tauri/Cargo.toml +++ b/rust/gui-client/src-tauri/Cargo.toml @@ -98,3 +98,6 @@ tempfile = { workspace = true } [lints] workspace = true + +[tslink] +enum_representation = "discriminated" diff --git a/rust/gui-client/src-tauri/src/auth.rs b/rust/gui-client/src-tauri/src/auth.rs index fad3c070c..e4c1ea26c 100644 --- a/rust/gui-client/src-tauri/src/auth.rs +++ b/rust/gui-client/src-tauri/src/auth.rs @@ -82,7 +82,6 @@ pub(crate) struct Response { pub(crate) state: SecretString, } -#[tslink::tslink(target = "./gui-client/src-frontend/generated/Session.ts")] #[derive(Default, Clone, Deserialize, Serialize)] pub struct Session { pub(crate) account_slug: String, diff --git a/rust/gui-client/src-tauri/src/controller.rs b/rust/gui-client/src-tauri/src/controller.rs index 994cbb6cd..7bd8602a3 100644 --- a/rust/gui-client/src-tauri/src/controller.rs +++ b/rust/gui-client/src-tauri/src/controller.rs @@ -6,7 +6,7 @@ use crate::{ service, settings::{self, AdvancedSettings, GeneralSettings, MdmSettings}, updates, uptime, - view::GeneralSettingsForm, + view::{GeneralSettingsForm, SessionViewModel}, }; use anyhow::{Context, Result, anyhow, bail}; use connlib_model::ResourceView; @@ -65,8 +65,7 @@ pub struct Controller { } pub trait GuiIntegration { - fn notify_signed_in(&self, session: &auth::Session) -> Result<()>; - fn notify_signed_out(&self) -> Result<()>; + fn notify_session_changed(&self, session: &SessionViewModel) -> Result<()>; fn notify_settings_changed( &self, mdm_settings: MdmSettings, @@ -84,7 +83,7 @@ pub trait GuiIntegration { fn show_update_notification(&self, ctlr_tx: CtlrTx, title: &str, url: url::Url) -> Result<()>; fn set_window_visible(&self, visible: bool) -> Result<()>; - fn show_overview_page(&self, current_session: Option<&auth::Session>) -> Result<()>; + fn show_overview_page(&self, session: &SessionViewModel) -> Result<()>; fn show_settings_page( &self, mdm_settings: MdmSettings, @@ -288,10 +287,12 @@ impl Controller { tracing::info!("No token / actor_name on disk, starting in signed-out state"); } - self.refresh_system_tray_menu(); + self.refresh_ui_state(); if !ran_before::get().await? || !self.general_settings.start_minimized { - self.integration.show_overview_page(self.auth.session())?; + let (_, session_view_model) = self.build_ui_state(); + + self.integration.show_overview_page(&session_view_model)?; } loop { @@ -432,13 +433,12 @@ impl Controller { }; let session = self.auth.session().context("Missing session")?; - self.integration.notify_signed_in(session)?; self.general_settings.account_slug = Some(session.account_slug.clone()); settings::save_general(&self.general_settings).await?; self.notify_settings_changed()?; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); Ok(()) } @@ -505,7 +505,7 @@ impl Controller { tracing::debug!("Applied new settings. Log level will take effect immediately."); // Refresh the menu in case the favorites were reset. - self.refresh_system_tray_menu(); + self.refresh_ui_state(); self.integration.show_notification("Settings saved", "")? } @@ -563,11 +563,10 @@ impl Controller { .context("Couldn't start sign-in flow")?; let url = req.to_url(&auth_url, account_slug.as_deref()); - self.refresh_system_tray_menu(); + self.refresh_ui_state(); self.integration .open_url(url.expose_secret()) .context("Couldn't open auth page")?; - self.integration.set_window_visible(false)?; } SystemTrayMenu(system_tray::Event::AddFavorite(resource_id)) => { self.general_settings.favorite_resources.insert(resource_id); @@ -648,7 +647,7 @@ impl Controller { tracing::info!("User clicked Quit in the menu"); self.status = Status::Quitting; self.send_ipc(&service::ClientMsg::Disconnect).await?; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); } UpdateNotificationClicked(download_url) => { tracing::info!("UpdateNotificationClicked in run_controller!"); @@ -658,13 +657,11 @@ impl Controller { } UpdateState => { self.notify_settings_changed()?; - 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)?; + + self.refresh_ui_state(); } } Ok(()) @@ -732,7 +729,7 @@ impl Controller { } tracing::debug!(len = resources.len(), "Got new Resources"); self.status = Status::TunnelReady { resources }; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); self.update_disabled_resources().await?; } @@ -759,7 +756,7 @@ impl Controller { "Firezone connected", "You are now signed in and able to access resources.", )?; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); } service::ServerMsg::Hello => {} } @@ -790,7 +787,9 @@ impl Controller { } }, gui::ClientMsg::NewInstance => { - self.integration.show_overview_page(self.auth.session())?; + let (_, session_view_model) = self.build_ui_state(); + + self.integration.show_overview_page(&session_view_model)?; } } @@ -817,7 +816,7 @@ impl Controller { self.status = Status::WaitingForTunnel { start_instant: *start_instant, }; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); Ok(()) } Err(service::ConnectError::Io(error)) => { @@ -830,7 +829,7 @@ impl Controller { self.status = Status::RetryingConnection { token: token.expose_secret().clone().into(), }; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); Ok(()) } Err(service::ConnectError::Other(error)) => { @@ -851,13 +850,13 @@ impl Controller { ) -> Result<()> { let Some(notification) = notification else { self.release = None; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); return Ok(()); }; let release = notification.release; self.release = Some(release.clone()); - self.refresh_system_tray_menu(); + self.refresh_ui_state(); if notification.tell_user { let title = format!("Firezone {} available for download", release.version); @@ -891,7 +890,7 @@ impl Controller { disabled_resources, )) .await?; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); Ok(()) } @@ -899,41 +898,70 @@ impl Controller { /// Saves the current settings (including favorites) to disk and refreshes the tray menu async fn refresh_favorite_resources(&mut self) -> Result<()> { settings::save_general(&self.general_settings).await?; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); Ok(()) } - /// Builds a new system tray menu and applies it to the app - fn refresh_system_tray_menu(&mut self) { + fn build_ui_state(&self) -> (system_tray::ConnlibState, SessionViewModel) { // TODO: Refactor `Controller` and the auth module so that "Are we logged in?" // doesn't require such complicated control flow to answer. - let connlib = if let Some(auth_session) = self.auth.session() { + if let Some(auth_session) = self.auth.session() { match &self.status { Status::Disconnected => { // If we have an `auth_session` but no connlib session, we are most likely configured to // _not_ auto-connect on startup. Thus, we treat this the same as being signed out. - system_tray::ConnlibState::SignedOut + ( + system_tray::ConnlibState::SignedOut, + SessionViewModel::SignedOut, + ) } - Status::Quitting => system_tray::ConnlibState::Quitting, - Status::RetryingConnection { .. } => system_tray::ConnlibState::RetryingConnection, - Status::TunnelReady { resources } => { + Status::Quitting => ( + system_tray::ConnlibState::Quitting, + SessionViewModel::Loading, + ), + Status::RetryingConnection { .. } => ( + system_tray::ConnlibState::RetryingConnection, + SessionViewModel::Loading, + ), + Status::TunnelReady { resources } => ( system_tray::ConnlibState::SignedIn(system_tray::SignedIn { actor_name: auth_session.actor_name.clone(), favorite_resources: self.general_settings.favorite_resources.clone(), internet_resource_enabled: self.general_settings.internet_resource_enabled, resources: resources.clone(), - }) - } - Status::WaitingForPortal { .. } => system_tray::ConnlibState::WaitingForPortal, - Status::WaitingForTunnel { .. } => system_tray::ConnlibState::WaitingForTunnel, + }), + SessionViewModel::SignedIn { + account_slug: auth_session.account_slug.clone(), + actor_name: auth_session.actor_name.clone(), + }, + ), + Status::WaitingForPortal { .. } => ( + system_tray::ConnlibState::WaitingForPortal, + SessionViewModel::Loading, + ), + Status::WaitingForTunnel { .. } => ( + system_tray::ConnlibState::WaitingForTunnel, + SessionViewModel::Loading, + ), } } else if self.auth.ongoing_request().is_some() { // Signing in, waiting on deep link callback - system_tray::ConnlibState::WaitingForBrowser + ( + system_tray::ConnlibState::WaitingForBrowser, + SessionViewModel::Loading, + ) } else { - system_tray::ConnlibState::SignedOut - }; + ( + system_tray::ConnlibState::SignedOut, + SessionViewModel::SignedOut, + ) + } + } + + /// Refreshes our UI state (i.e. tray-menu and GUI). + fn refresh_ui_state(&mut self) { + let (connlib, session_view_model) = self.build_ui_state(); self.integration.set_tray_menu(system_tray::AppState { connlib, @@ -944,6 +972,9 @@ impl Controller { .is_some_and(|hide| hide), support_url: self.mdm_settings.support_url.clone(), }); + if let Err(e) = self.integration.notify_session_changed(&session_view_model) { + tracing::warn!("Failed to send notify session change: {e:#}") + } } /// If we're in the `RetryingConnection` state, use the token to retry the Portal connection @@ -973,13 +1004,12 @@ impl Controller { | Status::WaitingForTunnel { .. } => {} } self.auth.sign_out()?; - self.integration.notify_signed_out()?; self.status = Status::Disconnected; tracing::debug!("disconnecting connlib"); // This is redundant if the token is expired, in that case // connlib already disconnected itself. self.send_ipc(&service::ClientMsg::Disconnect).await?; - self.refresh_system_tray_menu(); + self.refresh_ui_state(); Ok(()) } diff --git a/rust/gui-client/src-tauri/src/gui.rs b/rust/gui-client/src-tauri/src/gui.rs index 382357e5f..7d05cadf3 100644 --- a/rust/gui-client/src-tauri/src/gui.rs +++ b/rust/gui-client/src-tauri/src/gui.rs @@ -4,7 +4,6 @@ //! The real macOS Client is in `swift/apple` use crate::{ - auth, controller::{Controller, ControllerRequest, CtlrTx, Failure, GuiIntegration}, deep_link, ipc::{self, ClientRead, ClientWrite, SocketId}, @@ -14,6 +13,7 @@ use crate::{ GeneralSettingsViewModel, MdmSettings, }, updates, + view::SessionViewModel, }; use anyhow::{Context, Result, bail}; use firezone_logging::err_with_src; @@ -82,20 +82,10 @@ impl Drop for TauriIntegration { } impl GuiIntegration for TauriIntegration { - fn notify_signed_in(&self, session: &auth::Session) -> Result<()> { + fn notify_session_changed(&self, session: &SessionViewModel) -> Result<()> { self.app - .emit("signed_in", session) - .context("Failed to send `signed_in` event")?; - - Ok(()) - } - - fn notify_signed_out(&self) -> Result<()> { - self.app - .emit("signed_out", ()) - .context("Failed to send `signed_out` event")?; - - Ok(()) + .emit("session_changed", session) + .context("Failed to send `session_changed` event") } fn notify_settings_changed( @@ -166,12 +156,9 @@ impl GuiIntegration for TauriIntegration { Ok(()) } - fn show_overview_page(&self, current_session: Option<&auth::Session>) -> Result<()> { + fn show_overview_page(&self, session: &SessionViewModel) -> 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.notify_session_changed(session)?; self.navigate("overview")?; self.set_window_visible(true)?; diff --git a/rust/gui-client/src-tauri/src/view.rs b/rust/gui-client/src-tauri/src/view.rs index 1635208e8..d69e68c75 100644 --- a/rust/gui-client/src-tauri/src/view.rs +++ b/rust/gui-client/src-tauri/src/view.rs @@ -20,6 +20,17 @@ pub struct GeneralSettingsForm { pub account_slug: String, } +#[tslink::tslink(target = "./gui-client/src-frontend/generated/SessionViewModel.ts")] +#[derive(Clone, serde::Serialize)] +pub enum SessionViewModel { + SignedIn { + account_slug: String, + actor_name: String, + }, + Loading, + SignedOut, +} + pub fn generate_handler() -> impl Fn(Invoke) -> bool + Send + Sync + 'static { tauri::generate_handler![ clear_logs, diff --git a/website/src/components/Changelog/GUI.tsx b/website/src/components/Changelog/GUI.tsx index 91d469d14..b1dee97de 100644 --- a/website/src/components/Changelog/GUI.tsx +++ b/website/src/components/Changelog/GUI.tsx @@ -19,6 +19,10 @@ export default function GUI({ os }: { os: OS }) { Fixes an issue where disabling the update checker via MDM would cause the Client to hang upon sign-in. + + Fixes an issue where disabling "connect on start" would incorrectly + show the Client as "Signed in" on the next launch. +