diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 164bae69..d27201b5 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -43,6 +43,7 @@ hydrate = [ "console_error_panic_hook", ] ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"] +multi-delimiter = [] [lints] workspace = true diff --git a/crates/frontend/src/pages/default_config.rs b/crates/frontend/src/pages/default_config.rs index d1521c25..209a1f9f 100644 --- a/crates/frontend/src/pages/default_config.rs +++ b/crates/frontend/src/pages/default_config.rs @@ -6,14 +6,18 @@ use crate::components::skeleton::Skeleton; use crate::components::stat::Stat; use crate::components::table::{types::Column, Table}; +use crate::schema::HtmlDisplay; use crate::types::BreadCrums; -use crate::utils::{ - get_local_storage, set_local_storage, unwrap_option_or_default_with_error, -}; +use crate::utils::{get_local_storage, set_local_storage}; use leptos::*; use leptos_router::{use_navigate, use_query_map}; use serde_json::{json, Map, Value}; -use std::collections::HashSet; + +#[cfg(feature = "multi-delimiter")] +const GROUPING_DELIMITER: &[char] = &['.', ':']; + +#[cfg(not(feature = "multi-delimiter"))] +const GROUPING_DELIMITER: &[char] = &['.']; #[derive(Clone, Debug, Default)] pub struct RowData { @@ -40,7 +44,7 @@ pub fn default_config() -> impl IntoView { 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())); + let bread_crums = Signal::derive(move || utils::get_bread_crums(key_prefix.get())); create_effect(move |_| { let enable_grouping_val = @@ -59,7 +63,7 @@ pub fn default_config() -> impl IntoView { } }); - let folder_click_handler = move |key_name: Option| { + let on_folder_click = move |key_name: Option| { let tenant = tenant_rs.get(); let redirect_url = match key_name { Some(prefix) => format!("admin/{tenant}/default-config?prefix={prefix}"), @@ -73,27 +77,25 @@ pub fn default_config() -> impl IntoView { let table_columns = create_memo(move |_| { let grouping_enabled = enable_grouping.get(); let actions_col_formatter = move |_: &str, row: &Map| { - let row_key = row["key"].to_string().replace('"', ""); - let is_folder = row_key.contains('.'); - let row_value = row["value"].to_string().replace('"', ""); + let row_key = row["key"].html_display(); + let row_value = row["value"].html_display(); + let is_folder = row_key.contains(GROUPING_DELIMITER); - let schema = row["schema"].clone().to_string(); - let schema_object = - serde_json::from_str::(&schema).unwrap_or(Value::Null); + let schema = row["schema"].clone(); - let function_name = row["function_name"].to_string(); + let function_name = row["function_name"].html_display(); let fun_name = match function_name.as_str() { "null" => None, - _ => Some(json!(function_name.replace('"', ""))), + _ => Some(json!(function_name)), }; - let key_name = StoredValue::new(row_key.clone()); + let row_key = StoredValue::new(row_key.clone()); let edit_click_handler = move |_| { let row_data = RowData { - key: row_key.clone(), + key: row_key.get_value(), value: row_value.clone(), - schema: schema_object.clone(), + schema: schema.clone(), function_name: fun_name.clone(), }; logging::log!("{:?}", row_data); @@ -107,7 +109,7 @@ pub fn default_config() -> impl IntoView { spawn_local({ async move { let _ = delete_default_config( - format!("{prefix}{}", key_name.get_value()), + format!("{prefix}{}", row_key.get_value()), tenant, ) .await; @@ -134,9 +136,9 @@ pub fn default_config() -> impl IntoView { }; let expand = move |_: &str, row: &Map| { - let key_name = row["key"].to_string().replace('"', ""); + let key_name = row["key"].html_display(); let label = key_name.clone(); - let is_folder = key_name.contains('.'); + let is_folder = key_name.contains(GROUPING_DELIMITER); if is_folder && grouping_enabled { view! { @@ -147,7 +149,7 @@ pub fn default_config() -> impl IntoView { if let Some(prefix_) = key_prefix.get() { key = prefix_.clone() + &key; } - folder_click_handler(Some(key.clone())) + on_folder_click(Some(key.clone())) } > @@ -228,47 +230,21 @@ pub fn default_config() -> impl IntoView { } }} {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 - .first() - .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(); + let data = default_config_resource.get().unwrap_or(vec![]); + let rows = utils::get_rows(data.clone(), key_prefix.get(), enable_grouping.get()); + let total_items = rows.len().to_string(); view! {
- +
- +
@@ -308,37 +284,32 @@ pub fn default_config() -> impl IntoView { } #[component] -pub fn bread_crums( +pub fn bread_crums( bread_crums: Vec, - folder_click_handler: NF, -) -> impl IntoView -where - NF: Fn(Option) + 'static + Clone, -{ + #[prop(into)] on_folder_click: Callback, ()>, +) -> impl IntoView { view! {
{bread_crums - .iter() - .enumerate() - .map(|(_, ele)| { + .into_iter() + .map(|ele| { let value = ele.value.clone(); - let is_link = ele.is_link; - let handler = folder_click_handler.clone(); + let item_class = if ele.is_link { + "cursor-pointer text-blue-500 underline underline-offset-2" + } else { + "" + }; view! {

{ele.key.clone()} @@ -351,107 +322,119 @@ where } } -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, +mod utils { + use std::collections::HashSet; + + use serde_json::{Map, Value}; + + use crate::{ + types::{BreadCrums, DefaultConfig}, + utils::unwrap_option_or_default_with_error, }; - if let Some(last_crumb) = bread_crums.last_mut() { - last_crumb.is_link = false; + + use super::GROUPING_DELIMITER; + + pub fn get_bread_crums(prefix: Option) -> Vec { + let mut bread_crums = vec![BreadCrums { + key: "Default Config".to_string(), + value: None, + is_link: true, + }]; + + if let Some(s) = prefix { + let mut acc = String::new(); + for frag in s.split_inclusive(GROUPING_DELIMITER) { + acc.push_str(frag); + bread_crums.push(BreadCrums { + key: frag.trim_matches(GROUPING_DELIMITER).to_string(), + value: Some(acc.clone()), + is_link: true, + }) + } + } + + if let Some(last_crumb) = bread_crums.last_mut() { + last_crumb.is_link = false; + } + bread_crums } - bread_crums -} -pub fn modify_rows( - filtered_rows: Vec>, - key_prefix: Option, - cols: Vec<&str>, -) -> Vec> { - let mut groups: HashSet = HashSet::new(); - let mut grouped_rows: Vec> = 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.first().unwrap().to_owned().replace('"', ""); - if new_key.len() == 1 { - // key - ele.insert("key".to_string(), json!(key)); + pub fn get_rows( + data: Vec, + prefix: Option, + enable_grouping: bool, + ) -> Vec> { + if !enable_grouping { + return data.iter().map(|v| v.into_row()).collect(); + } + + let mut groups: HashSet<&str> = HashSet::new(); + let mut rows: Vec> = vec![]; + for config_key in data.iter() { + let suffix = prefix.as_ref().map_or(Some(config_key.key.as_str()), |s| { + config_key.key.split(s).nth(1) + }); + + if let Some(suff) = suffix { + let splits = suff + .split_inclusive(GROUPING_DELIMITER) + .collect::>(); + let is_leaf = splits.len() == 1; + + let grp_name = splits.first().expect( + format!( + "invalid key name, cannot end with characters {:?}", + GROUPING_DELIMITER + ) + .as_str(), + ); + + if !is_leaf && groups.contains(grp_name) { + continue; + } + groups.insert(grp_name); + + let mut row_map = config_key.into_row(); + if is_leaf { + row_map + .insert("key".to_string(), Value::String(grp_name.to_string())); } 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() - }), + DefaultConfig::table_cols().iter().for_each(|c| { + if *c == "key" { + row_map.insert( + c.to_string(), + Value::String(grp_name.to_string()), ); - }); - groups.insert(folder); - } else { - return None; - } + } else { + row_map.insert(c.to_string(), Value::String("-".to_string())); + } + }) } - Some(ele) - } else { - None + + rows.push(row_map); } - }) - .collect(); - grouped_rows.sort_by(|a, b| { - let key_a = - unwrap_option_or_default_with_error(a.get("key").and_then(Value::as_str), ""); - let key_b = - unwrap_option_or_default_with_error(b.get("key").and_then(Value::as_str), ""); - - match (key_a.contains('.'), key_b.contains('.')) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => std::cmp::Ordering::Equal, } - }); - grouped_rows + + rows.sort_by(|a, b| { + let key_a = unwrap_option_or_default_with_error( + a.get("key").and_then(Value::as_str), + "", + ); + let key_b = unwrap_option_or_default_with_error( + b.get("key").and_then(Value::as_str), + "", + ); + + match ( + key_a.contains(GROUPING_DELIMITER), + key_b.contains(GROUPING_DELIMITER), + ) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => std::cmp::Ordering::Equal, + } + }); + + rows + } } diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index f95cf91b..a9d74023 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -244,7 +244,7 @@ impl DropdownOption for Dimension { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, Default)] pub struct DefaultConfig { pub key: String, pub value: Value, @@ -254,6 +254,31 @@ pub struct DefaultConfig { pub function_name: Option, } +impl DefaultConfig { + pub fn table_cols() -> Vec<&'static str> { + vec![ + "key", + "value", + "created_at", + "created_by", + "schema", + "function_name", + ] + } + + pub fn into_row(&self) -> Map { + let mut map = json!(self) + .as_object() + .expect("failed to generate row map from default_config") + .to_owned(); + map.insert( + "created_at".to_string(), + json!(self.created_at.format("%v").to_string()), + ); + map + } +} + impl DropdownOption for DefaultConfig { fn key(&self) -> String { self.key.clone() diff --git a/crates/superposition_types/Cargo.toml b/crates/superposition_types/Cargo.toml index 5a706aee..fa423f3b 100644 --- a/crates/superposition_types/Cargo.toml +++ b/crates/superposition_types/Cargo.toml @@ -21,6 +21,7 @@ regex = { workspace = true } [features] disable_db_data_validation = [] result = ["dep:diesel", "dep:anyhow", "dep:thiserror"] +multi-demlimiter = [] [lints] workspace = true diff --git a/crates/superposition_types/src/lib.rs b/crates/superposition_types/src/lib.rs index e9b1897c..6338bbb8 100644 --- a/crates/superposition_types/src/lib.rs +++ b/crates/superposition_types/src/lib.rs @@ -183,8 +183,20 @@ impl Condition { impl_try_from_map!(Cac, Condition, Condition::validate_data_for_cac); impl_try_from_map!(Exp, Condition, Condition::validate_data_for_exp); +#[cfg(feature = "multi-delimiter")] +const ALPHANUMERIC_WITH_DOT: &str = + "^[a-zA-Z0-9-_]([a-zA-Z0-9-_.:]{0,254}[a-zA-Z0-9-_])?$"; + +#[cfg(feature = "multi-delimiter")] +const ALPHANUMERIC_WITH_DOT_WORDS: &str = + "It can contain the following characters only [a-zA-Z0-9-_.:] \ + and it should not start or end with a ':' or '.' character."; + +#[cfg(not(feature = "multi-delimiter"))] const ALPHANUMERIC_WITH_DOT: &str = "^[a-zA-Z0-9-_]([a-zA-Z0-9-_.]{0,254}[a-zA-Z0-9-_])?$"; + +#[cfg(not(feature = "multi-delimiter"))] const ALPHANUMERIC_WITH_DOT_WORDS: &str = "It can contain the following characters only [a-zA-Z0-9-_.] \ and it should not start or end with a '.' character.";