diff --git a/web-ui/crate/src/api/auth.rs b/web-ui/crate/src/api/auth.rs index 1d0c5fb..518459a 100644 --- a/web-ui/crate/src/api/auth.rs +++ b/web-ui/crate/src/api/auth.rs @@ -5,20 +5,45 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{console, Request, RequestCredentials, RequestInit, RequestMode, Response}; +/// A struct representing an error coming back from the REST API +/// endpoint. The API server encodes any errors as JSON objects with a +/// "message" property containing the error, and a bad status code. +#[derive(Debug, Serialize, Deserialize)] +struct ApiError { + message: Option, +} + #[derive(Debug, Serialize, Deserialize)] struct LoginResponse { jwt_token: String, } -async fn make_request(request: Request) -> Result { +async fn make_request(request: Request) -> Result +where + T: for<'a> Deserialize<'a>, +{ let window = web_sys::window().unwrap(); let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; let resp: Response = resp_value.dyn_into().unwrap(); + let ok = resp.ok(); let json = JsFuture::from(resp.json()?).await?; console::log_1(&json); - Ok(json) + //if ok, attempt to deserialize into T. + //if not ok, attempt to deserialize into struct with message, and fall back + //if that fails. + if ok { + let data: T = json.into_serde()?; + Ok(data) + } else { + let data: ApiError = json.into_serde()?; + Err(UiError::ApiError(data.message.unwrap_or_else(|| { + let status_text = resp.status_text(); + let status = resp.status(); + format!("[{}] - {} - unknown api error", status, status_text) + }))) + } } pub async fn login(username: &str, password: &str) -> Result { @@ -42,10 +67,7 @@ pub async fn login(username: &str, password: &str) -> Result { request.headers().set("Content-Type", "application/json")?; request.headers().set("Accept", "application/json")?; - //TODO don't unwrap the response. OR... change it so we have a standard response. - let response = make_request(request).await?; - let response: LoginResponse = response.into_serde().unwrap(); - + let response: LoginResponse = make_request(request).await?; Ok(response.jwt_token) } @@ -62,8 +84,6 @@ pub async fn refresh_jwt() -> Result { request.headers().set("Accept", "application/json")?; //TODO don't unwrap the response. OR... change it so we have a standard response. - let response = make_request(request).await?; - let response: LoginResponse = response.into_serde().unwrap(); - + let response: LoginResponse = make_request(request).await?; Ok(response.jwt_token) } diff --git a/web-ui/crate/src/components/error_message.rs b/web-ui/crate/src/components/error_message.rs new file mode 100644 index 0000000..18a8ed5 --- /dev/null +++ b/web-ui/crate/src/components/error_message.rs @@ -0,0 +1,62 @@ +use crate::api; +use crate::error::UiError; +use crate::state::{Action, WebUiDispatcher}; +use std::rc::Rc; +use wasm_bindgen::{prelude::Closure, JsCast}; +use wasm_bindgen_futures::spawn_local; +use web_sys::FocusEvent; +use yew::prelude::*; +use yewdux::dispatch::Dispatcher; +use yewdux::prelude::*; +use yewtil::NeqAssign; + +#[doc(hidden)] +pub(crate) struct YewduxErrorMessage { + dispatch: WebUiDispatcher, + link: ComponentLink, +} + +pub(crate) type ErrorMessage = WithDispatch; + +impl YewduxErrorMessage { + fn view_error(&self, error: &str) -> Html { + html! { + + } + } +} + +impl Component for YewduxErrorMessage { + type Message = (); + type Properties = WebUiDispatcher; + + fn create(dispatch: Self::Properties, link: ComponentLink) -> Self { + Self { dispatch, link } + } + + fn update(&mut self, action: Self::Message) -> ShouldRender { + false + } + + fn change(&mut self, dispatch: Self::Properties) -> ShouldRender { + self.dispatch.neq_assign(dispatch) + } + + fn rendered(&mut self, _first_render: bool) {} + + fn view(&self) -> Html { + html! { +
+ { + for self.dispatch.state().error_messages.iter().map(|error| { + self.view_error(error) + }) + } +
+ } + } + + fn destroy(&mut self) {} +} diff --git a/web-ui/crate/src/components/mod.rs b/web-ui/crate/src/components/mod.rs index 320cbbb..33ea54d 100644 --- a/web-ui/crate/src/components/mod.rs +++ b/web-ui/crate/src/components/mod.rs @@ -1 +1,2 @@ +pub mod error_message; pub mod login; diff --git a/web-ui/crate/src/lib.rs b/web-ui/crate/src/lib.rs index 7efcd90..c26d219 100644 --- a/web-ui/crate/src/lib.rs +++ b/web-ui/crate/src/lib.rs @@ -1,3 +1,4 @@ +use crate::components::error_message::ErrorMessage; use crate::components::login::Login; use rooms::RoomList; use rooms::YewduxRoomList; @@ -42,6 +43,7 @@ fn render_route(routes: &AppRoute) -> Html { html! {
+
} @@ -49,47 +51,6 @@ fn render_route(routes: &AppRoute) -> Html { } } -// struct AppMenu; - -// impl Component for AppMenu { -// type Message = (); -// type Properties = (); - -// fn create(_: Self::Properties, _link: ComponentLink) -> Self { -// Self -// } - -// fn update(&mut self, _msg: Self::Message) -> ShouldRender { -// false -// } - -// fn change(&mut self, _: Self::Properties) -> ShouldRender { -// false -// } - -// fn view(&self) -> Html { -// html! { -//
    -//
  • -// {"Home"} -//
  • -//
  • -// {"Oaths"} -//
  • -//
  • -// {"Commitments"} -//
  • -//
  • -// {"Studies"} -//
  • -//
  • -// {"Runic Divination"} -//
  • -//
