Web API, Web UI #86
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct LoginResponse {
|
||||
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 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<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("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<String, UiError> {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod error_message;
|
||||
pub mod login;
|
||||
|
|
|
@ -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! {
|
||||
<div>
|
||||
<Login />
|
||||
<ErrorMessage />
|
||||
<RoomList />
|
||||
</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;
|
||||
|
||||
impl Component for App {
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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<String>,
|
||||
pub rooms: Vec<Room>,
|
||||
pub error_messages: Vec<String>,
|
||||
}
|
||||
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue