mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
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:
parent
8d44717588
commit
b2cfc0ed03
206
Cargo.lock
generated
206
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"]
|
||||||
|
@ -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! {
|
||||||
|
@ -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! {
|
||||||
|
@ -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>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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!{}
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -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)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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)?)
|
||||||
|
.method(Method::POST);
|
||||||
|
}
|
||||||
|
let response = request.send().await?;
|
||||||
|
if response.ok() {
|
||||||
|
Ok(response.text().await?)
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("{}[{}]: {}", error_message, status, data))
|
Err(anyhow!(
|
||||||
}
|
"{}[{} {}]: {}",
|
||||||
});
|
|
||||||
FetchService::fetch_with_options(request, get_default_options(), handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn call_server_json_with_error_message<CallbackResult, RB, Req>(
|
|
||||||
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)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user