mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	app: Add a component to delete a user
Also adds a way to hook to the bootstrap modals to show or hide them.
This commit is contained in:
		
							parent
							
								
									770cff7e4f
								
							
						
					
					
						commit
						f18d37b6e8
					
				@ -11,6 +11,10 @@
 | 
			
		||||
      integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
      as="style" />
 | 
			
		||||
    <script
 | 
			
		||||
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"
 | 
			
		||||
      integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ"
 | 
			
		||||
      crossorigin="anonymous"></script>
 | 
			
		||||
    <link
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								app/queries/delete_user.graphql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/queries/delete_user.graphql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
mutation DeleteUserQuery($user: String!) {
 | 
			
		||||
  deleteUser(userId: $user) {
 | 
			
		||||
    ok
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										156
									
								
								app/src/components/delete_user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								app/src/components/delete_user.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,156 @@
 | 
			
		||||
use crate::infra::{api::HostService, modal::Modal};
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew::services::fetch::FetchTask;
 | 
			
		||||
use yewtil::NeqAssign;
 | 
			
		||||
 | 
			
		||||
#[derive(GraphQLQuery)]
 | 
			
		||||
#[graphql(
 | 
			
		||||
    schema_path = "../schema.graphql",
 | 
			
		||||
    query_path = "queries/delete_user.graphql",
 | 
			
		||||
    response_derives = "Debug",
 | 
			
		||||
    custom_scalars_module = "crate::infra::graphql"
 | 
			
		||||
)]
 | 
			
		||||
pub struct DeleteUserQuery;
 | 
			
		||||
 | 
			
		||||
pub struct DeleteUser {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    props: DeleteUserProps,
 | 
			
		||||
    node_ref: NodeRef,
 | 
			
		||||
    modal: Option<Modal>,
 | 
			
		||||
    _task: Option<FetchTask>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(yew::Properties, Clone, PartialEq, Debug)]
 | 
			
		||||
pub struct DeleteUserProps {
 | 
			
		||||
    pub username: String,
 | 
			
		||||
    pub on_user_deleted: Callback<String>,
 | 
			
		||||
    pub on_error: Callback<Error>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
    ClickedDeleteUser,
 | 
			
		||||
    ConfirmDeleteUser,
 | 
			
		||||
    DismissModal,
 | 
			
