diff --git a/crates/frontend/src/components/condition_pills.rs b/crates/frontend/src/components/condition_pills.rs index b39e45be..02d4bc1d 100644 --- a/crates/frontend/src/components/condition_pills.rs +++ b/crates/frontend/src/components/condition_pills.rs @@ -1,15 +1,12 @@ -pub mod types; -pub mod utils; +use crate::{ + logic::{Condition, Conditions, Operand, Operator}, + schema::HtmlDisplay, +}; use leptos::{leptos_dom::helpers::WindowListenerHandle, *}; -use serde_json::Value; use wasm_bindgen::JsCast; use web_sys::Element; -use crate::components::condition_pills::types::ConditionOperator; - -use self::types::Condition; - use derive_more::{Deref, DerefMut}; #[derive(Debug, Clone, Deref, DerefMut, Default)] @@ -60,30 +57,13 @@ pub fn condition_expression( } else { ("condition-item-collapsed", "condition-value-collapsed") }; - let Condition { left_operand: dimension, operator, right_operand: value } = condition - .get_value(); - let filtered_vals: Vec = value - .into_iter() - .filter_map(|v| { - if v.is_object() && v.get("var").is_some() { - None - } else { - match v { - Value::String(s) => Some(s.to_string()), - Value::Number(n) => Some(n.to_string()), - Value::Bool(b) => Some(b.to_string()), - Value::Array(arr) => { - Some( - arr - .iter() - .map(|v| v.to_string()) - .collect::>() - .join(","), - ) - } - Value::Object(o) => serde_json::to_string_pretty(&o).ok(), - _ => None, - } + let Condition { dimension, operator, operands } = condition.get_value(); + let operand_str: Vec = operands + .iter() + .filter_map(|operand| { + match operand { + Operand::Dimension(_) => None, + Operand::Value(v) => Some(v.html_display()), } }) .collect(); @@ -106,18 +86,18 @@ pub fn condition_expression( {match operator { - ConditionOperator::Between => { - if filtered_vals.len() == 2 { + Operator::Between => { + if operand_str.len() == 2 { view! { <> - {&filtered_vals[0]} + {&operand_str[0]} {"and"} - {&filtered_vals[1]} + {&operand_str[1]} } @@ -132,7 +112,7 @@ pub fn condition_expression( } } _ => { - let rendered_value = filtered_vals.join(", "); + let rendered_value = operand_str.join(", "); view! { {rendered_value} }.into_view() } }} @@ -146,7 +126,7 @@ pub fn condition_expression( #[component] pub fn condition( #[prop(into)] id: String, - #[prop(into)] conditions: Vec, + #[prop(into)] conditions: Conditions, #[prop(into, default=String::new())] class: String, #[prop(default = true)] grouped_view: bool, ) -> impl IntoView { @@ -164,11 +144,17 @@ pub fn condition( .clone()> {conditions .get_value() - .into_iter() + .iter() .enumerate() .map(|(idx, condition)| { let item_id = format!("{}-{}", id, idx); - view! { } + view! { + + } }) .collect::>()} diff --git a/crates/frontend/src/components/condition_pills/types.rs b/crates/frontend/src/components/condition_pills/types.rs deleted file mode 100644 index 9bff2921..00000000 --- a/crates/frontend/src/components/condition_pills/types.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::fmt::Display; - -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; -use superposition_types::Context; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ConditionOperator { - Is, - In, - Has, - Between, - Other(String), -} - -impl Display for ConditionOperator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Has => f.write_str("has"), - Self::Is => f.write_str("is"), - Self::In => f.write_str("in"), - Self::Between => f.write_str("between"), - Self::Other(o) => f.write_str(o), - } - } -} - -impl From for ConditionOperator { - fn from(op: String) -> Self { - match op.as_str() { - "==" => ConditionOperator::Is, - "<=" => ConditionOperator::Between, - "in" => ConditionOperator::In, - "has" => ConditionOperator::Has, - other => ConditionOperator::Other(other.to_string()), - } - } -} - -impl From<(String, &Vec)> for ConditionOperator { - fn from(value: (String, &Vec)) -> Self { - let (operator, operands) = value; - let operand_0 = operands.first(); - let operand_1 = operands.get(1); - let operand_2 = operands.get(2); - match (operator.as_str(), operand_0, operand_1, operand_2) { - // assuming there will be only two operands, one with the dimension name and other with the value - ("==", _, _, None) => ConditionOperator::Is, - ("<=", Some(_), Some(Value::Object(a)), Some(_)) if a.contains_key("var") => { - ConditionOperator::Between - } - // assuming there will be only two operands, one with the dimension name and other with the value - ("in", Some(Value::Object(a)), Some(_), None) if a.contains_key("var") => { - ConditionOperator::In - } - // assuming there will be only two operands, one with the dimension name and other with the value - ("in", Some(_), Some(Value::Object(a)), None) if a.contains_key("var") => { - ConditionOperator::Has - } - _ => ConditionOperator::Other(operator), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Condition { - pub left_operand: String, - pub operator: ConditionOperator, - pub right_operand: Vec, -} - -#[derive(Default, Clone)] -pub struct Conditions(pub Vec); - -impl TryFrom<&Map> for Condition { - type Error = &'static str; - fn try_from(source: &Map) -> Result { - if let Some(operator) = source.keys().next() { - let emty_vec = vec![]; - let operands = source[operator].as_array().unwrap_or(&emty_vec); - - let operator = ConditionOperator::from((operator.to_owned(), operands)); - - let dimension_name = operands - .iter() - .find_map(|item| match item.as_object() { - Some(o) if o.contains_key("var") => { - Some(o["var"].as_str().unwrap_or("")) - } - _ => None, - }) - .unwrap_or(""); - - return Ok(Condition { - operator, - left_operand: dimension_name.to_owned(), - right_operand: operands - .to_vec() - .into_iter() - .filter(|operand| { - !(operand.is_object() && operand.get("var").is_some()) - }) - .collect(), - }); - } - - Err("not a valid condition map") - } -} - -impl TryFrom<&Value> for Condition { - type Error = &'static str; - fn try_from(value: &Value) -> Result { - let obj = value - .as_object() - .ok_or("not a valid condition value, should be an object")?; - Condition::try_from(obj) - } -} - -impl TryFrom<&Context> for Conditions { - type Error = &'static str; - fn try_from(context: &Context) -> Result { - let obj: Map = context.condition.clone().into(); - match obj.get("and") { - Some(v) => v - .as_array() - .ok_or("failed to parse value of and as array") - .and_then(|arr| { - arr.iter() - .map(Condition::try_from) - .collect::, &'static str>>() - }), - None => Condition::try_from(&obj).map(|v| vec![v]), - } - .map(Self) - } -} diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs deleted file mode 100644 index 53f3092c..00000000 --- a/crates/frontend/src/components/condition_pills/utils.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::types::Condition; -use serde_json::Value; - -pub fn extract_conditions(context: &Value) -> Vec { - context - .as_object() - .and_then(|obj| { - obj.get("and") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|condition| Condition::try_from(condition).ok()) - .collect::>() - }) - .or_else(|| Condition::try_from(obj).ok().map(|v| vec![v])) - }) - .unwrap_or_default() -} diff --git a/crates/frontend/src/components/context_card.rs b/crates/frontend/src/components/context_card.rs index 36a7e9d4..e6104442 100644 --- a/crates/frontend/src/components/context_card.rs +++ b/crates/frontend/src/components/context_card.rs @@ -2,10 +2,12 @@ use leptos::*; use serde_json::{Map, Value}; use superposition_types::Context; -use crate::components::condition_pills::types::{ConditionOperator, Conditions}; -use crate::components::{ - condition_pills::Condition as ConditionComponent, - table::{types::Column, Table}, +use crate::{ + components::{ + condition_pills::Condition as ConditionComponent, + table::{types::Column, Table}, + }, + logic::{Conditions, Operator}, }; #[component] @@ -50,12 +52,12 @@ pub fn context_card( && !conditions .0 .iter() - .any(|condition| condition.left_operand == "variantIds"); + .any(|condition| condition.dimension == "variantIds"); let edit_unsupported = conditions .0 .iter() - .any(|condition| matches!(condition.operator, ConditionOperator::Other(_))); + .any(|condition| matches!(condition.operator, Operator::Other(_))); view! {
@@ -109,7 +111,7 @@ pub fn context_card(
diff --git a/crates/frontend/src/components/context_form.rs b/crates/frontend/src/components/context_form.rs index 4852854f..844fa6ab 100644 --- a/crates/frontend/src/components/context_form.rs +++ b/crates/frontend/src/components/context_form.rs @@ -1,469 +1,379 @@ pub mod utils; use std::collections::{HashMap, HashSet}; -use crate::components::{ - condition_pills::types::ConditionOperator, - dropdown::{Dropdown, DropdownDirection}, - input_components::{BooleanToggle, EnumDropdown}, -}; +use crate::components::input::{Input, InputType}; +use crate::logic::{Condition, Conditions, Operand, Operands, Operator}; +use crate::schema::EnumVariants; use crate::types::Dimension; -use crate::utils::get_key_type; +use crate::{ + components::dropdown::{Dropdown, DropdownDirection}, + schema::SchemaType, +}; use leptos::*; -use serde_json::{Map, Value}; -use web_sys::MouseEvent; +use serde_json::Value; + +#[component] +pub fn condition_input( + disabled: bool, + resolve_mode: bool, + allow_remove: bool, + condition: StoredValue, + input_type: StoredValue, + schema_type: StoredValue, + #[prop(into)] on_remove: Callback, + #[prop(into)] on_value_change: Callback<(usize, Value), ()>, + #[prop(into)] on_operator_change: Callback, +) -> impl IntoView { + let Condition { + dimension, + operator, + operands, + } = condition.get_value(); + + view! { +
+
+ + +
+
+ + + + +
+
+ +
+ + {operands + .0 + .clone() + .into_iter() + .enumerate() + .map(|(idx, operand): (usize, Operand)| { + match operand { + Operand::Dimension(_) => view! {}.into_view(), + Operand::Value(v) => { + view! { + + } + .into_view() + } + } + }) + .collect_view()} + + +
+
+
+ } +} #[component] pub fn context_form( handle_change: NF, + context: Conditions, dimensions: Vec, - #[prop(default = false)] is_standalone: bool, - context: Vec, - #[prop(default = String::new())] heading_sub_text: String, #[prop(default = false)] disabled: bool, - #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, #[prop(default = false)] resolve_mode: bool, + #[prop(default = String::new())] heading_sub_text: String, + #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, ) -> impl IntoView where - NF: Fn(Vec) + 'static, + NF: Fn(Conditions) + 'static, { - // let _has_dimensions = !dimensions.is_empty(); - - let (used_dimensions, set_used_dimensions) = create_signal( + let dimension_map = store_value( + dimensions + .iter() + .map(|v| (v.dimension.clone(), v.clone())) + .collect::>(), + ); + let (used_dimensions_rs, used_dimensions_ws) = create_signal( context .iter() - .map(|condition| condition.left_operand.clone()) + .map(|condition| condition.dimension.clone()) .collect::>(), ); - let (context, set_context) = create_signal(context.clone()); + let (context_rs, context_ws) = create_signal(context.clone()); let dimensions = StoredValue::new(dimensions); let mandatory_dimensions = StoredValue::new( dimensions .get_value() .into_iter() - .filter_map(|dim| { - if dim.mandatory { - Some(dim.dimension) - } else { - None - } - }) + .filter(|dim| dim.mandatory) + .map(|dim| dim.dimension) .collect::>(), ); - let last_idx = create_memo(move |_| context.get().len().max(1) - 1); - - let on_click = move |event: MouseEvent| { - event.prevent_default(); - logging::log!("Context form submit"); - //TODO: submit logic for this - }; + let last_idx = create_memo(move |_| context_rs.get().len().max(1) - 1); create_effect(move |_| { - let f_context = context.get(); // context will now be a Value + let f_context = context_rs.get(); // context will now be a Value logging::log!("Context form effect {:?}", f_context); handle_change(f_context.clone()); // handle_change now expects Value }); - let handle_select_dropdown_option = - Callback::new(move |selected_dimension: Dimension| { - let dimension_name = selected_dimension.dimension; - set_used_dimensions.update(|value: &mut HashSet| { + let on_select_dimension = Callback::new(move |selected_dimension: Dimension| { + let dimension_name = selected_dimension.dimension; + + if let Ok(r#type) = SchemaType::try_from(selected_dimension.schema) { + used_dimensions_ws.update(|value: &mut HashSet| { value.insert(dimension_name.clone()); }); - set_context.update(|value| { - value.push(Condition { - left_operand: dimension_name.clone(), - operator: ConditionOperator::Is, - right_operand: vec![Value::String("".to_string())], - }) + context_ws.update(|value| { + value.push( + Condition::try_from((Operator::Is, dimension_name, r#type)).unwrap(), + ) }); - }); - - view! { -
-
-
- -
-
-
- -
- -
-
- {move || { - let dimensions_map = dimensions - .get_value() - .into_iter() - .map(|ele| (ele.dimension.clone(), ele)) - .collect::>(); - view! { - >() - } - - key=|(idx, condition)| { - format!("{}-{}-{}", condition.left_operand, idx, condition.operator) - } - - children=move |(idx, condition)| { - let dimension_label = condition.left_operand.to_string(); - // let dimension_label = dimension.to_string(); - let dimension_name = StoredValue::new( - condition.left_operand.to_string(), - ); - let schema: Map = serde_json::from_value( - dimensions_map.get(&dimension_label).unwrap().schema.clone(), - ) - .unwrap(); - let dimension_type = get_key_type(&schema); - if let ConditionOperator::Other(ref op_str) = condition.operator { - if op_str.is_empty() { - set_context.update_untracked(|curr_context| { - curr_context[idx].operator = ConditionOperator::Is; - }); - let mut_operator = String::from("=="); - set_context.update_untracked(|curr_context| { - curr_context[idx].operator = ConditionOperator::Other(mut_operator.clone()); - }); - } - } - view! { - // - -
-
- - -
-
- - - - -
-
- -
+ } + // TODO show alert in case of invalid dimension + }); - { - // Generate input fields based on the condition's right_operand (Vec) - let input_fields = match &condition.right_operand { - values => { - // Filter out any elements that are objects containing a "var" key - let filtered_elements: Vec<_> = values - .iter() - .filter(|v| !v.is_object() || !v.get("var").is_some()) // Exclude elements with "var" - .collect(); + let on_operator_change = Callback::new( + move |(idx, d_name, d_type, operator): (usize, String, SchemaType, Operator)| { + if let Ok(operands) = + Operands::try_from((&operator, d_name.as_str(), &d_type)) + { + context_ws.update(|v| { + if idx < v.len() { + v[idx].operator = operator; + v[idx].operands = operands.clone(); + } + }) + } + // TODO show alert in case of invalid dimension operator combinations + }, + ); - // Directly return the input fields - filtered_elements - .into_iter() // Use `into_iter` to consume the filtered_elements - .enumerate() - .map(|(i, element)| match element { - Value::String(s) => view! { - - }.into_view(), + let on_value_change = + Callback::new(move |(idx, operand_idx, value): (usize, usize, Value)| { + context_ws.update(|v| { + if idx < v.len() { + let operands = &(v[idx].operands); + if operand_idx < operands.len() + && matches!(operands[operand_idx], Operand::Value(_)) + { + v[idx].operands[operand_idx] = Operand::from_operand_json(value); + } + } + }) + }); - Value::Number(n) => view! { - () { // Try to parse input as f64 - set_context.update(|curr_context| { - if let Some(elem) = curr_context[idx].right_operand.get_mut(i) { - if !elem.is_object() || !elem.get("var").is_some() { // Exclude elements with "var" - *elem = Value::Number(serde_json::Number::from_f64(parsed).unwrap()); - } - } - }); - } - } - name="context-dimension-value" - type="number" - placeholder="Type here" - class="input input-bordered w-full bg-white text-gray-700 shadow-md" - /> - }.into_view(), + let on_remove = Callback::new(move |(idx, d_name): (usize, String)| { + used_dimensions_ws.update(|value| { + value.remove(&d_name); + }); + context_ws.update(|v| { + v.remove(idx); + }); + }); - Value::Bool(b) => view! { - +
+ +
+
+
+ +
+ - }.into_view(), +
+
+ >() + } - _ => view! { - - }.into_view(), - }) - .collect::>() // Collect the result into a Vec - } - }; - match condition.operator { - ConditionOperator::Is => { - match dimension_type.as_str() { - "ENUM" => { - let filtered_value = condition.right_operand - .iter() - .find(|v| match v { - Value::Object(obj) => !obj.contains_key("var"), - _ => true, - }) - .map(|v| v.to_string().replace('"', "")) - .unwrap_or_else(|| String::new()); + key=|(idx, condition)| { + format!("{}-{}-{}", condition.dimension, idx, condition.operator) + } - view! { - - } - .into_view() - } - "BOOLEAN" => { - // Ensure we handle Value properly and default to false if not a valid boolean - let is_checked = match &condition.right_operand[0] { - Value::Bool(b) => *b, // Extract the boolean value directly - _ => false, // Default to false if not a boolean - }; - view! { - - } - .into_view() - } - _ => view! { - { - logging::log!("Condition operator and saurav {:?}", condition.operator); - input_fields.into_view() - } - } // Fallback to input field if not ENUM or BOOLEAN - } - } - _ => view! { - {input_fields.into_view()} - } // For other operators, use input_fields as default - } - } - - - -
-
-
+ let schema_type = store_value(schema_type.unwrap()); + let allow_remove = !disabled + && !mandatory_dimensions.get_value().contains(&condition.dimension); + let input_type = store_value( + InputType::from(( + schema_type.get_value(), + enum_variants.unwrap(), + condition.operator.clone(), + )), + ); + logging::log!("here {:?} {:?}", input_type.get_value(), condition.operator); + let condition = store_value(condition); + let on_remove = move |d_name| on_remove.call((idx, d_name)); + let on_value_change = move |(operand_idx, value)| { + on_value_change.call((idx, operand_idx, value)) + }; + let on_operator_change = move |operator| { + on_operator_change + .call(( + idx, + condition.with_value(|v| v.dimension.clone()), + schema_type.get_value(), + operator, + )) + }; + view! { + // TODO: get rid of unwraps here - {move || { - if last_idx.get() != idx { - view! { -
- "&&" -
- } - .into_view() - } else { - view! {}.into_view() - } - }} - } + + {move || { + if last_idx.get() != idx { + view! { +
+ "&&" +
} + .into_view() + } else { + view! {}.into_view() + } + }} + }.into_view() + } + /> + + +
+ + {move || { + let dimensions = dimensions + .get_value() + .into_iter() + .filter(|dimension| { + !used_dimensions_rs.get().contains(&dimension.dimension) + }) + .collect::>(); + view! { + } }} - -
- - {move || { - let dimensions = dimensions - .get_value() - .into_iter() - .filter(|dimension| { - !used_dimensions.get().contains(&dimension.dimension) - }) - .collect::>(); - view! { - - } - }} - -
-
-
-
+ +
- -
- -
-
- } +
+ } } diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index 6d0a1c76..02e31bc4 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,231 +1,27 @@ -use crate::{ - components::condition_pills::types::{Condition, ConditionOperator}, - types::Dimension, - utils::{ - construct_request_headers, get_config_value, get_host, parse_json_response, - request, ConfigType, - }, -}; +use super::Conditions; +use crate::utils::{construct_request_headers, get_host, parse_json_response, request}; + use anyhow::Result; use serde_json::{json, Map, Value}; -pub fn get_condition_schema( - condition: &Condition, - dimensions: Vec, -) -> Result { - let var = &condition.left_operand; // Dimension name - let op = &condition.operator; // Operator type - let val = &condition.right_operand; // Vec - - // Extract non-"var" elements from the right_operand - let filtered_values: Vec<&Value> = val - .iter() - .filter(|v| !v.is_object() || !v.get("var").is_some()) // Ignore objects with "var" - .collect(); - let dimensions_clone = dimensions.clone(); - - match op { - ConditionOperator::Between => { - // Expecting three elements for "Between" condition: two operands and one "var" object - if filtered_values.len() != 2 { - return Err( - "Invalid number of operands for 'between' condition.".to_string() - ); - } - - let first_operand = &filtered_values[0]; // The first value - let third_operand = &filtered_values[1]; // The third value - - let first_operand_value = get_config_value( - var, - first_operand, - &dimensions - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - - let third_operand_value = get_config_value( - var, - third_operand, - &dimensions_clone - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - - Ok(json!({ - "<=": [ - first_operand_value, - { "var": var }, - third_operand_value - ] - })) - } - ConditionOperator::Is => { - // Expecting two elements for "Is" condition: one "var" object and one value - if filtered_values.len() != 1 { - return Err("Invalid number of operands for 'is' condition.".to_string()); - } - - let value = &filtered_values[0]; // The value after "var" - let first_operand_value = get_config_value( - var, - value, - &dimensions - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - - Ok(json!({ - "==": [ - { "var": var }, - first_operand_value - ] - })) - } - ConditionOperator::In => { - if filtered_values.len() != 1 { - return Err("Invalid number of operands for 'in' condition.".to_string()); - } - let value = &filtered_values[0]; // The value after "var" - let first_operand_value = get_config_value( - var, - value, - &dimensions - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - - Ok(json!({ - "in": [ - { "var": var }, - first_operand_value - ] - })) - } - ConditionOperator::Has => { - if filtered_values.len() != 1 { - return Err("Invalid number of operands for 'has' condition.".to_string()); - } - let value = &filtered_values[0]; // The value after "var" - let first_operand_value = get_config_value( - var, - value, - &dimensions - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - - Ok(json!({ - "in": [ - first_operand_value, - { "var": var } - ] - })) - } - ConditionOperator::Other(op) => { - if filtered_values.len() == 1 { - let value = &filtered_values[0]; // The value after "var" - let first_operand_value = get_config_value( - var, - value, - &dimensions - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - Ok(json!({ - op: [ - { "var": var }, - first_operand_value - ] - })) - } else if filtered_values.len() == 2 { - let first_operand = &filtered_values[0]; // The first value - let second_operand = &filtered_values[1]; // The second value - let first_operand_value = get_config_value( - var, - first_operand, - &dimensions - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - let second_operand_value = get_config_value( - var, - second_operand, - &dimensions_clone - .into_iter() - .map(ConfigType::Dimension) - .collect::>(), - )?; - Ok(json!({ - op: [ - first_operand_value, - { "var": var }, - second_operand_value - ] - })) - } else { - Err("Invalid number of operands for custom operator.".to_string()) - } - } - } -} - -pub fn construct_context( - conditions: Vec, - dimensions: Vec, -) -> Value { - if conditions.is_empty() { - json!({}) - } else { - let condition_schemas = conditions - .iter() - .map(|condition| get_condition_schema(condition, dimensions.clone()).unwrap()) - .collect::>(); - - if condition_schemas.len() == 1 { - condition_schemas[0].clone() - } else { - json!({ "and": condition_schemas }) - } - } -} - -pub fn construct_request_payload( - overrides: Map, - conditions: Vec, - dimensions: Vec, -) -> Value { - // Construct the override section - let override_section: Map = overrides; - - // Construct the context section - let context_section = construct_context(conditions, dimensions.clone()); - - // Construct the entire request payload - let request_payload = json!({ - "override": override_section, - "context": context_section +pub fn context_payload(overrides: Map, conditions: Conditions) -> Value { + let context: Value = conditions.to_context_json(); + let payload = json!({ + "override": overrides, + "context": context }); - request_payload + payload } pub async fn create_context( tenant: String, overrides: Map, - conditions: Vec, - dimensions: Vec, -) -> Result { + conditions: Conditions, +) -> Result { let host = get_host(); let url = format!("{host}/context"); - let request_payload = construct_request_payload(overrides, conditions, dimensions); + let request_payload = context_payload(overrides, conditions); let response = request( url, reqwest::Method::PUT, @@ -240,13 +36,11 @@ pub async fn create_context( pub async fn update_context( tenant: String, overrides: Map, - conditions: Vec, - dimensions: Vec, + conditions: Conditions, ) -> Result { let host = get_host(); let url = format!("{host}/context/overrides"); - let request_payload = - construct_request_payload(overrides, conditions, dimensions.clone()); + let request_payload = context_payload(overrides, conditions); let response = request( url, reqwest::Method::PUT, diff --git a/crates/frontend/src/components/experiment.rs b/crates/frontend/src/components/experiment.rs index f61db1f8..bb042320 100644 --- a/crates/frontend/src/components/experiment.rs +++ b/crates/frontend/src/components/experiment.rs @@ -2,8 +2,10 @@ use leptos::*; use serde_json::{Map, Value}; use std::collections::HashMap; -use crate::components::table::Table; -use crate::schema::HtmlDisplay; +use crate::{components::table::Table, schema::HtmlDisplay}; + +use crate::logic::{Condition, Operand}; + use crate::types::{Experiment, ExperimentStatusType}; use crate::{ components::table::types::Column, @@ -65,166 +67,165 @@ where gen_variant_table(&experiment.with_value(|v| v.variants.clone())).unwrap(); view! { -
-

- {experiment.with_value(|v| v.name.clone())} - {experiment.with_value(|v| v.status.to_string())} -

-
-
- - {move || { - let handle_start = handle_start.clone(); - let handle_conclude = handle_conclude.clone(); - let handle_ramp = handle_ramp.clone(); - let handle_edit = handle_edit.clone(); - match experiment.with_value(|v| v.status) { - ExperimentStatusType::CREATED => { - view! { - - +
+

+ {experiment.with_value(|v| v.name.clone())} + {experiment.with_value(|v| v.status.to_string())} +

+
+
+ + {move || { + let handle_start = handle_start.clone(); + let handle_conclude = handle_conclude.clone(); + let handle_ramp = handle_ramp.clone(); + let handle_edit = handle_edit.clone(); + match experiment.with_value(|v| v.status) { + ExperimentStatusType::CREATED => { + view! { + + + } + .into_view() } - .into_view() - } - ExperimentStatusType::INPROGRESS => { - view! { - - + ExperimentStatusType::INPROGRESS => { + view! { + + + } + .into_view() } - .into_view() - } - ExperimentStatusType::CONCLUDED => { - view! { -
-
Chosen Variant
-
- {match experiment.with_value(|v| v.chosen_variant.clone()) { - Some(ref v) => v.to_string(), - None => String::new(), - }} + ExperimentStatusType::CONCLUDED => { + view! { +
+
Chosen Variant
+
+ {match experiment.with_value(|v| v.chosen_variant.clone()) { + Some(ref v) => v.to_string(), + None => String::new(), + }} +
-
+ } + .into_view() } - .into_view() } - } - }} + }} -
-
-
-
Experiment ID
-
{experiment.with_value(|v| v.id.clone())}
-
-
Current Traffic Percentage
-
- {experiment.with_value(|v| v.traffic_percentage)} +
+
+
Experiment ID
+
{experiment.with_value(|v| v.id.clone())}
-
-
-
Created by
-
- {experiment.with_value(|v| v.created_by.clone())} +
+
Current Traffic Percentage
+
+ {experiment.with_value(|v| v.traffic_percentage)} +
-
-
-
Created at
-
- {format!("{}", experiment.with_value(|v| v.created_at.format("%v")))} +
+
Created by
+
+ {experiment.with_value(|v| v.created_by.clone())} +
-
-
-
Last Modified
-
+
+
Created at
+
+ {format!("{}", experiment.with_value(|v| v.created_at.format("%v")))} +
+
+
+
Last Modified
+
- {format!("{}", experiment.with_value(|v| v.last_modified.format("%v")))} + {format!("{}", experiment.with_value(|v| v.last_modified.format("%v")))} +
-
-
-
-

Context

-
- {move || { - let mut view = Vec::new(); - for token in contexts.clone() { - let (dimension, values) = (token.left_operand, token.right_operand); - let mut value_views = Vec::new(); - for value in values.iter() { - if value.is_object() && value.get("var").is_some() { - continue; - } - value_views - .push( - view! { -
- {value.html_display()} -
- }, - ); - } - view.push( +
+
+

Context

+
+ + {contexts + .iter() + .map(|condition| { + let Condition { dimension, operands, .. } = condition; + let operand_views = operands + .iter() + .filter_map(|op| { + match op { + Operand::Dimension(_) => None, + Operand::Value(v) => { + Some( + view! { +
{v.html_display()}
+ }, + ) + } + } + }) + .collect_view(); view! {
{dimension}
- {value_views} + {operand_views}
- }, - ); - } - view - }} + } + }) + .collect_view()} +
-
-
-
-

Variants

- - +
+
+

Variants

+ +
+ - - } + } } diff --git a/crates/frontend/src/components/experiment_conclude_form/utils.rs b/crates/frontend/src/components/experiment_conclude_form/utils.rs index b73829c9..7113a55a 100644 --- a/crates/frontend/src/components/experiment_conclude_form/utils.rs +++ b/crates/frontend/src/components/experiment_conclude_form/utils.rs @@ -1,13 +1,13 @@ use leptos::logging::log; use serde_json::json; -use crate::{types::Experiment, utils::get_host}; +use crate::{types::ExperimentResponse, utils::get_host}; pub async fn conclude_experiment( exp_id: String, variant_id: String, tenant: &String, -) -> Result { +) -> Result { let client = reqwest::Client::new(); let host = get_host(); match client @@ -20,7 +20,7 @@ pub async fn conclude_experiment( Ok(experiment) => { log!("experiment response {:?}", experiment); Ok(experiment - .json::() + .json::() .await .map_err(|err| err.to_string())?) } diff --git a/crates/frontend/src/components/experiment_form.rs b/crates/frontend/src/components/experiment_form.rs index b85f9973..e042660a 100644 --- a/crates/frontend/src/components/experiment_form.rs +++ b/crates/frontend/src/components/experiment_form.rs @@ -9,7 +9,7 @@ use crate::types::{DefaultConfig, Dimension, VariantFormT, VariantType}; use leptos::*; use web_sys::MouseEvent; -use super::condition_pills::types::Condition; +use crate::logic::Conditions; fn default_variants_for_form() -> Vec<(String, VariantFormT)> { vec![ @@ -50,7 +50,7 @@ pub fn experiment_form( #[prop(default = false)] edit: bool, #[prop(default = String::new())] id: String, name: String, - context: Vec, + context: Conditions, variants: Vec, handle_submit: NF, default_config: Vec, @@ -68,7 +68,7 @@ where let (f_variants, set_variants) = create_signal(init_variants); let (req_inprogess_rs, req_inprogress_ws) = create_signal(false); - let handle_context_form_change = move |updated_ctx: Vec| { + let handle_context_form_change = move |updated_ctx: Conditions| { set_context.set_untracked(updated_ctx); }; @@ -103,14 +103,8 @@ where let result = if edit { update_experiment(experiment_id, f_variants, tenant).await } else { - create_experiment( - f_context, - f_variants, - f_experiment_name, - tenant, - dimensions.get_value().clone(), - ) - .await + create_experiment(f_context, f_variants, f_experiment_name, tenant) + .await }; match result { @@ -152,11 +146,9 @@ where let context = f_context.get(); view! { dimensions=dimensions.get_value() context=context handle_change=handle_context_form_change - is_standalone=false disabled=edit heading_sub_text=String::from( "Define rules under which this experiment would run", diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index 431b85fa..06b44e9f 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -1,7 +1,6 @@ use super::types::{ExperimentCreateRequest, ExperimentUpdateRequest}; -use crate::components::condition_pills::types::Condition; -use crate::components::context_form::utils::construct_context; -use crate::types::{Dimension, VariantFormT}; +use crate::logic::Conditions; +use crate::types::VariantFormT; use crate::utils::{construct_request_headers, get_host, parse_json_response, request}; use serde_json::Value; @@ -13,16 +12,15 @@ pub fn validate_experiment(experiment: &ExperimentCreateRequest) -> Result, + conditions: Conditions, variants: Vec, name: String, tenant: String, - dimensions: Vec, ) -> Result { let payload = ExperimentCreateRequest { name, variants: FromIterator::from_iter(variants), - context: construct_context(conditions, dimensions.clone()), + context: conditions.to_context_json(), }; let _ = validate_experiment(&payload)?; diff --git a/crates/frontend/src/components/experiment_ramp_form/utils.rs b/crates/frontend/src/components/experiment_ramp_form/utils.rs index 7018ad61..9722c7be 100644 --- a/crates/frontend/src/components/experiment_ramp_form/utils.rs +++ b/crates/frontend/src/components/experiment_ramp_form/utils.rs @@ -1,13 +1,13 @@ use leptos::logging::log; use serde_json::json; -use crate::{types::Experiment, utils::get_host}; +use crate::{types::ExperimentResponse, utils::get_host}; pub async fn ramp_experiment( exp_id: &String, percent: u8, tenant: &String, -) -> Result { +) -> Result { let client = reqwest::Client::new(); let host = get_host(); match client @@ -20,7 +20,7 @@ pub async fn ramp_experiment( Ok(experiment) => { log!("experiment response {:?}", experiment); Ok(experiment - .json::() + .json::() .await .map_err(|err| err.to_string())?) } diff --git a/crates/frontend/src/components/input.rs b/crates/frontend/src/components/input.rs index 49a805c9..5961ddd0 100644 --- a/crates/frontend/src/components/input.rs +++ b/crates/frontend/src/components/input.rs @@ -12,6 +12,8 @@ use crate::{ schema::{EnumVariants, HtmlDisplay, JsonSchemaType, SchemaType}, }; +use crate::logic::Operator; + #[derive(Debug, Clone, PartialEq)] pub enum InputType { Text, @@ -64,7 +66,30 @@ impl From<(SchemaType, EnumVariants)> for InputType { } } -fn str_to_value(s: &str, type_: &JsonSchemaType) -> Result { +impl From<(SchemaType, EnumVariants, Operator)> for InputType { + fn from( + (schema_type, enum_variants, operator): (SchemaType, EnumVariants, Operator), + ) -> Self { + if operator == Operator::In { + return InputType::Text; + } + + if !enum_variants.is_empty() { + return InputType::Select(enum_variants); + } + + match schema_type { + SchemaType::Single(JsonSchemaType::Number) => InputType::Number, + SchemaType::Single(JsonSchemaType::Integer) => InputType::Integer, + SchemaType::Single(JsonSchemaType::Boolean) => InputType::Toggle, + SchemaType::Single(JsonSchemaType::Null) => InputType::Disabled, + _ => InputType::Text, + } + } +} + +// TODO: Also add schema validation in frontend ::::: +fn parse(s: &str, type_: &JsonSchemaType) -> Result { match type_ { JsonSchemaType::String => Ok(Value::String(s.to_string())), JsonSchemaType::Number => s @@ -90,14 +115,55 @@ fn str_to_value(s: &str, type_: &JsonSchemaType) -> Result { } } -fn parse_input_value(value: String, schema_type: SchemaType) -> Result { +fn parse_with_operator( + s: &str, + type_: &JsonSchemaType, + op: &Operator, +) -> Result { + match op { + Operator::In => match type_ { + JsonSchemaType::String => serde_json::from_str::>(s) + .map(|v| json!(v)) + .map_err(|_| "not a valid array of strings".to_string()), + JsonSchemaType::Number => serde_json::from_str::>(s) + .map(|v| json!(v)) + .map_err(|_| "not a valid array of numbers".to_string()), + JsonSchemaType::Integer => serde_json::from_str::>(s) + .map(|v| json!(v)) + .map_err(|_| "not a valid array of integers".to_string()), + JsonSchemaType::Boolean => serde_json::from_str::>(s) + .map(|v| json!(v)) + .map_err(|_| "not a valid array of booleans".to_string()), + JsonSchemaType::Array => serde_json::from_str::>(s) + .map(|v| json!(v)) + .map_err(|_| "not a valid array of arrays".to_string()), + JsonSchemaType::Object => serde_json::from_str::>>(s) + .map(|v| json!(v)) + .map_err(|_| "not a valid array of objects".to_string()), + JsonSchemaType::Null if s == "null" => Ok(Value::Null), + JsonSchemaType::Null => Err("not a null value".to_string()), + }, + _ => parse(s, type_), + } +} + +fn parse_input( + value: String, + schema_type: SchemaType, + op: &Option, +) -> Result { + let parse_single = |r#type: &JsonSchemaType| match op { + Some(op) => parse_with_operator(&value, r#type, op), + None => parse(&value, r#type), + }; + match schema_type { - SchemaType::Single(ref type_) => str_to_value(&value, type_), + SchemaType::Single(ref r#type) => parse_single(r#type), SchemaType::Multiple(mut types) => { types.sort_by(|a, b| a.precedence().cmp(&b.precedence())); - for type_ in types.iter() { - let v = str_to_value(&value, type_); + for r#type in types.iter() { + let v = parse_single(r#type); if v.is_ok() { return v; } @@ -183,6 +249,7 @@ fn basic_input( value: Value, schema_type: SchemaType, on_change: Callback, + #[prop(default = None)] operator: Option, ) -> impl IntoView { let schema_type = store_value(schema_type); let (error_rs, error_ws) = create_signal::>(None); @@ -208,7 +275,7 @@ fn basic_input( value=value.html_display() on:change=move |e| { let v = event_target_value(&e); - match parse_input_value(v, schema_type.get_value()) { + match parse_input(v, schema_type.get_value(), &operator) { Ok(v) => { on_change.call(v); error_ws.set(None); @@ -244,6 +311,7 @@ pub fn monaco_input( value: Value, on_change: Callback, schema_type: SchemaType, + #[prop(default = None)] operator: Option, ) -> impl IntoView { let id = store_value(id); let schema_type = store_value(schema_type); @@ -273,7 +341,7 @@ pub fn monaco_input( logging::log!("Saving editor value: {}", editor_value); let parsed_value = - parse_input_value(editor_value.clone(), schema_type.get_value()); + parse_input(editor_value.clone(), schema_type.get_value(), &operator); match parsed_value { Ok(v) => { logging::log!("Saving parsed value: {}", editor_value); @@ -421,12 +489,13 @@ pub fn monaco_input( pub fn input( value: Value, schema_type: SchemaType, - on_change: Callback, + #[prop(into)] on_change: Callback, #[prop(into)] r#type: InputType, #[prop(default = false)] disabled: bool, #[prop(into, default = String::new())] id: String, #[prop(into, default = String::new())] class: String, #[prop(into, default = String::new())] name: String, + #[prop(default = None)] operator: Option, ) -> impl IntoView { match r#type { InputType::Toggle => match value.as_bool() { @@ -438,7 +507,7 @@ pub fn input( InputType::Select(ref options) => view! { - {default_configs - .iter() - .map(|(k, v)| rows(k, v, false)) - .collect::>()} + {rows(&default_configs, false)}
@@ -187,7 +176,7 @@ pub fn home() -> impl IntoView { }, ); - let (context_rs, context_ws) = create_signal::>(vec![]); + let (context_rs, context_ws) = create_signal::(Conditions::default()); let (selected_tab_rs, selected_tab_ws) = create_signal(ResolveTab::AllConfig); let (req_inprogess_rs, req_inprogress_ws) = create_signal(false); @@ -245,42 +234,6 @@ pub fn home() -> impl IntoView { } }; - let gen_query_context = |query: Vec| -> String { - let mut context: Vec = vec![]; - for condition in query.iter() { - let dimension = condition.left_operand.clone(); - let op = match condition.operator.clone() { - ConditionOperator::Is => Cow::Borrowed("="), - ConditionOperator::In => Cow::Borrowed("IN"), - ConditionOperator::Has => Cow::Borrowed("HAS"), - ConditionOperator::Between => Cow::Borrowed("BETWEEN"), - ConditionOperator::Other(op) => Cow::Owned(op), - }; - let value = condition - .right_operand - .clone() - .into_iter() - .filter_map(|value| { - if value.is_object() && value.get("var").is_some() { - None - } else { - Some(value) - } - }) - .map(|value| match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => String::from("null"), - _ => format!("{}", value), - }) - .collect::>() - .join(","); - context.push(format!("{}{op}{}", dimension, value)); - } - context.join("&").to_string() - }; - let resolve_click = move |ev: MouseEvent| { ev.prevent_default(); req_inprogress_ws.set(true); @@ -308,7 +261,7 @@ pub fn home() -> impl IntoView { let context_updated = context_rs.get(); // resolve the context and get the config that would apply spawn_local(async move { - let context = gen_query_context(context_updated); + let context = context_updated.to_query_string(); let mut config = match resolve_config(tenant_rs.get_untracked(), context) .await .unwrap() @@ -386,10 +339,9 @@ pub fn home() -> impl IntoView { , + pub(crate) context: Conditions, pub(crate) status: ExperimentStatusType, pub(crate) override_keys: Value, pub(crate) created_by: String, @@ -217,7 +215,10 @@ impl From for Experiment { last_modified: value.last_modified, chosen_variant: value.chosen_variant, variants: serde_json::from_value(value.variants).unwrap_or_default(), - context: extract_conditions(&value.context), + context: Conditions::from_context_json( + value.context.as_object().unwrap_or(&Map::new()), + ) + .unwrap_or(Conditions(vec![])), } } } diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index b9e390b6..6760618b 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -3,7 +3,7 @@ use std::env; use crate::{ components::alert::AlertType, providers::alert_provider::enqueue_alert, - types::{DefaultConfig, Dimension, Envs, ErrorResponse}, + types::{Envs, ErrorResponse}, }; use cfg_if::cfg_if; use leptos::*; @@ -194,164 +194,6 @@ pub fn check_url_and_return_val(s: String) -> String { } } -pub enum ConfigType { - DefaultConfig(DefaultConfig), - Dimension(Dimension), -} - -#[derive(Clone, Debug)] -pub enum ConfigValueType { - Boolean, - Number, - String, - Null, - Integer, - Other, -} - -impl FromStr for ConfigValueType { - type Err = String; - fn from_str(s: &str) -> Result { - match s { - "boolean" => Ok(ConfigValueType::Boolean), - "number" => Ok(ConfigValueType::Number), - "string" => Ok(ConfigValueType::String), - "null" => Ok(ConfigValueType::Null), - "integer" => Ok(ConfigValueType::Integer), - _ => Ok(ConfigValueType::Other), - } - } -} - -pub fn get_config_type( - configs: &[ConfigType], - key_name: &str, -) -> Option> { - let config = configs.iter().find(|conf| match conf { - ConfigType::DefaultConfig(default_conf) => default_conf.key == key_name, - ConfigType::Dimension(dimension) => dimension.dimension == key_name, - }); - - let type_from_str = |type_str: Option<&str>| { - type_str - .map(|t| ConfigValueType::from_str(t).ok()) - .flatten() - .unwrap_or(ConfigValueType::Other) - }; - - let types_mapping = |schema_type: &Value| match schema_type { - Value::Array(types) => types - .iter() - .map(|item: &Value| type_from_str(item.as_str())) - .collect::>(), - Value::String(type_str) => vec![type_from_str(Some(type_str.as_str()))], - _ => vec![ConfigValueType::Other], - }; - - config.and_then(|config| match config { - ConfigType::DefaultConfig(default_conf) => { - default_conf.schema.get("type").map(|t| types_mapping(t)) - } - - ConfigType::Dimension(dimension) => { - dimension.schema.get("type").map(|t| types_mapping(t)) - } - }) -} - -pub fn parse_value(val: &Value, config_type: ConfigValueType) -> Result { - match config_type { - ConfigValueType::Boolean => { - match val { - Value::Bool(_) => Ok(val.clone()), - - Value::String(s) => { - // Attempting to parse the string as a boolean - match s.to_lowercase().as_str() { - "true" => Ok(Value::Bool(true)), - "false" => Ok(Value::Bool(false)), - _ => Err(format!("Invalid boolean string: {:?}", s)), // Error if not a valid boolean string - } - } - - _ => Err(format!("Invalid boolean value: {:?}", val)), - } - } - - ConfigValueType::Number | ConfigValueType::Integer => { - match val { - Value::Number(num) => Ok(Value::Number(num.clone())), - - Value::String(s) => { - // Attempting to parse as integer first, then as float - if let Ok(int_val) = s.parse::() { - Ok(Value::Number(int_val.into())) - } else if let Ok(float_val) = s.parse::() { - Ok(Value::Number( - serde_json::Number::from_f64(float_val).unwrap(), - )) - } else { - Err(format!("Invalid number format: {:?}", s)) - } - } - - Value::Array(arr) => { - // Ensuring all items in the array are numbers - if arr.iter().all(|item| item.is_number()) { - Ok(val.clone()) - } else { - Err("Array contains non-number value".to_string()) - } - } - - _ => Err(format!( - "{:?} is neither a valid number nor an array of numbers.", - val - )), - } - } - - ConfigValueType::String => match val { - Value::String(_) => Ok(val.clone()), - Value::Array(arr) => { - if arr.iter().all(|item| item.is_string()) { - Ok(val.clone()) - } else { - Err("Array contains non-string value".to_string()) - } - } - _ => Err(format!( - "{:?} is neither a valid string nor an array of strings.", - val - )), - }, - - ConfigValueType::Null if val.is_null() => Ok(Value::Null), - - _ => Ok(val.clone()), - } -} - -pub fn get_config_value( - name: &str, - val: &Value, - configs: &[ConfigType], -) -> Result { - let config_type = get_config_type(configs, name); - - match config_type { - Some(possible_types) => { - for possible_type in possible_types { - if let Ok(parsed_value) = parse_value(val, possible_type) { - return Ok(parsed_value); - } - } - Err("Error parsing config value".to_string()) - } - None => Ok(val.clone()), - } -} - /********* Request Utils **********/ use once_cell::sync::Lazy;