diff --git a/app/Cargo.toml b/app/Cargo.toml
index e4cac1a..a861efa 100644
--- a/app/Cargo.toml
+++ b/app/Cargo.toml
@@ -22,6 +22,8 @@ features = [
"Document",
"Element",
"HtmlDocument",
+ "HtmlInputElement",
+ "HtmlOptionElement",
"console",
]
diff --git a/app/queries/add_user_to_group.graphql b/app/queries/add_user_to_group.graphql
new file mode 100644
index 0000000..8921d2e
--- /dev/null
+++ b/app/queries/add_user_to_group.graphql
@@ -0,0 +1,5 @@
+mutation AddUserToGroup($user: String!, $group: Int!) {
+ addUserToGroup(userId: $user, groupId: $group) {
+ ok
+ }
+}
diff --git a/app/queries/get_group_list.graphql b/app/queries/get_group_list.graphql
new file mode 100644
index 0000000..28f56e4
--- /dev/null
+++ b/app/queries/get_group_list.graphql
@@ -0,0 +1,6 @@
+query GetGroupList {
+ groups {
+ id
+ displayName
+ }
+}
diff --git a/app/queries/get_user_details.graphql b/app/queries/get_user_details.graphql
index 150b80e..8497ac0 100644
--- a/app/queries/get_user_details.graphql
+++ b/app/queries/get_user_details.graphql
@@ -7,6 +7,7 @@ query GetUserDetails($id: String!) {
lastName
creationDate
groups {
+ id
displayName
}
}
diff --git a/app/queries/remove_user_from_group.graphql b/app/queries/remove_user_from_group.graphql
new file mode 100644
index 0000000..a45fe51
--- /dev/null
+++ b/app/queries/remove_user_from_group.graphql
@@ -0,0 +1,5 @@
+mutation RemoveUserFromGroup($user: String!, $group: Int!) {
+ removeUserFromGroup(userId: $user, groupId: $group) {
+ ok
+ }
+}
diff --git a/app/src/components/app.rs b/app/src/components/app.rs
index e6bb92f..c0fc593 100644
--- a/app/src/components/app.rs
+++ b/app/src/components/app.rs
@@ -117,7 +117,7 @@ impl Component for App {
AppRoute::UserDetails(username) => html! {
-
+
},
AppRoute::ChangePassword(username) => html! {
diff --git a/app/src/components/logout.rs b/app/src/components/logout.rs
index 7b1cf10..b770860 100644
--- a/app/src/components/logout.rs
+++ b/app/src/components/logout.rs
@@ -65,7 +65,7 @@ impl Component for LogoutButton {
fn view(&self) -> Html {
html! {
-
+
}
}
}
diff --git a/app/src/components/user_details.rs b/app/src/components/user_details.rs
index f144d41..5959848 100644
--- a/app/src/components/user_details.rs
+++ b/app/src/components/user_details.rs
@@ -2,8 +2,9 @@ use crate::{
components::router::{AppRoute, NavButton},
infra::api::HostService,
};
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, bail, Error, Result};
use graphql_client::GraphQLQuery;
+use std::collections::HashSet;
use yew::{
prelude::*,
services::{fetch::FetchTask, ConsoleService},
@@ -13,12 +14,23 @@ use yew::{
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/get_user_details.graphql",
- response_derives = "Debug",
+ response_derives = "Debug, Hash, PartialEq, Eq",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct GetUserDetails;
type User = get_user_details::GetUserDetailsUser;
+type Group = get_user_details::GetUserDetailsUserGroups;
+type GroupListGroup = get_group_list::GetGroupListGroups;
+
+impl From for Group {
+ fn from(group: GroupListGroup) -> Self {
+ Self {
+ id: group.id,
+ display_name: group.display_name,
+ }
+ }
+}
#[derive(GraphQLQuery)]
#[graphql(
@@ -30,31 +42,83 @@ type User = get_user_details::GetUserDetailsUser;
)]
pub struct UpdateUser;
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "../schema.graphql",
+ query_path = "queries/add_user_to_group.graphql",
+ response_derives = "Debug",
+ variables_derives = "Clone",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct AddUserToGroup;
+
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "../schema.graphql",
+ query_path = "queries/remove_user_from_group.graphql",
+ response_derives = "Debug",
+ variables_derives = "Clone",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct RemoveUserFromGroup;
+
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "../schema.graphql",
+ query_path = "queries/get_group_list.graphql",
+ response_derives = "Debug",
+ variables_derives = "Clone",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct GetGroupList;
+
pub struct UserDetails {
link: ComponentLink,
+ /// Which user this is about.
username: String,
+ /// The user info. If none, the error is in `error`. If `error` is None, then we haven't
+ /// received the server response yet.
user: Option,
// Needed for the form.
node_ref: NodeRef,
- // Error message displayed to the user.
- error: Option,
- // The request, while we're waiting for the server to reply.
+ /// Error message displayed to the user.
+ error: Option,
+ /// The request, while we're waiting for the server to reply.
update_request: Option,
- // True iff we just finished updating the user, to display a successful message.
+ /// True iff we just finished updating the user, to display a successful message.
update_successful: bool,
+ /// Whether the "+" button has been clicked.
+ add_group: bool,
+ is_admin: bool,
+ /// The list of existing groups, initially not loaded.
+ group_list: Option>,
+ /// The group that we're requesting to remove, if any.
+ group_to_remove: Option,
// Used to keep the request alive long enough.
_task: Option,
}
+/// State machine describing the possible transitions of the component state.
+/// It starts out by fetching the user's details from the backend when loading.
pub enum Msg {
+ /// Received the user details response, either the user data or an error.
UserDetailsResponse(Result),
- SubmitForm,
+ /// The user changed some fields and submitted the form for update.
+ SubmitUserUpdateForm,
+ /// Response after updating the user's details.
UpdateFinished(Result),
+ AddGroupButtonClicked,
+ GroupListResponse(Result),
+ SubmitAddGroup,
+ AddGroupResponse(Result),
+ SubmitRemoveGroup(Group),
+ RemoveGroupResponse(Result),
}
#[derive(yew::Properties, Clone, PartialEq)]
pub struct Props {
pub username: String,
+ pub is_admin: bool,
}
#[allow(clippy::ptr_arg)]
@@ -77,158 +141,196 @@ impl UserDetails {
})
.ok();
}
+
+ fn submit_user_update_form(&mut self) -> Result {
+ let base_user = self.user.as_ref().unwrap();
+ let mut user_input = update_user::UpdateUserInput {
+ id: self.username.clone(),
+ email: None,
+ displayName: None,
+ firstName: None,
+ lastName: None,
+ };
+ let mut should_send_form = false;
+ let email = get_element("email")
+ .filter(not_empty)
+ .ok_or_else(|| anyhow!("Missing email"))?;
+ if base_user.email != email {
+ should_send_form = true;
+ user_input.email = Some(email);
+ }
+ if base_user.display_name != get_element_or_empty("display_name") {
+ should_send_form = true;
+ user_input.displayName = Some(get_element_or_empty("display_name"));
+ }
+ if base_user.first_name != get_element_or_empty("first_name") {
+ should_send_form = true;
+ user_input.firstName = Some(get_element_or_empty("first_name"));
+ }
+ if base_user.last_name != get_element_or_empty("last_name") {
+ should_send_form = true;
+ user_input.lastName = Some(get_element_or_empty("last_name"));
+ }
+ if !should_send_form {
+ return Ok(false);
+ }
+ self.update_request = Some(user_input.clone());
+ let req = update_user::Variables { user: user_input };
+ self._task = Some(HostService::graphql_query::(
+ req,
+ self.link.callback(Msg::UpdateFinished),
+ "Error trying to update user",
+ )?);
+ Ok(false)
+ }
+
+ fn user_update_finished(&mut self, r: Result) -> Result {
+ match r {
+ Err(e) => return Err(e),
+ Ok(_) => {
+ ConsoleService::log("Successfully updated user");
+ self.update_successful = true;
+ let User {
+ id,
+ display_name,
+ first_name,
+ last_name,
+ email,
+ creation_date,
+ groups,
+ } = self.user.take().unwrap();
+ let new_user = self.update_request.take().unwrap();
+ self.user = Some(User {
+ id,
+ email: new_user.email.unwrap_or(email),
+ display_name: new_user.displayName.unwrap_or(display_name),
+ first_name: new_user.firstName.unwrap_or(first_name),
+ last_name: new_user.lastName.unwrap_or(last_name),
+ creation_date,
+ groups,
+ });
+ }
+ };
+ Ok(true)
+ }
+
+ fn get_group_list(&mut self) {
+ self._task = HostService::graphql_query::(
+ get_group_list::Variables,
+ self.link.callback(Msg::GroupListResponse),
+ "Error trying to fetch group list",
+ )
+ .map_err(|e| {
+ ConsoleService::log(&e.to_string());
+ e
+ })
+ .ok();
+ }
+
+ fn submit_add_group(&mut self) -> Result {
+ if self.group_list.is_none() {
+ return Ok(false);
+ }
+ let group_id = get_selected_group()
+ .expect("could not get selected group")
+ .id;
+ self._task = HostService::graphql_query::(
+ add_user_to_group::Variables {
+ user: self.username.clone(),
+ group: group_id,
+ },
+ self.link.callback(Msg::AddGroupResponse),
+ "Error trying to initiate adding the user to a group",
+ )
+ .map_err(|e| {
+ ConsoleService::log(&e.to_string());
+ e
+ })
+ .ok();
+ Ok(true)
+ }
+
+ fn submit_remove_group(&mut self, group: Group) -> Result {
+ self._task = HostService::graphql_query::(
+ remove_user_from_group::Variables {
+ user: self.username.clone(),
+ group: group.id,
+ },
+ self.link.callback(Msg::RemoveGroupResponse),
+ "Error trying to initiate removing the user from a group",
+ )
+ .map_err(|e| {
+ ConsoleService::log(&e.to_string());
+ e
+ })
+ .ok();
+ self.group_to_remove = Some(group);
+ Ok(true)
+ }
+
fn handle_msg(&mut self, msg: ::Message) -> Result {
self.update_successful = false;
match msg {
- Msg::UserDetailsResponse(Ok(user)) => {
- self.user = Some(user.user);
+ Msg::UserDetailsResponse(response) => match response {
+ Ok(user) => self.user = Some(user.user),
+ Err(e) => {
+ self.user = None;
+ bail!("Error getting user details: {}", e);
+ }
+ },
+ Msg::SubmitUserUpdateForm => return self.submit_user_update_form(),
+ Msg::UpdateFinished(r) => return self.user_update_finished(r),
+ Msg::AddGroupButtonClicked => {
+ if self.group_list.is_none() {
+ self.get_group_list();
+ }
+ self.add_group = true;
}
- Msg::UserDetailsResponse(Err(e)) => {
- self.error = Some(anyhow!("Error getting user details: {}", e));
- self.user = None;
+ Msg::GroupListResponse(response) => {
+ let user_groups = self
+ .user
+ .as_ref()
+ .unwrap()
+ .groups
+ .iter()
+ .collect::>();
+ self.group_list = Some(
+ response?
+ .groups
+ .into_iter()
+ .map(Into::into)
+ .filter(|g| !user_groups.contains(g))
+ .collect(),
+ );
}
- Msg::SubmitForm => {
- let base_user = self.user.as_ref().unwrap();
- let mut user_input = update_user::UpdateUserInput {
- id: self.username.clone(),
- email: None,
- displayName: None,
- firstName: None,
- lastName: None,
- };
- let mut should_send_form = false;
- let email = get_element("email")
- .filter(not_empty)
- .ok_or_else(|| anyhow!("Missing email"))?;
- if base_user.email != email {
- should_send_form = true;
- user_input.email = Some(email);
- }
- if base_user.display_name != get_element_or_empty("display_name") {
- should_send_form = true;
- user_input.displayName = Some(get_element_or_empty("display_name"));
- }
- if base_user.first_name != get_element_or_empty("first_name") {
- should_send_form = true;
- user_input.firstName = Some(get_element_or_empty("first_name"));
- }
- if base_user.last_name != get_element_or_empty("last_name") {
- should_send_form = true;
- user_input.lastName = Some(get_element_or_empty("last_name"));
- }
- if !should_send_form {
- return Ok(false);
- }
- self.update_request = Some(user_input.clone());
- let req = update_user::Variables { user: user_input };
- self._task = Some(HostService::graphql_query::(
- req,
- self.link.callback(Msg::UpdateFinished),
- "Error trying to update user",
- )?);
- return Ok(false);
+ Msg::SubmitAddGroup => return self.submit_add_group(),
+ Msg::AddGroupResponse(response) => {
+ response?;
+ // Adding the user to the group succeeded, we're not in the process of adding a
+ // group anymore.
+ self.add_group = false;
+ let group = get_selected_group().expect("Could not get selected group");
+ // Remove the group from the dropdown.
+ self.group_list.as_mut().unwrap().retain(|g| g != &group);
+ self.user.as_mut().unwrap().groups.push(group);
}
- Msg::UpdateFinished(r) => {
- match r {
- Err(e) => return Err(e),
- Ok(_) => {
- ConsoleService::log("Successfully updated user");
- self.update_successful = true;
- let User {
- id,
- display_name,
- first_name,
- last_name,
- email,
- creation_date,
- groups,
- } = self.user.take().unwrap();
- let new_user = self.update_request.take().unwrap();
- self.user = Some(User {
- id,
- email: new_user.email.unwrap_or(email),
- display_name: new_user.displayName.unwrap_or(display_name),
- first_name: new_user.firstName.unwrap_or(first_name),
- last_name: new_user.lastName.unwrap_or(last_name),
- creation_date,
- groups,
- });
- }
- };
+ Msg::SubmitRemoveGroup(group) => return self.submit_remove_group(group),
+ Msg::RemoveGroupResponse(response) => {
+ response?;
+ let group = self.group_to_remove.take().unwrap();
+ // Remove the group from the user and add it to the dropdown.
+ self.user.as_mut().unwrap().groups.retain(|g| g != &group);
+ if let Some(groups) = self.group_list.as_mut() {
+ groups.push(group);
+ }
}
}
Ok(true)
}
-}
-fn get_element(name: &str) -> Option {
- use wasm_bindgen::JsCast;
- Some(
- web_sys::window()?
- .document()?
- .get_element_by_id(name)?
- .dyn_into::()
- .ok()?
- .value(),
- )
-}
-
-fn get_element_or_empty(name: &str) -> String {
- get_element(name).unwrap_or_default()
-}
-
-impl Component for UserDetails {
- type Message = Msg;
- // The username.
- type Properties = Props;
-
- fn create(props: Self::Properties, link: ComponentLink) -> Self {
- let mut table = UserDetails {
- link,
- username: props.username,
- node_ref: NodeRef::default(),
- _task: None,
- user: None,
- error: None,
- update_request: None,
- update_successful: false,
- };
- table.get_user_details();
- table
- }
-
- fn update(&mut self, msg: Self::Message) -> ShouldRender {
- self.error = None;
- match self.handle_msg(msg) {
- Err(e) => {
- ConsoleService::error(&e.to_string());
- self.error = Some(e);
- true
- }
- Ok(b) => b,
- }
- }
-
- fn change(&mut self, _: Self::Properties) -> ShouldRender {
- false
- }
-
- fn view(&self) -> Html {
- type Group = get_user_details::GetUserDetailsUserGroups;
- let make_group_row = |group: &Group| {
- html! {
-
- |
- {&group.display_name} |
-
- }
- };
- match (&self.user, &self.error) {
- (None, None) => html! {{"Loading..."}},
- (None, Some(e)) => html! {{"Error: "}{e.to_string()}
},
- (Some(u), error) => {
- html! {
-