From 55225bc15bd407101d6a42a51d2816a4e379552f Mon Sep 17 00:00:00 2001 From: Austin Alvarado Date: Thu, 18 Jan 2024 05:41:06 +0000 Subject: [PATCH] ui: add user attributes page todo --- .devcontainer/Dockerfile | 2 +- app/queries/create_user_attribute.graphql | 5 + app/queries/delete_user_attribute.graphql | 5 + .../get_user_attributes_schema.graphql | 14 ++ app/src/components/app.rs | 22 ++ app/src/components/create_user_attribute.rs | 235 ++++++++++++++++++ app/src/components/delete_user_attribute.rs | 172 +++++++++++++ app/src/components/mod.rs | 3 + app/src/components/router.rs | 4 + app/src/components/user_attributes_table.rs | 160 ++++++++++++ 10 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 app/queries/create_user_attribute.graphql create mode 100644 app/queries/delete_user_attribute.graphql create mode 100644 app/queries/get_user_attributes_schema.graphql create mode 100644 app/src/components/create_user_attribute.rs create mode 100644 app/src/components/delete_user_attribute.rs create mode 100644 app/src/components/user_attributes_table.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index dda062b..3f915d1 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.72 +FROM rust:1.74 ARG USERNAME=lldapdev # We need to keep the user as 1001 to match the GitHub runner's UID. diff --git a/app/queries/create_user_attribute.graphql b/app/queries/create_user_attribute.graphql new file mode 100644 index 0000000..bdd6466 --- /dev/null +++ b/app/queries/create_user_attribute.graphql @@ -0,0 +1,5 @@ +mutation CreateUserAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!, $isEditable: Boolean!) { + addUserAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: $isEditable) { + ok + } +} \ No newline at end of file diff --git a/app/queries/delete_user_attribute.graphql b/app/queries/delete_user_attribute.graphql new file mode 100644 index 0000000..9e0d31d --- /dev/null +++ b/app/queries/delete_user_attribute.graphql @@ -0,0 +1,5 @@ +mutation DeleteUserAttributeQuery($name: String!) { + deleteUserAttribute(name: $name) { + ok + } +} \ No newline at end of file diff --git a/app/queries/get_user_attributes_schema.graphql b/app/queries/get_user_attributes_schema.graphql new file mode 100644 index 0000000..0560285 --- /dev/null +++ b/app/queries/get_user_attributes_schema.graphql @@ -0,0 +1,14 @@ +query GetUserAttributesSchema { + schema { + userSchema { + attributes { + name + attributeType + isList + isVisible + isEditable + isHardcoded + } + } + } +} diff --git a/app/src/components/app.rs b/app/src/components/app.rs index 1721cc7..c7e503f 100644 --- a/app/src/components/app.rs +++ b/app/src/components/app.rs @@ -3,6 +3,7 @@ use crate::{ change_password::ChangePasswordForm, create_group::CreateGroupForm, create_user::CreateUserForm, + create_user_attribute::CreateUserAttributeForm, group_details::GroupDetails, group_table::GroupTable, login::LoginForm, @@ -10,6 +11,7 @@ use crate::{ reset_password_step1::ResetPasswordStep1Form, reset_password_step2::ResetPasswordStep2Form, router::{AppRoute, Link, Redirect}, + user_attributes_table::UserAttributesTable, user_details::UserDetails, user_table::UserTable, }, @@ -227,6 +229,9 @@ impl App { AppRoute::CreateGroup => html! { }, + AppRoute::CreateUserAttribute => html! { + + }, AppRoute::ListGroups => html! {
@@ -236,6 +241,15 @@ impl App {
}, + AppRoute::ListUserAttributes => html! { +
+ + + + {"Create an attribute"} + +
+ }, AppRoute::GroupDetails { group_id } => html! { }, @@ -291,6 +305,14 @@ impl App { {"Groups"} +
  • + + + {"User attributes"} + +
  • } } else { html!{} } } diff --git a/app/src/components/create_user_attribute.rs b/app/src/components/create_user_attribute.rs new file mode 100644 index 0000000..0e0f479 --- /dev/null +++ b/app/src/components/create_user_attribute.rs @@ -0,0 +1,235 @@ +use crate::{ + components::router::AppRoute, + infra::common_component::{CommonComponent, CommonComponentParts}, +}; +use anyhow::{bail, Result}; +use gloo_console::log; +use graphql_client::GraphQLQuery; +use validator_derive::Validate; +use yew::prelude::*; +use yew_form_derive::Model; +use yew_router::{prelude::History, scope_ext::RouterScopeExt}; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/create_user_attribute.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct CreateUserAttribute; + +type AttributeType = create_user_attribute::AttributeType; + +pub struct CreateUserAttributeForm { + common: CommonComponentParts, + form: yew_form::Form, +} + +#[derive(Model, Validate, PartialEq, Eq, Clone, Default, Debug)] +pub struct CreateUserAttributeModel { + #[validate(length(min = 1, message = "attribute_name is required"))] + attribute_name: String, + #[validate(length(min = 1, message = "attribute_type is required"))] + attribute_type: String, + is_editable: bool, + is_list: bool, + is_visible: bool, +} + +pub enum Msg { + Update, + SubmitForm, + CreateUserAttributeResponse(Result), +} + +impl CommonComponent for CreateUserAttributeForm { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { + match msg { + Msg::Update => Ok(true), + Msg::SubmitForm => { + if !self.form.validate() { + bail!("Check the form for errors"); + } + let model = self.form.model(); + if model.is_editable && !model.is_visible { + bail!("Editable attributes must also be visible"); + } + let attribute_type = match model.attribute_type.as_str() { + "Jpeg" => AttributeType::JPEG_PHOTO, + "DateTime" => AttributeType::DATE_TIME, + "Integer" => AttributeType::INTEGER, + "String" => AttributeType::STRING, + _ => bail!("Check the form for errors"), + }; + let req = create_user_attribute::Variables { + name: model.attribute_name, + attribute_type: attribute_type, + is_editable: model.is_editable, + is_list: model.is_list, + is_visible: model.is_visible, + }; + self.common.call_graphql::( + ctx, + req, + Msg::CreateUserAttributeResponse, + "Error trying to create user attribute", + ); + Ok(true) + } + Msg::CreateUserAttributeResponse(response) => { + response?; + let model = self.form.model(); + log!(&format!( + "Created user attribute '{}'", + model.attribute_name + )); + ctx.link() + .history() + .unwrap() + .push(AppRoute::ListUserAttributes); + Ok(true) + } + } + } + + fn mut_common(&mut self) -> &mut CommonComponentParts { + &mut self.common + } +} + +impl Component for CreateUserAttributeForm { + type Message = Msg; + type Properties = (); + + fn create(_: &Context) -> Self { + let mut model = CreateUserAttributeModel::default(); + model.attribute_type = "String".to_string(); + Self { + common: CommonComponentParts::::create(), + form: yew_form::Form::::new(model), + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) + } + + fn view(&self, ctx: &Context) -> Html { + let link = ctx.link(); + type Field = yew_form::Field; + type Select = yew_form::Select; + type Checkbox = yew_form::CheckBox; + html! { +
    +
    +
    +
    {"Create a user attribute"}
    +
    +
    + +
    + +
    + {&self.form.field_message("attribute_name")} +
    +
    +
    +
    + +
    + +
    + {&self.form.field_message("attribute_type")} +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + { if let Some(e) = &self.common.error { + html! { +
    + {e.to_string() } +
    + } + } else { html! {} } + } +
    + } + } +} diff --git a/app/src/components/delete_user_attribute.rs b/app/src/components/delete_user_attribute.rs new file mode 100644 index 0000000..254424d --- /dev/null +++ b/app/src/components/delete_user_attribute.rs @@ -0,0 +1,172 @@ +use crate::infra::{ + common_component::{CommonComponent, CommonComponentParts}, + modal::Modal, +}; +use anyhow::{Error, Result}; +use graphql_client::GraphQLQuery; +use yew::prelude::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/delete_user_attribute.graphql", + response_derives = "Debug", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct DeleteUserAttributeQuery; + +pub struct DeleteUserAttribute { + common: CommonComponentParts, + node_ref: NodeRef, + modal: Option, +} + +#[derive(yew::Properties, Clone, PartialEq, Debug)] +pub struct DeleteUserAttributeProps { + pub attribute_name: String, + pub on_attribute_deleted: Callback, + pub on_error: Callback, +} + +pub enum Msg { + ClickedDeleteUserAttribute, + ConfirmDeleteUserAttribute, + DismissModal, + DeleteUserAttributeResponse(Result), +} + +impl CommonComponent for DeleteUserAttribute { + fn handle_msg( + &mut self, + ctx: &Context, + msg: ::Message, + ) -> Result { + match msg { + Msg::ClickedDeleteUserAttribute => { + self.modal.as_ref().expect("modal not initialized").show(); + } + Msg::ConfirmDeleteUserAttribute => { + self.update(ctx, Msg::DismissModal); + self.common.call_graphql::( + ctx, + delete_user_attribute_query::Variables { + name: ctx.props().attribute_name.clone(), + }, + Msg::DeleteUserAttributeResponse, + "Error trying to delete user attribute", + ); + } + Msg::DismissModal => { + self.modal.as_ref().expect("modal not initialized").hide(); + } + Msg::DeleteUserAttributeResponse(response) => { + response?; + ctx.props() + .on_attribute_deleted + .emit(ctx.props().attribute_name.clone()); + } + } + Ok(true) + } + + fn mut_common(&mut self) -> &mut CommonComponentParts { + &mut self.common + } +} + +impl Component for DeleteUserAttribute { + type Message = Msg; + type Properties = DeleteUserAttributeProps; + + fn create(_: &Context) -> Self { + Self { + common: CommonComponentParts::::create(), + node_ref: NodeRef::default(), + modal: None, + } + } + + fn rendered(&mut self, _: &Context, first_render: bool) { + if first_render { + self.modal = Some(Modal::new( + self.node_ref + .cast::() + .expect("Modal node is not an element"), + )); + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update_and_report_error( + self, + ctx, + msg, + ctx.props().on_error.clone(), + ) + } + + fn view(&self, ctx: &Context) -> Html { + let link = &ctx.link(); + html! { + <> + + {self.show_modal(ctx)} + + } + } +} + +impl DeleteUserAttribute { + fn show_modal(&self, ctx: &Context) -> Html { + let link = &ctx.link(); + html! { + + } + } +} diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs index f78dcf9..26e01ed 100644 --- a/app/src/components/mod.rs +++ b/app/src/components/mod.rs @@ -4,8 +4,10 @@ pub mod app; pub mod change_password; pub mod create_group; pub mod create_user; +pub mod create_user_attribute; pub mod delete_group; pub mod delete_user; +pub mod delete_user_attribute; pub mod group_details; pub mod group_table; pub mod login; @@ -15,6 +17,7 @@ pub mod reset_password_step1; pub mod reset_password_step2; pub mod router; pub mod select; +pub mod user_attributes_table; pub mod user_details; pub mod user_details_form; pub mod user_table; diff --git a/app/src/components/router.rs b/app/src/components/router.rs index 3b03b61..7ff3174 100644 --- a/app/src/components/router.rs +++ b/app/src/components/router.rs @@ -24,6 +24,10 @@ pub enum AppRoute { GroupDetails { group_id: i64 }, #[at("/")] Index, + #[at("/user-attributes")] + ListUserAttributes, + #[at("/user-attributes/create")] + CreateUserAttribute, } pub type Link = yew_router::components::Link; diff --git a/app/src/components/user_attributes_table.rs b/app/src/components/user_attributes_table.rs new file mode 100644 index 0000000..e0658e5 --- /dev/null +++ b/app/src/components/user_attributes_table.rs @@ -0,0 +1,160 @@ +use crate::{ + components::delete_user_attribute::DeleteUserAttribute, + infra::common_component::{CommonComponent, CommonComponentParts}, +}; +use anyhow::{Error, Result}; +use graphql_client::GraphQLQuery; +use yew::prelude::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/get_user_attributes_schema.graphql", + response_derives = "Debug,Clone,PartialEq,Eq", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct GetUserAttributesSchema; + +use get_user_attributes_schema::ResponseData; + +pub type Attribute = get_user_attributes_schema::GetUserAttributesSchemaSchemaUserSchemaAttributes; +pub type AttributeType = get_user_attributes_schema::AttributeType; + +pub struct UserAttributesTable { + common: CommonComponentParts, + attributes: Option>, +} + +pub enum Msg { + ListAttributesResponse(Result), + OnAttributeDeleted(String), + OnError(Error), +} + +impl CommonComponent for UserAttributesTable { + fn handle_msg(&mut self, _: &Context, msg: ::Message) -> Result { + match msg { + Msg::ListAttributesResponse(schema) => { + self.attributes = Some(schema?.schema.user_schema.attributes.into_iter().collect()); + Ok(true) + } + Msg::OnError(e) => Err(e), + Msg::OnAttributeDeleted(attribute_name) => { + debug_assert!(self.attributes.is_some()); + self.attributes + .as_mut() + .unwrap() + .retain(|a| a.name != attribute_name); + Ok(true) + } + } + } + + fn mut_common(&mut self) -> &mut CommonComponentParts { + &mut self.common + } +} + +impl Component for UserAttributesTable { + type Message = Msg; + type Properties = (); + + fn create(ctx: &Context) -> Self { + let mut table = UserAttributesTable { + common: CommonComponentParts::::create(), + attributes: None, + }; + table.common.call_graphql::( + ctx, + get_user_attributes_schema::Variables {}, + Msg::ListAttributesResponse, + "Error trying to fetch user schema", + ); + table + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + CommonComponentParts::::update(self, ctx, msg) + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
    + {self.view_attributes(ctx)} + {self.view_errors()} +
    + } + } +} + +impl UserAttributesTable { + fn view_attributes(&self, ctx: &Context) -> Html { + let make_table = |attributes: &Vec| { + html! { +
    + + + + + + + + + + + + {attributes.iter().map(|u| self.view_attribute(ctx, u)).collect::>()} + +
    {"Attribute name"}{"Type"}{"Editable"}{"Visible"}{"Delete"}
    +
    + } + }; + match &self.attributes { + None => html! {{"Loading..."}}, + Some(attributes) => make_table(attributes), + } + } + + fn view_attribute(&self, ctx: &Context, attribute: &Attribute) -> Html { + let link = ctx.link(); + let attribute_type = match attribute.attribute_type { + AttributeType::STRING => "String", + AttributeType::INTEGER => "Integer", + AttributeType::JPEG_PHOTO => "Jpeg", + AttributeType::DATE_TIME => "DateTime", + _ => "Unknown", + }; + let checkmark = html! { + + + + }; + html! { + + {&attribute.name} + {if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}} + {if attribute.is_editable {checkmark.clone()} else {html!{}}} + {if attribute.is_visible {checkmark.clone()} else {html!{}}} + {if attribute.is_hardcoded {html!{ + + }} else {html!{ + + }}} + + } + } + + fn view_errors(&self) -> Html { + match &self.common.error { + None => html! {}, + Some(e) => html! {
    {"Error: "}{e.to_string()}
    }, + } + } +}