diff --git a/crates/experimentation_platform/src/db/models.rs b/crates/experimentation_platform/src/db/models.rs index 830f9042..3711c011 100644 --- a/crates/experimentation_platform/src/db/models.rs +++ b/crates/experimentation_platform/src/db/models.rs @@ -1,21 +1,12 @@ use crate::db::schema::*; use chrono::{DateTime, NaiveDateTime, Utc}; -use diesel::{ - query_builder::QueryId, Insertable, Queryable, QueryableByName, Selectable, -}; +use diesel::{Insertable, Queryable, QueryableByName, Selectable}; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive( - Debug, - Clone, - Copy, - PartialEq, - Deserialize, - Serialize, - diesel_derive_enum::DbEnum, - QueryId, + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, diesel_derive_enum::DbEnum, )] #[DbValueStyle = "UPPERCASE"] #[ExistingTypePath = "crate::db::schema::sql_types::ExperimentStatusType"] diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 398391e3..e22853b8 100644 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -11,8 +11,8 @@ use crate::pages::function::{ }; use crate::pages::Dimensions::Dimensions::Dimensions; use crate::pages::{ - ContextOverride::context_override::ContextOverride, - DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, + default_config::default_config::DefaultConfig, + ContextOverride::context_override::ContextOverride, Experiment::ExperimentPage, Home::Home::Home, }; use crate::types::Envs; diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs index 4f94b4bc..feef3a6e 100644 --- a/crates/frontend/src/components/default_config_form/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -23,6 +23,7 @@ pub fn default_config_form( #[prop(default = String::new())] config_pattern: String, #[prop(default = String::new())] config_value: String, #[prop(default = None)] function_name: Option, + #[prop(default = None)] prefix: Option, handle_submit: NF, ) -> impl IntoView where @@ -65,7 +66,9 @@ where let on_submit = move |ev: MouseEvent| { ev.prevent_default(); - let f_name = config_key.get(); + let f_name = prefix + .clone() + .map_or_else(|| config_key.get(), |prefix| prefix + &config_key.get()); let f_type = config_type.get(); let f_pattern = config_pattern.get(); let f_value = config_value.get(); diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs deleted file mode 100644 index ad2d1b28..00000000 --- a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs +++ /dev/null @@ -1,214 +0,0 @@ -use crate::api::fetch_default_config; -use crate::components::default_config_form::default_config_form::DefaultConfigForm; -use crate::components::drawer::drawer::{close_drawer, open_drawer, Drawer, DrawerBtn}; -use crate::components::stat::stat::Stat; -use crate::components::table::{table::Table, types::Column}; -use leptos::*; -use serde_json::{json, Map, Value}; -use std::collections::HashMap; - -#[derive(Clone, Debug, Default)] -pub struct RowData { - pub key: String, - pub value: String, - pub pattern: String, - pub type_: String, - pub function_name: Option, -} - -#[component] -pub fn DefaultConfig() -> impl IntoView { - let tenant_rs = use_context::>().unwrap(); - let default_config_resource = create_blocking_resource( - move || tenant_rs.get(), - |current_tenant| async move { - match fetch_default_config(current_tenant).await { - Ok(data) => data, - Err(_) => vec![], - } - }, - ); - - let selected_config = create_rw_signal::>(None); - - let table_columns = create_memo(move |_| { - let edit_col_formatter = move |_: &str, row: &Map| { - logging::log!("{:?}", row); - let row_key = row["key"].clone().to_string().replace("\"", ""); - let row_value = row["value"].clone().to_string().replace("\"", ""); - - let schema = row["schema"].clone().to_string(); - let schema_object = serde_json::from_str::>(&schema) - .unwrap_or(HashMap::new()); - - let function_name = row["function_name"].clone().to_string(); - let fun_name = match function_name.as_str() { - "null" => None, - _ => Some(json!(function_name.replace("\"", ""))), - }; - - let pattern_or_enum = schema_object - .keys() - .find(|key| { - key.to_string() == "pattern".to_string() - || key.to_string() == "enum".to_string() - }) - .and_then(|val| Some(val.clone())) - .unwrap_or(String::new()); - - let row_type = match schema_object.get("type") { - Some(Value::String(type_)) if type_ == "string" => { - pattern_or_enum.clone() - } - Some(Value::String(type_)) if type_ == "number" => type_.clone(), - Some(Value::String(_)) => String::from("other"), - Some(_) | None => String::new(), - }; - - let row_pattern = match schema_object.get("type") { - Some(Value::String(type_)) - if type_ == "string" && pattern_or_enum == "pattern" => - { - schema_object - .get(&pattern_or_enum) - .and_then(|val| Some(val.clone().to_string())) - .unwrap_or(String::new()) - .replace("\"", "") - } - Some(Value::String(type_)) - if type_ == "string" && pattern_or_enum == "enum" => - { - schema_object - .get(&pattern_or_enum) - .and_then(|val| { - if let Value::Array(v) = val { - return format!( - "[{}]", - v.iter() - .map(|v| v.to_string()) - .collect::>() - .join(",") - ) - .into(); - } - None - }) - .unwrap_or(String::new()) - } - Some(Value::String(type_)) if type_ == "number" => String::new(), - Some(Value::String(_)) => schema, - _ => String::new(), - }; - - let edit_click_handler = move |_| { - let row_data = RowData { - key: row_key.clone(), - value: row_value.clone(), - type_: row_type.clone(), - pattern: row_pattern.clone(), - function_name: fun_name.clone(), - }; - logging::log!("{:?}", row_data); - selected_config.set(Some(row_data)); - open_drawer("default_config_drawer"); - }; - - let edit_icon: HtmlElement = - view! { }; - - view! { {edit_icon} }.into_view() - }; - vec![ - Column::default("key".to_string()), - Column::default("schema".to_string()), - Column::default("value".to_string()), - Column::default("function_name".to_string()), - Column::default("created_at".to_string()), - Column::default("created_by".to_string()), - Column::new("EDIT".to_string(), None, edit_col_formatter), - ] - }); - - view! { -
- {move || { - let handle_close = move || { - close_drawer("default_config_drawer"); - selected_config.set(None); - }; - if let Some(selected_config_data) = selected_config.get() { - view! { - - - - } - } else { - view! { - - - - } - } - }} - - "Loading (Suspense Fallback)..."

} - }> - {move || { - let default_config = default_config_resource.get().unwrap_or(vec![]); - let total_default_config_keys = default_config.len().to_string(); - let table_rows = default_config - .into_iter() - .map(|config| { - let mut ele_map = json!(config).as_object().unwrap().to_owned(); - ele_map - .insert( - "created_at".to_string(), - json!(config.created_at.format("%v").to_string()), - ); - ele_map - }) - .collect::>>(); - view! { -
- -
-
-
-
-

- "Default Config" -

- - Create Key - -
- - - - } - }} - - - } -} diff --git a/crates/frontend/src/pages/DefaultConfig/mod.rs b/crates/frontend/src/pages/DefaultConfig/mod.rs deleted file mode 100644 index 1a618fd7..00000000 --- a/crates/frontend/src/pages/DefaultConfig/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod DefaultConfig; diff --git a/crates/frontend/src/pages/default_config/default_config.rs b/crates/frontend/src/pages/default_config/default_config.rs new file mode 100644 index 00000000..58d1ba31 --- /dev/null +++ b/crates/frontend/src/pages/default_config/default_config.rs @@ -0,0 +1,420 @@ +use crate::api::fetch_default_config; +use crate::components::default_config_form::default_config_form::DefaultConfigForm; +use crate::components::drawer::drawer::{close_drawer, open_drawer, Drawer, DrawerBtn}; +use crate::components::stat::stat::Stat; +use crate::components::table::{table::Table, types::Column}; +use crate::types::BreadCrums; +use leptos::*; +use leptos_router::{use_navigate, use_query_map}; +use serde_json::{json, Map, Value}; +use std::collections::{HashMap, HashSet}; + +#[derive(Clone, Debug, Default)] +pub struct RowData { + pub key: String, + pub value: String, + pub pattern: String, + pub type_: String, + pub function_name: Option, +} + +#[component] +pub fn default_config() -> impl IntoView { + let tenant_rs = use_context::>().unwrap(); + let default_config_resource = create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_default_config(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let selected_config = create_rw_signal::>(None); + let key_prefix = create_rw_signal::>(None); + let enable_grouping = create_rw_signal(false); + let query_params = use_query_map(); + let bread_crums = Signal::derive(move || get_bread_crums(key_prefix.get())); + + create_effect(move |_| { + let query_params_map = query_params.try_get(); + if let Some(query_map) = query_params_map { + let opt_prefix = query_map.get("prefix"); + key_prefix.set(opt_prefix.cloned()); + if opt_prefix.is_some() { + enable_grouping.set(true); + } + } + }); + + let folder_click_handler = move |key_name: Option| { + let tenant = tenant_rs.get(); + let redirect_url = match key_name { + Some(prefix) => format!("admin/{tenant}/default-config?prefix={prefix}"), + None => format!("admin/{tenant}/default-config"), + }; + logging::log!("redirecting to {:?}", redirect_url.clone()); + let navigate = use_navigate(); + navigate(redirect_url.as_str(), Default::default()); + }; + + let table_columns = create_memo(move |_| { + let edit_col_formatter = move |_: &str, row: &Map| { + logging::log!("{:?}", row); + let row_key = row["key"].clone().to_string().replace("\"", ""); + let is_folder = row_key.contains("."); + let row_value = row["value"].clone().to_string().replace("\"", ""); + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::>(&schema) + .unwrap_or(HashMap::new()); + + let function_name = row["function_name"].clone().to_string(); + let fun_name = match function_name.as_str() { + "null" => None, + _ => Some(json!(function_name.replace("\"", ""))), + }; + + let pattern_or_enum = schema_object + .keys() + .find(|key| { + key.to_string() == "pattern".to_string() + || key.to_string() == "enum".to_string() + }) + .and_then(|val| Some(val.clone())) + .unwrap_or(String::new()); + + let row_type = match schema_object.get("type") { + Some(Value::String(type_)) if type_ == "string" => { + pattern_or_enum.clone() + } + Some(Value::String(type_)) if type_ == "number" => type_.clone(), + Some(Value::String(_)) => String::from("other"), + Some(_) | None => String::new(), + }; + + let row_pattern = match schema_object.get("type") { + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "pattern" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| Some(val.clone().to_string())) + .unwrap_or(String::new()) + .replace("\"", "") + } + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "enum" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| { + if let Value::Array(v) = val { + return format!( + "[{}]", + v.iter() + .map(|v| v.to_string()) + .collect::>() + .join(",") + ) + .into(); + } + None + }) + .unwrap_or(String::new()) + } + Some(Value::String(type_)) if type_ == "number" => String::new(), + Some(Value::String(_)) => schema, + _ => String::new(), + }; + + let edit_click_handler = move |_| { + let row_data = RowData { + key: row_key.clone(), + value: row_value.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + function_name: fun_name.clone(), + }; + logging::log!("{:?}", row_data); + selected_config.set(Some(row_data)); + open_drawer("default_config_drawer"); + }; + + let edit_icon: HtmlElement = + view! { }; + + if is_folder { + view! { {"-"} }.into_view() + } else { + view! { {edit_icon} }.into_view() + } + }; + + let expand = move |_: &str, row: &Map| { + let key_name = row["key"].clone().to_string().replace("\"", ""); + let label = key_name.clone(); + let is_folder = key_name.contains("."); + + if is_folder && enable_grouping.get() { + view! { + {label} + } + .into_view() + } else { + view! { {key_name} }.into_view() + } + }; + + vec![ + Column::new("key".to_string(), None, expand), + Column::default("schema".to_string()), + Column::default("value".to_string()), + Column::default("function_name".to_string()), + Column::default("created_at".to_string()), + Column::default("created_by".to_string()), + Column::new("EDIT".to_string(), None, edit_col_formatter), + ] + }); + + let handle_close = move || { + selected_config.set(None); + close_drawer("default_config_drawer"); + }; + + view! { +
+ "Loading (Suspense Fallback)..."

} + }> + { + move || { + let prefix = key_prefix.get(); + if let Some(selected_config_data) = selected_config.get() { + view! { + + + + } + } else { + view! { + + + + } + } + } + } + { + move || { + let default_config = default_config_resource.get().unwrap_or(vec![]); + let table_rows = default_config + .into_iter() + .map(|config| { + let mut ele_map = json!(config).as_object().unwrap().to_owned(); + ele_map + .insert( + "created_at".to_string(), + json!(config.created_at.format("%v").to_string()), + ); + ele_map + }) + .collect::>>(); + + let mut filtered_rows = table_rows.clone(); + if enable_grouping.get() { + let empty_map = Map::new(); + let cols = filtered_rows.get(0).unwrap_or(&empty_map).keys().map(|key| key.as_str()).collect(); + filtered_rows = modify_rows(filtered_rows.clone(), key_prefix.get(), cols); + } + + let total_default_config_keys = filtered_rows.len().to_string(); + + view! { +
+ +
+
+
+
+ +
+ + + Create Key + +
+
+
+ + + } + } + } + + + } +} + +#[component] +pub fn bread_crums( + bread_crums: Vec, + folder_click_handler: NF, +) -> impl IntoView +where + NF: Fn(Option) + 'static + Clone, +{ + let last_index = bread_crums.len() - 1; + view! { +
+ { + bread_crums.iter().enumerate().map(|(index, ele)| { + let value = ele.value.clone(); + let is_link = ele.is_link.clone(); + let handler = folder_click_handler.clone(); + view! { +
+

+ {ele.key.clone()} +

+

+ {if index < last_index {">"} else {""}} +

+
+ } + }).collect_view() + }
+ } +} + +pub fn get_bread_crums(key_prefix: Option) -> Vec { + let mut default_bread_crums = vec![BreadCrums { + key: "Default Config".to_string(), + value: None, + is_link: true, + }]; + + let mut bread_crums = match key_prefix { + Some(prefix) => { + let prefix_arr = prefix + .trim_matches('.') + .split(".") + .map(str::to_string) + .collect::>(); + prefix_arr + .into_iter() + .fold(String::new(), |mut prefix, ele| { + prefix.push_str(&ele); + prefix.push('.'); + default_bread_crums.push(BreadCrums { + key: ele.clone(), + value: Some(prefix.clone()), + is_link: true, + }); + prefix + }); + default_bread_crums + } + None => default_bread_crums, + }; + if let Some(last_crumb) = bread_crums.last_mut() { + last_crumb.is_link = false; + } + bread_crums +} + +pub fn modify_rows( + filtered_rows: Vec>, + key_prefix: Option, + cols: Vec<&str>, +) -> Vec> { + let mut groups: HashSet = HashSet::new(); + filtered_rows + .into_iter() + .filter_map(|mut ele| { + let key = ele.get("key").unwrap().to_owned(); + + let key_arr = match &key_prefix { + Some(prefix) => key + .to_string() + .split(prefix) + .map(str::to_string) + .collect::>(), + None => vec!["".to_string(), key.to_string()], + }; + // key_arr.get(1) retrieves the remaining part of the key, after removing the prefix. + if let Some(filtered_key) = key_arr.get(1) { + let new_key = filtered_key + .split(".") + .map(str::to_string) + .collect::>(); + let key = new_key.get(0).unwrap().to_owned().replace("\"", ""); + if new_key.len() == 1 { + // key + ele.insert("key".to_string(), json!(key)); + } else { + // folder + let folder = key + "."; + if !groups.contains(&folder) { + cols.iter().for_each(|col| { + ele.insert( + col.to_string(), + json!(if *col == "key" { + folder.clone() + } else { + "-".to_string() + }), + ); + }); + groups.insert(folder); + } else { + return None; + } + } + Some(ele) + } else { + None + } + }) + .collect() +} diff --git a/crates/frontend/src/pages/default_config/mod.rs b/crates/frontend/src/pages/default_config/mod.rs new file mode 100644 index 00000000..87890fa7 --- /dev/null +++ b/crates/frontend/src/pages/default_config/mod.rs @@ -0,0 +1 @@ +pub mod default_config; diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs index 84fb7280..42cf6c79 100644 --- a/crates/frontend/src/pages/mod.rs +++ b/crates/frontend/src/pages/mod.rs @@ -1,10 +1,10 @@ #![allow(non_snake_case)] pub mod ContextOverride; -pub mod DefaultConfig; pub mod Dimensions; pub mod Experiment; pub mod Home; pub mod NotFound; +pub mod default_config; pub mod experiment_list; pub mod function; diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index c0f61e8f..a054e681 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -207,3 +207,10 @@ impl DropdownOption for FunctionsName { self.clone() } } + +#[derive(Debug, Clone)] +pub struct BreadCrums { + pub key: String, + pub value: Option, + pub is_link: bool, +}