From 305b272cdfa217cc71438dd5fd4a5ca9e1121e79 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 22 Oct 2024 00:25:38 +0200 Subject: [PATCH] app: Add support for group attributes --- app/Cargo.toml | 7 + app/queries/get_group_details.graphql | 17 ++ app/queries/update_group.graphql | 6 + app/src/components/app.rs | 2 +- app/src/components/group_details.rs | 97 ++++---- app/src/components/group_details_form.rs | 272 +++++++++++++++++++++++ app/src/components/mod.rs | 1 + server/src/infra/graphql/mutation.rs | 13 +- 8 files changed, 367 insertions(+), 48 deletions(-) create mode 100644 app/queries/update_group.graphql create mode 100644 app/src/components/group_details_form.rs diff --git a/app/Cargo.toml b/app/Cargo.toml index 7529df9..aee6f61 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -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'] diff --git a/app/queries/get_group_details.graphql b/app/queries/get_group_details.graphql index 00400d7..335384b 100644 --- a/app/queries/get_group_details.graphql +++ b/app/queries/get_group_details.graphql @@ -8,5 +8,22 @@ query GetGroupDetails($id: Int!) { id displayName } + attributes { + name + value + } + } + schema { + groupSchema { + attributes { + name + attributeType + isList + isVisible + isEditable + isHardcoded + isReadonly + } + } } } diff --git a/app/queries/update_group.graphql b/app/queries/update_group.graphql new file mode 100644 index 0000000..0640ff7 --- /dev/null +++ b/app/queries/update_group.graphql @@ -0,0 +1,6 @@ +mutation UpdateGroup($group: UpdateGroupInput!) { + updateGroup(group: $group) { + ok + } +} + diff --git a/app/src/components/app.rs b/app/src/components/app.rs index b0a11c5..988d3bd 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -234,7 +234,7 @@ impl App { }, AppRoute::GroupDetails { group_id } => html! { - + }, AppRoute::UserDetails { user_id } => html! { diff --git a/app/src/components/group_details.rs b/app/src/components/group_details.rs index ae32436..1435003 100644 --- a/app/src/components/group_details.rs +++ b/app/src/components/group_details.rs @@ -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, /// 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_and_schema: Option<(Group, Vec)>, } /// 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, g: &Group, schema: Vec) -> Html { html! { <>

{g.display_name.to_string()}

-
-
-
- -
- {g.display_name.to_string()} -
-
-
- -
- {g.creation_date.naive_local().date()} -
-
-
- -
- {g.uuid.to_string()} -
-
-
-
+ } } @@ -182,29 +180,38 @@ impl GroupDetails { } impl CommonComponent for GroupDetails { - fn handle_msg(&mut self, _: &Context, msg: ::Message) -> Result { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { 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 { let mut table = Self { common: CommonComponentParts::::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) -> Html { - match (&self.group, &self.common.error) { + match (&self.group_and_schema, &self.common.error) { (None, None) => html! {{"Loading..."}}, (None, Some(e)) => html! {
{"Error: "}{e.to_string()}
}, - (Some(u), error) => { + (Some((group, schema)), error) => { html! {
- {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)}
} diff --git a/app/src/components/group_details_form.rs b/app/src/components/group_details_form.rs new file mode 100644 index 0000000..50b4189 --- /dev/null +++ b/app/src/components/group_details_form.rs @@ -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, + /// 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), +} + +#[derive(yew::Properties, Clone, PartialEq)] +pub struct Props { + /// The current group details. + pub group: Group, + pub group_attributes_schema: Vec, + pub is_admin: bool, + pub on_display_name_updated: Callback<()>, +} + +impl CommonComponent for GroupDetailsForm { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { + 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 { + &mut self.common + } +} + +impl Component for GroupDetailsForm { + type Message = Msg; + type Properties = Props; + + fn create(ctx: &Context) -> Self { + Self { + common: CommonComponentParts::::create(), + just_updated: false, + updated_group_name: false, + group: ctx.props().group.clone(), + form_ref: NodeRef::default(), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + self.just_updated = false; + CommonComponentParts::::update(self, ctx, msg) + } + + fn view(&self, ctx: &Context) -> 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! { +
+
+ + {&self.group.id} + + { + ctx + .props() + .group_attributes_schema + .iter() + .filter(|a| a.is_hardcoded && a.name != "group_id") + .map(display_field) + .collect::>() + } + { + ctx + .props() + .group_attributes_schema + .iter() + .filter(|a| !a.is_hardcoded) + .map(display_field) + .collect::>() + } + + + { + if let Some(e) = &self.common.error { + html! { +
+ {e.to_string() } +
+ } + } else { html! {} } + } + +
+ } + } +} + +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! { + ::into(attribute_schema.attribute_type.clone())} + values={values} + /> + } + } else { + html! { + ::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! { + + {values.into_iter().map(|x| html!{
{x}
}).collect::>()} +
+ } +} + +impl GroupDetailsForm { + fn submit_group_update_form(&mut self, ctx: &Context) -> Result { + 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> = if all_values.is_empty() { + None + } else { + Some(all_values.iter().map(|a| a.name.clone()).collect()) + }; + let insert_attributes: Option> = + 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::( + ctx, + req, + Msg::GroupUpdated, + "Error trying to update group", + ); + Ok(false) + } +} diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs index 39897ed..9c09161 100644 --- a/app/src/components/mod.rs +++ b/app/src/components/mod.rs @@ -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; diff --git a/server/src/infra/graphql/mutation.rs b/server/src/infra/graphql/mutation.rs index d7bcaf9..2c2312d 100644 --- a/server/src/infra/graphql/mutation.rs +++ b/server/src/infra/graphql/mutation.rs @@ -298,7 +298,14 @@ impl Mutation { 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 Mutation { .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::, _>>()?; 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,