diff --git a/app/queries/create_group_attribute.graphql b/app/queries/create_group_attribute.graphql
new file mode 100644
index 0000000..10615ac
--- /dev/null
+++ b/app/queries/create_group_attribute.graphql
@@ -0,0 +1,5 @@
+mutation CreateGroupAttribute($name: String!, $attributeType: AttributeType!, $isList: Boolean!, $isVisible: Boolean!) {
+ addGroupAttribute(name: $name, attributeType: $attributeType, isList: $isList, isVisible: $isVisible, isEditable: false) {
+ ok
+ }
+}
\ No newline at end of file
diff --git a/app/queries/delete_group_attribute.graphql b/app/queries/delete_group_attribute.graphql
new file mode 100644
index 0000000..7a52b32
--- /dev/null
+++ b/app/queries/delete_group_attribute.graphql
@@ -0,0 +1,5 @@
+mutation DeleteGroupAttributeQuery($name: String!) {
+ deleteGroupAttribute(name: $name) {
+ ok
+ }
+}
\ No newline at end of file
diff --git a/app/queries/get_group_attributes_schema.graphql b/app/queries/get_group_attributes_schema.graphql
new file mode 100644
index 0000000..979591d
--- /dev/null
+++ b/app/queries/get_group_attributes_schema.graphql
@@ -0,0 +1,13 @@
+query GetGroupAttributesSchema {
+ schema {
+ groupSchema {
+ attributes {
+ name
+ attributeType
+ isList
+ isVisible
+ isHardcoded
+ }
+ }
+ }
+}
diff --git a/app/src/components/app.rs b/app/src/components/app.rs
index fbb48c1..b9856c0 100644
--- a/app/src/components/app.rs
+++ b/app/src/components/app.rs
@@ -2,9 +2,11 @@ use crate::{
components::{
change_password::ChangePasswordForm,
create_group::CreateGroupForm,
+ create_group_attribute::CreateGroupAttributeForm,
create_user::CreateUserForm,
create_user_attribute::CreateUserAttributeForm,
group_details::GroupDetails,
+ group_schema_table::ListGroupSchema,
group_table::GroupTable,
login::LoginForm,
logout::LogoutButton,
@@ -232,6 +234,9 @@ impl App {
AppRoute::CreateUserAttribute => html! {
},
+ AppRoute::CreateGroupAttribute => html! {
+
+ },
AppRoute::ListGroups => html! {
@@ -244,6 +249,9 @@ impl App {
AppRoute::ListUserSchema => html! {
},
+ AppRoute::ListGroupSchema => html! {
+
+ },
AppRoute::GroupDetails { group_id } => html! {
},
@@ -307,6 +315,14 @@ impl App {
{"User schema"}
+
+
+
+ {"Group schema"}
+
+
>
} } else { html!{} } }
diff --git a/app/src/components/create_group_attribute.rs b/app/src/components/create_group_attribute.rs
new file mode 100644
index 0000000..1ae1562
--- /dev/null
+++ b/app/src/components/create_group_attribute.rs
@@ -0,0 +1,168 @@
+use crate::{
+ components::{
+ form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
+ router::AppRoute,
+ },
+ convert_attribute_type,
+ infra::{
+ common_component::{CommonComponent, CommonComponentParts},
+ schema::{validate_attribute_type, AttributeType},
+ },
+};
+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_group_attribute.graphql",
+ response_derives = "Debug",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct CreateGroupAttribute;
+
+convert_attribute_type!(create_group_attribute::AttributeType);
+
+pub struct CreateGroupAttributeForm {
+ common: CommonComponentParts
,
+ form: yew_form::Form,
+}
+
+#[derive(Model, Validate, PartialEq, Eq, Clone, Default, Debug)]
+pub struct CreateGroupAttributeModel {
+ #[validate(length(min = 1, message = "attribute_name is required"))]
+ attribute_name: String,
+ #[validate(custom = "validate_attribute_type")]
+ attribute_type: String,
+ is_list: bool,
+ is_visible: bool, // remove when backend doesn't return group attributes for normal users
+}
+
+pub enum Msg {
+ Update,
+ SubmitForm,
+ CreateGroupAttributeResponse(Result),
+}
+
+impl CommonComponent for CreateGroupAttributeForm {
+ 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();
+ let attribute_type = model.attribute_type.parse::().unwrap();
+ let req = create_group_attribute::Variables {
+ name: model.attribute_name,
+ attribute_type: create_group_attribute::AttributeType::from(attribute_type),
+ is_list: model.is_list,
+ is_visible: model.is_visible,
+ };
+ self.common.call_graphql::(
+ ctx,
+ req,
+ Msg::CreateGroupAttributeResponse,
+ "Error trying to create group attribute",
+ );
+ Ok(true)
+ }
+ Msg::CreateGroupAttributeResponse(response) => {
+ response?;
+ let model = self.form.model();
+ log!(&format!(
+ "Created group attribute '{}'",
+ model.attribute_name
+ ));
+ ctx.link()
+ .history()
+ .unwrap()
+ .push(AppRoute::ListGroupSchema);
+ Ok(true)
+ }
+ }
+ }
+
+ fn mut_common(&mut self) -> &mut CommonComponentParts {
+ &mut self.common
+ }
+}
+
+impl Component for CreateGroupAttributeForm {
+ type Message = Msg;
+ type Properties = ();
+
+ fn create(_: &Context) -> Self {
+ let model = CreateGroupAttributeModel {
+ attribute_type: AttributeType::String.to_string(),
+ ..Default::default()
+ };
+ 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();
+ html! {
+
+
+ { if let Some(e) = &self.common.error {
+ html! {
+
+ {e.to_string() }
+
+ }
+ } else { html! {} }
+ }
+
+ }
+ }
+}
diff --git a/app/src/components/create_user_attribute.rs b/app/src/components/create_user_attribute.rs
index 569c457..88c78fe 100644
--- a/app/src/components/create_user_attribute.rs
+++ b/app/src/components/create_user_attribute.rs
@@ -1,5 +1,3 @@
-use std::str::FromStr;
-
use crate::{
components::{
form::{checkbox::CheckBox, field::Field, select::Select, submit::Submit},
@@ -8,13 +6,12 @@ use crate::{
convert_attribute_type,
infra::{
common_component::{CommonComponent, CommonComponentParts},
- schema::AttributeType,
+ schema::{validate_attribute_type, AttributeType},
},
};
use anyhow::{bail, Result};
use gloo_console::log;
use graphql_client::GraphQLQuery;
-use validator::ValidationError;
use validator_derive::Validate;
use yew::prelude::*;
use yew_form_derive::Model;
@@ -47,14 +44,6 @@ pub struct CreateUserAttributeModel {
is_visible: bool,
}
-fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
- let result = AttributeType::from_str(attribute_type);
- match result {
- Ok(_) => Ok(()),
- _ => Err(ValidationError::new("Invalid attribute type")),
- }
-}
-
pub enum Msg {
Update,
SubmitForm,
@@ -146,7 +135,7 @@ impl Component for CreateUserAttributeForm {
label="Type"
required={true}
form={&self.form}
- field_name="attribute_name"
+ field_name="attribute_type"
oninput={link.callback(|_| Msg::Update)}>
diff --git a/app/src/components/delete_group_attribute.rs b/app/src/components/delete_group_attribute.rs
new file mode 100644
index 0000000..435ee58
--- /dev/null
+++ b/app/src/components/delete_group_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_group_attribute.graphql",
+ response_derives = "Debug",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct DeleteGroupAttributeQuery;
+
+pub struct DeleteGroupAttribute {
+ common: CommonComponentParts,
+ node_ref: NodeRef,
+ modal: Option,
+}
+
+#[derive(yew::Properties, Clone, PartialEq, Debug)]
+pub struct DeleteGroupAttributeProps {
+ pub attribute_name: String,
+ pub on_attribute_deleted: Callback,
+ pub on_error: Callback,
+}
+
+pub enum Msg {
+ ClickedDeleteGroupAttribute,
+ ConfirmDeleteGroupAttribute,
+ DismissModal,
+ DeleteGroupAttributeResponse(Result),
+}
+
+impl CommonComponent for DeleteGroupAttribute {
+ fn handle_msg(
+ &mut self,
+ ctx: &Context,
+ msg: ::Message,
+ ) -> Result {
+ match msg {
+ Msg::ClickedDeleteGroupAttribute => {
+ self.modal.as_ref().expect("modal not initialized").show();
+ }
+ Msg::ConfirmDeleteGroupAttribute => {
+ self.update(ctx, Msg::DismissModal);
+ self.common.call_graphql::(
+ ctx,
+ delete_group_attribute_query::Variables {
+ name: ctx.props().attribute_name.clone(),
+ },
+ Msg::DeleteGroupAttributeResponse,
+ "Error trying to delete group attribute",
+ );
+ }
+ Msg::DismissModal => {
+ self.modal.as_ref().expect("modal not initialized").hide();
+ }
+ Msg::DeleteGroupAttributeResponse(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 DeleteGroupAttribute {
+ type Message = Msg;
+ type Properties = DeleteGroupAttributeProps;
+
+ 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 DeleteGroupAttribute {
+ fn show_modal(&self, ctx: &Context) -> Html {
+ let link = &ctx.link();
+ html! {
+
+
+
+
+
+
+ {"Are you sure you want to delete group attribute "}
+ {&ctx.props().attribute_name}{"?"}
+
+
+
+
+
+
+ }
+ }
+}
diff --git a/app/src/components/group_schema_table.rs b/app/src/components/group_schema_table.rs
new file mode 100644
index 0000000..0a6a4f7
--- /dev/null
+++ b/app/src/components/group_schema_table.rs
@@ -0,0 +1,198 @@
+use crate::{
+ components::{
+ delete_group_attribute::DeleteGroupAttribute,
+ router::{AppRoute, Link},
+ },
+ convert_attribute_type,
+ infra::{
+ common_component::{CommonComponent, CommonComponentParts},
+ schema::AttributeType,
+ },
+};
+use anyhow::{anyhow, Error, Result};
+use gloo_console::log;
+use graphql_client::GraphQLQuery;
+use yew::prelude::*;
+
+#[derive(GraphQLQuery)]
+#[graphql(
+ schema_path = "../schema.graphql",
+ query_path = "queries/get_group_attributes_schema.graphql",
+ response_derives = "Debug,Clone,PartialEq,Eq",
+ custom_scalars_module = "crate::infra::graphql"
+)]
+pub struct GetGroupAttributesSchema;
+
+use get_group_attributes_schema::ResponseData;
+
+pub type Attribute =
+ get_group_attributes_schema::GetGroupAttributesSchemaSchemaGroupSchemaAttributes;
+
+convert_attribute_type!(get_group_attributes_schema::AttributeType);
+
+#[derive(yew::Properties, Clone, PartialEq, Eq)]
+pub struct Props {
+ pub hardcoded: bool,
+}
+
+pub struct GroupSchemaTable {
+ common: CommonComponentParts,
+ attributes: Option>,
+}
+
+pub enum Msg {
+ ListAttributesResponse(Result),
+ OnAttributeDeleted(String),
+ OnError(Error),
+}
+
+impl CommonComponent for GroupSchemaTable {
+ fn handle_msg(&mut self, _: &Context, msg: ::Message) -> Result {
+ match msg {
+ Msg::ListAttributesResponse(schema) => {
+ self.attributes =
+ Some(schema?.schema.group_schema.attributes.into_iter().collect());
+ Ok(true)
+ }
+ Msg::OnError(e) => Err(e),
+ Msg::OnAttributeDeleted(attribute_name) => {
+ match self.attributes {
+ None => {
+ log!(format!("Attribute {attribute_name} was deleted but component has no attributes"));
+ Err(anyhow!("invalid state"))
+ }
+ 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 GroupSchemaTable {
+ type Message = Msg;
+ type Properties = Props;
+
+ fn create(ctx: &Context) -> Self {
+ let mut table = GroupSchemaTable {
+ common: CommonComponentParts::::create(),
+ attributes: None,
+ };
+ table.common.call_graphql::(
+ ctx,
+ get_group_attributes_schema::Variables {},
+ Msg::ListAttributesResponse,
+ "Error trying to fetch group 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 GroupSchemaTable {
+ fn view_attributes(&self, ctx: &Context) -> Html {
+ let hardcoded = ctx.props().hardcoded;
+ let make_table = |attributes: &Vec| {
+ html! {
+
+
{if hardcoded {"Hardcoded"} else {"User-defined"}}{" attributes"}
+
+
+
+ | {"Attribute name"} |
+ {"Type"} |
+ {"Visible"} |
+ {if hardcoded {html!{}} else {html!{{"Delete"} | }}}
+
+
+
+ {attributes.iter().map(|u| self.view_attribute(ctx, u)).collect::>()}
+
+
+
+ }
+ };
+ match &self.attributes {
+ None => html! {{"Loading..."}},
+ Some(attributes) => {
+ let mut attributes = attributes.clone();
+ attributes.retain(|attribute| attribute.is_hardcoded == ctx.props().hardcoded);
+ make_table(&attributes)
+ }
+ }
+ }
+
+ fn view_attribute(&self, ctx: &Context, attribute: &Attribute) -> Html {
+ let link = ctx.link();
+ let attribute_type = AttributeType::from(attribute.attribute_type.clone());
+ let checkmark = html! {
+
+ };
+ let hardcoded = ctx.props().hardcoded;
+ html! {
+
+ | {&attribute.name} |
+ {if attribute.is_list { format!("List<{attribute_type}>")} else {attribute_type.to_string()}} |
+ {if attribute.is_visible {checkmark.clone()} else {html!{}}} |
+ {
+ if hardcoded {
+ html!{}
+ } else {
+ html!{
+
+
+ |
+ }
+ }
+ }
+
+ }
+ }
+
+ fn view_errors(&self) -> Html {
+ match &self.common.error {
+ None => html! {},
+ Some(e) => html! {{"Error: "}{e.to_string()}
},
+ }
+ }
+}
+
+#[function_component(ListGroupSchema)]
+pub fn list_group_schema() -> Html {
+ html! {
+
+
+
+
+
+ {"Create an attribute"}
+
+
+ }
+}
diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs
index 0fe1c13..5229f44 100644
--- a/app/src/components/mod.rs
+++ b/app/src/components/mod.rs
@@ -3,13 +3,16 @@ pub mod add_user_to_group;
pub mod app;
pub mod change_password;
pub mod create_group;
+pub mod create_group_attribute;
pub mod create_user;
pub mod create_user_attribute;
pub mod delete_group;
+pub mod delete_group_attribute;
pub mod delete_user;
pub mod delete_user_attribute;
pub mod form;
pub mod group_details;
+pub mod group_schema_table;
pub mod group_table;
pub mod login;
pub mod logout;
diff --git a/app/src/components/router.rs b/app/src/components/router.rs
index 09e7782..172ded0 100644
--- a/app/src/components/router.rs
+++ b/app/src/components/router.rs
@@ -26,6 +26,10 @@ pub enum AppRoute {
ListUserSchema,
#[at("/user-attributes/create")]
CreateUserAttribute,
+ #[at("/group-attributes")]
+ ListGroupSchema,
+ #[at("/group-attributes/create")]
+ CreateGroupAttribute,
#[at("/")]
Index,
}
diff --git a/app/src/infra/schema.rs b/app/src/infra/schema.rs
index 7aecc9e..f59bc76 100644
--- a/app/src/infra/schema.rs
+++ b/app/src/infra/schema.rs
@@ -1,5 +1,6 @@
use anyhow::Result;
use std::{fmt::Display, str::FromStr};
+use validator::ValidationError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeType {
@@ -58,6 +59,7 @@ macro_rules! convert_attribute_type {
};
}
+<<<<<<< HEAD
#[derive(Clone, PartialEq, Eq)]
pub struct Attribute {
pub name: String,
@@ -85,4 +87,10 @@ macro_rules! combine_schema_and_values {
}
}).collect();
};
+=======
+pub fn validate_attribute_type(attribute_type: &str) -> Result<(), ValidationError> {
+ AttributeType::from_str(attribute_type)
+ .map_err(|_| ValidationError::new("Invalid attribute type"))?;
+ Ok(())
+>>>>>>> 8f2391a (app: create group attribute schema page (#825))
}