CLI: Toasts & Tweaks (#2702)

* progress: cli toasts

* forgot the html

* progress: toasts

* revision: don't open splash on desktop

* fix: fmt, spellcheck
This commit is contained in:
Miles Murgaw 2024-07-25 17:38:45 -04:00 committed by GitHub
parent beeee0dda1
commit bd58a92441
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 363 additions and 31 deletions

View file

@ -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");

View file

@ -0,0 +1,206 @@
<style>
/* Inter Font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
#dx-toast-template {
display: none;
visibility: hidden;
}
.dx-toast {
position: absolute;
top: 10px;
right: 0;
padding-right: 10px;
user-select: none;
transition: transform 0.2s ease;
overflow: hidden;
}
.dx-toast .dx-toast-inner {
transition: right 0.2s ease-out;
position: relative;
background-color: #181B20;
color: #ffffff;
font-family: "Inter", sans-serif;
display: grid;
grid-template-columns: auto auto;
min-width: 280px;
min-height: 92px;
width: min-content;
border-radius: 5px;
}
.dx-toast:hover {
cursor: pointer;
transform: translateX(-5px);
}
.dx-toast .dx-toast-level-bar-container {
height: 100%;
width: 6px;
}
.dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
width: 100%;
height: 100%;
border-radius: 5px 0px 0px 5px;
}
.dx-toast .dx-toast-content {
padding: 13px;
}
.dx-toast .dx-toast-header {
display: flex;
flex-direction: row;
justify-content: start;
align-items: end;
margin-bottom: 13px;
}
.dx-toast .dx-toast-header>svg {
height: 22px;
margin-right: 5px;
}
.dx-toast .dx-toast-header .dx-toast-header-text {
font-size: 16px;
font-weight: 700;
padding: 0;
margin: 0;
}
.dx-toast .dx-toast-msg {
font-size: 14px;
font-weight: 400;
padding: 0;
margin: 0;
}
.dx-toast-level-bar.info {
background-color: #428EFF;
}
.dx-toast-level-bar.success {
background-color: #42FF65;
}
.dx-toast-level-bar.error {
background-color: #FF4242;
}
</style>
<div id="dx-toast-template" class="dx-toast">
<div class="dx-toast-inner" style="right:-300px;">
<!-- Level/Color decor -->
<div class="dx-toast-level-bar-container">
<div class="dx-toast-level-bar info"></div>
</div>
<!-- Content -->
<div class="dx-toast-content">
<!-- Header -->
<div class="dx-toast-header">
<!-- Dioxus Logo -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
<path
d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z"
fill="#e96020" />
<path
d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z"
fill="#2d323b" />
<path
d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z"
fill="#00a8d6" />
</svg>
<!-- Toast Title Text -->
<h3 class="dx-toast-header-text">Your app is being rebuilt.</h3>
</div>
<!-- Message -->
<p class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
</div>
</div>
</div>
<script>
const STORAGE_KEY = "SCHEDULED-DX-TOAST";
let currentToast = null;
let currentTimeout = null;
// Show a toast, removing the previous one.
function showDXToast(headerText, message, progressLevel, durationMs) {
// Close current toast if exists.
closeDXToast();
// Clone template and add unique id.
let toastTemplate = document.getElementById("dx-toast-template");
let cloned = toastTemplate.cloneNode(true);
let toastId = `dx-toast`;
cloned.id = toastId;
currentToast = cloned;
let innerElem = currentToast.querySelector(`#${toastId} .dx-toast-inner`);
// Set the progress level
let progressBarElem = innerElem.querySelector(".dx-toast-inner .dx-toast-level-bar-container .dx-toast-level-bar");
progressBarElem.className = `dx-toast-level-bar ${progressLevel}`;
// Set header text
let headerTextElem = innerElem.querySelector(".dx-toast-inner .dx-toast-header .dx-toast-header-text");
headerTextElem.innerText = headerText;
// Set message
let messageElem = innerElem.querySelector(".dx-toast-inner .dx-toast-msg");
messageElem.innerText = message;
document.body.appendChild(currentToast);
// Add listener to close toasts when clicked.
// Safety: Calling `closeToast` removes the element and all event listeners with it.
currentToast.addEventListener("click", closeDXToast);
// Wait a bit of time so animation plays correctly.
setTimeout(() => {
innerElem.style.right = "0";
currentTimeout = setTimeout(() => {
closeDXToast();
}, durationMs);
}, 100);
}
// Schedule a toast to be displayed after reload.
function scheduleDXToast(headerText, message, level, durationMs) {
let data = {
headerText,
message,
level,
durationMs,
};
let jsonData = JSON.stringify(data);
sessionStorage.setItem(STORAGE_KEY, jsonData);
}
// Close the current toast.
function closeDXToast() {
if (currentToast) {
currentToast.remove();
}
clearTimeout(currentTimeout);
}
// Handle any scheduled toasts after reload.
let potentialData = sessionStorage.getItem(STORAGE_KEY);
if (potentialData) {
sessionStorage.removeItem(STORAGE_KEY);
let data = JSON.parse(potentialData);
showDXToast(data.headerText, data.message, data.level, data.durationMs);
}
</script>

View file

@ -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 {
});
}
);
</script></body"#,
</script>
{DX_TOAST_UTILITIES}
</body"#,
);
*html = match self.serve && !self.build_arguments.release {
true => 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(
"</head",

View file

@ -43,6 +43,7 @@ impl std::fmt::Display for Stage {
}
}
#[derive(Debug, Clone)]
pub struct UpdateBuildProgress {
pub stage: Stage,
pub update: UpdateStage,

View file

@ -1,3 +1,4 @@
use crate::builder::{Stage, UpdateBuildProgress, UpdateStage};
use crate::cli::serve::Serve;
use crate::dioxus_crate::DioxusCrate;
use crate::Result;
@ -106,9 +107,17 @@ pub async fn serve_all(serve: Serve, dioxus_crate: DioxusCrate) -> 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;

View file

@ -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<RustlsConfig> = 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;
}
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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;

View file

@ -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<HotReloadMsg> {
// 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<HotReloadMsg>, 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<HotReloadMsg>, 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.
///