Web API, Web UI #86

Merged
projectmoon merged 37 commits from web-api into master 2021-07-15 15:04:54 +00:00
6 changed files with 145 additions and 62 deletions
Showing only changes of commit 4c0d5ea2d8 - Show all commits

View File

@ -5,20 +5,45 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
use web_sys::{console, Request, RequestCredentials, RequestInit, RequestMode, Response}; 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<String>,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct LoginResponse { struct LoginResponse {
jwt_token: String, jwt_token: String,
} }
async fn make_request(request: Request) -> Result<JsValue, UiError> { async fn make_request<T>(request: Request) -> Result<T, UiError>
where
T: for<'a> Deserialize<'a>,
{
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().unwrap(); let resp: Response = resp_value.dyn_into().unwrap();
let ok = resp.ok();
let json = JsFuture::from(resp.json()?).await?; let json = JsFuture::from(resp.json()?).await?;
console::log_1(&json); 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<String, UiError> { pub async fn login(username: &str, password: &str) -> Result<String, UiError> {
@ -42,10 +67,7 @@ pub async fn login(username: &str, password: &str) -> Result<String, UiError> {
request.headers().set("Content-Type", "application/json")?; request.headers().set("Content-Type", "application/json")?;
request.headers().set("Accept", "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: LoginResponse = make_request(request).await?;
let response = make_request(request).await?;
let response: LoginResponse = response.into_serde().unwrap();
Ok(response.jwt_token) Ok(response.jwt_token)
} }
@ -62,8 +84,6 @@ pub async fn refresh_jwt() -> Result<String, UiError> {
request.headers().set("Accept", "application/json")?; request.headers().set("Accept", "application/json")?;
//TODO don't unwrap the response. OR... change it so we have a standard response. //TODO don't unwrap the response. OR... change it so we have a standard response.
let response = make_request(request).await?; let response: LoginResponse = make_request(request).await?;
let response: LoginResponse = response.into_serde().unwrap();
Ok(response.jwt_token) Ok(response.jwt_token)
} }

View File

@ -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<YewduxErrorMessage>,
}
pub(crate) type ErrorMessage = WithDispatch<YewduxErrorMessage>;
impl YewduxErrorMessage {
fn view_error(&self, error: &str) -> Html {
html! {
<div class="alert alert-danger" role="alert">
{ error }
</div>
}
}
}
impl Component for YewduxErrorMessage {
type Message = ();
type Properties = WebUiDispatcher;
fn create(dispatch: Self::Properties, link: ComponentLink<Self>) -> 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! {
<div>
{
for self.dispatch.state().error_messages.iter().map(|error| {
self.view_error(error)
})
}
</div>
}
}
fn destroy(&mut self) {}
}

View File

@ -1 +1,2 @@
pub mod error_message;
pub mod login; pub mod login;

View File

@ -1,3 +1,4 @@
use crate::components::error_message::ErrorMessage;
use crate::components::login::Login; use crate::components::login::Login;
use rooms::RoomList; use rooms::RoomList;
use rooms::YewduxRoomList; use rooms::YewduxRoomList;
@ -42,6 +43,7 @@ fn render_route(routes: &AppRoute) -> Html {
html! { html! {
<div> <div>
<Login /> <Login />
<ErrorMessage />
<RoomList /> <RoomList />
</div> </div>
} }
@ -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 {
// Self
// }
// fn update(&mut self, _msg: Self::Message) -> ShouldRender {
// false
// }
// fn change(&mut self, _: Self::Properties) -> ShouldRender {
// false
// }
// fn view(&self) -> Html {
// html! {
// <ul>
// <li>
// <AppAnchor route=AppRoute::Index>{"Home"}</AppAnchor>
// </li>
// <li>
// <AppAnchor route=AppRoute::Oaths>{"Oaths"}</AppAnchor>
// </li>
// <li>
// <AppAnchor route=AppRoute::Commitments>{"Commitments"}</AppAnchor>
// </li>
// <li>
// <AppAnchor route=AppRoute::Studies>{"Studies"}</AppAnchor>
// </li>
// <li>
// <AppAnchor route=AppRoute::RunicDivination>{"Runic Divination"}</AppAnchor>
// </li>
// </ul>
// }
// }
// }
struct App; struct App;
impl Component for App { impl Component for App {

View File

@ -1,6 +1,6 @@
use crate::api; use crate::api;
use crate::error::UiError; use crate::error::UiError;
use crate::state::{Action, Room, WebUiDispatcher}; use crate::state::{Action, DispatchExt, Room, WebUiDispatcher};
use std::sync::Arc; use std::sync::Arc;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use web_sys::console; use web_sys::console;
@ -44,10 +44,13 @@ async fn load_rooms(dispatch: &WebUiDispatcher) -> Result<(), UiError> {
Ok(()) Ok(())
} }
async fn do_refresh_jwt(dispatch: &WebUiDispatcher) -> Result<(), UiError> { async fn do_refresh_jwt(dispatch: &WebUiDispatcher) {
let jwt = api::auth::refresh_jwt().await?; let refresh = api::auth::refresh_jwt().await;
dispatch.send(Action::UpdateJwt(jwt));
Ok(()) match refresh {
Ok(jwt) => dispatch.send(Action::UpdateJwt(jwt)),
Err(e) => dispatch.dispatch_error(e),
}
} }
impl Component for YewduxRoomList { impl Component for YewduxRoomList {
@ -101,9 +104,7 @@ impl Component for YewduxRoomList {
let refresh_jwt = self.link.callback(move |_| { let refresh_jwt = self.link.callback(move |_| {
let dispatch = dispatch3.clone(); let dispatch = dispatch3.clone();
spawn_local(async move { spawn_local(async move { do_refresh_jwt(&*dispatch).await });
do_refresh_jwt(&*dispatch).await;
});
}); });
html! { html! {

View File

@ -1,3 +1,5 @@
use crate::error::UiError;
use wasm_bindgen::{prelude::Closure, JsCast};
use yewdux::prelude::*; use yewdux::prelude::*;
#[derive(Clone)] #[derive(Clone)]
@ -10,27 +12,33 @@ pub(crate) struct Room {
pub(crate) struct WebUiState { pub(crate) struct WebUiState {
pub jwt_token: Option<String>, pub jwt_token: Option<String>,
pub rooms: Vec<Room>, pub rooms: Vec<Room>,
pub error_messages: Vec<String>,
} }
pub(crate) enum Action { pub(crate) enum Action {
UpdateJwt(String), UpdateJwt(String),
AddRoom(Room), AddRoom(Room),
ErrorMessage(UiError),
ClearErrorMessage,
} }
impl Reducer for WebUiState { impl Reducer for WebUiState {
type Action = Action; type Action = Action;
fn new() -> Self { fn new() -> Self {
Self { Self::default()
jwt_token: None,
rooms: vec![],
}
} }
fn reduce(&mut self, action: Self::Action) -> bool { fn reduce(&mut self, action: Self::Action) -> bool {
match action { match action {
Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token), Action::UpdateJwt(jwt_token) => self.jwt_token = Some(jwt_token),
Action::AddRoom(room) => self.rooms.push(room.clone()), 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 true
@ -38,3 +46,33 @@ impl Reducer for WebUiState {
} }
pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>; pub(crate) type WebUiDispatcher = DispatchProps<ReducerStore<WebUiState>>;
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<dyn Fn()>
);
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();
}
}