mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
app: Implement group management
Except group creation
This commit is contained in:
parent
42da86cf72
commit
8bd1dec180
5
app/queries/delete_group.graphql
Normal file
5
app/queries/delete_group.graphql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mutation DeleteGroupQuery($groupId: Int!) {
|
||||||
|
deleteGroup(groupId: $groupId) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
10
app/queries/get_group_details.graphql
Normal file
10
app/queries/get_group_details.graphql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
query GetGroupDetails($id: Int!) {
|
||||||
|
group(groupId: $id) {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,3 +8,9 @@ query ListUsersQuery($filters: RequestFilter) {
|
|||||||
creationDate
|
creationDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
query ListUserNames($filters: RequestFilter) {
|
||||||
|
users(filters: $filters) {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
202
app/src/components/add_group_member.rs
Normal file
202
app/src/components/add_group_member.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use crate::{
|
||||||
|
components::select::{Select, SelectOption, SelectOptionProps},
|
||||||
|
infra::api::HostService,
|
||||||
|
};
|
||||||
|
use anyhow::{Error, Result};
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use yew::{
|
||||||
|
prelude::*,
|
||||||
|
services::{fetch::FetchTask, ConsoleService},
|
||||||
|
};
|
||||||
|
use yewtil::NeqAssign;
|
||||||
|
|
||||||
|
#[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/list_users.graphql",
|
||||||
|
response_derives = "Debug,Clone,PartialEq,Eq,Hash",
|
||||||
|
variables_derives = "Clone",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
pub struct ListUserNames;
|
||||||
|
pub type User = list_user_names::ListUserNamesUsers;
|
||||||
|
|
||||||
|
pub struct AddGroupMemberComponent {
|
||||||
|
link: ComponentLink<Self>,
|
||||||
|
props: Props,
|
||||||
|
/// The list of existing users, initially not loaded.
|
||||||
|
user_list: Option<Vec<User>>,
|
||||||
|
/// The currently selected user.
|
||||||
|
selected_user: Option<User>,
|
||||||
|
// Used to keep the request alive long enough.
|
||||||
|
_task: Option<FetchTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
UserListResponse(Result<list_user_names::ResponseData>),
|
||||||
|
SubmitAddMember,
|
||||||
|
AddMemberResponse(Result<add_user_to_group::ResponseData>),
|
||||||
|
SelectionChanged(Option<SelectOptionProps>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(yew::Properties, Clone, PartialEq)]
|
||||||
|
pub struct Props {
|
||||||
|
pub group_id: i64,
|
||||||
|
pub users: Vec<User>,
|
||||||
|
pub on_user_added_to_group: Callback<User>,
|
||||||
|
pub on_error: Callback<Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddGroupMemberComponent {
|
||||||
|
fn get_user_list(&mut self) {
|
||||||
|
self._task = HostService::graphql_query::<ListUserNames>(
|
||||||
|
list_user_names::Variables { filters: None },
|
||||||
|
self.link.callback(Msg::UserListResponse),
|
||||||
|
"Error trying to fetch user list",
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
ConsoleService::log(&e.to_string());
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_add_member(&mut self) -> Result<bool> {
|
||||||
|
let user_id = match self.selected_user.clone() {
|
||||||
|
None => return Ok(false),
|
||||||
|
Some(user) => user.id,
|
||||||
|
};
|
||||||
|
self._task = HostService::graphql_query::<AddUserToGroup>(
|
||||||
|
add_user_to_group::Variables {
|
||||||
|
user: user_id,
|
||||||
|
group: self.props.group_id,
|
||||||
|
},
|
||||||
|
self.link.callback(Msg::AddMemberResponse),
|
||||||
|
"Error trying to initiate adding the user to a group",
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
ConsoleService::log(&e.to_string());
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
|
||||||
|
match msg {
|
||||||
|
Msg::UserListResponse(response) => {
|
||||||
|
self.user_list = Some(response?.users);
|
||||||
|
}
|
||||||
|
Msg::SubmitAddMember => return self.submit_add_member(),
|
||||||
|
Msg::AddMemberResponse(response) => {
|
||||||
|
response?;
|
||||||
|
let user = self
|
||||||
|
.selected_user
|
||||||
|
.as_ref()
|
||||||
|
.expect("Could not get selected user")
|
||||||
|
.clone();
|
||||||
|
// Remove the user from the dropdown.
|
||||||
|
self.props.on_user_added_to_group.emit(user);
|
||||||
|
}
|
||||||
|
Msg::SelectionChanged(option_props) => {
|
||||||
|
self.selected_user = option_props.map(|u| User {
|
||||||
|
id: u.value,
|
||||||
|
display_name: u.text,
|
||||||
|
});
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_selectable_user_list(&self, user_list: &[User]) -> Vec<User> {
|
||||||
|
let user_groups = self.props.users.iter().collect::<HashSet<_>>();
|
||||||
|
user_list
|
||||||
|
.iter()
|
||||||
|
.filter(|u| !user_groups.contains(u))
|
||||||
|
.map(Clone::clone)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for AddGroupMemberComponent {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = Props;
|
||||||
|
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||||
|
let mut res = Self {
|
||||||
|
link,
|
||||||
|
props,
|
||||||
|
user_list: None,
|
||||||
|
selected_user: None,
|
||||||
|
_task: None,
|
||||||
|
};
|
||||||
|
res.get_user_list();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, msg: Self::Message) -> ShouldRender {
|
||||||
|
match self.handle_msg(msg) {
|
||||||
|
Err(e) => {
|
||||||
|
ConsoleService::error(&e.to_string());
|
||||||
|
self.props.on_error.emit(e);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Ok(b) => b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change(&mut self, props: Self::Properties) -> ShouldRender {
|
||||||
|
self.props.neq_assign(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self) -> Html {
|
||||||
|
if let Some(user_list) = &self.user_list {
|
||||||
|
let to_add_user_list = self.get_selectable_user_list(user_list);
|
||||||
|
#[allow(unused_braces)]
|
||||||
|
let make_select_option = |user: User| {
|
||||||
|
html_nested! {
|
||||||
|
<SelectOption value=user.id.clone() text=user.display_name.clone() key=user.id />
|
||||||
|
}
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<td>
|
||||||
|
<Select on_selection_change=self.link.callback(Msg::SelectionChanged)>
|
||||||
|
{
|
||||||
|
to_add_user_list
|
||||||
|
.into_iter()
|
||||||
|
.map(make_select_option)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn-success"
|
||||||
|
onclick=self.link.callback(|_| Msg::SubmitAddMember)>
|
||||||
|
{"Add"}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<td>{"Loading groups"}</td>
|
||||||
|
<td></td>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,13 @@ use crate::{
|
|||||||
components::{
|
components::{
|
||||||
change_password::ChangePasswordForm,
|
change_password::ChangePasswordForm,
|
||||||
create_user::CreateUserForm,
|
create_user::CreateUserForm,
|
||||||
|
group_details::GroupDetails,
|
||||||
|
group_table::GroupTable,
|
||||||
login::LoginForm,
|
login::LoginForm,
|
||||||
logout::LogoutButton,
|
logout::LogoutButton,
|
||||||
router::{AppRoute, NavButton},
|
router::{AppRoute, NavButton},
|
||||||
user_details::UserDetails,
|
user_details::UserDetails,
|
||||||
user_table::UserTable,
|
user_table::UserTable,
|
||||||
group_table::GroupTable,
|
|
||||||
},
|
},
|
||||||
infra::cookies::get_cookie,
|
infra::cookies::get_cookie,
|
||||||
};
|
};
|
||||||
@ -104,36 +105,28 @@ impl Component for App {
|
|||||||
<LoginForm on_logged_in=link.callback(Msg::Login)/>
|
<LoginForm on_logged_in=link.callback(Msg::Login)/>
|
||||||
},
|
},
|
||||||
AppRoute::CreateUser => html! {
|
AppRoute::CreateUser => html! {
|
||||||
<div>
|
<CreateUserForm/>
|
||||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
|
||||||
<CreateUserForm/>
|
|
||||||
</div>
|
|
||||||
},
|
},
|
||||||
AppRoute::Index | AppRoute::ListUsers => html! {
|
AppRoute::Index | AppRoute::ListUsers => html! {
|
||||||
<div>
|
<div>
|
||||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
|
||||||
<UserTable />
|
<UserTable />
|
||||||
<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
|
<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
|
||||||
</div>
|
</div>
|
||||||
},
|
},
|
||||||
AppRoute::ListGroups => html! {
|
AppRoute::ListGroups => html! {
|
||||||
<div>
|
<div>
|
||||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
|
||||||
<GroupTable />
|
<GroupTable />
|
||||||
//<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
|
//<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
|
||||||
</div>
|
</div>
|
||||||
},
|
},
|
||||||
|
AppRoute::GroupDetails(group_id) => html! {
|
||||||
|
<GroupDetails group_id=group_id />
|
||||||
|
},
|
||||||
AppRoute::UserDetails(username) => html! {
|
AppRoute::UserDetails(username) => html! {
|
||||||
<div>
|
<UserDetails username=username.clone() is_admin=is_admin />
|
||||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
|
||||||
<UserDetails username=username.clone() is_admin=is_admin />
|
|
||||||
</div>
|
|
||||||
},
|
},
|
||||||
AppRoute::ChangePassword(username) => html! {
|
AppRoute::ChangePassword(username) => html! {
|
||||||
<div>
|
<ChangePasswordForm username=username.clone() is_admin=is_admin />
|
||||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
|
||||||
<ChangePasswordForm username=username.clone() is_admin=is_admin />
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -182,10 +175,9 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view_banner(&self) -> Html {
|
fn view_banner(&self) -> Html {
|
||||||
if !self.is_admin() {
|
html! {
|
||||||
html!{}
|
<>
|
||||||
} else {
|
{if self.is_admin() { html! {
|
||||||
html!{
|
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<NavButton
|
<NavButton
|
||||||
@ -202,7 +194,11 @@ impl App {
|
|||||||
</NavButton>
|
</NavButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
} } else { html!{} } }
|
||||||
|
{if self.user_info.is_some() { html! {
|
||||||
|
<LogoutButton on_logged_out=self.link.callback(|_| Msg::Logout) />
|
||||||
|
}} else { html! {} }}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
158
app/src/components/delete_group.rs
Normal file
158
app/src/components/delete_group.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use crate::{
|
||||||
|
components::group_table::Group,
|
||||||
|
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_group.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
pub struct DeleteGroupQuery;
|
||||||
|
|
||||||
|
pub struct DeleteGroup {
|
||||||
|
link: ComponentLink<Self>,
|
||||||
|
props: DeleteGroupProps,
|
||||||
|
node_ref: NodeRef,
|
||||||
|
modal: Option<Modal>,
|
||||||
|
_task: Option<FetchTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(yew::Properties, Clone, PartialEq, Debug)]
|
||||||
|
pub struct DeleteGroupProps {
|
||||||
|
pub group: Group,
|
||||||
|
pub on_group_deleted: Callback<i64>,
|
||||||
|
pub on_error: Callback<Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
ClickedDeleteGroup,
|
||||||
|
ConfirmDeleteGroup,
|
||||||
|
DismissModal,
|
||||||
|
DeleteGroupResponse(Result<delete_group_query::ResponseData>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for DeleteGroup {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = DeleteGroupProps;
|
||||||
|
|
||||||
|
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::ClickedDeleteGroup => {
|
||||||
|
self.modal.as_ref().expect("modal not initialized").show();
|
||||||
|
}
|
||||||
|
Msg::ConfirmDeleteGroup => {
|
||||||
|
self.update(Msg::DismissModal);
|
||||||
|
self._task = HostService::graphql_query::<DeleteGroupQuery>(
|
||||||
|
delete_group_query::Variables {
|
||||||
|
group_id: self.props.group.id,
|
||||||
|
},
|
||||||
|
self.link.callback(Msg::DeleteGroupResponse),
|
||||||
|
"Error trying to delete group",
|
||||||
|
)
|
||||||
|
.map_err(|e| self.props.on_error.emit(e))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Msg::DismissModal => {
|
||||||
|
self.modal.as_ref().expect("modal not initialized").hide();
|
||||||
|
}
|
||||||
|
Msg::DeleteGroupResponse(response) => {
|
||||||
|
if let Err(e) = response {
|
||||||
|
self.props.on_error.emit(e);
|
||||||
|
} else {
|
||||||
|
self.props.on_group_deleted.emit(self.props.group.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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::ClickedDeleteGroup)>
|
||||||
|
<i class="bi-x-circle-fill" aria-label="Delete group" />
|
||||||
|
</button>
|
||||||
|
{self.show_modal()}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteGroup {
|
||||||
|
fn show_modal(&self) -> Html {
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="exampleModal".to_string() + &self.props.group.id.to_string()
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="exampleModalLabel"
|
||||||
|
aria-hidden="true"
|
||||||
|
ref=self.node_ref.clone()>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exampleModalLabel">{"Delete group?"}</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 group "}
|
||||||
|
<b>{&self.props.group.display_name}</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::ConfirmDeleteGroup)
|
||||||
|
class="btn btn-danger">{"Yes, I'm sure"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
236
app/src/components/group_details.rs
Normal file
236
app/src/components/group_details.rs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use crate::{
|
||||||
|
components::{
|
||||||
|
add_group_member::{self, AddGroupMemberComponent},
|
||||||
|
remove_user_from_group::RemoveUserFromGroupComponent,
|
||||||
|
router::{AppRoute, Link},
|
||||||
|
},
|
||||||
|
infra::api::HostService,
|
||||||
|
};
|
||||||
|
use anyhow::{bail, Error, Result};
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
use yew::{
|
||||||
|
prelude::*,
|
||||||
|
services::{fetch::FetchTask, ConsoleService},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "../schema.graphql",
|
||||||
|
query_path = "queries/get_group_details.graphql",
|
||||||
|
response_derives = "Debug, Hash, PartialEq, Eq, Clone",
|
||||||
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
|
)]
|
||||||
|
pub struct GetGroupDetails;
|
||||||
|
|
||||||
|
pub type Group = get_group_details::GetGroupDetailsGroup;
|
||||||
|
pub type User = get_group_details::GetGroupDetailsGroupUsers;
|
||||||
|
pub type AddGroupMemberUser = add_group_member::User;
|
||||||
|
|
||||||
|
pub struct GroupDetails {
|
||||||
|
link: ComponentLink<Self>,
|
||||||
|
props: Props,
|
||||||
|
/// The group info. If none, the error is in `error`. If `error` is None, then we haven't
|
||||||
|
/// received the server response yet.
|
||||||
|
group: Option<Group>,
|
||||||
|
/// Error message displayed to the user.
|
||||||
|
error: Option<Error>,
|
||||||
|
// Used to keep the request alive long enough.
|
||||||
|
_task: Option<FetchTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 group details response, either the group data or an error.
|
||||||
|
GroupDetailsResponse(Result<get_group_details::ResponseData>),
|
||||||
|
OnError(Error),
|
||||||
|
OnUserAddedToGroup(AddGroupMemberUser),
|
||||||
|
OnUserRemovedFromGroup((String, i64)),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(yew::Properties, Clone, PartialEq)]
|
||||||
|
pub struct Props {
|
||||||
|
pub group_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GroupDetails {
|
||||||
|
fn get_group_details(&mut self) {
|
||||||
|
self._task = HostService::graphql_query::<GetGroupDetails>(
|
||||||
|
get_group_details::Variables {
|
||||||
|
id: self.props.group_id,
|
||||||
|
},
|
||||||
|
self.link.callback(Msg::GroupDetailsResponse),
|
||||||
|
"Error trying to fetch group details",
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
ConsoleService::log(&e.to_string());
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
|
||||||
|
match msg {
|
||||||
|
Msg::GroupDetailsResponse(response) => match response {
|
||||||
|
Ok(group) => self.group = Some(group.group),
|
||||||
|
Err(e) => {
|
||||||
|
self.group = None;
|
||||||
|
bail!("Error getting user details: {}", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Msg::OnError(e) => return Err(e),
|
||||||
|
Msg::OnUserAddedToGroup(user) => {
|
||||||
|
self.group.as_mut().unwrap().users.push(User {
|
||||||
|
id: user.id,
|
||||||
|
display_name: user.display_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Msg::OnUserRemovedFromGroup((user_id, _)) => {
|
||||||
|
self.group
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.users
|
||||||
|
.retain(|u| u.id != user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_messages(&self, error: &Option<Error>) -> Html {
|
||||||
|
if let Some(e) = error {
|
||||||
|
html! {
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<span>{"Error: "}{e.to_string()}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_user_list(&self, g: &Group) -> Html {
|
||||||
|
let make_user_row = |user: &User| {
|
||||||
|
let user_id = user.id.clone();
|
||||||
|
let display_name = user.display_name.clone();
|
||||||
|
html! {
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<Link route=AppRoute::UserDetails(user_id.clone())>
|
||||||
|
{user_id.clone()}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>{display_name}</td>
|
||||||
|
<td>
|
||||||
|
<RemoveUserFromGroupComponent
|
||||||
|
username=user_id
|
||||||
|
group_id=g.id
|
||||||
|
on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
|
||||||
|
on_error=self.link.callback(Msg::OnError)/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<h3>{"Members"}</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr key="headerRow">
|
||||||
|
<th>{"User Id"}</th>
|
||||||
|
<th>{"Display name"}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{if g.users.is_empty() {
|
||||||
|
html! {
|
||||||
|
<tr key="EmptyRow">
|
||||||
|
<td>{"No members"}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {<>{g.users.iter().map(make_user_row).collect::<Vec<_>>()}</>}
|
||||||
|
}}
|
||||||
|
<hr/>
|
||||||
|
<tr key="groupToAddRow">
|
||||||
|
{self.view_add_user_button(g)}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_add_user_button(&self, g: &Group) -> Html {
|
||||||
|
let users: Vec<_> = g
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.map(|u| AddGroupMemberUser {
|
||||||
|
id: u.id.clone(),
|
||||||
|
display_name: u.display_name.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
html! {
|
||||||
|
<AddGroupMemberComponent
|
||||||
|
group_id=g.id
|
||||||
|
users=users
|
||||||
|
on_error=self.link.callback(Msg::OnError)
|
||||||
|
on_user_added_to_group=self.link.callback(Msg::OnUserAddedToGroup)/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for GroupDetails {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = Props;
|
||||||
|
|
||||||
|
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
|
||||||
|
let mut table = Self {
|
||||||
|
link,
|
||||||
|
props,
|
||||||
|
_task: None,
|
||||||
|
group: None,
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
table.get_group_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 {
|
||||||
|
match (&self.group, &self.error) {
|
||||||
|
(None, None) => html! {{"Loading..."}},
|
||||||
|
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
|
||||||
|
(Some(u), error) => {
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
/*
|
||||||
|
<GroupDetailsForm
|
||||||
|
user=u.clone()
|
||||||
|
on_error=self.link.callback(Msg::OnError)/>
|
||||||
|
*/
|
||||||
|
{self.view_user_list(u)}
|
||||||
|
{self.view_messages(error)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
//components::{
|
components::{
|
||||||
// delete_group::DeleteGroup,
|
delete_group::DeleteGroup,
|
||||||
// router::{AppRoute, Link},
|
router::{AppRoute, Link},
|
||||||
//},
|
},
|
||||||
infra::api::HostService,
|
infra::api::HostService,
|
||||||
};
|
};
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
@ -14,14 +14,14 @@ use yew::services::{fetch::FetchTask, ConsoleService};
|
|||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "../schema.graphql",
|
schema_path = "../schema.graphql",
|
||||||
query_path = "queries/get_group_list.graphql",
|
query_path = "queries/get_group_list.graphql",
|
||||||
response_derives = "Debug",
|
response_derives = "Debug,Clone,PartialEq",
|
||||||
custom_scalars_module = "crate::infra::graphql"
|
custom_scalars_module = "crate::infra::graphql"
|
||||||
)]
|
)]
|
||||||
pub struct GetGroupList;
|
pub struct GetGroupList;
|
||||||
|
|
||||||
use get_group_list::ResponseData;
|
use get_group_list::ResponseData;
|
||||||
|
|
||||||
type Group = get_group_list::GetGroupListGroups;
|
pub type Group = get_group_list::GetGroupListGroups;
|
||||||
|
|
||||||
pub struct GroupTable {
|
pub struct GroupTable {
|
||||||
link: ComponentLink<Self>,
|
link: ComponentLink<Self>,
|
||||||
@ -116,9 +116,8 @@ impl GroupTable {
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{"Group ID"}</th>
|
<th>{"Groups"}</th>
|
||||||
<th>{"Display name"}</th>
|
<th>{"Delete"}</th>
|
||||||
//<th>{"Delete"}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -136,16 +135,18 @@ impl GroupTable {
|
|||||||
|
|
||||||
fn view_group(&self, group: &Group) -> Html {
|
fn view_group(&self, group: &Group) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<tr key=group.id.clone()>
|
<tr key=group.id>
|
||||||
//<td><Link route=AppRoute::GroupDetails(group.id.clone())>{&group.id}</Link></td>
|
<td>
|
||||||
<td>{&group.id}</td>
|
<Link route=AppRoute::GroupDetails(group.id)>
|
||||||
<td>{&group.display_name}</td>
|
{&group.display_name}
|
||||||
//<td>
|
</Link>
|
||||||
// <DeleteGroup
|
</td>
|
||||||
// groupname=group.id.clone()
|
<td>
|
||||||
// on_group_deleted=self.link.callback(Msg::OnGroupDeleted)
|
<DeleteGroup
|
||||||
// on_error=self.link.callback(Msg::OnError)/>
|
group=group.clone()
|
||||||
//</td>
|
on_group_deleted=self.link.callback(Msg::OnGroupDeleted)
|
||||||
|
on_error=self.link.callback(Msg::OnError)/>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
pub mod add_group_member;
|
||||||
pub mod add_user_to_group;
|
pub mod add_user_to_group;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod change_password;
|
pub mod change_password;
|
||||||
pub mod create_user;
|
pub mod create_user;
|
||||||
|
pub mod delete_group;
|
||||||
pub mod delete_user;
|
pub mod delete_user;
|
||||||
|
pub mod group_details;
|
||||||
pub mod group_table;
|
pub mod group_table;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{components::user_details::Group, infra::api::HostService};
|
use crate::infra::api::HostService;
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use graphql_client::GraphQLQuery;
|
use graphql_client::GraphQLQuery;
|
||||||
use yew::{
|
use yew::{
|
||||||
@ -26,9 +26,8 @@ pub struct RemoveUserFromGroupComponent {
|
|||||||
#[derive(yew::Properties, Clone, PartialEq)]
|
#[derive(yew::Properties, Clone, PartialEq)]
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub group: Group,
|
pub group_id: i64,
|
||||||
pub is_admin: bool,
|
pub on_user_removed_from_group: Callback<(String, i64)>,
|
||||||
pub on_user_removed_from_group: Callback<Group>,
|
|
||||||
pub on_error: Callback<Error>,
|
pub on_error: Callback<Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ pub enum Msg {
|
|||||||
|
|
||||||
impl RemoveUserFromGroupComponent {
|
impl RemoveUserFromGroupComponent {
|
||||||
fn submit_remove_group(&mut self) -> Result<bool> {
|
fn submit_remove_group(&mut self) -> Result<bool> {
|
||||||
let group = self.props.group.id;
|
let group = self.props.group_id;
|
||||||
self._task = HostService::graphql_query::<RemoveUserFromGroup>(
|
self._task = HostService::graphql_query::<RemoveUserFromGroup>(
|
||||||
remove_user_from_group::Variables {
|
remove_user_from_group::Variables {
|
||||||
user: self.props.username.clone(),
|
user: self.props.username.clone(),
|
||||||
@ -63,7 +62,7 @@ impl RemoveUserFromGroupComponent {
|
|||||||
response?;
|
response?;
|
||||||
self.props
|
self.props
|
||||||
.on_user_removed_from_group
|
.on_user_removed_from_group
|
||||||
.emit(self.props.group.clone());
|
.emit((self.props.username.clone(), self.props.group_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -97,21 +96,12 @@ impl Component for RemoveUserFromGroupComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Html {
|
fn view(&self) -> Html {
|
||||||
let group = &self.props.group;
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<button
|
||||||
<td>{&group.display_name}</td>
|
class="btn btn-danger"
|
||||||
{ if self.props.is_admin { html! {
|
onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>
|
||||||
<td>
|
<i class="bi-x-circle-fill" aria-label="Remove user from group" />
|
||||||
<button
|
</button>
|
||||||
class="btn btn-danger"
|
|
||||||
onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>
|
|
||||||
<i class="bi-x-circle-fill" aria-label="Remove user from group" />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
}} else { html!{} }
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ pub enum AppRoute {
|
|||||||
UserDetails(String),
|
UserDetails(String),
|
||||||
#[to = "/groups"]
|
#[to = "/groups"]
|
||||||
ListGroups,
|
ListGroups,
|
||||||
|
#[to = "/group/{group_id}"]
|
||||||
|
GroupDetails(i64),
|
||||||
#[to = "/"]
|
#[to = "/"]
|
||||||
Index,
|
Index,
|
||||||
}
|
}
|
||||||
|
@ -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, NavButton},
|
router::{AppRoute, Link, NavButton},
|
||||||
user_details_form::UserDetailsForm,
|
user_details_form::UserDetailsForm,
|
||||||
},
|
},
|
||||||
infra::api::HostService,
|
infra::api::HostService,
|
||||||
@ -45,7 +45,7 @@ pub enum Msg {
|
|||||||
UserDetailsResponse(Result<get_user_details::ResponseData>),
|
UserDetailsResponse(Result<get_user_details::ResponseData>),
|
||||||
OnError(Error),
|
OnError(Error),
|
||||||
OnUserAddedToGroup(Group),
|
OnUserAddedToGroup(Group),
|
||||||
OnUserRemovedFromGroup(Group),
|
OnUserRemovedFromGroup((String, i64)),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(yew::Properties, Clone, PartialEq)]
|
#[derive(yew::Properties, Clone, PartialEq)]
|
||||||
@ -83,8 +83,12 @@ impl UserDetails {
|
|||||||
Msg::OnUserAddedToGroup(group) => {
|
Msg::OnUserAddedToGroup(group) => {
|
||||||
self.user.as_mut().unwrap().groups.push(group);
|
self.user.as_mut().unwrap().groups.push(group);
|
||||||
}
|
}
|
||||||
Msg::OnUserRemovedFromGroup(group) => {
|
Msg::OnUserRemovedFromGroup((_, group_id)) => {
|
||||||
self.user.as_mut().unwrap().groups.retain(|g| g != &group);
|
self.user
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.groups
|
||||||
|
.retain(|g| g.id != group_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -107,12 +111,24 @@ impl UserDetails {
|
|||||||
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>
|
||||||
<RemoveUserFromGroupComponent
|
{if self.props.is_admin { html! {
|
||||||
username=u.id.clone()
|
<>
|
||||||
group=group.clone()
|
<td>
|
||||||
is_admin=self.props.is_admin
|
<Link route=AppRoute::GroupDetails(group.id)>
|
||||||
on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
|
{&group.display_name}
|
||||||
on_error=self.link.callback(Msg::OnError)/>
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<RemoveUserFromGroupComponent
|
||||||
|
username=u.id.clone()
|
||||||
|
group_id=group.id
|
||||||
|
on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
|
||||||
|
on_error=self.link.callback(Msg::OnError)/>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
} } else { html! {
|
||||||
|
<td>{&group.display_name}</td>
|
||||||
|
} } }
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user