mirror of
https://github.com/lldap/lldap.git
synced 2024-11-25 09:06:03 +00:00
app: Add support for group attributes
This commit is contained in:
parent
a95ac38083
commit
305b272cdf
8 changed files with 367 additions and 48 deletions
|
@ -75,3 +75,10 @@ rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
|
|||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.dev]
|
||||
wasm-opt = ['--enable-bulk-memory']
|
||||
[package.metadata.wasm-pack.profile.profiling]
|
||||
wasm-opt = ['--enable-bulk-memory']
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ['--enable-bulk-memory']
|
||||
|
|
|
@ -8,5 +8,22 @@ query GetGroupDetails($id: Int!) {
|
|||
id
|
||||
displayName
|
||||
}
|
||||
attributes {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
schema {
|
||||
groupSchema {
|
||||
attributes {
|
||||
name
|
||||
attributeType
|
||||
isList
|
||||
isVisible
|
||||
isEditable
|
||||
isHardcoded
|
||||
isReadonly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
6
app/queries/update_group.graphql
Normal file
6
app/queries/update_group.graphql
Normal file
|
@ -0,0 +1,6 @@
|
|||
mutation UpdateGroup($group: UpdateGroupInput!) {
|
||||
updateGroup(group: $group) {
|
||||
ok
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ impl App {
|
|||
<ListGroupSchema />
|
||||
},
|
||||
AppRoute::GroupDetails { group_id } => html! {
|
||||
<GroupDetails group_id={*group_id} />
|
||||
<GroupDetails group_id={*group_id} is_admin={is_admin} />
|
||||
},
|
||||
AppRoute::UserDetails { user_id } => html! {
|
||||
<UserDetails username={user_id.clone()} is_admin={is_admin} />
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
use crate::{
|
||||
components::{
|
||||
add_group_member::{self, AddGroupMemberComponent},
|
||||
group_details_form::GroupDetailsForm,
|
||||
remove_user_from_group::RemoveUserFromGroupComponent,
|
||||
router::{AppRoute, Link},
|
||||
},
|
||||
infra::common_component::{CommonComponent, CommonComponentParts},
|
||||
convert_attribute_type,
|
||||
infra::{
|
||||
common_component::{CommonComponent, CommonComponentParts},
|
||||
form_utils::GraphQlAttributeSchema,
|
||||
},
|
||||
};
|
||||
use anyhow::{bail, Error, Result};
|
||||
use graphql_client::GraphQLQuery;
|
||||
|
@ -22,12 +27,28 @@ 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 type Attribute = get_group_details::GetGroupDetailsGroupAttributes;
|
||||
pub type AttributeSchema = get_group_details::GetGroupDetailsSchemaGroupSchemaAttributes;
|
||||
pub type AttributeType = get_group_details::AttributeType;
|
||||
|
||||
convert_attribute_type!(AttributeType);
|
||||
|
||||
impl From<&AttributeSchema> for GraphQlAttributeSchema {
|
||||
fn from(attr: &AttributeSchema) -> Self {
|
||||
Self {
|
||||
name: attr.name.clone(),
|
||||
is_list: attr.is_list,
|
||||
is_readonly: attr.is_readonly,
|
||||
is_editable: attr.is_editable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GroupDetails {
|
||||
common: CommonComponentParts<Self>,
|
||||
/// 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>,
|
||||
group_and_schema: Option<(Group, Vec<AttributeSchema>)>,
|
||||
}
|
||||
|
||||
/// State machine describing the possible transitions of the component state.
|
||||
|
@ -38,11 +59,13 @@ pub enum Msg {
|
|||
OnError(Error),
|
||||
OnUserAddedToGroup(AddGroupMemberUser),
|
||||
OnUserRemovedFromGroup((String, i64)),
|
||||
DisplayNameUpdated,
|
||||
}
|
||||
|
||||
#[derive(yew::Properties, Clone, PartialEq, Eq)]
|
||||
pub struct Props {
|
||||
pub group_id: i64,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl GroupDetails {
|
||||
|
@ -69,41 +92,16 @@ impl GroupDetails {
|
|||
}
|
||||
}
|
||||
|
||||
fn view_details(&self, g: &Group) -> Html {
|
||||
fn view_details(&self, ctx: &Context<Self>, g: &Group, schema: Vec<AttributeSchema>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<h3>{g.display_name.to_string()}</h3>
|
||||
<div class="py-3">
|
||||
<form class="form">
|
||||
<div class="form-group row mb-3">
|
||||
<label for="displayName"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Group: "}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<span id="groupId" class="form-constrol-static">{g.display_name.to_string()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="creationDate"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"Creation date: "}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<span id="creationDate" class="form-constrol-static">{g.creation_date.naive_local().date()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-3">
|
||||
<label for="uuid"
|
||||
class="form-label col-4 col-form-label">
|
||||
{"UUID: "}
|
||||
</label>
|
||||
<div class="col-8">
|
||||
<span id="uuid" class="form-constrol-static">{g.uuid.to_string()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<GroupDetailsForm
|
||||
group={g.clone()}
|
||||
group_attributes_schema={schema}
|
||||
is_admin={ctx.props().is_admin}
|
||||
on_display_name_updated={ctx.link().callback(|_| Msg::DisplayNameUpdated)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
@ -182,29 +180,38 @@ impl GroupDetails {
|
|||
}
|
||||
|
||||
impl CommonComponent<GroupDetails> for GroupDetails {
|
||||
fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
|
||||
fn handle_msg(
|
||||
&mut self,
|
||||
ctx: &Context<Self>,
|
||||
msg: <Self as Component>::Message,
|
||||
) -> Result<bool> {
|
||||
match msg {
|
||||
Msg::GroupDetailsResponse(response) => match response {
|
||||
Ok(group) => self.group = Some(group.group),
|
||||
Ok(group) => {
|
||||
self.group_and_schema =
|
||||
Some((group.group, group.schema.group_schema.attributes))
|
||||
}
|
||||
Err(e) => {
|
||||
self.group = None;
|
||||
self.group_and_schema = None;
|
||||
bail!("Error getting user details: {}", e);
|
||||
}
|
||||
},
|
||||
Msg::OnError(e) => return Err(e),
|
||||
Msg::OnUserAddedToGroup(user) => {
|
||||
self.group.as_mut().unwrap().users.push(User {
|
||||
self.group_and_schema.as_mut().unwrap().0.users.push(User {
|
||||
id: user.id,
|
||||
display_name: user.display_name,
|
||||
});
|
||||
}
|
||||
Msg::OnUserRemovedFromGroup((user_id, _)) => {
|
||||
self.group
|
||||
self.group_and_schema
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.0
|
||||
.users
|
||||
.retain(|u| u.id != user_id);
|
||||
}
|
||||
Msg::DisplayNameUpdated => self.get_group_details(ctx),
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
@ -221,7 +228,7 @@ impl Component for GroupDetails {
|
|||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let mut table = Self {
|
||||
common: CommonComponentParts::<Self>::create(),
|
||||
group: None,
|
||||
group_and_schema: None,
|
||||
};
|
||||
table.get_group_details(ctx);
|
||||
table
|
||||
|
@ -232,15 +239,15 @@ impl Component for GroupDetails {
|
|||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
match (&self.group, &self.common.error) {
|
||||
match (&self.group_and_schema, &self.common.error) {
|
||||
(None, None) => html! {{"Loading..."}},
|
||||
(None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
|
||||
(Some(u), error) => {
|
||||
(Some((group, schema)), error) => {
|
||||
html! {
|
||||
<div>
|
||||
{self.view_details(u)}
|
||||
{self.view_user_list(ctx, u)}
|
||||
{self.view_add_user_button(ctx, u)}
|
||||
{self.view_details(ctx, group, schema.clone())}
|
||||
{self.view_user_list(ctx, group)}
|
||||
{self.view_add_user_button(ctx, group)}
|
||||
{self.view_messages(error)}
|
||||
</div>
|
||||
}
|
||||
|
|
272
app/src/components/group_details_form.rs
Normal file
272
app/src/components/group_details_form.rs
Normal file
|
@ -0,0 +1,272 @@
|
|||
use crate::{
|
||||
components::{
|
||||
form::{
|
||||
attribute_input::{ListAttributeInput, SingleAttributeInput},
|
||||
static_value::StaticValue,
|
||||
submit::Submit,
|
||||
},
|
||||
group_details::{Attribute, AttributeSchema, Group},
|
||||
},
|
||||
infra::{
|
||||
common_component::{CommonComponent, CommonComponentParts},
|
||||
form_utils::{read_all_form_attributes, AttributeValue},
|
||||
schema::AttributeType,
|
||||
},
|
||||
};
|
||||
use anyhow::{Ok, Result};
|
||||
use graphql_client::GraphQLQuery;
|
||||
use yew::prelude::*;
|
||||
|
||||
/// The GraphQL query sent to the server to update the group details.
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "../schema.graphql",
|
||||
query_path = "queries/update_group.graphql",
|
||||
response_derives = "Debug",
|
||||
variables_derives = "Clone,PartialEq,Eq",
|
||||
custom_scalars_module = "crate::infra::graphql"
|
||||
)]
|
||||
pub struct UpdateGroup;
|
||||
|
||||
/// A [yew::Component] to display the group details, with a form allowing to edit them.
|
||||
pub struct GroupDetailsForm {
|
||||
common: CommonComponentParts<Self>,
|
||||
/// True if we just successfully updated the group, to display a success message.
|
||||
just_updated: bool,
|
||||
updated_group_name: bool,
|
||||
group: Group,
|
||||
form_ref: NodeRef,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
/// A form field changed.
|
||||
Update,
|
||||
/// The "Submit" button was clicked.
|
||||
SubmitClicked,
|
||||
/// We got the response from the server about our update message.
|
||||
GroupUpdated(Result<update_group::ResponseData>),
|
||||
}
|
||||
|
||||
#[derive(yew::Properties, Clone, PartialEq)]
|
||||
pub struct Props {
|
||||
/// The current group details.
|
||||
pub group: Group,
|
||||
pub group_attributes_schema: Vec<AttributeSchema>,
|
||||
pub is_admin: bool,
|
||||
pub on_display_name_updated: Callback<()>,
|
||||
}
|
||||
|
||||
impl CommonComponent<GroupDetailsForm> for GroupDetailsForm {
|
||||
fn handle_msg(
|
||||
&mut self,
|
||||
ctx: &Context<Self>,
|
||||
msg: <Self as Component>::Message,
|
||||
) -> Result<bool> {
|
||||
match msg {
|
||||
Msg::Update => Ok(true),
|
||||
Msg::SubmitClicked => self.submit_group_update_form(ctx),
|
||||
Msg::GroupUpdated(Err(e)) => Err(e),
|
||||
Msg::GroupUpdated(Result::Ok(_)) => {
|
||||
self.just_updated = true;
|
||||
if self.updated_group_name {
|
||||
self.updated_group_name = false;
|
||||
ctx.props().on_display_name_updated.emit(());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mut_common(&mut self) -> &mut CommonComponentParts<Self> {
|
||||
&mut self.common
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for GroupDetailsForm {
|
||||
type Message = Msg;
|
||||
type Properties = Props;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
common: CommonComponentParts::<Self>::create(),
|
||||
just_updated: false,
|
||||
updated_group_name: false,
|
||||
group: ctx.props().group.clone(),
|
||||
form_ref: NodeRef::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
self.just_updated = false;
|
||||
CommonComponentParts::<Self>::update(self, ctx, msg)
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let link = &ctx.link();
|
||||
|
||||
let can_edit =
|
||||
|a: &AttributeSchema| (ctx.props().is_admin || a.is_editable) && !a.is_readonly;
|
||||
let display_field = |a: &AttributeSchema| {
|
||||
if can_edit(a) {
|
||||
get_custom_attribute_input(a, &self.group.attributes)
|
||||
} else {
|
||||
get_custom_attribute_static(a, &self.group.attributes)
|
||||
}
|
||||
};
|
||||
html! {
|
||||
<div class="py-3">
|
||||
<form
|
||||
class="form"
|
||||
ref={self.form_ref.clone()}>
|
||||
<StaticValue label="Group ID" id="groupId">
|
||||
<i>{&self.group.id}</i>
|
||||
</StaticValue>
|
||||
{
|
||||
ctx
|
||||
.props()
|
||||
.group_attributes_schema
|
||||
.iter()
|
||||
.filter(|a| a.is_hardcoded && a.name != "group_id")
|
||||
.map(display_field)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
{
|
||||
ctx
|
||||
.props()
|
||||
.group_attributes_schema
|
||||
.iter()
|
||||
.filter(|a| !a.is_hardcoded)
|
||||
.map(display_field)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
<Submit
|
||||
text="Save changes"
|
||||
disabled={self.common.is_task_running()}
|
||||
onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})} />
|
||||
</form>
|
||||
{
|
||||
if let Some(e) = &self.common.error {
|
||||
html! {
|
||||
<div class="alert alert-danger">
|
||||
{e.to_string() }
|
||||
</div>
|
||||
}
|
||||
} else { html! {} }
|
||||
}
|
||||
<div hidden={!self.just_updated}>
|
||||
<div class="alert alert-success mt-4">{"Group successfully updated!"}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_custom_attribute_input(
|
||||
attribute_schema: &AttributeSchema,
|
||||
group_attributes: &[Attribute],
|
||||
) -> Html {
|
||||
let values = group_attributes
|
||||
.iter()
|
||||
.find(|a| a.name == attribute_schema.name)
|
||||
.map(|attribute| attribute.value.clone())
|
||||
.unwrap_or_default();
|
||||
if attribute_schema.is_list {
|
||||
html! {
|
||||
<ListAttributeInput
|
||||
name={attribute_schema.name.clone()}
|
||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
||||
values={values}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<SingleAttributeInput
|
||||
name={attribute_schema.name.clone()}
|
||||
attribute_type={Into::<AttributeType>::into(attribute_schema.attribute_type.clone())}
|
||||
value={values.first().cloned().unwrap_or_default()}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_custom_attribute_static(
|
||||
attribute_schema: &AttributeSchema,
|
||||
group_attributes: &[Attribute],
|
||||
) -> Html {
|
||||
let values = group_attributes
|
||||
.iter()
|
||||
.find(|a| a.name == attribute_schema.name)
|
||||
.map(|attribute| attribute.value.clone())
|
||||
.unwrap_or_default();
|
||||
html! {
|
||||
<StaticValue label={attribute_schema.name.clone()} id={attribute_schema.name.clone()}>
|
||||
{values.into_iter().map(|x| html!{<div>{x}</div>}).collect::<Vec<_>>()}
|
||||
</StaticValue>
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupDetailsForm {
|
||||
fn submit_group_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
|
||||
let mut all_values = read_all_form_attributes(
|
||||
ctx.props().group_attributes_schema.iter(),
|
||||
&self.form_ref,
|
||||
ctx.props().is_admin,
|
||||
false,
|
||||
)?;
|
||||
let base_attributes = &self.group.attributes;
|
||||
all_values.retain(|a| {
|
||||
let base_val = base_attributes
|
||||
.iter()
|
||||
.find(|base_val| base_val.name == a.name);
|
||||
base_val
|
||||
.map(|v| v.value != a.values)
|
||||
.unwrap_or(!a.values.is_empty())
|
||||
});
|
||||
if all_values.iter().any(|a| a.name == "display_name") {
|
||||
self.updated_group_name = true;
|
||||
}
|
||||
let remove_attributes: Option<Vec<String>> = if all_values.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(all_values.iter().map(|a| a.name.clone()).collect())
|
||||
};
|
||||
let insert_attributes: Option<Vec<update_group::AttributeValueInput>> =
|
||||
if remove_attributes.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
all_values
|
||||
.into_iter()
|
||||
.filter(|a| !a.values.is_empty())
|
||||
.map(
|
||||
|AttributeValue { name, values }| update_group::AttributeValueInput {
|
||||
name,
|
||||
value: values,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
let mut group_input = update_group::UpdateGroupInput {
|
||||
id: self.group.id,
|
||||
displayName: None,
|
||||
removeAttributes: None,
|
||||
insertAttributes: None,
|
||||
};
|
||||
let default_group_input = group_input.clone();
|
||||
group_input.removeAttributes = remove_attributes;
|
||||
group_input.insertAttributes = insert_attributes;
|
||||
// Nothing changed.
|
||||
if group_input == default_group_input {
|
||||
return Ok(false);
|
||||
}
|
||||
let req = update_group::Variables { group: group_input };
|
||||
self.common.call_graphql::<UpdateGroup, _>(
|
||||
ctx,
|
||||
req,
|
||||
Msg::GroupUpdated,
|
||||
"Error trying to update group",
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ pub mod delete_user;
|
|||
pub mod delete_user_attribute;
|
||||
pub mod form;
|
||||
pub mod group_details;
|
||||
pub mod group_details_form;
|
||||
pub mod group_schema_table;
|
||||
pub mod group_table;
|
||||
pub mod login;
|
||||
|
|
|
@ -298,7 +298,14 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
let handler = context
|
||||
.get_admin_handler()
|
||||
.ok_or_else(field_error_callback(&span, "Unauthorized group update"))?;
|
||||
if group.id == 1 && group.display_name.is_some() {
|
||||
let new_display_name = group.display_name.clone().or_else(|| {
|
||||
group.insert_attributes.as_ref().and_then(|a| {
|
||||
a.iter()
|
||||
.find(|attr| attr.name == "display_name")
|
||||
.map(|attr| attr.value[0].clone())
|
||||
})
|
||||
});
|
||||
if group.id == 1 && new_display_name.is_some() {
|
||||
span.in_scope(|| debug!("Cannot change lldap_admin group name"));
|
||||
return Err("Cannot change lldap_admin group name".into());
|
||||
}
|
||||
|
@ -307,16 +314,18 @@ impl<Handler: BackendHandler> Mutation<Handler> {
|
|||
.insert_attributes
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|attr| attr.name != "display_name")
|
||||
.map(|attr| deserialize_attribute(&schema.get_schema().group_attributes, attr, true))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
handler
|
||||
.update_group(UpdateGroupRequest {
|
||||
group_id: GroupId(group.id),
|
||||
display_name: group.display_name.map(Into::into),
|
||||
display_name: new_display_name.map(|s| s.as_str().into()),
|
||||
delete_attributes: group
|
||||
.remove_attributes
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|attr| attr != "display_name")
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
insert_attributes,
|
||||
|
|
Loading…
Reference in a new issue