mirror of
https://github.com/lldap/lldap.git
synced 2024-11-24 16:47:01 +00:00
app: Implement group management
Except group creation
This commit is contained in:
parent
42da86cf72
commit
8bd1dec180
12 changed files with 694 additions and 69 deletions
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
|
||||
}
|
||||
}
|
||||
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::{
|
||||
change_password::ChangePasswordForm,
|
||||
create_user::CreateUserForm,
|
||||
group_details::GroupDetails,
|
||||
group_table::GroupTable,
|
||||
login::LoginForm,
|
||||
logout::LogoutButton,
|
||||
router::{AppRoute, NavButton},
|
||||
user_details::UserDetails,
|
||||
user_table::UserTable,
|
||||
group_table::GroupTable,
|
||||
},
|
||||
infra::cookies::get_cookie,
|
||||
};
|
||||
|
@ -104,36 +105,28 @@ impl Component for App {
|
|||
<LoginForm on_logged_in=link.callback(Msg::Login)/>
|
||||
},
|
||||
AppRoute::CreateUser => html! {
|
||||
<div>
|
||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
||||
<CreateUserForm/>
|
||||
</div>
|
||||
<CreateUserForm/>
|
||||
},
|
||||
AppRoute::Index | AppRoute::ListUsers => html! {
|
||||
<div>
|
||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
||||
<UserTable />
|
||||
<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
|
||||
</div>
|
||||
},
|
||||
AppRoute::ListGroups => html! {
|
||||
<div>
|
||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
||||
<GroupTable />
|
||||
//<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
|
||||
</div>
|
||||
},
|
||||
AppRoute::GroupDetails(group_id) => html! {
|
||||
<GroupDetails group_id=group_id />
|
||||
},
|
||||
AppRoute::UserDetails(username) => html! {
|
||||
<div>
|
||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
||||
<UserDetails username=username.clone() is_admin=is_admin />
|
||||
</div>
|
||||
<UserDetails username=username.clone() is_admin=is_admin />
|
||||
},
|
||||
AppRoute::ChangePassword(username) => html! {
|
||||
<div>
|
||||
<LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
|
||||
<ChangePasswordForm username=username.clone() is_admin=is_admin />
|
||||
</div>
|
||||
<ChangePasswordForm username=username.clone() is_admin=is_admin />
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -182,10 +175,9 @@ impl App {
|
|||
}
|
||||
|
||||
fn view_banner(&self) -> Html {
|
||||
if !self.is_admin() {
|
||||
html!{}
|
||||
} else {
|
||||
html!{
|
||||
html! {
|
||||
<>
|
||||
{if self.is_admin() { html! {
|
||||
<>
|
||||
<div>
|
||||
<NavButton
|
||||
|
@ -202,7 +194,11 @@ impl App {
|
|||
</NavButton>
|
||||
</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::{
|
||||
//components::{
|
||||
// delete_group::DeleteGroup,
|
||||
// router::{AppRoute, Link},
|
||||
//},
|
||||
components::{
|
||||
delete_group::DeleteGroup,
|
||||
router::{AppRoute, Link},
|
||||
},
|
||||
infra::api::HostService,
|
||||
};
|
||||
use anyhow::{Error, Result};
|
||||
|
@ -14,14 +14,14 @@ use yew::services::{fetch::FetchTask, ConsoleService};
|
|||
#[graphql(
|
||||
schema_path = "../schema.graphql",
|
||||
query_path = "queries/get_group_list.graphql",
|
||||
response_derives = "Debug",
|
||||
response_derives = "Debug,Clone,PartialEq",
|
||||
custom_scalars_module = "crate::infra::graphql"
|
||||
)]
|
||||
pub struct GetGroupList;
|
||||
|
||||
use get_group_list::ResponseData;
|
||||
|
||||
type Group = get_group_list::GetGroupListGroups;
|
||||
pub type Group = get_group_list::GetGroupListGroups;
|
||||
|
||||
pub struct GroupTable {
|
||||
link: ComponentLink<Self>,
|
||||
|
@ -116,9 +116,8 @@ impl GroupTable {
|
|||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{"Group ID"}</th>
|
||||
<th>{"Display name"}</th>
|
||||
//<th>{"Delete"}</th>
|
||||
<th>{"Groups"}</th>
|
||||
<th>{"Delete"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -136,16 +135,18 @@ impl GroupTable {
|
|||
|
||||
fn view_group(&self, group: &Group) -> Html {
|
||||
html! {
|
||||
<tr key=group.id.clone()>
|
||||
//<td><Link route=AppRoute::GroupDetails(group.id.clone())>{&group.id}</Link></td>
|
||||
<td>{&group.id}</td>
|
||||
<td>{&group.display_name}</td>
|
||||
//<td>
|
||||
// <DeleteGroup
|
||||
// groupname=group.id.clone()
|
||||
// on_group_deleted=self.link.callback(Msg::OnGroupDeleted)
|
||||
// on_error=self.link.callback(Msg::OnError)/>
|
||||
//</td>
|
||||
<tr key=group.id>
|
||||
<td>
|
||||
<Link route=AppRoute::GroupDetails(group.id)>
|
||||
{&group.display_name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<DeleteGroup
|
||||
group=group.clone()
|
||||
on_group_deleted=self.link.callback(Msg::OnGroupDeleted)
|
||||
on_error=self.link.callback(Msg::OnError)/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
pub mod add_group_member;
|
||||
pub mod add_user_to_group;
|
||||
pub mod app;
|
||||
pub mod change_password;
|
||||
pub mod create_user;
|
||||
pub mod delete_group;
|
||||
pub mod delete_user;
|
||||
pub mod group_details;
|
||||
pub mod group_table;
|
||||
pub mod login;
|
||||
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 graphql_client::GraphQLQuery;
|
||||
use yew::{
|
||||
|
@ -26,9 +26,8 @@ pub struct RemoveUserFromGroupComponent {
|
|||
#[derive(yew::Properties, Clone, PartialEq)]
|
||||
pub struct Props {
|
||||
pub username: String,
|
||||
pub group: Group,
|
||||
pub is_admin: bool,
|
||||
pub on_user_removed_from_group: Callback<Group>,
|
||||
pub group_id: i64,
|
||||
pub on_user_removed_from_group: Callback<(String, i64)>,
|
||||
pub on_error: Callback<Error>,
|
||||
}
|
||||
|
||||
|
@ -39,7 +38,7 @@ pub enum Msg {
|
|||
|
||||
impl RemoveUserFromGroupComponent {
|
||||
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>(
|
||||
remove_user_from_group::Variables {
|
||||
user: self.props.username.clone(),
|
||||
|
@ -63,7 +62,7 @@ impl RemoveUserFromGroupComponent {
|
|||
response?;
|
||||
self.props
|
||||
.on_user_removed_from_group
|
||||
.emit(self.props.group.clone());
|
||||
.emit((self.props.username.clone(), self.props.group_id));
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
|
@ -97,21 +96,12 @@ impl Component for RemoveUserFromGroupComponent {
|
|||
}
|
||||
|
||||
fn view(&self) -> Html {
|
||||
let group = &self.props.group;
|
||||
html! {
|
||||
<>
|
||||
<td>{&group.display_name}</td>
|
||||
{ if self.props.is_admin { html! {
|
||||
<td>
|
||||
<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!{} }
|
||||
}
|
||||
</>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>
|
||||
<i class="bi-x-circle-fill" aria-label="Remove user from group" />
|
||||
</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ pub enum AppRoute {
|
|||
UserDetails(String),
|
||||
#[to = "/groups"]
|
||||
ListGroups,
|
||||
#[to = "/group/{group_id}"]
|
||||
GroupDetails(i64),
|
||||
#[to = "/"]
|
||||
Index,
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
|||
components::{
|
||||
add_user_to_group::AddUserToGroupComponent,
|
||||
remove_user_from_group::RemoveUserFromGroupComponent,
|
||||
router::{AppRoute, NavButton},
|
||||
router::{AppRoute, Link, NavButton},
|
||||
user_details_form::UserDetailsForm,
|
||||
},
|
||||
infra::api::HostService,
|
||||
|
@ -45,7 +45,7 @@ pub enum Msg {
|
|||
UserDetailsResponse(Result<get_user_details::ResponseData>),
|
||||
OnError(Error),
|
||||
OnUserAddedToGroup(Group),
|
||||
OnUserRemovedFromGroup(Group),
|
||||
OnUserRemovedFromGroup((String, i64)),
|
||||
}
|
||||
|
||||
#[derive(yew::Properties, Clone, PartialEq)]
|
||||
|
@ -83,8 +83,12 @@ impl UserDetails {
|
|||
Msg::OnUserAddedToGroup(group) => {
|
||||
self.user.as_mut().unwrap().groups.push(group);
|
||||
}
|
||||
Msg::OnUserRemovedFromGroup(group) => {
|
||||
self.user.as_mut().unwrap().groups.retain(|g| g != &group);
|
||||
Msg::OnUserRemovedFromGroup((_, group_id)) => {
|
||||
self.user
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.groups
|
||||
.retain(|g| g.id != group_id);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
|
@ -107,12 +111,24 @@ impl UserDetails {
|
|||
let display_name = group.display_name.clone();
|
||||
html! {
|
||||
<tr key="groupRow_".to_string() + &display_name>
|
||||
<RemoveUserFromGroupComponent
|
||||
username=u.id.clone()
|
||||
group=group.clone()
|
||||
is_admin=self.props.is_admin
|
||||
on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
|
||||
on_error=self.link.callback(Msg::OnError)/>
|
||||
{if self.props.is_admin { html! {
|
||||
<>
|
||||
<td>
|
||||
<Link route=AppRoute::GroupDetails(group.id)>
|
||||
{&group.display_name}
|
||||
</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>
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue