From bd58a924415f71d061d7f52deb4e222b8853f659 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Thu, 25 Jul 2024 17:38:45 -0400 Subject: [PATCH] CLI: Toasts & Tweaks (#2702) * progress: cli toasts * forgot the html * progress: toasts * revision: don't open splash on desktop * fix: fmt, spellcheck --- packages/cli/assets/loading.html | 2 - packages/cli/assets/toast.html | 206 +++++++++++++++++++++++ packages/cli/src/builder/prepare_html.rs | 10 +- packages/cli/src/builder/progress.rs | 1 + packages/cli/src/serve/mod.rs | 17 +- packages/cli/src/serve/server.rs | 47 ++++-- packages/desktop/src/app.rs | 10 +- packages/hot-reload/src/lib.rs | 8 +- packages/liveview/src/pool.rs | 11 +- packages/web/src/hot_reload.rs | 82 ++++++++- 10 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 packages/cli/assets/toast.html diff --git a/packages/cli/assets/loading.html b/packages/cli/assets/loading.html index 4d09ac811..1d580c9c2 100644 --- a/packages/cli/assets/loading.html +++ b/packages/cli/assets/loading.html @@ -325,7 +325,6 @@ ws.onmessage = (event) => { // Parse the message as json let data = JSON.parse(event.data); - console.log(data); // If the message is "Ready", reload the page if (data.type === "Ready") { @@ -364,7 +363,6 @@ let errorBlock = document.getElementById("error-block"); errorBlock.innerHTML = formatting2; - console.log(data.data.error); } else if (data.type === "Building") { // Show correct view for message. let errorContainer = document.getElementById("error"); diff --git a/packages/cli/assets/toast.html b/packages/cli/assets/toast.html new file mode 100644 index 000000000..7e700108c --- /dev/null +++ b/packages/cli/assets/toast.html @@ -0,0 +1,206 @@ + + +
+
+ +
+
+
+ + +
+ +
+ + + + + + + +

Your app is being rebuilt.

+
+ + +

A non-hot-reloadable change occurred and we must rebuild.

+
+
+
+ + \ No newline at end of file diff --git a/packages/cli/src/builder/prepare_html.rs b/packages/cli/src/builder/prepare_html.rs index f82922bff..a21dbab60 100644 --- a/packages/cli/src/builder/prepare_html.rs +++ b/packages/cli/src/builder/prepare_html.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use tracing::Level; const DEFAULT_HTML: &str = include_str!("../../assets/index.html"); +const TOAST_HTML: &str = include_str!("../../assets/toast.html"); impl BuildRequest { pub(crate) fn prepare_html( @@ -110,9 +111,16 @@ impl BuildRequest { }); } ); - + {DX_TOAST_UTILITIES} + html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML), + false => html.replace("{DX_TOAST_UTILITIES}", ""), + }; + // And try to insert preload links for the wasm and js files *html = html.replace( " Result<()> { // We also can check the status of the builds here in case we have multiple ongoing builds match application { Ok(BuilderUpdate::Progress { platform, update }) => { - let update_stage = update.stage; - screen.new_build_logs(platform, update); - server.update_build_status(screen.build_progress.progress(), update_stage.to_string()).await; + let update_clone = update.clone(); + screen.new_build_logs(platform, update_clone); + server.update_build_status(screen.build_progress.progress(), update.stage.to_string()).await; + + match update { + // Send rebuild start message. + UpdateBuildProgress { stage: Stage::Compiling, update: UpdateStage::Start } => server.send_reload_start().await, + // Send rebuild failed message. + UpdateBuildProgress { stage: Stage::Finished, update: UpdateStage::Failed(_) } => server.send_reload_failed().await, + _ => {}, + } } Ok(BuilderUpdate::Ready { results }) => { if !results.is_empty() { @@ -130,7 +139,7 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> Result<()> { screen.new_ready_app(&mut builder, results); // And then finally tell the server to reload - server.send_reload().await; + server.send_reload_command().await; }, Err(err) => { server.send_build_error(err).await; diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index 5f243ab28..c8fd0339e 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -131,6 +131,7 @@ impl Server { // Actually just start the server, cloning in a few bits of config let web_config = cfg.dioxus_config.web.https.clone(); let base_path = cfg.dioxus_config.web.app.base_path.clone(); + let platform = serve.platform(); let _server_task = tokio::spawn(async move { let web_config = web_config.clone(); // HTTPS @@ -139,7 +140,7 @@ impl Server { let rustls: Option = get_rustls(&web_config).await.unwrap(); // Open the browser - if start_browser { + if start_browser && platform != Platform::Desktop { open_browser(base_path, addr, rustls.is_some()); } @@ -175,6 +176,7 @@ impl Server { } } + /// Sends the current build status to all clients. async fn send_build_status(&mut self) { let mut i = 0; while i < self.build_status_sockets.len() { @@ -190,6 +192,7 @@ impl Server { } } + /// Sends a start build message to all clients. pub async fn start_build(&mut self) { self.build_status.set(Status::Building { progress: 0.0, @@ -198,6 +201,7 @@ impl Server { self.send_build_status().await; } + /// Sends an updated build status to all clients. pub async fn update_build_status(&mut self, progress: f64, build_message: String) { if !matches!(self.build_status.get(), Status::Building { .. }) { return; @@ -209,6 +213,7 @@ impl Server { self.send_build_status().await; } + /// Sends hot reloadable changes to all clients. pub async fn send_hotreload(&mut self, reload: HotReloadMsg) { let msg = DevserverMsg::HotReload(reload); let msg = serde_json::to_string(&msg).unwrap(); @@ -275,6 +280,7 @@ impl Server { None } + /// Converts a `cargo` error to HTML and sends it to clients. pub async fn send_build_error(&mut self, error: Error) { let error = error.to_string(); self.build_status.set(Status::BuildError { @@ -283,25 +289,36 @@ impl Server { self.send_build_status().await; } - pub async fn send_reload(&mut self) { - self.build_status.set(Status::Ready); - self.send_build_status().await; - for socket in self.hot_reload_sockets.iter_mut() { - _ = socket - .send(Message::Text( - serde_json::to_string(&DevserverMsg::FullReload).unwrap(), - )) - .await; - } + /// Tells all clients that a full rebuild has started. + pub async fn send_reload_start(&mut self) { + self.send_devserver_message(DevserverMsg::FullReloadStart) + .await; } - /// Send a shutdown message to all connected clients + /// Tells all clients that a full rebuild has failed. + pub async fn send_reload_failed(&mut self) { + self.send_devserver_message(DevserverMsg::FullReloadFailed) + .await; + } + + /// Tells all clients to reload if possible for new changes. + pub async fn send_reload_command(&mut self) { + self.build_status.set(Status::Ready); + self.send_build_status().await; + self.send_devserver_message(DevserverMsg::FullReloadCommand) + .await; + } + + /// Send a shutdown message to all connected clients. pub async fn send_shutdown(&mut self) { + self.send_devserver_message(DevserverMsg::Shutdown).await; + } + + /// Sends a devserver message to all connected clients. + async fn send_devserver_message(&mut self, msg: DevserverMsg) { for socket in self.hot_reload_sockets.iter_mut() { _ = socket - .send(Message::Text( - serde_json::to_string(&DevserverMsg::Shutdown).unwrap(), - )) + .send(Message::Text(serde_json::to_string(&msg).unwrap())) .await; } } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 38fffac95..875b35e78 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -330,8 +330,10 @@ impl App { not(target_os = "ios") ))] pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::DevserverMsg) { + use dioxus_hot_reload::DevserverMsg; + match msg { - dioxus_hot_reload::DevserverMsg::HotReload(hr_msg) => { + DevserverMsg::HotReload(hr_msg) => { for webview in self.webviews.values_mut() { dioxus_hot_reload::apply_changes(&mut webview.dom, &hr_msg); webview.poll_vdom(); @@ -343,11 +345,13 @@ impl App { } } } - dioxus_hot_reload::DevserverMsg::FullReload => { + DevserverMsg::FullReloadCommand + | DevserverMsg::FullReloadStart + | DevserverMsg::FullReloadFailed => { // usually only web gets this message - what are we supposed to do? // Maybe we could just binary patch ourselves in place without losing window state? } - dioxus_hot_reload::DevserverMsg::Shutdown => { + DevserverMsg::Shutdown => { self.control_flow = ControlFlow::Exit; } } diff --git a/packages/hot-reload/src/lib.rs b/packages/hot-reload/src/lib.rs index e552d00d0..55c5c1e76 100644 --- a/packages/hot-reload/src/lib.rs +++ b/packages/hot-reload/src/lib.rs @@ -22,8 +22,14 @@ pub enum DevserverMsg { /// This includes all the templates/literals/assets/binary patches that have changed in one shot HotReload(HotReloadMsg), + /// The devserver is starting a full rebuild. + FullReloadStart, + + /// The full reload failed. + FullReloadFailed, + /// The app should reload completely if it can - FullReload, + FullReloadCommand, /// The program is shutting down completely - maybe toss up a splash screen or something? Shutdown, diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 2aa48ba8a..a180f6770 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -6,6 +6,7 @@ use crate::{ LiveViewError, }; use dioxus_core::prelude::*; +use dioxus_hot_reload::DevserverMsg; use dioxus_html::{EventData, HtmlEvent, PlatformEventData}; use dioxus_interpreter_js::MutationState; use futures_util::{pin_mut, SinkExt, StreamExt}; @@ -213,16 +214,18 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li Some(msg) = hot_reload_wait => { #[cfg(all(feature = "hot-reload", debug_assertions))] match msg{ - dioxus_hot_reload::DevserverMsg::HotReload(msg)=> { + DevserverMsg::HotReload(msg)=> { dioxus_hot_reload::apply_changes(&mut vdom, &msg); } - dioxus_hot_reload::DevserverMsg::Shutdown => { + DevserverMsg::Shutdown => { std::process::exit(0); }, - dioxus_hot_reload::DevserverMsg::FullReload => { + DevserverMsg::FullReloadCommand + | DevserverMsg::FullReloadStart + | DevserverMsg::FullReloadFailed => { // usually only web gets this message - what are we supposed to do? // Maybe we could just binary patch ourselves in place without losing window state? - } + }, } #[cfg(not(all(feature = "hot-reload", debug_assertions)))] let () = msg; diff --git a/packages/web/src/hot_reload.rs b/packages/web/src/hot_reload.rs index 0598e9c5c..1af362e29 100644 --- a/packages/web/src/hot_reload.rs +++ b/packages/web/src/hot_reload.rs @@ -3,7 +3,12 @@ //! This sets up a websocket connection to the devserver and handles messages from it. //! We also set up a little recursive timer that will attempt to reconnect if the connection is lost. +use std::fmt::Display; +use std::time::Duration; + +use dioxus_core::ScopeId; use dioxus_hot_reload::{DevserverMsg, HotReloadMsg}; +use dioxus_html::prelude::eval; use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use js_sys::JsString; use wasm_bindgen::JsCast; @@ -14,6 +19,9 @@ const POLL_INTERVAL_MIN: i32 = 250; const POLL_INTERVAL_MAX: i32 = 4000; const POLL_INTERVAL_SCALE_FACTOR: i32 = 2; +/// Amount of time that toats should be displayed. +const TOAST_TIMEOUT: Duration = Duration::from_secs(5); + pub(crate) fn init() -> UnboundedReceiver { // Create the tx/rx pair that we'll use for the top-level future in the dioxus loop let (tx, rx) = unbounded(); @@ -62,8 +70,34 @@ fn make_ws(tx: UnboundedSender, poll_interval: i32, reload: bool) web_sys::console::error_1(&"Connection to the devserver was closed".into()) } + // The devserver is telling us that it started a full rebuild. This does not mean that it is ready. + Ok(DevserverMsg::FullReloadStart) => show_toast( + "Your app is being rebuilt.", + "A non-hot-reloadable change occurred and we must rebuild.", + ToastLevel::Info, + TOAST_TIMEOUT, + false, + ), + // The devserver is telling us that the full rebuild failed. + Ok(DevserverMsg::FullReloadFailed) => show_toast( + "Oops! The build failed.", + "We tried to rebuild your app, but something went wrong.", + ToastLevel::Error, + TOAST_TIMEOUT, + false, + ), + // The devserver is telling us to reload the whole page - Ok(DevserverMsg::FullReload) => window().unwrap().location().reload().unwrap(), + Ok(DevserverMsg::FullReloadCommand) => { + show_toast( + "Successfully rebuilt.", + "Your app was rebuilt successfully and without error.", + ToastLevel::Success, + TOAST_TIMEOUT, + true, + ); + window().unwrap().location().reload().unwrap() + } Err(e) => web_sys::console::error_1( &format!("Error parsing devserver message: {}", e).into(), @@ -133,6 +167,52 @@ fn make_ws(tx: UnboundedSender, poll_interval: i32, reload: bool) } } +/// Represents what color the toast should have. +enum ToastLevel { + /// Green + Success, + /// Blue + Info, + /// Red + Error, +} + +impl Display for ToastLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ToastLevel::Success => write!(f, "success"), + ToastLevel::Info => write!(f, "info"), + ToastLevel::Error => write!(f, "error"), + } + } +} + +/// Displays a toast to the developer. +fn show_toast( + header_text: &str, + message: &str, + level: ToastLevel, + duration: Duration, + after_reload: bool, +) { + let as_ms = duration.as_millis(); + + let js_fn_name = match after_reload { + true => "scheduleDXToast", + false => "showDXToast", + }; + + ScopeId::ROOT.in_runtime(|| { + eval(&format!( + r#" + if (typeof {js_fn_name} !== "undefined") {{ + {js_fn_name}("{header_text}", "{message}", "{level}", {as_ms}); + }} + "#, + )); + }); +} + /// Force a hotreload of the assets on this page by walking them and changing their URLs to include /// some extra entropy. ///