Make secret more logsafe, implement add and edit for links

This commit is contained in:
2021-08-12 15:48:02 +02:00
committed by dietrich
parent d503d49917
commit b782d97920
18 changed files with 909 additions and 158 deletions
+34 -1
View File
@@ -5,6 +5,7 @@ pub mod pages;
use pages::list_links;
use pages::list_users;
use seed::{div, log, prelude::*, App, Url, C};
use shared::datatypes::User;
use crate::i18n::{I18n, Lang};
@@ -14,6 +15,7 @@ use crate::i18n::{I18n, Lang};
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.subscribe(Msg::UrlChanged);
orders.send_msg(Msg::GetLoggedUser);
log!(url);
@@ -24,6 +26,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
base_url: Url::new().add_path_part("app"),
page: Page::init(url, orders, lang.clone()),
i18n: lang,
user: None,
}
}
@@ -37,6 +40,7 @@ struct Model {
base_url: Url,
page: Page,
i18n: i18n::I18n,
user: Option<User>,
}
#[derive(Debug)]
@@ -76,6 +80,8 @@ pub enum Msg {
UrlChanged(subs::UrlChanged),
ListLinks(list_links::Msg),
ListUsers(list_users::Msg),
GetLoggedUser,
UserReceived(User),
NoMessage,
}
@@ -97,6 +103,29 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
Msg::NoMessage => (),
Msg::GetLoggedUser => {
orders.skip(); // No need to rerender/ complicated way to move into the closure
orders.perform_cmd(async {
let response = fetch(
Request::new("/admin/json/get_logged_user/")
.method(Method::Post)
.json(&())
.expect("serialization failed"),
)
.await
.expect("HTTP request failed");
let user: User = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
.await
.expect("deserialization failed");
Msg::UserReceived(user)
});
}
Msg::UserReceived(user) => model.user = Some(user),
}
}
@@ -140,6 +169,10 @@ impl<'a> Urls<'a> {
self.base_url().add_path_part("list_links")
}
#[must_use]
pub fn create_link(self) -> Url {
self.list_links().add_path_part("create_link")
}
#[must_use]
pub fn list_users(self) -> Url {
self.base_url().add_path_part("list_users")
}
@@ -156,7 +189,7 @@ impl<'a> Urls<'a> {
fn view(model: &Model) -> Node<Msg> {
div![
C!["page"],
navigation::navigation(&model.i18n, &model.base_url,),
navigation::navigation(&model.i18n, &model.base_url, &model.user),
view_content(&model.page, &model.base_url),
]
}
+25 -20
View File
@@ -1,29 +1,34 @@
use fluent::fluent_args;
use seed::{a, attrs, div, li, nav, ol, prelude::*, Url};
use shared::datatypes::User;
use crate::{i18n::I18n, Msg};
#[must_use]
pub fn navigation(i18n: &I18n, base_url: &Url) -> Node<Msg> {
let username = fluent_args![ "username" => "enaut"];
macro_rules! t {
{ $key:expr } => {
{
i18n.translate($key, None)
}
};
{ $key:expr, $args:expr } => {
{
i18n.translate($key, Some(&$args))
}
};
}
pub fn navigation(i18n: &I18n, base_url: &Url, user: &Option<User>) -> Node<Msg> {
let t = move |key: &str| i18n.translate(key, None);
let welcome = if let Some(user) = user {
i18n.translate(
"welcome-user",
Some(&fluent_args![ "username" => user.username.clone()]),
)
} else {
t("welcome")
};
nav![
ol![
li![a![
attrs! {At::Href => crate::Urls::new(base_url).list_links()},
t!("list-links"),
t("list-links"),
],],
li![a![
attrs! {At::Href => crate::Urls::new(base_url).create_link()},
ev(Ev::Click, |_| Msg::ListLinks(
super::pages::list_links::Msg::Edit(
super::pages::list_links::EditMsg::CreateNewLink
)
)),
t("add-link"),
],],
li![a![ev(Ev::Click, |_| Msg::NoMessage), t!("add-link"),],],
li![a![
attrs! {At::Href => crate::Urls::new(base_url).create_user()},
ev(Ev::Click, |_| Msg::ListUsers(
@@ -31,19 +36,19 @@ pub fn navigation(i18n: &I18n, base_url: &Url) -> Node<Msg> {
super::pages::list_users::UserEditMsg::CreateNewUser
)
)),
t!("invite-user"),
t("invite-user"),
],],
li![a![
attrs! {At::Href => crate::Urls::new(base_url).list_users()},
t!("list-users"),
t("list-users"),
],],
],
ol![
li![div![t!("welcome-user", username)]],
li![div![welcome]],
li![a![
attrs! {At::Href => "#"},
ev(Ev::Click, |_| Msg::NoMessage),
t!("logout"),
t("logout"),
]]
]
]
+412 -60
View File
@@ -1,25 +1,50 @@
use std::cell::RefCell;
use enum_map::EnumMap;
use seed::{a, attrs, button, h1, input, log, prelude::*, section, table, td, th, tr, Url, C};
use fluent::fluent_args;
use seed::{
a, attrs, button, div, h1, img, input, log, prelude::*, section, span, table, td, th, tr, Url,
C,
};
use shared::{
apirequests::general::Ordering,
apirequests::{
general::Operation,
links::{LinkOverviewColumns, LinkRequestForm},
general::{EditMode, Message, Operation, Status},
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
},
datatypes::FullLink,
};
use crate::i18n::I18n;
pub fn init(_: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
orders.send_msg(Msg::Fetch);
/// Unwrap a result and return it's content, or return from the function with another expression.
macro_rules! unwrap_or_return {
( $e:expr, $result:expr) => {
match $e {
Ok(x) => x,
Err(_) => return $result,
}
};
}
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
log!(url);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
let edit_link = match url.next_path_part() {
Some("create_link") => Some(RefCell::new(LinkDelta::default())),
None | Some(_) => None,
};
log!(edit_link);
Model {
links: Vec::new(),
i18n,
formconfig: LinkRequestForm::default(),
inputs: EnumMap::default(),
edit_link,
last_message: None,
question: None,
}
}
@@ -29,6 +54,9 @@ pub struct Model {
i18n: I18n,
formconfig: LinkRequestForm,
inputs: EnumMap<LinkOverviewColumns, FilterInput>,
edit_link: Option<RefCell<LinkDelta>>,
last_message: Option<Status>,
question: Option<EditMsg>,
}
#[derive(Default, Debug, Clone)]
@@ -38,6 +66,14 @@ struct FilterInput {
#[derive(Clone)]
pub enum Msg {
Query(QueryMsg),
Edit(EditMsg),
ClearAll,
SetMessage(String),
}
#[derive(Clone)]
pub enum QueryMsg {
Fetch,
OrderBy(LinkOverviewColumns),
Received(Vec<FullLink>),
@@ -46,36 +82,51 @@ pub enum Msg {
TargetFilterChanged(String),
AuthorFilterChanged(String),
}
/// All the messages on link editing
#[derive(Clone, Debug)]
pub enum EditMsg {
EditSelected(LinkDelta),
CreateNewLink,
Created(Status),
EditCodeChanged(String),
EditDescriptionChanged(String),
EditTargetChanged(String),
MayDeleteSelected(LinkDelta),
DeleteSelected(LinkDelta),
SaveLink,
FailedToCreateLink,
FailedToDeleteLink,
DeletedLink(Status),
}
/// # Panics
/// Sould only panic on bugs.
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::Fetch => {
orders.skip(); // No need to rerender
let data = model.formconfig.clone(); // complicated way to move into the closure
orders.perform_cmd(async {
let data = data;
let response = fetch(
Request::new("/admin/json/list_links/")
.method(Method::Post)
.json(&data)
.expect("serialization failed"),
)
.await
.expect("HTTP request failed");
let user: Vec<FullLink> = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
.await
.expect("deserialization failed");
Msg::Received(user)
});
Msg::Query(msg) => process_query_messages(msg, model, orders),
Msg::Edit(msg) => process_edit_messages(msg, model, orders),
Msg::ClearAll => {
model.edit_link = None;
model.last_message = None;
model.question = None;
}
Msg::OrderBy(column) => {
Msg::SetMessage(msg) => {
model.edit_link = None;
model.question = None;
model.last_message = Some(Status::Error(Message { message: msg }));
}
}
}
/// # Panics
/// Sould only panic on bugs.
pub fn process_query_messages(msg: QueryMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
QueryMsg::Fetch => {
orders.skip(); // No need to rerender
load_links(model, orders)
}
QueryMsg::OrderBy(column) => {
model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|| {
Some(Operation {
@@ -94,7 +145,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
})
},
);
orders.send_msg(Msg::Fetch);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
model.links.sort_by(match column {
LinkOverviewColumns::Code => {
@@ -114,44 +165,231 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
})
}
Msg::Received(response) => {
QueryMsg::Received(response) => {
model.links = response;
}
Msg::CodeFilterChanged(s) => {
QueryMsg::CodeFilterChanged(s) => {
log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Code].sieve = sanit;
orders.send_msg(Msg::Fetch);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
Msg::DescriptionFilterChanged(s) => {
QueryMsg::DescriptionFilterChanged(s) => {
log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Description].sieve = sanit;
orders.send_msg(Msg::Fetch);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
Msg::TargetFilterChanged(s) => {
QueryMsg::TargetFilterChanged(s) => {
log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Target].sieve = sanit;
orders.send_msg(Msg::Fetch);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
Msg::AuthorFilterChanged(s) => {
QueryMsg::AuthorFilterChanged(s) => {
log!("Filter is: ", &s);
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
model.formconfig.filter[LinkOverviewColumns::Author].sieve = sanit;
orders.send_msg(Msg::Fetch);
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
}
}
fn load_links(model: &Model, orders: &mut impl Orders<Msg>) {
let data = model.formconfig.clone(); // complicated way to move into the closure
orders.perform_cmd(async {
let data = data;
let request = unwrap_or_return!(
Request::new("/admin/json/list_links/")
.method(Method::Post)
.json(&data),
Msg::SetMessage("Failed to parse data".to_string())
);
let response = unwrap_or_return!(
fetch(request).await,
Msg::SetMessage("Failed to send data".to_string())
);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code".to_string())
);
let links: Vec<FullLink> = unwrap_or_return!(
response.json().await,
Msg::SetMessage("Invalid response".to_string())
);
Msg::Query(QueryMsg::Received(links))
});
}
pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
EditMsg::EditSelected(link) => {
log!("Editing link: ", link);
model.last_message = None;
model.edit_link = Some(RefCell::new(link))
}
EditMsg::CreateNewLink => {
log!("Create new link!");
model.edit_link = Some(RefCell::new(LinkDelta::default()))
}
EditMsg::Created(success_msg) => {
model.last_message = Some(success_msg);
model.edit_link = None;
orders.send_msg(Msg::Query(QueryMsg::Fetch));
}
EditMsg::EditCodeChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut().expect("Failed to borrow mutably").code = s;
}
}
EditMsg::EditDescriptionChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut().expect("Failed to borrow mutably").title = s;
}
}
EditMsg::EditTargetChanged(s) => {
if let Some(ref le) = model.edit_link {
le.try_borrow_mut()
.expect("Failed to borrow mutably")
.target = s;
}
}
EditMsg::SaveLink => {
save_link(model, orders);
}
EditMsg::FailedToCreateLink => {
log!("Failed to create Link");
}
link @ EditMsg::MayDeleteSelected(..) => {
log!("Deleting link: ", link);
model.last_message = None;
model.edit_link = None;
model.question = Some(link)
}
EditMsg::DeleteSelected(link) => {
orders.perform_cmd(async {
let data = link;
let response = unwrap_or_return!(
fetch(
Request::new("/admin/json/delete_link/")
.method(Method::Post)
.json(&data)
.expect("serialization failed"),
)
.await,
Msg::Edit(EditMsg::FailedToDeleteLink)
);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code!".to_string())
);
let message: Status = unwrap_or_return!(
response.json().await,
Msg::SetMessage(
"Failed to parse the response the link might be deleted however!"
.to_string()
)
);
Msg::Edit(EditMsg::DeletedLink(message))
});
}
EditMsg::FailedToDeleteLink => {
log!("Failed to delete Link");
}
EditMsg::DeletedLink(message) => {
log!("Deleted link", message);
}
}
}
fn save_link(model: &Model, orders: &mut impl Orders<Msg>) {
let data = model
.edit_link
.as_ref()
.expect("should exist!")
.borrow()
.clone();
orders.perform_cmd(async {
let data = data;
let request = unwrap_or_return!(
Request::new(match data.edit {
EditMode::Create => "/admin/json/create_link/",
EditMode::Edit => "/admin/json/edit_link/",
})
.method(Method::Post)
.json(&data),
Msg::SetMessage("Failed to encode the link!".to_string())
);
let response =
unwrap_or_return!(fetch(request).await, Msg::Edit(EditMsg::FailedToCreateLink));
log!(response);
let response = unwrap_or_return!(
response.check_status(),
Msg::SetMessage("Wrong response code".to_string())
);
let message: Status = unwrap_or_return!(
response.json().await,
Msg::SetMessage("Invalid response!".to_string())
);
Msg::Edit(EditMsg::Created(message))
});
}
#[must_use]
/// # Panics
/// Sould only panic on bugs.
pub fn view(model: &Model) -> Node<Msg> {
let lang = model.i18n.clone();
let lang = &model.i18n.clone();
let t = move |key: &str| lang.translate(key, None);
section![
h1!("List Links Page from list_links"),
if let Some(message) = &model.last_message {
div![
C!["message", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
match message {
Status::Success(m) | Status::Error(m) => &m.message,
}
]
} else {
section![]
},
if let Some(question) = &model.question {
div![
C!["message", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
if let EditMsg::MayDeleteSelected(l) = question.clone() {
div![
lang.translate(
"really-delete",
Some(&fluent_args!["code" => l.code.clone()])
),
a![t("no"), C!["button"], ev(Ev::Click, |_| Msg::ClearAll)],
a![
t("yes"),
C!["button"],
ev(Ev::Click, move |_| Msg::Edit(EditMsg::DeleteSelected(l)))
]
]
} else {
span!()
}
]
} else {
section![]
},
table![
// Add the headlines
view_link_table_head(&t),
@@ -160,34 +398,51 @@ pub fn view(model: &Model) -> Node<Msg> {
// Add all the content lines
model.links.iter().map(view_link)
],
button![ev(Ev::Click, |_| Msg::Fetch), "Fetch links"]
button![
ev(Ev::Click, |_| Msg::Query(QueryMsg::Fetch)),
"Fetch links"
],
if let Some(l) = &model.edit_link {
edit_or_create_link(l, t)
} else {
section!()
}
]
}
fn view_link_table_head<F: Fn(&str) -> String>(t: F) -> Node<Msg> {
tr![
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Code)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Code
))),
t("link-code")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Description
)),
))),
t("link-description")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Target)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Target
))),
t("link-target")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Author)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Author
))),
t("username")
],
th![
ev(Ev::Click, |_| Msg::OrderBy(LinkOverviewColumns::Statistics)),
ev(Ev::Click, |_| Msg::Query(QueryMsg::OrderBy(
LinkOverviewColumns::Statistics
))),
t("statistics")
]
],
th![]
]
}
@@ -200,7 +455,7 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search",
At::Placeholder => t("search-placeholder")
},
input_ev(Ev::Input, Msg::CodeFilterChanged),
input_ev(Ev::Input, |s| Msg::Query(QueryMsg::CodeFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Code].filter_input),
]],
td![input![
@@ -210,7 +465,9 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search",
At::Placeholder => t("search-placeholder")
},
input_ev(Ev::Input, Msg::DescriptionFilterChanged),
input_ev(Ev::Input, |s| Msg::Query(
QueryMsg::DescriptionFilterChanged(s)
)),
el_ref(&model.inputs[LinkOverviewColumns::Description].filter_input),
]],
td![input![
@@ -220,7 +477,7 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search",
At::Placeholder => t("search-placeholder")
},
input_ev(Ev::Input, Msg::TargetFilterChanged),
input_ev(Ev::Input, |s| Msg::Query(QueryMsg::TargetFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Target].filter_input),
]],
td![input![
@@ -230,19 +487,114 @@ fn view_link_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> N
At::Type => "search",
At::Placeholder => t("search-placeholder")
},
input_ev(Ev::Input, Msg::AuthorFilterChanged),
input_ev(Ev::Input, |s| Msg::Query(QueryMsg::AuthorFilterChanged(s))),
el_ref(&model.inputs[LinkOverviewColumns::Author].filter_input),
]],
td![]
td![],
td![],
]
}
fn view_link(l: &FullLink) -> Node<Msg> {
// Ugly hack
let link = LinkDelta::from(l.clone());
let link2 = LinkDelta::from(l.clone());
let link3 = LinkDelta::from(l.clone());
let link4 = LinkDelta::from(l.clone());
let link5 = LinkDelta::from(l.clone());
tr![
td![&l.link.code],
td![&l.link.title],
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link))),
&l.link.code
]
},
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link2))),
&l.link.title
]
},
td![a![attrs![At::Href => &l.link.target], &l.link.target]],
td![&l.user.username],
td![&l.clicks.number]
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link3))),
&l.user.username
]
},
{
td![
ev(Ev::Click, |_| Msg::Edit(EditMsg::EditSelected(link4))),
&l.clicks.number
]
},
{
td![img![
ev(Ev::Click, |_| Msg::Edit(EditMsg::MayDeleteSelected(link5))),
C!["trashicon"],
attrs!(At::Src => "/static/trash.svg")
]]
},
]
}
fn edit_or_create_link<F: Fn(&str) -> String>(l: &RefCell<LinkDelta>, t: F) -> Node<Msg> {
let link = l.borrow();
div![
C!["editdialog", "center"],
div![
C!["closebutton"],
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
h1![match &link.edit {
EditMode::Edit => t("edit-link"),
EditMode::Create => t("create-link"),
}],
table![
tr![
th![t("link-description")],
td![input![
attrs! {
At::Value => &link.title,
At::Type => "text",
At::Placeholder => t("link-description")
},
input_ev(Ev::Input, |s| {
Msg::Edit(EditMsg::EditDescriptionChanged(s))
}),
]]
],
tr![
th![t("link-target")],
td![input![
attrs! {
At::Value => &link.target,
At::Type => "text",
At::Placeholder => t("link-target")
},
input_ev(Ev::Input, |s| { Msg::Edit(EditMsg::EditTargetChanged(s)) }),
]]
],
tr![
th![t("link-code")],
td![input![
attrs! {
At::Value => &link.code,
At::Type => "text",
At::Placeholder => t("password")
},
input_ev(Ev::Input, |s| { Msg::Edit(EditMsg::EditCodeChanged(s)) }),
],]
]
],
a![
match &link.edit {
EditMode::Edit => t("edit-link"),
EditMode::Create => t("create-link"),
},
C!["button"],
ev(Ev::Click, |_| Msg::Edit(EditMsg::SaveLink))
]
]
}
+9 -6
View File
@@ -7,7 +7,7 @@ use seed::{
use shared::{
apirequests::general::{Operation, Ordering},
apirequests::{
general::{EditMode, SuccessMessage},
general::{EditMode, Status},
users::{UserDelta, UserOverviewColumns, UserRequestForm},
},
datatypes::User,
@@ -37,7 +37,7 @@ pub struct Model {
formconfig: UserRequestForm,
inputs: EnumMap<UserOverviewColumns, FilterInput>,
user_edit: Option<RefCell<UserDelta>>,
last_message: Option<SuccessMessage>,
last_message: Option<Status>,
}
#[derive(Default, Debug, Clone)]
@@ -68,7 +68,7 @@ pub enum UserQueryMsg {
pub enum UserEditMsg {
EditUserSelected(UserDelta),
CreateNewUser,
UserCreated(SuccessMessage),
UserCreated(Status),
EditUsernameChanged(String),
EditEmailChanged(String),
EditPasswordChanged(String),
@@ -236,7 +236,7 @@ pub fn process_user_edit_messages(
Err(_) => return Msg::Edit(UserEditMsg::FailedToCreateUser),
};
let message: SuccessMessage = response
let message: Status = response
.check_status() // ensure we've got 2xx status
.expect("status check failed")
.json()
@@ -268,7 +268,6 @@ pub fn view(model: &Model) -> Node<Msg> {
keyboard_ev(Ev::KeyDown, |keyboard_event| {
IF!(keyboard_event.key() == "Escape" => Msg::ClearAll)
}),
h1!("List Users Page from list_users"),
if let Some(message) = &model.last_message {
div![
C!["message", "center"],
@@ -277,7 +276,11 @@ pub fn view(model: &Model) -> Node<Msg> {
a!["\u{d7}"],
ev(Ev::Click, |_| Msg::ClearAll)
],
&message.message
match message {
Status::Success(m) | Status::Error(m) => {
&m.message
}
}
]
} else {
section![]