app: update yew to 0.19

This is a massive change to all the components, since the interface
changed.

There are opportunities to greatly simplify some components by turning
them into functional_components, but this work has tried to stay as
mechanical as possible.
This commit is contained in:
Valentin Tolmer 2023-03-08 18:05:08 +01:00 committed by nitnelave
parent 8d44717588
commit b2cfc0ed03
25 changed files with 893 additions and 1127 deletions

206
Cargo.lock generated
View File

@ -352,12 +352,6 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "anymap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.6" version = "0.3.6"
@ -660,12 +654,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg-match"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8100e46ff92eb85bf6dc2930c73f2a4f7176393c84a9446b3d501e1b354e7b34"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.23" version = "0.4.23"
@ -1524,14 +1512,18 @@ checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
[[package]] [[package]]
name = "gloo" name = "gloo"
version = "0.2.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ce6f2dfa9f57f15b848efa2aade5e1850dc72986b87a2b0752d44ca08f4967" checksum = "23947965eee55e3e97a5cd142dd4c10631cc349b48cecca0ed230fd296f568cd"
dependencies = [ dependencies = [
"gloo-console-timer", "gloo-console",
"gloo-dialogs",
"gloo-events", "gloo-events",
"gloo-file", "gloo-file",
"gloo-render",
"gloo-storage",
"gloo-timers", "gloo-timers",
"gloo-utils",
] ]
[[package]] [[package]]
@ -1548,11 +1540,12 @@ dependencies = [
] ]
[[package]] [[package]]
name = "gloo-console-timer" name = "gloo-dialogs"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b48675544b29ac03402c6dffc31a912f716e38d19f7e74b78b7e900ec3c941ea" checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
dependencies = [ dependencies = [
"wasm-bindgen",
"web-sys", "web-sys",
] ]
@ -1568,22 +1561,70 @@ dependencies = [
[[package]] [[package]]
name = "gloo-file" name = "gloo-file"
version = "0.1.0" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49" checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
dependencies = [ dependencies = [
"futures-channel",
"gloo-events", "gloo-events",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
] ]
[[package]]
name = "gloo-net"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10"
dependencies = [
"futures-channel",
"futures-core",
"futures-sink",
"gloo-utils",
"js-sys",
"pin-project",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gloo-render"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-storage"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
dependencies = [
"gloo-utils",
"js-sys",
"serde",
"serde_json",
"thiserror",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "gloo-timers" name = "gloo-timers"
version = "0.2.6" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [ dependencies = [
"futures-channel",
"futures-core",
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
] ]
@ -2258,19 +2299,6 @@ dependencies = [
"webpki-roots", "webpki-roots",
] ]
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"bitflags",
"cfg-if",
"ryu",
"static_assertions",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.139" version = "0.2.139"
@ -2388,6 +2416,8 @@ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"chrono", "chrono",
"gloo-console", "gloo-console",
"gloo-file",
"gloo-net",
"graphql_client 0.10.0", "graphql_client 0.10.0",
"http", "http",
"image", "image",
@ -2401,12 +2431,12 @@ dependencies = [
"validator", "validator",
"validator_derive 0.16.0", "validator_derive 0.16.0",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
"yew", "yew",
"yew-router", "yew-router",
"yew_form", "yew_form",
"yew_form_derive", "yew_form_derive",
"yewtil",
] ]
[[package]] [[package]]
@ -2586,17 +2616,6 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff"
[[package]]
name = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -3287,6 +3306,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "route-recognizer"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.6.1" version = "0.6.1"
@ -3411,6 +3436,12 @@ dependencies = [
"windows-sys 0.42.0", "windows-sys 0.42.0",
] ]
[[package]]
name = "scoped-tls-hkt"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -3576,6 +3607,18 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-wasm-bindgen"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "618365e8e586c22123d692b72a7d791d5ee697817b65a218cdf12a98870af0f7"
dependencies = [
"fnv",
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]] [[package]]
name = "serde_bytes" name = "serde_bytes"
version = "0.11.9" version = "0.11.9"
@ -4480,8 +4523,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"serde",
"serde_json",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
@ -4727,26 +4768,17 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "yew" name = "yew"
version = "0.18.0" version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4d5154faef86dddd2eb333d4755ea5643787d20aca683e58759b0e53351409f" checksum = "2a1ccb53e57d3f7d847338cf5758befa811cabe207df07f543c06f502f9998cd"
dependencies = [ dependencies = [
"anyhow",
"anymap",
"bincode",
"cfg-if",
"cfg-match",
"console_error_panic_hook", "console_error_panic_hook",
"gloo", "gloo",
"http", "gloo-utils",
"indexmap", "indexmap",
"js-sys", "js-sys",
"log", "scoped-tls-hkt",
"ryu",
"serde",
"serde_json",
"slab", "slab",
"thiserror",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
@ -4755,12 +4787,13 @@ dependencies = [
[[package]] [[package]]
name = "yew-macro" name = "yew-macro"
version = "0.18.0" version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6e23bfe3dc3933fbe9592d149c9985f3047d08c637a884b9344c21e56e092ef" checksum = "5fab79082b556d768d6e21811869c761893f0450e1d550a67892b9bce303b7bb"
dependencies = [ dependencies = [
"boolinator", "boolinator",
"lazy_static", "lazy_static",
"proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -4768,60 +4801,52 @@ dependencies = [
[[package]] [[package]]
name = "yew-router" name = "yew-router"
version = "0.15.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27666236d9597eac9be560e841e415e20ba67020bc8cd081076be178e159c8bc" checksum = "155804f6f3aa309f596d5c3fa14486a94e7756f1edd7634569949e401d5099f2"
dependencies = [ dependencies = [
"cfg-if",
"cfg-match",
"gloo", "gloo",
"gloo-utils",
"js-sys", "js-sys",
"log", "route-recognizer",
"nom 5.1.2",
"serde", "serde",
"serde_json", "serde-wasm-bindgen",
"serde_urlencoded",
"thiserror",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"yew", "yew",
"yew-router-macro", "yew-router-macro",
"yew-router-route-parser",
] ]
[[package]] [[package]]
name = "yew-router-macro" name = "yew-router-macro"
version = "0.15.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0ace2924b7a175e2d1c0e62ee7022a5ad840040dcd52414ce5f410ab322dba" checksum = "39049d193b52eaad4ffc80916bf08806d142c90b5edcebd527644de438a7e19a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"yew-router-route-parser",
]
[[package]]
name = "yew-router-route-parser"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de4a67208fb46b900af18a7397938b01f379dfc18da34799cfa8347eec715697"
dependencies = [
"nom 5.1.2",
] ]
[[package]] [[package]]
name = "yew_form" name = "yew_form"
version = "0.1.8" version = "0.1.8"
source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed" source = "git+https://github.com/jfbilodeau/yew_form?rev=4b9fabffb63393ec7626a4477fd36de12a07fac9#4b9fabffb63393ec7626a4477fd36de12a07fac9"
dependencies = [ dependencies = [
"gloo-console",
"validator", "validator",
"validator_derive 0.14.0", "validator_derive 0.14.0",
"wasm-bindgen",
"web-sys",
"yew", "yew",
] ]
[[package]] [[package]]
name = "yew_form_derive" name = "yew_form_derive"
version = "0.1.8" version = "0.1.8"
source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed" source = "git+https://github.com/jfbilodeau/yew_form?rev=4b9fabffb63393ec7626a4477fd36de12a07fac9#4b9fabffb63393ec7626a4477fd36de12a07fac9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4829,19 +4854,6 @@ dependencies = [
"yew_form", "yew_form",
] ]
[[package]]
name = "yewtil"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8543663ac49cd613df079282a1d8bdbdebdad6e02bac229f870fd4237b5d9aaa"
dependencies = [
"log",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"yew",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.5.7" version = "1.5.7"

View File

@ -9,22 +9,24 @@ include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
anyhow = "1" anyhow = "1"
base64 = "0.13" base64 = "0.13"
gloo-console = "0.2.3" gloo-console = "0.2.3"
gloo-file = "0.2.3"
gloo-net = "*"
graphql_client = "0.10" graphql_client = "0.10"
http = "0.2" http = "0.2"
jwt = "0.13" jwt = "0.13"
rand = "0.8" rand = "0.8"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
url-escape = "0.1.1"
validator = "=0.14" validator = "=0.14"
validator_derive = "*" validator_derive = "*"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
yew = "0.18" wasm-bindgen-futures = "*"
yewtil = "*" yew = "0.19.3"
yew-router = "0.15" yew-router = "0.16"
# Needed because of https://github.com/tkaitchuck/aHash/issues/95 # Needed because of https://github.com/tkaitchuck/aHash/issues/95
indexmap = "=1.6.2" indexmap = "=1.6.2"
url-escape = "0.1.1"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3"
@ -57,11 +59,11 @@ version = "0.24"
[dependencies.yew_form] [dependencies.yew_form]
git = "https://github.com/jfbilodeau/yew_form" git = "https://github.com/jfbilodeau/yew_form"
rev = "67050812695b7a8a90b81b0637e347fc6629daed" rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
[dependencies.yew_form_derive] [dependencies.yew_form_derive]
git = "https://github.com/jfbilodeau/yew_form" git = "https://github.com/jfbilodeau/yew_form"
rev = "67050812695b7a8a90b81b0637e347fc6629daed" rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]

View File

@ -52,23 +52,25 @@ pub struct Props {
} }
impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent { impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::UserListResponse(response) => { Msg::UserListResponse(response) => {
self.user_list = Some(response?.users); self.user_list = Some(response?.users);
self.common.cancel_task();
} }
Msg::SubmitAddMember => return self.submit_add_member(), Msg::SubmitAddMember => return self.submit_add_member(ctx),
Msg::AddMemberResponse(response) => { Msg::AddMemberResponse(response) => {
response?; response?;
self.common.cancel_task();
let user = self let user = self
.selected_user .selected_user
.as_ref() .as_ref()
.expect("Could not get selected user") .expect("Could not get selected user")
.clone(); .clone();
// Remove the user from the dropdown. // Remove the user from the dropdown.
self.common.on_user_added_to_group.emit(user); ctx.props().on_user_added_to_group.emit(user);
} }
Msg::SelectionChanged(option_props) => { Msg::SelectionChanged(option_props) => {
let was_some = self.selected_user.is_some(); let was_some = self.selected_user.is_some();
@ -88,23 +90,25 @@ impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent {
} }
impl AddGroupMemberComponent { impl AddGroupMemberComponent {
fn get_user_list(&mut self) { fn get_user_list(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<ListUserNames, _>( self.common.call_graphql::<ListUserNames, _>(
ctx,
list_user_names::Variables { filters: None }, list_user_names::Variables { filters: None },
Msg::UserListResponse, Msg::UserListResponse,
"Error trying to fetch user list", "Error trying to fetch user list",
); );
} }
fn submit_add_member(&mut self) -> Result<bool> { fn submit_add_member(&mut self, ctx: &Context<Self>) -> Result<bool> {
let user_id = match self.selected_user.clone() { let user_id = match self.selected_user.clone() {
None => return Ok(false), None => return Ok(false),
Some(user) => user.id, Some(user) => user.id,
}; };
self.common.call_graphql::<AddUserToGroup, _>( self.common.call_graphql::<AddUserToGroup, _>(
ctx,
add_user_to_group::Variables { add_user_to_group::Variables {
user: user_id, user: user_id,
group: self.common.group_id, group: ctx.props().group_id,
}, },
Msg::AddMemberResponse, Msg::AddMemberResponse,
"Error trying to initiate adding the user to a group", "Error trying to initiate adding the user to a group",
@ -112,8 +116,8 @@ impl AddGroupMemberComponent {
Ok(true) Ok(true)
} }
fn get_selectable_user_list(&self, user_list: &[User]) -> Vec<User> { fn get_selectable_user_list(&self, ctx: &Context<Self>, user_list: &[User]) -> Vec<User> {
let user_groups = self.common.users.iter().collect::<HashSet<_>>(); let user_groups = ctx.props().users.iter().collect::<HashSet<_>>();
user_list user_list
.iter() .iter()
.filter(|u| !user_groups.contains(u)) .filter(|u| !user_groups.contains(u))
@ -126,32 +130,29 @@ impl Component for AddGroupMemberComponent {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut res = Self { let mut res = Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
user_list: None, user_list: None,
selected_user: None, selected_user: None,
}; };
res.get_user_list(); res.get_user_list(ctx);
res res
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error( CommonComponentParts::<Self>::update_and_report_error(
self, self,
ctx,
msg, msg,
self.common.on_error.clone(), ctx.props().on_error.clone(),
) )
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
if let Some(user_list) = &self.user_list { if let Some(user_list) = &self.user_list {
let to_add_user_list = self.get_selectable_user_list(user_list); let to_add_user_list = self.get_selectable_user_list(ctx, user_list);
#[allow(unused_braces)] #[allow(unused_braces)]
let make_select_option = |user: User| { let make_select_option = |user: User| {
html_nested! { html_nested! {

View File

@ -64,16 +64,18 @@ pub struct Props {
} }
impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent { impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::GroupListResponse(response) => { Msg::GroupListResponse(response) => {
self.group_list = Some(response?.groups.into_iter().map(Into::into).collect()); self.group_list = Some(response?.groups.into_iter().map(Into::into).collect());
self.common.cancel_task();
} }
Msg::SubmitAddGroup => return self.submit_add_group(), Msg::SubmitAddGroup => return self.submit_add_group(ctx),
Msg::AddGroupResponse(response) => { Msg::AddGroupResponse(response) => {
response?; response?;
self.common.cancel_task();
// Adding the user to the group succeeded, we're not in the process of adding a // Adding the user to the group succeeded, we're not in the process of adding a
// group anymore. // group anymore.
let group = self let group = self
@ -82,7 +84,7 @@ impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
.expect("Could not get selected group") .expect("Could not get selected group")
.clone(); .clone();
// Remove the group from the dropdown. // Remove the group from the dropdown.
self.common.on_user_added_to_group.emit(group); ctx.props().on_user_added_to_group.emit(group);
} }
Msg::SelectionChanged(option_props) => { Msg::SelectionChanged(option_props) => {
let was_some = self.selected_group.is_some(); let was_some = self.selected_group.is_some();
@ -102,22 +104,24 @@ impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
} }
impl AddUserToGroupComponent { impl AddUserToGroupComponent {
fn get_group_list(&mut self) { fn get_group_list(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<GetGroupList, _>( self.common.call_graphql::<GetGroupList, _>(
ctx,
get_group_list::Variables, get_group_list::Variables,
Msg::GroupListResponse, Msg::GroupListResponse,
"Error trying to fetch group list", "Error trying to fetch group list",
); );
} }
fn submit_add_group(&mut self) -> Result<bool> { fn submit_add_group(&mut self, ctx: &Context<Self>) -> Result<bool> {
let group_id = match &self.selected_group { let group_id = match &self.selected_group {
None => return Ok(false), None => return Ok(false),
Some(group) => group.id, Some(group) => group.id,
}; };
self.common.call_graphql::<AddUserToGroup, _>( self.common.call_graphql::<AddUserToGroup, _>(
ctx,
add_user_to_group::Variables { add_user_to_group::Variables {
user: self.common.username.clone(), user: ctx.props().username.clone(),
group: group_id, group: group_id,
}, },
Msg::AddGroupResponse, Msg::AddGroupResponse,
@ -126,8 +130,8 @@ impl AddUserToGroupComponent {
Ok(true) Ok(true)
} }
fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> { fn get_selectable_group_list(&self, props: &Props, group_list: &[Group]) -> Vec<Group> {
let user_groups = self.common.groups.iter().collect::<HashSet<_>>(); let user_groups = props.groups.iter().collect::<HashSet<_>>();
group_list group_list
.iter() .iter()
.filter(|g| !user_groups.contains(g)) .filter(|g| !user_groups.contains(g))
@ -139,32 +143,29 @@ impl AddUserToGroupComponent {
impl Component for AddUserToGroupComponent { impl Component for AddUserToGroupComponent {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut res = Self { let mut res = Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
group_list: None, group_list: None,
selected_group: None, selected_group: None,
}; };
res.get_group_list(); res.get_group_list(ctx);
res res
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error( CommonComponentParts::<Self>::update_and_report_error(
self, self,
ctx,
msg, msg,
self.common.on_error.clone(), ctx.props().on_error.clone(),
) )
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
if let Some(group_list) = &self.group_list { if let Some(group_list) = &self.group_list {
let to_add_group_list = self.get_selectable_group_list(group_list); let to_add_group_list = self.get_selectable_group_list(ctx.props(), group_list);
#[allow(unused_braces)] #[allow(unused_braces)]
let make_select_option = |group: Group| { let make_select_option = |group: Group| {
html_nested! { html_nested! {

View File

@ -9,7 +9,7 @@ use crate::{
logout::LogoutButton, logout::LogoutButton,
reset_password_step1::ResetPasswordStep1Form, reset_password_step1::ResetPasswordStep1Form,
reset_password_step2::ResetPasswordStep2Form, reset_password_step2::ResetPasswordStep2Form,
router::{AppRoute, Link, NavButton}, router::{AppRoute, Link, Redirect},
user_details::UserDetails, user_details::UserDetails,
user_table::UserTable, user_table::UserTable,
}, },
@ -17,21 +17,31 @@ use crate::{
}; };
use gloo_console::error; use gloo_console::error;
use yew::{prelude::*, services::fetch::FetchTask}; use yew::{
function_component,
html::Scope,
prelude::{html, Component, Html},
Context,
};
use yew_router::{ use yew_router::{
agent::{RouteAgentDispatcher, RouteRequest}, prelude::{History, Location},
route::Route, scope_ext::RouterScopeExt,
router::Router, BrowserRouter, Switch,
service::RouteService,
}; };
#[function_component(AppContainer)]
pub fn app_container() -> Html {
html! {
<BrowserRouter>
<App />
</BrowserRouter>
}
}
pub struct App { pub struct App {
link: ComponentLink<Self>,
user_info: Option<(String, bool)>, user_info: Option<(String, bool)>,
redirect_to: Option<AppRoute>, redirect_to: Option<AppRoute>,
route_dispatcher: RouteAgentDispatcher,
password_reset_enabled: Option<bool>, password_reset_enabled: Option<bool>,
task: Option<FetchTask>,
} }
pub enum Msg { pub enum Msg {
@ -44,9 +54,8 @@ impl Component for App {
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut app = Self { let app = Self {
link,
user_info: get_cookie("user_id") user_info: get_cookie("user_id")
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
error!(&e.to_string()); error!(&e.to_string());
@ -60,48 +69,40 @@ impl Component for App {
None None
}) })
}), }),
redirect_to: Self::get_redirect_route(), redirect_to: Self::get_redirect_route(ctx),
route_dispatcher: RouteAgentDispatcher::new(),
password_reset_enabled: None, password_reset_enabled: None,
task: None,
}; };
app.task = Some( ctx.link().send_future(async move {
HostService::probe_password_reset( Msg::PasswordResetProbeFinished(HostService::probe_password_reset().await)
app.link.callback_once(Msg::PasswordResetProbeFinished), });
) app.apply_initial_redirections(ctx);
.unwrap(),
);
app.apply_initial_redirections();
app app
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let history = ctx.link().history().unwrap();
match msg { match msg {
Msg::Login((user_name, is_admin)) => { Msg::Login((user_name, is_admin)) => {
self.user_info = Some((user_name.clone(), is_admin)); self.user_info = Some((user_name.clone(), is_admin));
self.route_dispatcher history.push(self.redirect_to.take().unwrap_or_else(|| {
.send(RouteRequest::ChangeRoute(Route::from(
self.redirect_to.take().unwrap_or_else(|| {
if is_admin { if is_admin {
AppRoute::ListUsers AppRoute::ListUsers
} else { } else {
AppRoute::UserDetails(user_name.clone()) AppRoute::UserDetails {
user_id: user_name.clone(),
} }
}), }
))); }));
} }
Msg::Logout => { Msg::Logout => {
self.user_info = None; self.user_info = None;
self.redirect_to = None; self.redirect_to = None;
self.route_dispatcher history.push(AppRoute::Login);
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
} }
Msg::PasswordResetProbeFinished(Ok(enabled)) => { Msg::PasswordResetProbeFinished(Ok(enabled)) => {
self.task = None;
self.password_reset_enabled = Some(enabled); self.password_reset_enabled = Some(enabled);
} }
Msg::PasswordResetProbeFinished(Err(err)) => { Msg::PasswordResetProbeFinished(Err(err)) => {
self.task = None;
self.password_reset_enabled = Some(false); self.password_reset_enabled = Some(false);
error!(&format!( error!(&format!(
"Could not probe for password reset support: {err:#}" "Could not probe for password reset support: {err:#}"
@ -111,24 +112,20 @@ impl Component for App {
true true
} }
fn change(&mut self, _: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
false let link = ctx.link().clone();
}
fn view(&self) -> Html {
let link = self.link.clone();
let is_admin = self.is_admin(); let is_admin = self.is_admin();
let password_reset_enabled = self.password_reset_enabled; let password_reset_enabled = self.password_reset_enabled;
html! { html! {
<div> <div>
{self.view_banner()} {self.view_banner(ctx)}
<div class="container py-3 bg-kug"> <div class="container py-3 bg-kug">
<div class="row justify-content-center" style="padding-bottom: 80px;"> <div class="row justify-content-center" style="padding-bottom: 80px;">
<div class="py-3" style="max-width: 1000px"> <main class="py-3" style="max-width: 1000px">
<Router<AppRoute> <Switch<AppRoute>
render={Router::render(move |s| Self::dispatch_route(s, &link, is_admin, password_reset_enabled))} render={Switch::render(move |routes| Self::dispatch_route(routes, &link, is_admin, password_reset_enabled))}
/> />
</div> </main>
</div> </div>
{self.view_footer()} {self.view_footer()}
</div> </div>
@ -138,59 +135,50 @@ impl Component for App {
} }
impl App { impl App {
fn get_redirect_route() -> Option<AppRoute> { // Get the page to land on after logging in, defaulting to the index.
let route_service = RouteService::<()>::new(); fn get_redirect_route(ctx: &Context<Self>) -> Option<AppRoute> {
let current_route = route_service.get_path(); let route = ctx.link().history().unwrap().location().route::<AppRoute>();
if current_route.is_empty() route.filter(|route| {
|| current_route == "/" !matches!(
|| current_route.contains("login") route,
|| current_route.contains("reset-password") AppRoute::Index
{ | AppRoute::Login
None | AppRoute::StartResetPassword
} else { | AppRoute::FinishResetPassword { token: _ }
use yew_router::Switch; )
AppRoute::from_route_part::<()>(current_route, None).0 })
}
} }
fn apply_initial_redirections(&mut self) { fn apply_initial_redirections(&self, ctx: &Context<Self>) {
let route_service = RouteService::<()>::new(); let history = ctx.link().history().unwrap();
let current_route = route_service.get_path(); let route = history.location().route::<AppRoute>();
if current_route.contains("reset-password") { let redirection = match (route, &self.user_info, &self.redirect_to) {
if self.password_reset_enabled == Some(false) { (
self.route_dispatcher Some(AppRoute::StartResetPassword | AppRoute::FinishResetPassword { token: _ }),
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login))); _,
} _,
return; ) if self.password_reset_enabled == Some(false) => Some(AppRoute::Login),
} (None, _, _) | (_, None, _) => Some(AppRoute::Login),
match &self.user_info { // User is logged in, a URL was given, don't redirect.
None => { (_, Some(_), Some(_)) => None,
self.route_dispatcher (_, Some((user_name, is_admin)), None) => {
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
}
Some((user_name, is_admin)) => match &self.redirect_to {
Some(url) => {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(url.clone())));
}
None => {
if *is_admin { if *is_admin {
self.route_dispatcher Some(AppRoute::ListUsers)
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::ListUsers)));
} else { } else {
self.route_dispatcher Some(AppRoute::UserDetails {
.send(RouteRequest::ReplaceRoute(Route::from( user_id: user_name.clone(),
AppRoute::UserDetails(user_name.clone()), })
)));
} }
} }
}, };
if let Some(redirect_to) = redirection {
history.push(redirect_to);
} }
} }
fn dispatch_route( fn dispatch_route(
switch: AppRoute, switch: &AppRoute,
link: &ComponentLink<Self>, link: &Scope<Self>,
is_admin: bool, is_admin: bool,
password_reset_enabled: Option<bool>, password_reset_enabled: Option<bool>,
) -> Html { ) -> Html {
@ -204,10 +192,10 @@ impl App {
AppRoute::Index | AppRoute::ListUsers => html! { AppRoute::Index | AppRoute::ListUsers => html! {
<div> <div>
<UserTable /> <UserTable />
<NavButton classes="btn btn-primary" route={AppRoute::CreateUser}> <Link classes="btn btn-primary" to={AppRoute::CreateUser}>
<i class="bi-person-plus me-2"></i> <i class="bi-person-plus me-2"></i>
{"Create a user"} {"Create a user"}
</NavButton> </Link>
</div> </div>
}, },
AppRoute::CreateGroup => html! { AppRoute::CreateGroup => html! {
@ -216,41 +204,40 @@ impl App {
AppRoute::ListGroups => html! { AppRoute::ListGroups => html! {
<div> <div>
<GroupTable /> <GroupTable />
<NavButton classes="btn btn-primary" route={AppRoute::CreateGroup}> <Link classes="btn btn-primary" to={AppRoute::CreateGroup}>
<i class="bi-plus-circle me-2"></i> <i class="bi-plus-circle me-2"></i>
{"Create a group"} {"Create a group"}
</NavButton> </Link>
</div> </div>
}, },
AppRoute::GroupDetails(group_id) => html! { AppRoute::GroupDetails { group_id } => html! {
<GroupDetails group_id={group_id} /> <GroupDetails group_id={*group_id} />
}, },
AppRoute::UserDetails(username) => html! { AppRoute::UserDetails { user_id } => html! {
<UserDetails username={username} is_admin={is_admin} /> <UserDetails username={user_id.clone()} is_admin={is_admin} />
}, },
AppRoute::ChangePassword(username) => html! { AppRoute::ChangePassword { user_id } => html! {
<ChangePasswordForm username={username} is_admin={is_admin} /> <ChangePasswordForm username={user_id.clone()} is_admin={is_admin} />
}, },
AppRoute::StartResetPassword => match password_reset_enabled { AppRoute::StartResetPassword => match password_reset_enabled {
Some(true) => html! { <ResetPasswordStep1Form /> }, Some(true) => html! { <ResetPasswordStep1Form /> },
Some(false) => { Some(false) => {
App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) html! { <Redirect to={AppRoute::Login}/> }
} }
None => html! {}, None => html! {},
}, },
AppRoute::FinishResetPassword(token) => match password_reset_enabled { AppRoute::FinishResetPassword { token } => match password_reset_enabled {
Some(true) => html! { <ResetPasswordStep2Form token={token} /> }, Some(true) => html! { <ResetPasswordStep2Form token={token.clone()} /> },
Some(false) => { Some(false) => {
App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled) html! { <Redirect to={AppRoute::Login}/> }
} }
None => html! {}, None => html! {},
}, },
} }
} }
fn view_banner(&self) -> Html { fn view_banner(&self, ctx: &Context<Self>) -> Html {
let link = &self.link;
html! { html! {
<header class="p-2 mb-3 border-bottom"> <header class="p-2 mb-3 border-bottom">
<div class="container"> <div class="container">
@ -265,7 +252,7 @@ impl App {
<li> <li>
<Link <Link
classes="nav-link px-2 link-dark h6" classes="nav-link px-2 link-dark h6"
route={AppRoute::ListUsers}> to={AppRoute::ListUsers}>
<i class="bi-people me-2"></i> <i class="bi-people me-2"></i>
{"Users"} {"Users"}
</Link> </Link>
@ -273,7 +260,7 @@ impl App {
<li> <li>
<Link <Link
classes="nav-link px-2 link-dark h6" classes="nav-link px-2 link-dark h6"
route={AppRoute::ListGroups}> to={AppRoute::ListGroups}>
<i class="bi-collection me-2"></i> <i class="bi-collection me-2"></i>
{"Groups"} {"Groups"}
</Link> </Link>
@ -281,9 +268,16 @@ impl App {
</> </>
} } else { html!{} } } } } else { html!{} } }
</ul> </ul>
{ self.view_user_menu(ctx) }
</div>
</div>
</header>
}
}
{ fn view_user_menu(&self, ctx: &Context<Self>) -> Html {
if let Some((user_id, _)) = &self.user_info { if let Some((user_id, _)) = &self.user_info {
let link = ctx.link();
html! { html! {
<div class="dropdown text-end"> <div class="dropdown text-end">
<a href="#" <a href="#"
@ -311,7 +305,7 @@ impl App {
<li> <li>
<Link <Link
classes="dropdown-item" classes="dropdown-item"
route={AppRoute::UserDetails(user_id.clone())}> to={AppRoute::UserDetails{ user_id: user_id.clone() }}>
{"View details"} {"View details"}
</Link> </Link>
</li> </li>
@ -322,11 +316,8 @@ impl App {
</ul> </ul>
</div> </div>
} }
} else { html!{} } } else {
} html! {}
</div>
</div>
</header>
} }
} }

View File

@ -1,21 +1,18 @@
use crate::{ use crate::{
components::router::{AppRoute, NavButton}, components::router::{AppRoute, Link},
infra::{ infra::{
api::HostService, api::HostService,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
}, },
}; };
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Result};
use gloo_console::error; use gloo_console::error;
use lldap_auth::*; use lldap_auth::*;
use validator_derive::Validate; use validator_derive::Validate;
use yew::prelude::*; use yew::prelude::*;
use yew_form::Form; use yew_form::Form;
use yew_form_derive::Model; use yew_form_derive::Model;
use yew_router::{ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
#[derive(PartialEq, Eq, Default)] #[derive(PartialEq, Eq, Default)]
enum OpaqueData { enum OpaqueData {
@ -57,7 +54,6 @@ pub struct ChangePasswordForm {
common: CommonComponentParts<Self>, common: CommonComponentParts<Self>,
form: Form<FormModel>, form: Form<FormModel>,
opaque_data: OpaqueData, opaque_data: OpaqueData,
route_dispatcher: RouteAgentDispatcher,
} }
#[derive(Clone, PartialEq, Eq, Properties)] #[derive(Clone, PartialEq, Eq, Properties)]
@ -76,15 +72,20 @@ pub enum Msg {
} }
impl CommonComponent<ChangePasswordForm> for ChangePasswordForm { impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
use anyhow::Context;
match msg { match msg {
Msg::FormUpdate => Ok(true), Msg::FormUpdate => Ok(true),
Msg::Submit => { Msg::Submit => {
if !self.form.validate() { if !self.form.validate() {
bail!("Check the form for errors"); bail!("Check the form for errors");
} }
if self.common.is_admin { if ctx.props().is_admin {
self.handle_msg(Msg::SubmitNewPassword) self.handle_msg(ctx, Msg::SubmitNewPassword)
} else { } else {
let old_password = self.form.model().old_password; let old_password = self.form.model().old_password;
if old_password.is_empty() { if old_password.is_empty() {
@ -96,14 +97,14 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
.context("Could not initialize login")?; .context("Could not initialize login")?;
self.opaque_data = OpaqueData::Login(login_start_request.state); self.opaque_data = OpaqueData::Login(login_start_request.state);
let req = login::ClientLoginStartRequest { let req = login::ClientLoginStartRequest {
username: self.common.username.clone(), username: ctx.props().username.clone(),
login_start_request: login_start_request.message, login_start_request: login_start_request.message,
}; };
self.common.call_backend( self.common.call_backend(
HostService::login_start, ctx,
req, HostService::login_start(req),
Msg::AuthenticationStartResponse, Msg::AuthenticationStartResponse,
)?; );
Ok(true) Ok(true)
} }
} }
@ -122,7 +123,7 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
} }
_ => panic!("Unexpected data in opaque_data field"), _ => panic!("Unexpected data in opaque_data field"),
}; };
self.handle_msg(Msg::SubmitNewPassword) self.handle_msg(ctx, Msg::SubmitNewPassword)
} }
Msg::SubmitNewPassword => { Msg::SubmitNewPassword => {
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
@ -131,15 +132,15 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
opaque::client::registration::start_registration(&new_password, &mut rng) opaque::client::registration::start_registration(&new_password, &mut rng)
.context("Could not initiate password change")?; .context("Could not initiate password change")?;
let req = registration::ClientRegistrationStartRequest { let req = registration::ClientRegistrationStartRequest {
username: self.common.username.clone(), username: ctx.props().username.clone(),
registration_start_request: registration_start_request.message, registration_start_request: registration_start_request.message,
}; };
self.opaque_data = OpaqueData::Registration(registration_start_request.state); self.opaque_data = OpaqueData::Registration(registration_start_request.state);
self.common.call_backend( self.common.call_backend(
HostService::register_start, ctx,
req, HostService::register_start(req),
Msg::RegistrationStartResponse, Msg::RegistrationStartResponse,
)?; );
Ok(true) Ok(true)
} }
Msg::RegistrationStartResponse(res) => { Msg::RegistrationStartResponse(res) => {
@ -159,22 +160,20 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
registration_upload: registration_finish.message, registration_upload: registration_finish.message,
}; };
self.common.call_backend( self.common.call_backend(
HostService::register_finish, ctx,
req, HostService::register_finish(req),
Msg::RegistrationFinishResponse, Msg::RegistrationFinishResponse,
) );
} }
_ => panic!("Unexpected data in opaque_data field"), _ => panic!("Unexpected data in opaque_data field"),
}?; };
Ok(false) Ok(false)
} }
Msg::RegistrationFinishResponse(response) => { Msg::RegistrationFinishResponse(response) => {
self.common.cancel_task();
if response.is_ok() { if response.is_ok() {
self.route_dispatcher ctx.link().history().unwrap().push(AppRoute::UserDetails {
.send(RouteRequest::ChangeRoute(Route::from( user_id: ctx.props().username.clone(),
AppRoute::UserDetails(self.common.username.clone()), });
)));
} }
response?; response?;
Ok(true) Ok(true)
@ -191,26 +190,21 @@ impl Component for ChangePasswordForm {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
ChangePasswordForm { ChangePasswordForm {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<FormModel>::new(FormModel::default()), form: yew_form::Form::<FormModel>::new(FormModel::default()),
opaque_data: OpaqueData::None, opaque_data: OpaqueData::None,
route_dispatcher: RouteAgentDispatcher::new(),
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let is_admin = ctx.props().is_admin;
} let link = ctx.link();
fn view(&self) -> Html {
let is_admin = self.common.is_admin;
let link = &self.common;
type Field = yew_form::Field<FormModel>; type Field = yew_form::Field<FormModel>;
html! { html! {
<> <>
@ -305,12 +299,12 @@ impl Component for ChangePasswordForm {
<i class="bi-save me-2"></i> <i class="bi-save me-2"></i>
{"Save changes"} {"Save changes"}
</button> </button>
<NavButton <Link
classes="btn btn-secondary ms-2 col-auto col-form-label" classes="btn btn-secondary ms-2 col-auto col-form-label"
route={AppRoute::UserDetails(self.common.username.clone())}> to={AppRoute::UserDetails{user_id: ctx.props().username.clone()}}>
<i class="bi-arrow-return-left me-2"></i> <i class="bi-arrow-return-left me-2"></i>
{"Back"} {"Back"}
</NavButton> </Link>
</div> </div>
</form> </form>
</> </>

View File

@ -8,10 +8,7 @@ use graphql_client::GraphQLQuery;
use validator_derive::Validate; use validator_derive::Validate;
use yew::prelude::*; use yew::prelude::*;
use yew_form_derive::Model; use yew_form_derive::Model;
use yew_router::{ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
#[derive(GraphQLQuery)] #[derive(GraphQLQuery)]
#[graphql( #[graphql(
@ -24,7 +21,6 @@ pub struct CreateGroup;
pub struct CreateGroupForm { pub struct CreateGroupForm {
common: CommonComponentParts<Self>, common: CommonComponentParts<Self>,
route_dispatcher: RouteAgentDispatcher,
form: yew_form::Form<CreateGroupModel>, form: yew_form::Form<CreateGroupModel>,
} }
@ -41,7 +37,11 @@ pub enum Msg {
} }
impl CommonComponent<CreateGroupForm> for CreateGroupForm { impl CommonComponent<CreateGroupForm> for CreateGroupForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::Update => Ok(true), Msg::Update => Ok(true),
Msg::SubmitForm => { Msg::SubmitForm => {
@ -53,6 +53,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
name: model.groupname, name: model.groupname,
}; };
self.common.call_graphql::<CreateGroup, _>( self.common.call_graphql::<CreateGroup, _>(
ctx,
req, req,
Msg::CreateGroupResponse, Msg::CreateGroupResponse,
"Error trying to create group", "Error trying to create group",
@ -64,8 +65,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
"Created group '{}'", "Created group '{}'",
&response?.create_group.display_name &response?.create_group.display_name
)); ));
self.route_dispatcher ctx.link().history().unwrap().push(AppRoute::ListGroups);
.send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListGroups)));
Ok(true) Ok(true)
} }
} }
@ -80,24 +80,19 @@ impl Component for CreateGroupForm {
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Self { Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
route_dispatcher: RouteAgentDispatcher::new(),
form: yew_form::Form::<CreateGroupModel>::new(CreateGroupModel::default()), form: yew_form::Form::<CreateGroupModel>::new(CreateGroupModel::default()),
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
type Field = yew_form::Field<CreateGroupModel>; type Field = yew_form::Field<CreateGroupModel>;
html! { html! {
<div class="row justify-content-center"> <div class="row justify-content-center">

View File

@ -5,17 +5,14 @@ use crate::{
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
}, },
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Result};
use gloo_console::log; use gloo_console::log;
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use lldap_auth::{opaque, registration}; use lldap_auth::{opaque, registration};
use validator_derive::Validate; use validator_derive::Validate;
use yew::prelude::*; use yew::prelude::*;
use yew_form_derive::Model; use yew_form_derive::Model;
use yew_router::{ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
#[derive(GraphQLQuery)] #[derive(GraphQLQuery)]
#[graphql( #[graphql(
@ -28,7 +25,6 @@ pub struct CreateUser;
pub struct CreateUserForm { pub struct CreateUserForm {
common: CommonComponentParts<Self>, common: CommonComponentParts<Self>,
route_dispatcher: RouteAgentDispatcher,
form: yew_form::Form<CreateUserModel>, form: yew_form::Form<CreateUserModel>,
} }
@ -73,7 +69,11 @@ pub enum Msg {
} }
impl CommonComponent<CreateUserForm> for CreateUserForm { impl CommonComponent<CreateUserForm> for CreateUserForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::Update => Ok(true), Msg::Update => Ok(true),
Msg::SubmitForm => { Msg::SubmitForm => {
@ -93,6 +93,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
}, },
}; };
self.common.call_graphql::<CreateUser, _>( self.common.call_graphql::<CreateUser, _>(
ctx,
req, req,
Msg::CreateUserResponse, Msg::CreateUserResponse,
"Error trying to create user", "Error trying to create user",
@ -122,12 +123,11 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
registration_start_request: message, registration_start_request: message,
}; };
self.common self.common
.call_backend(HostService::register_start, req, move |r| { .call_backend(ctx, HostService::register_start(req), move |r| {
Msg::RegistrationStartResponse((state, r)) Msg::RegistrationStartResponse((state, r))
}) });
.context("Error trying to create user")?;
} else { } else {
self.update(Msg::SuccessfulCreation); self.update(ctx, Msg::SuccessfulCreation);
} }
Ok(false) Ok(false)
} }
@ -143,22 +143,19 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
server_data: response.server_data, server_data: response.server_data,
registration_upload: registration_upload.message, registration_upload: registration_upload.message,
}; };
self.common self.common.call_backend(
.call_backend( ctx,
HostService::register_finish, HostService::register_finish(req),
req,
Msg::RegistrationFinishResponse, Msg::RegistrationFinishResponse,
) );
.context("Error trying to register user")?;
Ok(false) Ok(false)
} }
Msg::RegistrationFinishResponse(response) => { Msg::RegistrationFinishResponse(response) => {
response?; response?;
self.handle_msg(Msg::SuccessfulCreation) self.handle_msg(ctx, Msg::SuccessfulCreation)
} }
Msg::SuccessfulCreation => { Msg::SuccessfulCreation => {
self.route_dispatcher ctx.link().history().unwrap().push(AppRoute::ListUsers);
.send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListUsers)));
Ok(true) Ok(true)
} }
} }
@ -173,24 +170,19 @@ impl Component for CreateUserForm {
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Self { Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
route_dispatcher: RouteAgentDispatcher::new(),
form: yew_form::Form::<CreateUserModel>::new(CreateUserModel::default()), form: yew_form::Form::<CreateUserModel>::new(CreateUserModel::default()),
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = &ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
type Field = yew_form::Field<CreateUserModel>; type Field = yew_form::Field<CreateUserModel>;
html! { html! {
<div class="row justify-content-center"> <div class="row justify-content-center">

View File

@ -39,16 +39,21 @@ pub enum Msg {
} }
impl CommonComponent<DeleteGroup> for DeleteGroup { impl CommonComponent<DeleteGroup> for DeleteGroup {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::ClickedDeleteGroup => { Msg::ClickedDeleteGroup => {
self.modal.as_ref().expect("modal not initialized").show(); self.modal.as_ref().expect("modal not initialized").show();
} }
Msg::ConfirmDeleteGroup => { Msg::ConfirmDeleteGroup => {
self.update(Msg::DismissModal); self.update(ctx, Msg::DismissModal);
self.common.call_graphql::<DeleteGroupQuery, _>( self.common.call_graphql::<DeleteGroupQuery, _>(
ctx,
delete_group_query::Variables { delete_group_query::Variables {
group_id: self.common.group.id, group_id: ctx.props().group.id,
}, },
Msg::DeleteGroupResponse, Msg::DeleteGroupResponse,
"Error trying to delete group", "Error trying to delete group",
@ -58,12 +63,8 @@ impl CommonComponent<DeleteGroup> for DeleteGroup {
self.modal.as_ref().expect("modal not initialized").hide(); self.modal.as_ref().expect("modal not initialized").hide();
} }
Msg::DeleteGroupResponse(response) => { Msg::DeleteGroupResponse(response) => {
self.common.cancel_task();
response?; response?;
self.common ctx.props().on_group_deleted.emit(ctx.props().group.id);
.props
.on_group_deleted
.emit(self.common.group.id);
} }
} }
Ok(true) Ok(true)
@ -78,15 +79,15 @@ impl Component for DeleteGroup {
type Message = Msg; type Message = Msg;
type Properties = DeleteGroupProps; type Properties = DeleteGroupProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Self { Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
node_ref: NodeRef::default(), node_ref: NodeRef::default(),
modal: None, modal: None,
} }
} }
fn rendered(&mut self, first_render: bool) { fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
if first_render { if first_render {
self.modal = Some(Modal::new( self.modal = Some(Modal::new(
self.node_ref self.node_ref
@ -96,20 +97,17 @@ impl Component for DeleteGroup {
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error( CommonComponentParts::<Self>::update_and_report_error(
self, self,
ctx,
msg, msg,
self.common.on_error.clone(), ctx.props().on_error.clone(),
) )
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = &ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
html! { html! {
<> <>
<button <button
@ -118,19 +116,19 @@ impl Component for DeleteGroup {
onclick={link.callback(|_| Msg::ClickedDeleteGroup)}> onclick={link.callback(|_| Msg::ClickedDeleteGroup)}>
<i class="bi-x-circle-fill" aria-label="Delete group" /> <i class="bi-x-circle-fill" aria-label="Delete group" />
</button> </button>
{self.show_modal()} {self.show_modal(ctx)}
</> </>
} }
} }
} }
impl DeleteGroup { impl DeleteGroup {
fn show_modal(&self) -> Html { fn show_modal(&self, ctx: &Context<Self>) -> Html {
let link = &self.common; let link = &ctx.link();
html! { html! {
<div <div
class="modal fade" class="modal fade"
id={"deleteGroupModal".to_string() + &self.common.group.id.to_string()} id={"deleteGroupModal".to_string() + &ctx.props().group.id.to_string()}
tabindex="-1" tabindex="-1"
aria-labelledby="deleteGroupModalLabel" aria-labelledby="deleteGroupModalLabel"
aria-hidden="true" aria-hidden="true"
@ -148,7 +146,7 @@ impl DeleteGroup {
<div class="modal-body"> <div class="modal-body">
<span> <span>
{"Are you sure you want to delete group "} {"Are you sure you want to delete group "}
<b>{&self.common.group.display_name}</b>{"?"} <b>{&ctx.props().group.display_name}</b>{"?"}
</span> </span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -36,16 +36,21 @@ pub enum Msg {
} }
impl CommonComponent<DeleteUser> for DeleteUser { impl CommonComponent<DeleteUser> for DeleteUser {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::ClickedDeleteUser => { Msg::ClickedDeleteUser => {
self.modal.as_ref().expect("modal not initialized").show(); self.modal.as_ref().expect("modal not initialized").show();
} }
Msg::ConfirmDeleteUser => { Msg::ConfirmDeleteUser => {
self.update(Msg::DismissModal); self.update(ctx, Msg::DismissModal);
self.common.call_graphql::<DeleteUserQuery, _>( self.common.call_graphql::<DeleteUserQuery, _>(
ctx,
delete_user_query::Variables { delete_user_query::Variables {
user: self.common.username.clone(), user: ctx.props().username.clone(),
}, },
Msg::DeleteUserResponse, Msg::DeleteUserResponse,
"Error trying to delete user", "Error trying to delete user",
@ -55,12 +60,10 @@ impl CommonComponent<DeleteUser> for DeleteUser {
self.modal.as_ref().expect("modal not initialized").hide(); self.modal.as_ref().expect("modal not initialized").hide();
} }
Msg::DeleteUserResponse(response) => { Msg::DeleteUserResponse(response) => {
self.common.cancel_task();
response?; response?;
self.common ctx.props()
.props
.on_user_deleted .on_user_deleted
.emit(self.common.username.clone()); .emit(ctx.props().username.clone());
} }
} }
Ok(true) Ok(true)
@ -75,15 +78,15 @@ impl Component for DeleteUser {
type Message = Msg; type Message = Msg;
type Properties = DeleteUserProps; type Properties = DeleteUserProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Self { Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
node_ref: NodeRef::default(), node_ref: NodeRef::default(),
modal: None, modal: None,
} }
} }
fn rendered(&mut self, first_render: bool) { fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
if first_render { if first_render {
self.modal = Some(Modal::new( self.modal = Some(Modal::new(
self.node_ref self.node_ref
@ -93,20 +96,17 @@ impl Component for DeleteUser {
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error( CommonComponentParts::<Self>::update_and_report_error(
self, self,
ctx,
msg, msg,
self.common.on_error.clone(), ctx.props().on_error.clone(),
) )
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = &ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
html! { html! {
<> <>
<button <button
@ -115,19 +115,19 @@ impl Component for DeleteUser {
onclick={link.callback(|_| Msg::ClickedDeleteUser)}> onclick={link.callback(|_| Msg::ClickedDeleteUser)}>
<i class="bi-x-circle-fill" aria-label="Delete user" /> <i class="bi-x-circle-fill" aria-label="Delete user" />
</button> </button>
{self.show_modal()} {self.show_modal(ctx)}
</> </>
} }
} }
} }
impl DeleteUser { impl DeleteUser {
fn show_modal(&self) -> Html { fn show_modal(&self, ctx: &Context<Self>) -> Html {
let link = &self.common; let link = &ctx.link();
html! { html! {
<div <div
class="modal fade" class="modal fade"
id={"deleteUserModal".to_string() + &self.common.username} id={"deleteUserModal".to_string() + &ctx.props().username}
tabindex="-1" tabindex="-1"
//role="dialog" //role="dialog"
aria-labelledby="deleteUserModalLabel" aria-labelledby="deleteUserModalLabel"
@ -146,7 +146,7 @@ impl DeleteUser {
<div class="modal-body"> <div class="modal-body">
<span> <span>
{"Are you sure you want to delete user "} {"Are you sure you want to delete user "}
<b>{&self.common.username}</b>{"?"} <b>{&ctx.props().username}</b>{"?"}
</span> </span>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -46,10 +46,11 @@ pub struct Props {
} }
impl GroupDetails { impl GroupDetails {
fn get_group_details(&mut self) { fn get_group_details(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<GetGroupDetails, _>( self.common.call_graphql::<GetGroupDetails, _>(
ctx,
get_group_details::Variables { get_group_details::Variables {
id: self.common.group_id, id: ctx.props().group_id,
}, },
Msg::GroupDetailsResponse, Msg::GroupDetailsResponse,
"Error trying to fetch group details", "Error trying to fetch group details",
@ -107,14 +108,15 @@ impl GroupDetails {
} }
} }
fn view_user_list(&self, g: &Group) -> Html { fn view_user_list(&self, ctx: &Context<Self>, g: &Group) -> Html {
let link = ctx.link();
let make_user_row = |user: &User| { let make_user_row = |user: &User| {
let user_id = user.id.clone(); let user_id = user.id.clone();
let display_name = user.display_name.clone(); let display_name = user.display_name.clone();
html! { html! {
<tr> <tr>
<td> <td>
<Link route={AppRoute::UserDetails(user_id.clone())}> <Link to={AppRoute::UserDetails{user_id: user_id.clone()}}>
{user_id.clone()} {user_id.clone()}
</Link> </Link>
</td> </td>
@ -123,8 +125,8 @@ impl GroupDetails {
<RemoveUserFromGroupComponent <RemoveUserFromGroupComponent
username={user_id} username={user_id}
group_id={g.id} group_id={g.id}
on_user_removed_from_group={self.common.callback(Msg::OnUserRemovedFromGroup)} on_user_removed_from_group={link.callback(Msg::OnUserRemovedFromGroup)}
on_error={self.common.callback(Msg::OnError)}/> on_error={link.callback(Msg::OnError)}/>
</td> </td>
</tr> </tr>
} }
@ -159,7 +161,8 @@ impl GroupDetails {
} }
} }
fn view_add_user_button(&self, g: &Group) -> Html { fn view_add_user_button(&self, ctx: &Context<Self>, g: &Group) -> Html {
let link = ctx.link();
let users: Vec<_> = g let users: Vec<_> = g
.users .users
.iter() .iter()
@ -172,14 +175,14 @@ impl GroupDetails {
<AddGroupMemberComponent <AddGroupMemberComponent
group_id={g.id} group_id={g.id}
users={users} users={users}
on_error={self.common.callback(Msg::OnError)} on_error={link.callback(Msg::OnError)}
on_user_added_to_group={self.common.callback(Msg::OnUserAddedToGroup)}/> on_user_added_to_group={link.callback(Msg::OnUserAddedToGroup)}/>
} }
} }
} }
impl CommonComponent<GroupDetails> for GroupDetails { impl CommonComponent<GroupDetails> for GroupDetails {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg { match msg {
Msg::GroupDetailsResponse(response) => match response { Msg::GroupDetailsResponse(response) => match response {
Ok(group) => self.group = Some(group.group), Ok(group) => self.group = Some(group.group),
@ -215,24 +218,20 @@ impl Component for GroupDetails {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut table = Self { let mut table = Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
group: None, group: None,
}; };
table.get_group_details(); table.get_group_details(ctx);
table table
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
match (&self.group, &self.common.error) { match (&self.group, &self.common.error) {
(None, None) => html! {{"Loading..."}}, (None, None) => html! {{"Loading..."}},
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>}, (None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
@ -240,8 +239,8 @@ impl Component for GroupDetails {
html! { html! {
<div> <div>
{self.view_details(u)} {self.view_details(u)}
{self.view_user_list(u)} {self.view_user_list(ctx, u)}
{self.view_add_user_button(u)} {self.view_add_user_button(ctx, u)}
{self.view_messages(error)} {self.view_messages(error)}
</div> </div>
} }

View File

@ -34,7 +34,7 @@ pub enum Msg {
} }
impl CommonComponent<GroupTable> for GroupTable { impl CommonComponent<GroupTable> for GroupTable {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg { match msg {
Msg::ListGroupsResponse(groups) => { Msg::ListGroupsResponse(groups) => {
self.groups = Some(groups?.groups.into_iter().collect()); self.groups = Some(groups?.groups.into_iter().collect());
@ -58,12 +58,13 @@ impl Component for GroupTable {
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut table = GroupTable { let mut table = GroupTable {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
groups: None, groups: None,
}; };
table.common.call_graphql::<GetGroupList, _>( table.common.call_graphql::<GetGroupList, _>(
ctx,
get_group_list::Variables {}, get_group_list::Variables {},
Msg::ListGroupsResponse, Msg::ListGroupsResponse,
"Error trying to fetch groups", "Error trying to fetch groups",
@ -71,18 +72,14 @@ impl Component for GroupTable {
table table
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
html! { html! {
<div> <div>
{self.view_groups()} {self.view_groups(ctx)}
{self.view_errors()} {self.view_errors()}
</div> </div>
} }
@ -90,7 +87,7 @@ impl Component for GroupTable {
} }
impl GroupTable { impl GroupTable {
fn view_groups(&self) -> Html { fn view_groups(&self, ctx: &Context<Self>) -> Html {
let make_table = |groups: &Vec<Group>| { let make_table = |groups: &Vec<Group>| {
html! { html! {
<div class="table-responsive"> <div class="table-responsive">
@ -103,7 +100,7 @@ impl GroupTable {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{groups.iter().map(|u| self.view_group(u)).collect::<Vec<_>>()} {groups.iter().map(|u| self.view_group(ctx, u)).collect::<Vec<_>>()}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -115,11 +112,12 @@ impl GroupTable {
} }
} }
fn view_group(&self, group: &Group) -> Html { fn view_group(&self, ctx: &Context<Self>, group: &Group) -> Html {
let link = ctx.link();
html! { html! {
<tr key={group.id}> <tr key={group.id}>
<td> <td>
<Link route={AppRoute::GroupDetails(group.id)}> <Link to={AppRoute::GroupDetails{group_id: group.id}}>
{&group.display_name} {&group.display_name}
</Link> </Link>
</td> </td>
@ -129,8 +127,8 @@ impl GroupTable {
<td> <td>
<DeleteGroup <DeleteGroup
group={group.clone()} group={group.clone()}
on_group_deleted={self.common.callback(Msg::OnGroupDeleted)} on_group_deleted={link.callback(Msg::OnGroupDeleted)}
on_error={self.common.callback(Msg::OnError)}/> on_error={link.callback(Msg::OnError)}/>
</td> </td>
</tr> </tr>
} }

View File

@ -1,12 +1,12 @@
use crate::{ use crate::{
components::router::{AppRoute, NavButton}, components::router::{AppRoute, Link},
infra::{ infra::{
api::HostService, api::HostService,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
}, },
}; };
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Result};
use gloo_console::{debug, error}; use gloo_console::error;
use lldap_auth::*; use lldap_auth::*;
use validator_derive::Validate; use validator_derive::Validate;
use yew::prelude::*; use yew::prelude::*;
@ -48,7 +48,12 @@ pub enum Msg {
} }
impl CommonComponent<LoginForm> for LoginForm { impl CommonComponent<LoginForm> for LoginForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
use anyhow::Context;
match msg { match msg {
Msg::Update => Ok(true), Msg::Update => Ok(true),
Msg::Submit => { Msg::Submit => {
@ -65,9 +70,9 @@ impl CommonComponent<LoginForm> for LoginForm {
login_start_request: message, login_start_request: message,
}; };
self.common self.common
.call_backend(HostService::login_start, req, move |r| { .call_backend(ctx, HostService::login_start(req), move |r| {
Msg::AuthenticationStartResponse((state, r)) Msg::AuthenticationStartResponse((state, r))
})?; });
Ok(true) Ok(true)
} }
Msg::AuthenticationStartResponse((login_start, res)) => { Msg::AuthenticationStartResponse((login_start, res)) => {
@ -80,7 +85,6 @@ impl CommonComponent<LoginForm> for LoginForm {
// simple one to the user. // simple one to the user.
error!(&format!("Invalid username or password: {}", e)); error!(&format!("Invalid username or password: {}", e));
self.common.error = Some(anyhow!("Invalid username or password")); self.common.error = Some(anyhow!("Invalid username or password"));
self.common.cancel_task();
return Ok(true); return Ok(true);
} }
Ok(l) => l, Ok(l) => l,
@ -90,24 +94,22 @@ impl CommonComponent<LoginForm> for LoginForm {
credential_finalization: login_finish.message, credential_finalization: login_finish.message,
}; };
self.common.call_backend( self.common.call_backend(
HostService::login_finish, ctx,
req, HostService::login_finish(req),
Msg::AuthenticationFinishResponse, Msg::AuthenticationFinishResponse,
)?; );
Ok(false) Ok(false)
} }
Msg::AuthenticationFinishResponse(user_info) => { Msg::AuthenticationFinishResponse(user_info) => {
self.common.cancel_task(); ctx.props()
self.common
.on_logged_in .on_logged_in
.emit(user_info.context("Could not log in")?); .emit(user_info.context("Could not log in")?);
Ok(true) Ok(true)
} }
Msg::AuthenticationRefreshResponse(user_info) => { Msg::AuthenticationRefreshResponse(user_info) => {
self.refreshing = false; self.refreshing = false;
self.common.cancel_task();
if let Ok(user_info) = user_info { if let Ok(user_info) = user_info {
self.common.on_logged_in.emit(user_info); ctx.props().on_logged_in.emit(user_info);
} }
Ok(true) Ok(true)
} }
@ -123,34 +125,28 @@ impl Component for LoginForm {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut app = LoginForm { let mut app = LoginForm {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
form: Form::<FormModel>::new(FormModel::default()), form: Form::<FormModel>::new(FormModel::default()),
refreshing: true, refreshing: true,
}; };
if let Err(e) = app.common.call_backend(
app.common ctx,
.call_backend(HostService::refresh, (), Msg::AuthenticationRefreshResponse) HostService::refresh(),
{ Msg::AuthenticationRefreshResponse,
debug!(&format!("Could not refresh auth: {}", e)); );
app.refreshing = false;
}
app app
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
type Field = yew_form::Field<FormModel>; type Field = yew_form::Field<FormModel>;
let password_reset_enabled = self.common.password_reset_enabled; let password_reset_enabled = ctx.props().password_reset_enabled;
let link = &self.common; let link = &ctx.link();
if self.refreshing { if self.refreshing {
html! { html! {
<div> <div>
@ -204,12 +200,12 @@ impl Component for LoginForm {
</button> </button>
{ if password_reset_enabled { { if password_reset_enabled {
html! { html! {
<NavButton <Link
classes="btn-link btn" classes="btn-link btn"
disabled={self.common.is_task_running()} disabled={self.common.is_task_running()}
route={AppRoute::StartResetPassword}> to={AppRoute::StartResetPassword}>
{"Forgot your password?"} {"Forgot your password?"}
</NavButton> </Link>
} }
} else { } else {
html!{} html!{}

View File

@ -21,16 +21,20 @@ pub enum Msg {
} }
impl CommonComponent<LogoutButton> for LogoutButton { impl CommonComponent<LogoutButton> for LogoutButton {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::LogoutRequested => { Msg::LogoutRequested => {
self.common self.common
.call_backend(HostService::logout, (), Msg::LogoutCompleted)?; .call_backend(ctx, HostService::logout(), Msg::LogoutCompleted);
} }
Msg::LogoutCompleted(res) => { Msg::LogoutCompleted(res) => {
res?; res?;
delete_cookie("user_id")?; delete_cookie("user_id")?;
self.common.on_logged_out.emit(()); ctx.props().on_logged_out.emit(());
} }
} }
Ok(false) Ok(false)
@ -45,22 +49,18 @@ impl Component for LogoutButton {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
LogoutButton { LogoutButton {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = &ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
html! { html! {
<button <button
class="dropdown-item" class="dropdown-item"

View File

@ -31,15 +31,18 @@ pub enum Msg {
} }
impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupComponent { impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::SubmitRemoveGroup => self.submit_remove_group(), Msg::SubmitRemoveGroup => self.submit_remove_group(ctx),
Msg::RemoveGroupResponse(response) => { Msg::RemoveGroupResponse(response) => {
response?; response?;
self.common.cancel_task(); ctx.props()
self.common
.on_user_removed_from_group .on_user_removed_from_group
.emit((self.common.username.clone(), self.common.group_id)); .emit((ctx.props().username.clone(), ctx.props().group_id));
} }
} }
Ok(true) Ok(true)
@ -51,11 +54,12 @@ impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupCompon
} }
impl RemoveUserFromGroupComponent { impl RemoveUserFromGroupComponent {
fn submit_remove_group(&mut self) { fn submit_remove_group(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<RemoveUserFromGroup, _>( self.common.call_graphql::<RemoveUserFromGroup, _>(
ctx,
remove_user_from_group::Variables { remove_user_from_group::Variables {
user: self.common.username.clone(), user: ctx.props().username.clone(),
group: self.common.group_id, group: ctx.props().group_id,
}, },
Msg::RemoveGroupResponse, Msg::RemoveGroupResponse,
"Error trying to initiate removing the user from a group", "Error trying to initiate removing the user from a group",
@ -67,26 +71,23 @@ impl Component for RemoveUserFromGroupComponent {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Self { Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update_and_report_error( CommonComponentParts::<Self>::update_and_report_error(
self, self,
ctx,
msg, msg,
self.common.on_error.clone(), ctx.props().on_error.clone(),
) )
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = &ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
html! { html! {
<button <button
class="btn btn-danger" class="btn btn-danger"

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
components::router::{AppRoute, NavButton}, components::router::{AppRoute, Link},
infra::{ infra::{
api::HostService, api::HostService,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
@ -31,7 +31,11 @@ pub enum Msg {
} }
impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form { impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::Update => Ok(true), Msg::Update => Ok(true),
Msg::Submit => { Msg::Submit => {
@ -40,10 +44,10 @@ impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form {
} }
let FormModel { username } = self.form.model(); let FormModel { username } = self.form.model();
self.common.call_backend( self.common.call_backend(
HostService::reset_password_step1, ctx,
&username, HostService::reset_password_step1(username),
Msg::PasswordResetResponse, Msg::PasswordResetResponse,
)?; );
Ok(true) Ok(true)
} }
Msg::PasswordResetResponse(response) => { Msg::PasswordResetResponse(response) => {
@ -63,26 +67,22 @@ impl Component for ResetPasswordStep1Form {
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
ResetPasswordStep1Form { ResetPasswordStep1Form {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
form: Form::<FormModel>::new(FormModel::default()), form: Form::<FormModel>::new(FormModel::default()),
just_succeeded: false, just_succeeded: false,
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
self.just_succeeded = false; self.just_succeeded = false;
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
type Field = yew_form::Field<FormModel>; type Field = yew_form::Field<FormModel>;
let link = &self.common; let link = &ctx.link();
html! { html! {
<form <form
class="form center-block col-sm-4 col-offset-4"> class="form center-block col-sm-4 col-offset-4">
@ -113,16 +113,16 @@ impl Component for ResetPasswordStep1Form {
type="submit" type="submit"
class="btn btn-primary" class="btn btn-primary"
disabled={self.common.is_task_running()} disabled={self.common.is_task_running()}
onclick={self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}> onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
<i class="bi-check-circle me-2"/> <i class="bi-check-circle me-2"/>
{"Reset password"} {"Reset password"}
</button> </button>
<NavButton <Link
classes="btn-link btn" classes="btn-link btn"
disabled={self.common.is_task_running()} disabled={self.common.is_task_running()}
route={AppRoute::Login}> to={AppRoute::Login}>
{"Back"} {"Back"}
</NavButton> </Link>
</div> </div>
} }
}} }}

View File

@ -1,11 +1,11 @@
use crate::{ use crate::{
components::router::{AppRoute, NavButton}, components::router::{AppRoute, Link},
infra::{ infra::{
api::HostService, api::HostService,
common_component::{CommonComponent, CommonComponentParts}, common_component::{CommonComponent, CommonComponentParts},
}, },
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Result};
use lldap_auth::{ use lldap_auth::{
opaque::client::registration as opaque_registration, opaque::client::registration as opaque_registration,
password_reset::ServerPasswordResetResponse, registration, password_reset::ServerPasswordResetResponse, registration,
@ -14,10 +14,7 @@ use validator_derive::Validate;
use yew::prelude::*; use yew::prelude::*;
use yew_form::Form; use yew_form::Form;
use yew_form_derive::Model; use yew_form_derive::Model;
use yew_router::{ use yew_router::{prelude::History, scope_ext::RouterScopeExt};
agent::{RouteAgentDispatcher, RouteRequest},
route::Route,
};
/// The fields of the form, with the constraints. /// The fields of the form, with the constraints.
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)] #[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
@ -33,7 +30,6 @@ pub struct ResetPasswordStep2Form {
form: Form<FormModel>, form: Form<FormModel>,
username: Option<String>, username: Option<String>,
opaque_data: Option<opaque_registration::ClientRegistration>, opaque_data: Option<opaque_registration::ClientRegistration>,
route_dispatcher: RouteAgentDispatcher,
} }
#[derive(Clone, PartialEq, Eq, Properties)] #[derive(Clone, PartialEq, Eq, Properties)]
@ -50,11 +46,15 @@ pub enum Msg {
} }
impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form { impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
use anyhow::Context;
match msg { match msg {
Msg::ValidateTokenResponse(response) => { Msg::ValidateTokenResponse(response) => {
self.username = Some(response?.user_id); self.username = Some(response?.user_id);
self.common.cancel_task();
Ok(true) Ok(true)
} }
Msg::FormUpdate => Ok(true), Msg::FormUpdate => Ok(true),
@ -73,10 +73,10 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
}; };
self.opaque_data = Some(registration_start_request.state); self.opaque_data = Some(registration_start_request.state);
self.common.call_backend( self.common.call_backend(
HostService::register_start, ctx,
req, HostService::register_start(req),
Msg::RegistrationStartResponse, Msg::RegistrationStartResponse,
)?; );
Ok(true) Ok(true)
} }
Msg::RegistrationStartResponse(res) => { Msg::RegistrationStartResponse(res) => {
@ -94,17 +94,15 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
registration_upload: registration_finish.message, registration_upload: registration_finish.message,
}; };
self.common.call_backend( self.common.call_backend(
HostService::register_finish, ctx,
req, HostService::register_finish(req),
Msg::RegistrationFinishResponse, Msg::RegistrationFinishResponse,
)?; );
Ok(false) Ok(false)
} }
Msg::RegistrationFinishResponse(response) => { Msg::RegistrationFinishResponse(response) => {
self.common.cancel_task();
if response.is_ok() { if response.is_ok() {
self.route_dispatcher ctx.link().history().unwrap().push(AppRoute::Login);
.send(RouteRequest::ChangeRoute(Route::from(AppRoute::Login)));
} }
response?; response?;
Ok(true) Ok(true)
@ -121,36 +119,28 @@ impl Component for ResetPasswordStep2Form {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut component = ResetPasswordStep2Form { let mut component = ResetPasswordStep2Form {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::<FormModel>::new(FormModel::default()), form: yew_form::Form::<FormModel>::new(FormModel::default()),
opaque_data: None, opaque_data: None,
route_dispatcher: RouteAgentDispatcher::new(),
username: None, username: None,
}; };
let token = component.common.token.clone(); let token = ctx.props().token.clone();
component component.common.call_backend(
.common ctx,
.call_backend( HostService::reset_password_step2(token),
HostService::reset_password_step2,
&token,
Msg::ValidateTokenResponse, Msg::ValidateTokenResponse,
) );
.unwrap();
component component
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props) let link = &ctx.link();
}
fn view(&self) -> Html {
let link = &self.common;
match (&self.username, &self.common.error) { match (&self.username, &self.common.error) {
(None, None) => { (None, None) => {
return html! { return html! {
@ -163,12 +153,12 @@ impl Component for ResetPasswordStep2Form {
<div class="alert alert-danger"> <div class="alert alert-danger">
{e.to_string() } {e.to_string() }
</div> </div>
<NavButton <Link
classes="btn-link btn" classes="btn-link btn"
disabled={self.common.is_task_running()} disabled={self.common.is_task_running()}
route={AppRoute::Login}> to={AppRoute::Login}>
{"Back"} {"Back"}
</NavButton> </Link>
</> </>
} }
} }

View File

@ -1,34 +1,30 @@
use yew_router::{ use yew_router::Routable;
components::{RouterAnchor, RouterButton},
Switch,
};
#[derive(Switch, Debug, Clone)] #[derive(Routable, Debug, Clone, PartialEq)]
pub enum AppRoute { pub enum AppRoute {
#[to = "/login"] #[at("/login")]
Login, Login,
#[to = "/reset-password/step1"] #[at("/reset-password/step1")]
StartResetPassword, StartResetPassword,
#[to = "/reset-password/step2/{token}"] #[at("/reset-password/step2/:token")]
FinishResetPassword(String), FinishResetPassword { token: String },
#[to = "/users/create"] #[at("/users/create")]
CreateUser, CreateUser,
#[to = "/users"] #[at("/users")]
ListUsers, ListUsers,
#[to = "/user/{user_id}/password"] #[at("/user/:user_id/password")]
ChangePassword(String), ChangePassword { user_id: String },
#[to = "/user/{user_id}"] #[at("/user/:user_id")]
UserDetails(String), UserDetails { user_id: String },
#[to = "/groups/create"] #[at("/groups/create")]
CreateGroup, CreateGroup,
#[to = "/groups"] #[at("/groups")]
ListGroups, ListGroups,
#[to = "/group/{group_id}"] #[at("/group/:group_id")]
GroupDetails(i64), GroupDetails { group_id: i64 },
#[to = "/"] #[at("/")]
Index, Index,
} }
pub type Link = RouterAnchor<AppRoute>; pub type Link = yew_router::components::Link<AppRoute>;
pub type Redirect = yew_router::components::Redirect<AppRoute>;
pub type NavButton = RouterButton<AppRoute>;

View File

@ -1,9 +1,6 @@
use yew::{html::ChangeData, prelude::*}; use yew::prelude::*;
use yewtil::NeqAssign;
pub struct Select { pub struct Select {
link: ComponentLink<Self>,
props: SelectProps,
node_ref: NodeRef, node_ref: NodeRef,
} }
@ -14,100 +11,70 @@ pub struct SelectProps {
} }
pub enum SelectMsg { pub enum SelectMsg {
OnSelectChange(ChangeData), OnSelectChange,
} }
impl Select { impl Select {
fn get_nth_child_props(&self, nth: i32) -> Option<SelectOptionProps> { fn get_nth_child_props(&self, ctx: &Context<Self>, nth: i32) -> Option<SelectOptionProps> {
if nth == -1 { if nth == -1 {
return None; return None;
} }
self.props ctx.props()
.children .children
.iter() .iter()
.nth(nth as usize) .nth(nth as usize)
.map(|child| child.props) .map(|child| (*child.props).clone())
} }
fn send_selection_update(&self) { fn send_selection_update(&self, ctx: &Context<Self>) {
let select_node = self.node_ref.cast::<web_sys::HtmlSelectElement>().unwrap(); let select_node = self.node_ref.cast::<web_sys::HtmlSelectElement>().unwrap();
self.props ctx.props()
.on_selection_change .on_selection_change
.emit(self.get_nth_child_props(select_node.selected_index())) .emit(self.get_nth_child_props(ctx, select_node.selected_index()))
} }
} }
impl Component for Select { impl Component for Select {
type Message = SelectMsg; type Message = SelectMsg;
type Properties = SelectProps; type Properties = SelectProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Self { Self {
link,
props,
node_ref: NodeRef::default(), node_ref: NodeRef::default(),
} }
} }
fn rendered(&mut self, _first_render: bool) { fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
self.send_selection_update(); self.send_selection_update(ctx);
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, _: Self::Message) -> bool {
let SelectMsg::OnSelectChange(data) = msg; self.send_selection_update(ctx);
match data {
ChangeData::Select(_) => self.send_selection_update(),
_ => unreachable!(),
}
false false
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.props.children.neq_assign(props.children)
}
fn view(&self) -> Html {
html! { html! {
<select class="form-select" <select class="form-select"
ref={self.node_ref.clone()} ref={self.node_ref.clone()}
disabled={self.props.children.is_empty()} disabled={ctx.props().children.is_empty()}
onchange={self.link.callback(SelectMsg::OnSelectChange)}> onchange={ctx.link().callback(|_| SelectMsg::OnSelectChange)}>
{ self.props.children.clone() } { ctx.props().children.clone() }
</select> </select>
} }
} }
} }
pub struct SelectOption {
props: SelectOptionProps,
}
#[derive(yew::Properties, Clone, PartialEq, Eq, Debug)] #[derive(yew::Properties, Clone, PartialEq, Eq, Debug)]
pub struct SelectOptionProps { pub struct SelectOptionProps {
pub value: String, pub value: String,
pub text: String, pub text: String,
} }
impl Component for SelectOption { #[function_component(SelectOption)]
type Message = (); pub fn select_option(props: &SelectOptionProps) -> Html {
type Properties = SelectOptionProps;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
self.props.neq_assign(props)
}
fn view(&self) -> Html {
html! { html! {
<option value={self.props.value.clone()}> <option value={props.value.clone()}>
{&self.props.text} {&props.text}
</option> </option>
} }
}
} }

View File

@ -2,7 +2,7 @@ use crate::{
components::{ components::{
add_user_to_group::AddUserToGroupComponent, add_user_to_group::AddUserToGroupComponent,
remove_user_from_group::RemoveUserFromGroupComponent, remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, Link, NavButton}, router::{AppRoute, Link},
user_details_form::UserDetailsForm, user_details_form::UserDetailsForm,
}, },
infra::common_component::{CommonComponent, CommonComponentParts}, infra::common_component::{CommonComponent, CommonComponentParts},
@ -47,7 +47,7 @@ pub struct Props {
} }
impl CommonComponent<UserDetails> for UserDetails { impl CommonComponent<UserDetails> for UserDetails {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg { match msg {
Msg::UserDetailsResponse(response) => match response { Msg::UserDetailsResponse(response) => match response {
Ok(user) => self.user = Some(user.user), Ok(user) => self.user = Some(user.user),
@ -77,10 +77,11 @@ impl CommonComponent<UserDetails> for UserDetails {
} }
impl UserDetails { impl UserDetails {
fn get_user_details(&mut self) { fn get_user_details(&mut self, ctx: &Context<Self>) {
self.common.call_graphql::<GetUserDetails, _>( self.common.call_graphql::<GetUserDetails, _>(
ctx,
get_user_details::Variables { get_user_details::Variables {
id: self.common.username.clone(), id: ctx.props().username.clone(),
}, },
Msg::UserDetailsResponse, Msg::UserDetailsResponse,
"Error trying to fetch user details", "Error trying to fetch user details",
@ -99,16 +100,16 @@ impl UserDetails {
} }
} }
fn view_group_memberships(&self, u: &User) -> Html { fn view_group_memberships(&self, ctx: &Context<Self>, u: &User) -> Html {
let link = &self.common; let link = &ctx.link();
let make_group_row = |group: &Group| { let make_group_row = |group: &Group| {
let display_name = group.display_name.clone(); let display_name = group.display_name.clone();
html! { html! {
<tr key={"groupRow_".to_string() + &display_name}> <tr key={"groupRow_".to_string() + &display_name}>
{if self.common.is_admin { html! { {if ctx.props().is_admin { html! {
<> <>
<td> <td>
<Link route={AppRoute::GroupDetails(group.id)}> <Link to={AppRoute::GroupDetails{group_id: group.id}}>
{&group.display_name} {&group.display_name}
</Link> </Link>
</td> </td>
@ -134,7 +135,7 @@ impl UserDetails {
<thead> <thead>
<tr key="headerRow"> <tr key="headerRow">
<th>{"Group"}</th> <th>{"Group"}</th>
{ if self.common.is_admin { html!{ <th></th> }} else { html!{} }} { if ctx.props().is_admin { html!{ <th></th> }} else { html!{} }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -154,9 +155,9 @@ impl UserDetails {
} }
} }
fn view_add_group_button(&self, u: &User) -> Html { fn view_add_group_button(&self, ctx: &Context<Self>, u: &User) -> Html {
let link = &self.common; let link = &ctx.link();
if self.common.is_admin { if ctx.props().is_admin {
html! { html! {
<AddUserToGroupComponent <AddUserToGroupComponent
username={u.id.clone()} username={u.id.clone()}
@ -174,24 +175,20 @@ impl Component for UserDetails {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut table = Self { let mut table = Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
user: None, user: None,
}; };
table.get_user_details(); table.get_user_details(ctx);
table table
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
match (&self.user, &self.common.error) { match (&self.user, &self.common.error) {
(None, None) => html! {{"Loading..."}}, (None, None) => html! {{"Loading..."}},
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>}, (None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
@ -200,19 +197,19 @@ impl Component for UserDetails {
<> <>
<h3>{u.id.to_string()}</h3> <h3>{u.id.to_string()}</h3>
<div class="d-flex flex-row-reverse"> <div class="d-flex flex-row-reverse">
<NavButton <Link
route={AppRoute::ChangePassword(u.id.clone())} to={AppRoute::ChangePassword{user_id: u.id.clone()}}
classes="btn btn-secondary"> classes="btn btn-secondary">
<i class="bi-key me-2"></i> <i class="bi-key me-2"></i>
{"Modify password"} {"Modify password"}
</NavButton> </Link>
</div> </div>
<div> <div>
<h5 class="row m-3 fw-bold">{"User details"}</h5> <h5 class="row m-3 fw-bold">{"User details"}</h5>
</div> </div>
<UserDetailsForm user={u.clone()} /> <UserDetailsForm user={u.clone()} />
{self.view_group_memberships(u)} {self.view_group_memberships(ctx, u)}
{self.view_add_group_button(u)} {self.view_add_group_button(ctx, u)}
{self.view_messages(error)} {self.view_messages(error)}
</> </>
} }

View File

@ -5,15 +5,19 @@ use crate::{
infra::common_component::{CommonComponent, CommonComponentParts}, infra::common_component::{CommonComponent, CommonComponentParts},
}; };
use anyhow::{bail, Error, Result}; use anyhow::{bail, Error, Result};
use gloo_file::{
callbacks::{read_as_bytes, FileReader},
File,
};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use validator_derive::Validate; use validator_derive::Validate;
use wasm_bindgen::JsCast; use web_sys::{FileList, HtmlInputElement, InputEvent};
use yew::prelude::*; use yew::prelude::*;
use yew_form_derive::Model; use yew_form_derive::Model;
#[derive(PartialEq, Eq, Clone, Default)] #[derive(Default)]
struct JsFile { struct JsFile {
file: Option<web_sys::File>, file: Option<File>,
contents: Option<Vec<u8>>, contents: Option<Vec<u8>>,
} }
@ -21,7 +25,7 @@ impl ToString for JsFile {
fn to_string(&self) -> String { fn to_string(&self) -> String {
self.file self.file
.as_ref() .as_ref()
.map(web_sys::File::name) .map(File::name)
.unwrap_or_else(String::new) .unwrap_or_else(String::new)
} }
} }
@ -64,17 +68,21 @@ pub struct UserDetailsForm {
common: CommonComponentParts<Self>, common: CommonComponentParts<Self>,
form: yew_form::Form<UserModel>, form: yew_form::Form<UserModel>,
avatar: JsFile, avatar: JsFile,
reader: Option<FileReader>,
/// True if we just successfully updated the user, to display a success message. /// True if we just successfully updated the user, to display a success message.
just_updated: bool, just_updated: bool,
user: User,
} }
pub enum Msg { pub enum Msg {
/// A form field changed. /// A form field changed.
Update, Update,
/// A new file was selected.
FileSelected(File),
/// The "Submit" button was clicked. /// The "Submit" button was clicked.
SubmitClicked, SubmitClicked,
/// A picked file finished loading. /// A picked file finished loading.
FileLoaded(yew::services::reader::FileData), FileLoaded(String, Result<Vec<u8>>),
/// We got the response from the server about our update message. /// We got the response from the server about our update message.
UserUpdated(Result<update_user::ResponseData>), UserUpdated(Result<update_user::ResponseData>),
} }
@ -86,50 +94,47 @@ pub struct Props {
} }
impl CommonComponent<UserDetailsForm> for UserDetailsForm { impl CommonComponent<UserDetailsForm> for UserDetailsForm {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool> {
match msg { match msg {
Msg::Update => { Msg::Update => Ok(true),
let window = web_sys::window().expect("no global `window` exists"); Msg::FileSelected(new_avatar) => {
let document = window.document().expect("should have a document on window"); if self.avatar.file.as_ref().map(|f| f.name()) != Some(new_avatar.name()) {
let input = document let file_name = new_avatar.name();
.get_element_by_id("avatarInput") let link = ctx.link().clone();
.expect("Form field avatarInput should be present") self.reader = Some(read_as_bytes(&new_avatar, move |res| {
.dyn_into::<web_sys::HtmlInputElement>() link.send_message(Msg::FileLoaded(
.expect("Should be an HtmlInputElement"); file_name,
if let Some(files) = input.files() { res.map_err(|e| anyhow::anyhow!("{:#}", e)),
if files.length() > 0 { ))
let new_avatar = JsFile { }));
file: files.item(0), self.avatar = JsFile {
file: Some(new_avatar),
contents: None, contents: None,
}; };
if self.avatar.file.as_ref().map(|f| f.name())
!= new_avatar.file.as_ref().map(|f| f.name())
{
if let Some(ref file) = new_avatar.file {
self.mut_common().read_file(file.clone(), Msg::FileLoaded)?;
}
self.avatar = new_avatar;
}
}
} }
Ok(true) Ok(true)
} }
Msg::SubmitClicked => self.submit_user_update_form(), Msg::SubmitClicked => self.submit_user_update_form(ctx),
Msg::UserUpdated(response) => self.user_update_finished(response), Msg::UserUpdated(response) => self.user_update_finished(response),
Msg::FileLoaded(data) => { Msg::FileLoaded(file_name, data) => {
self.common.cancel_task();
if let Some(file) = &self.avatar.file { if let Some(file) = &self.avatar.file {
if file.name() == data.name { if file.name() == file_name {
if !is_valid_jpeg(data.content.as_slice()) { let data = data?;
if !is_valid_jpeg(data.as_slice()) {
// Clear the selection. // Clear the selection.
self.avatar = JsFile::default(); self.avatar = JsFile::default();
bail!("Chosen image is not a valid JPEG"); bail!("Chosen image is not a valid JPEG");
} else { } else {
self.avatar.contents = Some(data.content); self.avatar.contents = Some(data);
return Ok(true); return Ok(true);
} }
} }
} }
self.reader = None;
Ok(false) Ok(false)
} }
} }
@ -144,38 +149,36 @@ impl Component for UserDetailsForm {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let model = UserModel { let model = UserModel {
email: props.user.email.clone(), email: ctx.props().user.email.clone(),
display_name: props.user.display_name.clone(), display_name: ctx.props().user.display_name.clone(),
first_name: props.user.first_name.clone(), first_name: ctx.props().user.first_name.clone(),
last_name: props.user.last_name.clone(), last_name: ctx.props().user.last_name.clone(),
}; };
Self { Self {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
form: yew_form::Form::new(model), form: yew_form::Form::new(model),
avatar: JsFile::default(), avatar: JsFile::default(),
just_updated: false, just_updated: false,
reader: None,
user: ctx.props().user.clone(),
} }
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
self.just_updated = false; self.just_updated = false;
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
type Field = yew_form::Field<UserModel>; type Field = yew_form::Field<UserModel>;
let link = &self.common; let link = &ctx.link();
let avatar_base64 = maybe_to_base64(&self.avatar).unwrap_or_default(); let avatar_base64 = maybe_to_base64(&self.avatar).unwrap_or_default();
let avatar_string = avatar_base64 let avatar_string = avatar_base64
.as_deref() .as_deref()
.or(self.common.user.avatar.as_deref()) .or(self.user.avatar.as_deref())
.unwrap_or(""); .unwrap_or("");
html! { html! {
<div class="py-3"> <div class="py-3">
@ -186,7 +189,7 @@ impl Component for UserDetailsForm {
{"User ID: "} {"User ID: "}
</label> </label>
<div class="col-8"> <div class="col-8">
<span id="userId" class="form-control-static"><i>{&self.common.user.id}</i></span> <span id="userId" class="form-control-static"><i>{&self.user.id}</i></span>
</div> </div>
</div> </div>
<div class="form-group row mb-3"> <div class="form-group row mb-3">
@ -195,7 +198,7 @@ impl Component for UserDetailsForm {
{"Creation date: "} {"Creation date: "}
</label> </label>
<div class="col-8"> <div class="col-8">
<span id="creationDate" class="form-control-static">{&self.common.user.creation_date.naive_local().date()}</span> <span id="creationDate" class="form-control-static">{&self.user.creation_date.naive_local().date()}</span>
</div> </div>
</div> </div>
<div class="form-group row mb-3"> <div class="form-group row mb-3">
@ -204,7 +207,7 @@ impl Component for UserDetailsForm {
{"UUID: "} {"UUID: "}
</label> </label>
<div class="col-8"> <div class="col-8">
<span id="creationDate" class="form-control-static">{&self.common.user.uuid}</span> <span id="creationDate" class="form-control-static">{&self.user.uuid}</span>
</div> </div>
</div> </div>
<div class="form-group row mb-3"> <div class="form-group row mb-3">
@ -294,7 +297,10 @@ impl Component for UserDetailsForm {
id="avatarInput" id="avatarInput"
type="file" type="file"
accept="image/jpeg" accept="image/jpeg"
oninput={link.callback(|_| Msg::Update)} /> oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
Self::upload_files(input.files())
})} />
</div> </div>
<div class="col-4"> <div class="col-4">
<img <img
@ -335,7 +341,7 @@ impl Component for UserDetailsForm {
} }
impl UserDetailsForm { impl UserDetailsForm {
fn submit_user_update_form(&mut self) -> Result<bool> { fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
if !self.form.validate() { if !self.form.validate() {
bail!("Invalid inputs"); bail!("Invalid inputs");
} }
@ -346,9 +352,9 @@ impl UserDetailsForm {
{ {
bail!("Image file hasn't finished loading, try again"); bail!("Image file hasn't finished loading, try again");
} }
let base_user = &self.common.user; let base_user = &self.user;
let mut user_input = update_user::UpdateUserInput { let mut user_input = update_user::UpdateUserInput {
id: self.common.user.id.clone(), id: self.user.id.clone(),
email: None, email: None,
displayName: None, displayName: None,
firstName: None, firstName: None,
@ -377,6 +383,7 @@ impl UserDetailsForm {
} }
let req = update_user::Variables { user: user_input }; let req = update_user::Variables { user: user_input };
self.common.call_graphql::<UpdateUser, _>( self.common.call_graphql::<UpdateUser, _>(
ctx,
req, req,
Msg::UserUpdated, Msg::UserUpdated,
"Error trying to update user", "Error trying to update user",
@ -385,23 +392,30 @@ impl UserDetailsForm {
} }
fn user_update_finished(&mut self, r: Result<update_user::ResponseData>) -> Result<bool> { fn user_update_finished(&mut self, r: Result<update_user::ResponseData>) -> Result<bool> {
self.common.cancel_task(); r?;
match r {
Err(e) => return Err(e),
Ok(_) => {
let model = self.form.model(); let model = self.form.model();
self.common.user.email = model.email; self.user.email = model.email;
self.common.user.display_name = model.display_name; self.user.display_name = model.display_name;
self.common.user.first_name = model.first_name; self.user.first_name = model.first_name;
self.common.user.last_name = model.last_name; self.user.last_name = model.last_name;
if let Some(avatar) = maybe_to_base64(&self.avatar)? { if let Some(avatar) = maybe_to_base64(&self.avatar)? {
self.common.user.avatar = Some(avatar); self.user.avatar = Some(avatar);
} }
self.just_updated = true; self.just_updated = true;
}
};
Ok(true) Ok(true)
} }
fn upload_files(files: Option<FileList>) -> Msg {
if let Some(files) = files {
if files.length() > 0 {
Msg::FileSelected(File::from(files.item(0).unwrap()))
} else {
Msg::Update
}
} else {
Msg::Update
}
}
} }
fn is_valid_jpeg(bytes: &[u8]) -> bool { fn is_valid_jpeg(bytes: &[u8]) -> bool {

View File

@ -34,7 +34,7 @@ pub enum Msg {
} }
impl CommonComponent<UserTable> for UserTable { impl CommonComponent<UserTable> for UserTable {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
match msg { match msg {
Msg::ListUsersResponse(users) => { Msg::ListUsersResponse(users) => {
self.users = Some(users?.users.into_iter().collect()); self.users = Some(users?.users.into_iter().collect());
@ -55,8 +55,9 @@ impl CommonComponent<UserTable> for UserTable {
} }
impl UserTable { impl UserTable {
fn get_users(&mut self, req: Option<RequestFilter>) { fn get_users(&mut self, ctx: &Context<Self>, req: Option<RequestFilter>) {
self.common.call_graphql::<ListUsersQuery, _>( self.common.call_graphql::<ListUsersQuery, _>(
ctx,
list_users_query::Variables { filters: req }, list_users_query::Variables { filters: req },
Msg::ListUsersResponse, Msg::ListUsersResponse,
"Error trying to fetch users", "Error trying to fetch users",
@ -68,27 +69,23 @@ impl Component for UserTable {
type Message = Msg; type Message = Msg;
type Properties = (); type Properties = ();
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(ctx: &Context<Self>) -> Self {
let mut table = UserTable { let mut table = UserTable {
common: CommonComponentParts::<Self>::create(props, link), common: CommonComponentParts::<Self>::create(),
users: None, users: None,
}; };
table.get_users(None); table.get_users(ctx, None);
table table
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
CommonComponentParts::<Self>::update(self, msg) CommonComponentParts::<Self>::update(self, ctx, msg)
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn view(&self, ctx: &Context<Self>) -> Html {
self.common.change(props)
}
fn view(&self) -> Html {
html! { html! {
<div> <div>
{self.view_users()} {self.view_users(ctx)}
{self.view_errors()} {self.view_errors()}
</div> </div>
} }
@ -96,7 +93,7 @@ impl Component for UserTable {
} }
impl UserTable { impl UserTable {
fn view_users(&self) -> Html { fn view_users(&self, ctx: &Context<Self>) -> Html {
let make_table = |users: &Vec<User>| { let make_table = |users: &Vec<User>| {
html! { html! {
<div class="table-responsive"> <div class="table-responsive">
@ -113,7 +110,7 @@ impl UserTable {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.iter().map(|u| self.view_user(u)).collect::<Vec<_>>()} {users.iter().map(|u| self.view_user(ctx, u)).collect::<Vec<_>>()}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -125,11 +122,11 @@ impl UserTable {
} }
} }
fn view_user(&self, user: &User) -> Html { fn view_user(&self, ctx: &Context<Self>, user: &User) -> Html {
let link = &self.common; let link = &ctx.link();
html! { html! {
<tr key={user.id.clone()}> <tr key={user.id.clone()}>
<td><Link route={AppRoute::UserDetails(user.id.clone())}>{&user.id}</Link></td> <td><Link to={AppRoute::UserDetails{user_id: user.id.clone()}}>{&user.id}</Link></td>
<td>{&user.email}</td> <td>{&user.email}</td>
<td>{&user.display_name}</td> <td>{&user.display_name}</td>
<td>{&user.first_name}</td> <td>{&user.first_name}</td>

View File

@ -1,138 +1,84 @@
use super::cookies::set_cookie; use super::cookies::set_cookie;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use gloo_net::http::{Method, Request};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use lldap_auth::{login, registration, JWTClaims}; use lldap_auth::{login, registration, JWTClaims};
use yew::{ use serde::{de::DeserializeOwned, Serialize};
callback::Callback, use web_sys::RequestCredentials;
format::Json,
services::fetch::{Credentials, FetchOptions, FetchService, FetchTask, Request, Response},
};
#[derive(Default)] #[derive(Default)]
pub struct HostService {} pub struct HostService {}
fn get_default_options() -> FetchOptions {
FetchOptions {
credentials: Some(Credentials::SameOrigin),
..FetchOptions::default()
}
}
fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> { fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
use jwt::*; use jwt::*;
let token = Token::<header::Header, JWTClaims, token::Unverified>::parse_unverified(jwt)?; let token = Token::<header::Header, JWTClaims, token::Unverified>::parse_unverified(jwt)?;
Ok(token.claims().clone()) Ok(token.claims().clone())
} }
fn create_handler<Resp, CallbackResult, F>( const NO_BODY: Option<()> = None;
callback: Callback<Result<CallbackResult>>,
handler: F,
) -> Callback<Response<Result<Resp>>>
where
F: Fn(http::StatusCode, Resp) -> Result<CallbackResult> + 'static,
CallbackResult: 'static,
{
Callback::once(move |response: Response<Result<Resp>>| {
let (meta, maybe_data) = response.into_parts();
let message = maybe_data
.context("Could not reach server")
.and_then(|data| handler(meta.status, data));
callback.emit(message)
})
}
struct RequestBody<T>(T); async fn call_server(
impl<'a, R> From<&'a R> for RequestBody<Json<&'a R>>
where
R: serde::ser::Serialize,
{
fn from(request: &'a R) -> Self {
Self(Json(request))
}
}
impl From<yew::format::Nothing> for RequestBody<yew::format::Nothing> {
fn from(request: yew::format::Nothing) -> Self {
Self(request)
}
}
fn call_server<Req, CallbackResult, F, RB>(
url: &str, url: &str,
request: RB, body: Option<impl Serialize>,
callback: Callback<Result<CallbackResult>>,
error_message: &'static str, error_message: &'static str,
parse_response: F, ) -> Result<String> {
) -> Result<FetchTask> let mut request = Request::new(url)
where
F: Fn(String) -> Result<CallbackResult> + 'static,
CallbackResult: 'static,
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
let request = {
// If the request type is empty (if the size is 0), it's a get.
if std::mem::size_of::<RB>() == 0 {
Request::get(url)
} else {
Request::post(url)
}
}
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(request.into().0)?; .credentials(RequestCredentials::SameOrigin);
let handler = create_handler(callback, move |status: http::StatusCode, data: String| { if let Some(b) = body {
if status.is_success() { request = request
parse_response(data) .body(serde_json::to_string(&b)?)
} else { .method(Method::POST);
Err(anyhow!("{}[{}]: {}", error_message, status, data))
} }
}); let response = request.send().await?;
FetchService::fetch_with_options(request, get_default_options(), handler) if response.ok() {
} Ok(response.text().await?)
} else {
fn call_server_json_with_error_message<CallbackResult, RB, Req>( Err(anyhow!(
url: &str, "{}[{} {}]: {}",
request: RB,
callback: Callback<Result<CallbackResult>>,
error_message: &'static str,
) -> Result<FetchTask>
where
CallbackResult: serde::de::DeserializeOwned + 'static,
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
call_server(url, request, callback, error_message, |data: String| {
serde_json::from_str(&data).context("Could not parse response")
})
}
fn call_server_empty_response_with_error_message<RB, Req>(
url: &str,
request: RB,
callback: Callback<Result<()>>,
error_message: &'static str,
) -> Result<FetchTask>
where
RB: Into<RequestBody<Req>>,
Req: Into<yew::format::Text>,
{
call_server(
url,
request,
callback,
error_message, error_message,
|_data: String| Ok(()), response.status(),
) response.status_text(),
response.text().await?
))
}
}
async fn call_server_json_with_error_message<CallbackResult, Body: Serialize>(
url: &str,
request: Option<Body>,
error_message: &'static str,
) -> Result<CallbackResult>
where
CallbackResult: DeserializeOwned + 'static,
{
let data = call_server(url, request, error_message).await?;
serde_json::from_str(&data).context("Could not parse response")
}
async fn call_server_empty_response_with_error_message<Body: Serialize>(
url: &str,
request: Option<Body>,
error_message: &'static str,
) -> Result<()> {
call_server(url, request, error_message).await.map(|_| ())
}
fn set_cookies_from_jwt(response: login::ServerLoginResponse) -> Result<(String, bool)> {
let jwt_claims = get_claims_from_jwt(response.token.as_str()).context("Could not parse JWT")?;
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error setting cookie")
} }
impl HostService { impl HostService {
pub fn graphql_query<QueryType>( pub async fn graphql_query<QueryType>(
variables: QueryType::Variables, variables: QueryType::Variables,
callback: Callback<Result<QueryType::ResponseData>>,
error_message: &'static str, error_message: &'static str,
) -> Result<FetchTask> ) -> Result<QueryType::ResponseData>
where where
QueryType: GraphQLQuery + 'static, QueryType: GraphQLQuery + 'static,
{ {
@ -149,156 +95,103 @@ impl HostService {
) )
}) })
}; };
let parse_graphql_response = move |data: String| {
serde_json::from_str(&data)
.context("Could not parse response")
.and_then(unwrap_graphql_response)
};
let request_body = QueryType::build_query(variables); let request_body = QueryType::build_query(variables);
call_server( call_server_json_with_error_message::<graphql_client::Response<_>, _>(
"/api/graphql", "/api/graphql",
&request_body, Some(request_body),
callback,
error_message, error_message,
parse_graphql_response,
) )
.await
.and_then(unwrap_graphql_response)
} }
pub fn login_start( pub async fn login_start(
request: login::ClientLoginStartRequest, request: login::ClientLoginStartRequest,
callback: Callback<Result<Box<login::ServerLoginStartResponse>>>, ) -> Result<Box<login::ServerLoginStartResponse>> {
) -> Result<FetchTask> {
call_server_json_with_error_message( call_server_json_with_error_message(
"/auth/opaque/login/start", "/auth/opaque/login/start",
&request, Some(request),
callback,
"Could not start authentication: ", "Could not start authentication: ",
) )
.await
} }
pub fn login_finish( pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
request: login::ClientLoginFinishRequest, call_server_json_with_error_message::<login::ServerLoginResponse, _>(
callback: Callback<Result<(String, bool)>>,
) -> Result<FetchTask> {
let set_cookies = |jwt_claims: JWTClaims| {
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error clearing cookie")
};
let parse_token = move |data: String| {
serde_json::from_str::<login::ServerLoginResponse>(&data)
.context("Could not parse response")
.and_then(|r| {
get_claims_from_jwt(r.token.as_str())
.context("Could not parse response")
.and_then(set_cookies)
})
};
call_server(
"/auth/opaque/login/finish", "/auth/opaque/login/finish",
&request, Some(request),
callback,
"Could not finish authentication", "Could not finish authentication",
parse_token,
) )
.await
.and_then(set_cookies_from_jwt)
} }
pub fn register_start( pub async fn register_start(
request: registration::ClientRegistrationStartRequest, request: registration::ClientRegistrationStartRequest,
callback: Callback<Result<Box<registration::ServerRegistrationStartResponse>>>, ) -> Result<Box<registration::ServerRegistrationStartResponse>> {
) -> Result<FetchTask> {
call_server_json_with_error_message( call_server_json_with_error_message(
"/auth/opaque/register/start", "/auth/opaque/register/start",
&request, Some(request),
callback,
"Could not start registration: ", "Could not start registration: ",
) )
.await
} }
pub fn register_finish( pub async fn register_finish(
request: registration::ClientRegistrationFinishRequest, request: registration::ClientRegistrationFinishRequest,
callback: Callback<Result<()>>, ) -> Result<()> {
) -> Result<FetchTask> {
call_server_empty_response_with_error_message( call_server_empty_response_with_error_message(
"/auth/opaque/register/finish", "/auth/opaque/register/finish",
&request, Some(request),
callback,
"Could not finish registration", "Could not finish registration",
) )
.await
} }
pub fn refresh(_request: (), callback: Callback<Result<(String, bool)>>) -> Result<FetchTask> { pub async fn refresh() -> Result<(String, bool)> {
let set_cookies = |jwt_claims: JWTClaims| { call_server_json_with_error_message::<login::ServerLoginResponse, _>(
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error clearing cookie")
};
let parse_token = move |data: String| {
serde_json::from_str::<login::ServerLoginResponse>(&data)
.context("Could not parse response")
.and_then(|r| {
get_claims_from_jwt(r.token.as_str())
.context("Could not parse response")
.and_then(set_cookies)
})
};
call_server(
"/auth/refresh", "/auth/refresh",
yew::format::Nothing, NO_BODY,
callback,
"Could not start authentication: ", "Could not start authentication: ",
parse_token,
) )
.await
.and_then(set_cookies_from_jwt)
} }
// The `_request` parameter is to make it the same shape as the other functions. // The `_request` parameter is to make it the same shape as the other functions.
pub fn logout(_request: (), callback: Callback<Result<()>>) -> Result<FetchTask> { pub async fn logout() -> Result<()> {
call_server_empty_response_with_error_message( call_server_empty_response_with_error_message("/auth/logout", NO_BODY, "Could not logout")
"/auth/logout", .await
yew::format::Nothing,
callback,
"Could not logout",
)
} }
pub fn reset_password_step1( pub async fn reset_password_step1(username: String) -> Result<()> {
username: &str,
callback: Callback<Result<()>>,
) -> Result<FetchTask> {
call_server_empty_response_with_error_message( call_server_empty_response_with_error_message(
&format!("/auth/reset/step1/{}", url_escape::encode_query(username)), &format!("/auth/reset/step1/{}", url_escape::encode_query(&username)),
yew::format::Nothing, NO_BODY,
callback,
"Could not initiate password reset", "Could not initiate password reset",
) )
.await
} }
pub fn reset_password_step2( pub async fn reset_password_step2(
token: &str, token: String,
callback: Callback<Result<lldap_auth::password_reset::ServerPasswordResetResponse>>, ) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
) -> Result<FetchTask> {
call_server_json_with_error_message( call_server_json_with_error_message(
&format!("/auth/reset/step2/{}", token), &format!("/auth/reset/step2/{}", token),
yew::format::Nothing, NO_BODY,
callback,
"Could not validate token", "Could not validate token",
) )
.await
} }
pub fn probe_password_reset(callback: Callback<Result<bool>>) -> Result<FetchTask> { pub async fn probe_password_reset() -> Result<bool> {
let request = Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name") Ok(
gloo_net::http::Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(yew::format::Nothing)?; .send()
FetchService::fetch_with_options( .await?
request, .status()
get_default_options(), != http::StatusCode::NOT_FOUND,
create_handler(callback, move |status: http::StatusCode, _data: String| {
Ok(status != http::StatusCode::NOT_FOUND)
}),
) )
} }
} }

View File

@ -21,88 +21,62 @@
//! [`CommonComponentParts::update`]. This will in turn call [`CommonComponent::handle_msg`] and //! [`CommonComponentParts::update`]. This will in turn call [`CommonComponent::handle_msg`] and
//! take care of error and task handling. //! take care of error and task handling.
use std::{
future::Future,
marker::PhantomData,
sync::{Arc, Mutex},
};
use crate::infra::api::HostService; use crate::infra::api::HostService;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use gloo_console::{error, log}; use gloo_console::error;
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use yew::{ use yew::prelude::*;
prelude::*,
services::{
fetch::FetchTask,
reader::{FileData, ReaderService, ReaderTask},
},
};
use yewtil::NeqAssign;
/// Trait required for common components. /// Trait required for common components.
pub trait CommonComponent<C: Component + CommonComponent<C>>: Component { pub trait CommonComponent<C: Component + CommonComponent<C>>: Component {
/// Handle the incoming message. If an error is returned here, any running task will be /// Handle the incoming message. If an error is returned here, any running task will be
/// cancelled, the error will be written to the [`CommonComponentParts::error`] and the /// cancelled, the error will be written to the [`CommonComponentParts::error`] and the
/// component will be refreshed. /// component will be refreshed.
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool>; fn handle_msg(
&mut self,
ctx: &Context<Self>,
msg: <Self as Component>::Message,
) -> Result<bool>;
/// Get a mutable reference to the inner component parts, necessary for the CRTP. /// Get a mutable reference to the inner component parts, necessary for the CRTP.
fn mut_common(&mut self) -> &mut CommonComponentParts<C>; fn mut_common(&mut self) -> &mut CommonComponentParts<C>;
} }
enum AnyTask {
None,
FetchTask(FetchTask),
ReaderTask(ReaderTask),
}
impl AnyTask {
fn is_some(&self) -> bool {
!matches!(self, AnyTask::None)
}
}
impl From<Option<FetchTask>> for AnyTask {
fn from(task: Option<FetchTask>) -> Self {
match task {
Some(t) => AnyTask::FetchTask(t),
None => AnyTask::None,
}
}
}
/// Structure that contains the common parts needed by most components. /// Structure that contains the common parts needed by most components.
/// The fields of [`props`] are directly accessible through a `Deref` implementation. /// The fields of [`props`] are directly accessible through a `Deref` implementation.
pub struct CommonComponentParts<C: CommonComponent<C>> { pub struct CommonComponentParts<C: CommonComponent<C>> {
link: ComponentLink<C>,
pub props: <C as Component>::Properties,
pub error: Option<Error>, pub error: Option<Error>,
task: AnyTask, is_task_running: Arc<Mutex<bool>>,
_phantom: PhantomData<C>,
} }
impl<C: CommonComponent<C>> CommonComponentParts<C> { impl<C: CommonComponent<C>> CommonComponentParts<C> {
pub fn create() -> Self {
CommonComponentParts {
error: None,
is_task_running: Arc::new(Mutex::new(false)),
_phantom: PhantomData::<C>,
}
}
/// Whether there is a currently running task in the background. /// Whether there is a currently running task in the background.
pub fn is_task_running(&self) -> bool { pub fn is_task_running(&self) -> bool {
self.task.is_some() *self.is_task_running.lock().unwrap()
}
/// Cancel any background task.
pub fn cancel_task(&mut self) {
self.task = AnyTask::None;
}
pub fn create(props: <C as Component>::Properties, link: ComponentLink<C>) -> Self {
Self {
link,
props,
error: None,
task: AnyTask::None,
}
} }
/// This should be called from the [`yew::prelude::Component::update`]: it will in turn call /// This should be called from the [`yew::prelude::Component::update`]: it will in turn call
/// [`CommonComponent::handle_msg`] and handle any resulting error. /// [`CommonComponent::handle_msg`] and handle any resulting error.
pub fn update(com: &mut C, msg: <C as Component>::Message) -> ShouldRender { pub fn update(com: &mut C, ctx: &Context<C>, msg: <C as Component>::Message) -> bool {
com.mut_common().error = None; com.mut_common().error = None;
match com.handle_msg(msg) { match com.handle_msg(ctx, msg) {
Err(e) => { Err(e) => {
error!(&e.to_string()); error!(&e.to_string());
com.mut_common().error = Some(e); com.mut_common().error = Some(e);
com.mut_common().cancel_task(); assert!(!*com.mut_common().is_task_running.lock().unwrap());
true true
} }
Ok(b) => b, Ok(b) => b,
@ -112,10 +86,11 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
/// Same as above, but the resulting error is instead passed to the reporting function. /// Same as above, but the resulting error is instead passed to the reporting function.
pub fn update_and_report_error( pub fn update_and_report_error(
com: &mut C, com: &mut C,
ctx: &Context<C>,
msg: <C as Component>::Message, msg: <C as Component>::Message,
report_fn: Callback<Error>, report_fn: Callback<Error>,
) -> ShouldRender { ) -> bool {
let should_render = Self::update(com, msg); let should_render = Self::update(com, ctx, msg);
com.mut_common() com.mut_common()
.error .error
.take() .take()
@ -126,38 +101,24 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
.unwrap_or(should_render) .unwrap_or(should_render)
} }
/// This can be called from [`yew::prelude::Component::update`]: it will check if the
/// properties have changed and return whether the component should update.
pub fn change(&mut self, props: <C as Component>::Properties) -> ShouldRender
where
<C as yew::Component>::Properties: std::cmp::PartialEq,
{
self.props.neq_assign(props)
}
/// Create a callback from the link.
pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
where
M: Into<C::Message>,
F: Fn(IN) -> M + 'static,
{
self.link.callback(function)
}
/// Call `method` from the backend with the given `request`, and pass the `callback` for the /// Call `method` from the backend with the given `request`, and pass the `callback` for the
/// result. Returns whether _starting the call_ failed. /// result.
pub fn call_backend<M, Req, Cb, Resp>( pub fn call_backend<Fut, Cb, Resp>(&mut self, ctx: &Context<C>, fut: Fut, callback: Cb)
&mut self,
method: M,
req: Req,
callback: Cb,
) -> Result<()>
where where
M: Fn(Req, Callback<Resp>) -> Result<FetchTask>, Fut: Future<Output = Resp> + 'static,
Cb: FnOnce(Resp) -> <C as Component>::Message + 'static, Cb: FnOnce(Resp) -> <C as Component>::Message + 'static,
{ {
self.task = AnyTask::FetchTask(method(req, self.link.callback_once(callback))?); {
Ok(()) let mut running = self.is_task_running.lock().unwrap();
assert!(!*running);
*running = true;
}
let is_task_running = self.is_task_running.clone();
ctx.link().send_future(async move {
let res = fut.await;
*is_task_running.lock().unwrap() = false;
callback(res)
});
} }
/// Call the backend with a GraphQL query. /// Call the backend with a GraphQL query.
@ -165,6 +126,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
/// `EnumCallback` should usually be left as `_`. /// `EnumCallback` should usually be left as `_`.
pub fn call_graphql<QueryType, EnumCallback>( pub fn call_graphql<QueryType, EnumCallback>(
&mut self, &mut self,
ctx: &Context<C>,
variables: QueryType::Variables, variables: QueryType::Variables,
enum_callback: EnumCallback, enum_callback: EnumCallback,
error_message: &'static str, error_message: &'static str,
@ -172,41 +134,10 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
QueryType: GraphQLQuery + 'static, QueryType: GraphQLQuery + 'static,
EnumCallback: Fn(Result<QueryType::ResponseData>) -> <C as Component>::Message + 'static, EnumCallback: Fn(Result<QueryType::ResponseData>) -> <C as Component>::Message + 'static,
{ {
self.task = HostService::graphql_query::<QueryType>( self.call_backend(
variables, ctx,
self.link.callback(enum_callback), HostService::graphql_query::<QueryType>(variables, error_message),
error_message, enum_callback,
) );
.map_err::<(), _>(|e| {
log!(&e.to_string());
self.error = Some(e);
})
.ok()
.into();
}
pub(crate) fn read_file<Cb>(&mut self, file: web_sys::File, callback: Cb) -> Result<()>
where
Cb: FnOnce(FileData) -> <C as Component>::Message + 'static,
{
self.task = AnyTask::ReaderTask(ReaderService::read_file(
file,
self.link.callback_once(callback),
)?);
Ok(())
}
}
impl<C: Component + CommonComponent<C>> std::ops::Deref for CommonComponentParts<C> {
type Target = <C as Component>::Properties;
fn deref(&self) -> &<Self as std::ops::Deref>::Target {
&self.props
}
}
impl<C: Component + CommonComponent<C>> std::ops::DerefMut for CommonComponentParts<C> {
fn deref_mut(&mut self) -> &mut <Self as std::ops::Deref>::Target {
&mut self.props
} }
} }

View File

@ -1,6 +1,7 @@
#![recursion_limit = "256"] #![recursion_limit = "256"]
#![forbid(non_ascii_idents)] #![forbid(non_ascii_idents)]
#![allow(clippy::uninlined_format_args)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::let_unit_value)]
pub mod components; pub mod components;
pub mod infra; pub mod infra;
@ -9,7 +10,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
#[wasm_bindgen] #[wasm_bindgen]
pub fn run_app() -> Result<(), JsValue> { pub fn run_app() -> Result<(), JsValue> {
yew::start_app::<components::app::App>(); yew::start_app::<components::app::AppContainer>();
Ok(()) Ok(())
} }