		||||
    DeleteUserResponse(Result<delete_user_query::ResponseData>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Component for DeleteUser {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = DeleteUserProps;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            link,
 | 
			
		||||
            props,
 | 
			
		||||
            node_ref: NodeRef::default(),
 | 
			
		||||
            modal: None,
 | 
			
		||||
            _task: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn rendered(&mut self, first_render: bool) {
 | 
			
		||||
        if first_render {
 | 
			
		||||
            self.modal = Some(Modal::new(
 | 
			
		||||
                self.node_ref
 | 
			
		||||
                    .cast::<web_sys::Element>()
 | 
			
		||||
                    .expect("Modal node is not an element"),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ClickedDeleteUser => {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").show();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::ConfirmDeleteUser => {
 | 
			
		||||
                self.update(Msg::DismissModal);
 | 
			
		||||
                self._task = HostService::graphql_query::<DeleteUserQuery>(
 | 
			
		||||
                    delete_user_query::Variables {
 | 
			
		||||
                        user: self.props.username.clone(),
 | 
			
		||||
                    },
 | 
			
		||||
                    self.link.callback(Msg::DeleteUserResponse),
 | 
			
		||||
                    "Error trying to delete user",
 | 
			
		||||
                )
 | 
			
		||||
                .map_err(|e| self.props.on_error.emit(e))
 | 
			
		||||
                .ok();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::DismissModal => {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").hide();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::DeleteUserResponse(response) => {
 | 
			
		||||
                if let Err(e) = response {
 | 
			
		||||
                    self.props.on_error.emit(e);
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.props.on_user_deleted.emit(self.props.username.clone());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.props.neq_assign(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-danger"
 | 
			
		||||
            onclick=self.link.callback(|_| Msg::ClickedDeleteUser)>
 | 
			
		||||
            <i class="bi-x-circle-fill" aria-label="Delete user" />
 | 
			
		||||
          </button>
 | 
			
		||||
          {self.show_modal()}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DeleteUser {
 | 
			
		||||
    fn show_modal(&self) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <div
 | 
			
		||||
            class="modal fade"
 | 
			
		||||
            id="exampleModal".to_string() + &self.props.username
 | 
			
		||||
            tabindex="-1"
 | 
			
		||||
            //role="dialog"
 | 
			
		||||
            aria-labelledby="exampleModalLabel"
 | 
			
		||||
            aria-hidden="true"
 | 
			
		||||
            ref=self.node_ref.clone()>
 | 
			
		||||
            <div class="modal-dialog" /*role="document"*/>
 | 
			
		||||
              <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
                  <h5 class="modal-title" id="exampleModalLabel">{"Delete user?"}</h5>
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn-close"
 | 
			
		||||
                    aria-label="Close"
 | 
			
		||||
                    onclick=self.link.callback(|_| Msg::DismissModal) />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                <span>
 | 
			
		||||
                  {"Are you sure you want to delete user "}
 | 
			
		||||
                  <b>{&self.props.username}</b>{"?"}
 | 
			
		||||
                </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn btn-secondary"
 | 
			
		||||
                    onclick=self.link.callback(|_| Msg::DismissModal)>
 | 
			
		||||
                      {"Cancel"}
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onclick=self.link.callback(|_| Msg::ConfirmDeleteUser)
 | 
			
		||||
                    class="btn btn-danger">{"Yes, I'm sure"}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,6 +2,7 @@ pub mod add_user_to_group;
 | 
			
		||||
pub mod app;
 | 
			
		||||
pub mod change_password;
 | 
			
		||||
pub mod create_user;
 | 
			
		||||
pub mod delete_user;
 | 
			
		||||
pub mod login;
 | 
			
		||||
pub mod logout;
 | 
			
		||||
pub mod remove_user_from_group;
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::router::{AppRoute, Link},
 | 
			
		||||
    components::{
 | 
			
		||||
        delete_user::DeleteUser,
 | 
			
		||||
        router::{AppRoute, Link},
 | 
			
		||||
    },
 | 
			
		||||
    infra::api::HostService,
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{anyhow, Result};
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew::services::{fetch::FetchTask, ConsoleService};
 | 
			
		||||
@ -22,13 +25,16 @@ type User = list_users_query::ListUsersQueryUsers;
 | 
			
		||||
 | 
			
		||||
pub struct UserTable {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    users: Option<Result<Vec<User>>>,
 | 
			
		||||
    users: Option<Vec<User>>,
 | 
			
		||||
    error: Option<Error>,
 | 
			
		||||
    // Used to keep the request alive long enough.
 | 
			
		||||
    _task: Option<FetchTask>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
    ListUsersResponse(Result<ResponseData>),
 | 
			
		||||
    OnUserDeleted(String),
 | 
			
		||||
    OnError(Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserTable {
 | 
			
		||||
@ -55,21 +61,21 @@ impl Component for UserTable {
 | 
			
		||||
            link,
 | 
			
		||||
            _task: None,
 | 
			
		||||
            users: None,
 | 
			
		||||
            error: None,
 | 
			
		||||
        };
 | 
			
		||||
        table.get_users(None);
 | 
			
		||||
        table
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ListUsersResponse(Ok(users)) => {
 | 
			
		||||
                self.users = Some(Ok(users.users.into_iter().collect()));
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            Msg::ListUsersResponse(Err(e)) => {
 | 
			
		||||
                self.users = Some(Err(anyhow!("Error listing users: {}", e)));
 | 
			
		||||
        self.error = None;
 | 
			
		||||
        match self.handle_msg(msg) {
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                ConsoleService::error(&e.to_string());
 | 
			
		||||
                self.error = Some(e);
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            Ok(b) => b,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -78,18 +84,32 @@ impl Component for UserTable {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        let make_user_row = |user: &User| {
 | 
			
		||||
            html! {
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td><Link route=AppRoute::UserDetails(user.id.clone())>{&user.id}</Link></td>
 | 
			
		||||
                    <td>{&user.email}</td>
 | 
			
		||||
                    <td>{&user.display_name}</td>
 | 
			
		||||
                    <td>{&user.first_name}</td>
 | 
			
		||||
                    <td>{&user.last_name}</td>
 | 
			
		||||
                    <td>{&user.creation_date.with_timezone(&chrono::Local)}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
        html! {
 | 
			
		||||
            <div>
 | 
			
		||||
              {self.view_users()}
 | 
			
		||||
              {self.view_errors()}
 | 
			
		||||
            </div>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserTable {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ListUsersResponse(users) => {
 | 
			
		||||
                self.users = Some(users?.users.into_iter().collect());
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
            Msg::OnError(e) => Err(e),
 | 
			
		||||
            Msg::OnUserDeleted(user_id) => {
 | 
			
		||||
                debug_assert!(self.users.is_some());
 | 
			
		||||
                self.users.as_mut().unwrap().retain(|u| u.id != user_id);
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_users(&self) -> Html {
 | 
			
		||||
        let make_table = |users: &Vec<User>| {
 | 
			
		||||
            html! {
 | 
			
		||||
                <div class="table-responsive">
 | 
			
		||||
@ -102,10 +122,11 @@ impl Component for UserTable {
 | 
			
		||||
                        <th>{"First name"}</th>
 | 
			
		||||
                        <th>{"Last name"}</th>
 | 
			
		||||
                        <th>{"Creation date"}</th>
 | 
			
		||||
                        <th>{"Delete"}</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                      {users.iter().map(make_user_row).collect::<Vec<_>>()}
 | 
			
		||||
                      {users.iter().map(|u| self.view_user(u)).collect::<Vec<_>>()}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                  </table>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -113,8 +134,33 @@ impl Component for UserTable {
 | 
			
		||||
        };
 | 
			
		||||
        match &self.users {
 | 
			
		||||
            None => html! {{"Loading..."}},
 | 
			
		||||
            Some(Err(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
 | 
			
		||||
            Some(Ok(users)) => make_table(users),
 | 
			
		||||
            Some(users) => make_table(users),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_user(&self, user: &User) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <tr key=user.id.clone()>
 | 
			
		||||
              <td><Link route=AppRoute::UserDetails(user.id.clone())>{&user.id}</Link></td>
 | 
			
		||||
              <td>{&user.email}</td>
 | 
			
		||||
              <td>{&user.display_name}</td>
 | 
			
		||||
              <td>{&user.first_name}</td>
 | 
			
		||||
              <td>{&user.last_name}</td>
 | 
			
		||||
              <td>{&user.creation_date.with_timezone(&chrono::Local)}</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <DeleteUser
 | 
			
		||||
                  username=user.id.clone()
 | 
			
		||||
                  on_user_deleted=self.link.callback(Msg::OnUserDeleted)
 | 
			
		||||
                  on_error=self.link.callback(Msg::OnError)/>
 | 
			
		||||
              </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_errors(&self) -> Html {
 | 
			
		||||
        match &self.error {
 | 
			
		||||
            None => html! {},
 | 
			
		||||
            Some(e) => html! {<div>{"Error: "}{e.to_string()}</div>},
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
pub mod api;
 | 
			
		||||
pub mod cookies;
 | 
			
		||||
pub mod graphql;
 | 
			
		||||
pub mod modal;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								app/src/infra/modal.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/infra/modal.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
use wasm_bindgen::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[wasm_bindgen(module = "bootstrap")]
 | 
			
		||||
extern "C" {
 | 
			
		||||
    #[wasm_bindgen]
 | 
			
		||||
    pub type Modal;
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(constructor)]
 | 
			
		||||
    pub fn new(e: web_sys::Element) -> Modal;
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(method)]
 | 
			
		||||
    pub fn show(this: &Modal);
 | 
			
		||||
 | 
			
		||||
    #[wasm_bindgen(method)]
 | 
			
		||||
    pub fn hide(this: &Modal);
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user