-// } -// } -// } - struct App; impl Component for App { diff --git a/web-ui/crate/src/rooms.rs b/web-ui/crate/src/rooms.rs index 905c251..4e0e123 100644 --- a/web-ui/crate/src/rooms.rs +++ b/web-ui/crate/src/rooms.rs @@ -1,6 +1,6 @@ use crate::api; use crate::error::UiError; -use crate::state::{Action, Room, WebUiDispatcher}; +use crate::state::{Action, DispatchExt, Room, WebUiDispatcher}; use std::sync::Arc; use wasm_bindgen_futures::spawn_local; use web_sys::console; @@ -44,10 +44,13 @@ async fn load_rooms(dispatch: &WebUiDispatcher) -> Result<(), UiError> { Ok(()) } -async fn do_refresh_jwt(dispatch: &WebUiDispatcher) -> Result<(), UiError> { - let jwt = api::auth::refresh_jwt().await?; - dispatch.send(Action::UpdateJwt(jwt)); - Ok(()) +async fn do_refresh_jwt(dispatch: &WebUiDispatcher) { + let refresh = api::auth::refresh_jwt().await; + + match refresh { + Ok(jwt) => dispatch.send(Action::UpdateJwt(jwt)), + Err(e) => dispatch.dispatch_error(e), + } } impl Component for YewduxRoomList { @@ -101,9 +104,7 @@ impl Component for YewduxRoomList { let refresh_jwt = self.link.callback(move |_| { let dispatch = dispatch3.clone(); - spawn_local(async move { - do_refresh_jwt(&*dispatch).await; - }); + spawn_local(async move { do_refresh_jwt(&*dispatch).await }); }); html! { diff --git a/web-ui/crate/src/state.rs b/web-ui/crate/src/state.rs index 12d0b14..591b39f 100644 --- a/web-ui/crate/src/state.rs +++ b/web-ui/crate/src/state.rs @@ -1,3 +1,5 @@ +use crate::error::UiError; +use wasm_bindgen::{prelude::Closure, JsCast}; use yewdux::prelude::*; #[derive(Clone)] @@ -10,27 +12,33 @@ pub(crate) struct Room { pub(crate) struct WebUiState { pub jwt_token: Option, pub rooms: Vec, + pub error_messages: Vec, } pub(crate) enum Action { UpdateJwt(String), AddRoom(Room), + ErrorMessage(UiError), + ClearErrorMessage, } impl Reducer for WebUiState { type Action = Action; fn new() -> Self { - Self { - jwt_token: None, - rooms: vec![], - } + Self::default() } fn reduce(&mut self, action: Self::Action) -> bool { match action { Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token), Action::AddRoom(room) => self.rooms.push(room.clone()), + Action::ErrorMessage(error) => self.error_messages.push(error.to_string()), + Action::ClearErrorMessage => { + if self.error_messages.len() > 0 { + self.error_messages.remove(0); + } + } }; true @@ -38,3 +46,33 @@ impl Reducer for WebUiState { } pub(crate) type WebUiDispatcher = DispatchProps>; + +pub(crate) trait DispatchExt { + /// Dispatch an error message and then clear it from the state + /// after a few seconds. + fn dispatch_error(&self, error: UiError); +} + +impl DispatchExt for WebUiDispatcher { + fn dispatch_error(&self, error: UiError) { + self.send(Action::ErrorMessage(error)); + + // This is a very hacky way to do this. At the very least, we + // should not leak memory, and preferably there's a cleaner + // way to actually dispatch the clear action. + let window = web_sys::window().unwrap(); + let dispatch = self.clone(); + let clear_it = Closure::wrap( + Box::new(move || dispatch.send(Action::ClearErrorMessage)) as Box + ); + + window + .set_timeout_with_callback_and_timeout_and_arguments_0( + clear_it.as_ref().unchecked_ref(), + 4000, + ) + .expect("could not add clear error handler."); + + clear_it.forget(); + } +}