diff --git a/crates/experimentation_platform/src/api/experiments/helpers.rs b/crates/experimentation_platform/src/api/experiments/helpers.rs index 08e1537dc..a82a472b7 100644 --- a/crates/experimentation_platform/src/api/experiments/helpers.rs +++ b/crates/experimentation_platform/src/api/experiments/helpers.rs @@ -187,35 +187,9 @@ fn validate_keys_from_source( variant_override: &Overrides, source: &Map, ) -> bool { - for (override_key, value) in variant_override.iter() { - if let Some(val) = source.get(override_key) { - if val != value { - return false; - } - } else { - return false; - } - } - true -} - -fn validate_variants_delete_override_value( - delete_variant_overrides: &Vec, - resolved_config: &Map, -) -> bool { - for override_ in delete_variant_overrides { - if !validate_keys_from_source(override_, resolved_config) { - return false; - } - } - true -} - -fn validate_variants_control_override_value( - control_variant_overrides: &Overrides, - current_context_overrides: &Map, -) -> bool { - validate_keys_from_source(control_variant_overrides, current_context_overrides) + variant_override.iter().all(|(override_key, value)| { + source.get(override_key).is_some_and(|val| val == value) + }) } pub async fn validate_delete_experiment_variants( @@ -270,27 +244,26 @@ pub async fn validate_delete_experiment_variants( )); } - if !(validate_variants_delete_override_value( - &other_variants_overrides, - &partial_resolved_config, - )) { + if !(other_variants_overrides + .iter() + .all(|override_| validate_keys_from_source(override_, &partial_resolved_config))) + { log::error!( "validate_delete_experiment: Inconsistent value for variant's overrides delete keys" ); return Err(bad_argument!( - "Inconsistent value for variant's overrides delete keys" + "Incorrect value for experimental variant - it should be the same as the current resolved config with this override removed" )); } - if !(validate_variants_control_override_value( - &control_variant_override, - ¤t_context.override_, - )) { + // validate the variants control override values + if !(validate_keys_from_source(&control_variant_override, ¤t_context.override_)) + { log::error!( "validate_delete_experiment: Inconsistent value for variant's overrides keys" ); return Err(bad_argument!( - "Inconsistent value for variant's overrides keys" + "Incorrect value for control variant - it should be the same as the current override's key values" )); } Ok(()) diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index 23a8dd2e1..8a526176e 100755 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -27,6 +27,7 @@ use crate::pages::{ default_config::{CreateDefaultConfig, DefaultConfig, EditDefaultConfig}, default_config_list::DefaultConfigList, experiment::ExperimentPage, + experiment_page::{CreateExperiment, EditExperiment}, home::Home, organisations::Organisations, override_page::{CreateOverride, EditOverride, OverridePage}, @@ -246,6 +247,15 @@ pub fn App(app_envs: Envs) -> impl IntoView { path=join_route_parts([RouteSegment::Experiments]) view=ExperimentList /> + impl IntoView { ]) view=ExperimentPage /> + , + #[prop(default = String::new())] name: String, + context: Conditions, + #[prop(default = VariantFormTs::default())] variants: VariantFormTs, + #[prop(default = ExperimentFormType::Default)] + experiment_form_type: ExperimentFormType, + default_config: Vec, + dimensions: Vec, + #[prop(default = String::new())] description: String, + metrics: Metrics, + #[prop(default = None)] experiment_group_id: Option, + #[prop(into)] redirect_url_cancel: String, + #[prop(into)] redirect_url_success: String, +) -> impl IntoView { + let workspace = use_context::>().unwrap(); + let org = use_context::>().unwrap(); + let default_config = StoredValue::new(default_config); + let dimensions = StoredValue::new(dimensions); + let edit_id = StoredValue::new(edit_id); + let experiment_form_type = StoredValue::new(experiment_form_type); + + let (experiment_name, set_experiment_name) = create_signal(name); + let (context_rs, context_ws) = create_signal(context); + let init_variants = variants + .iter() + .map(|variant| { + let variant_value = if let ExperimentFormType::Delete(_) = + experiment_form_type.get_value() + && variant.variant_type == VariantType::EXPERIMENTAL + { + VariantFormT { + overrides: Vec::new(), + ..variant.clone() + } + } else { + variant.clone() + }; + (variant_value.id.clone(), variant_value) + }) + .collect::>(); + let (variants_rs, variants_ws) = create_signal(init_variants); + let (req_inprogress_rs, req_inprogress_ws) = create_signal(false); + let (description_rs, description_ws) = create_signal(description); + let (change_reason_rs, change_reason_ws) = create_signal(String::new()); + let metrics_rws = RwSignal::new(metrics); + let update_request_rws = RwSignal::new(None); + let (experiment_group_id_rs, experiment_group_id_ws) = + create_signal(experiment_group_id); + + let current_step = RwSignal::new(StepType::ContextStep); + + let steps = vec![ + Step { + title: "Context & Metadata".to_string(), + description: Some("Define context conditions and metadata".to_string()), + step_type: StepType::ContextStep, + }, + Step { + title: "Variants".to_string(), + description: Some("Configure experiment variants".to_string()), + step_type: StepType::OverrideStep, + }, + ]; + + let experiment_groups_resource: Resource<(String, String), Vec> = + create_blocking_resource( + move || (workspace.get().0, org.get().0), + |(workspace, org)| async move { + experiment_groups::list( + &ExpGroupFilters::default(), + &PaginationParams::all_entries(), + &DimensionQuery::default(), + &workspace, + &org, + ) + .await + .map(|data| data.data) + .unwrap_or_default() + }, + ); + + let fn_environment = Memo::new(move |_| { + let context = context_rs.get().into(); + let overrides = variants_rs + .get() + .iter() + .find(|(_, v)| v.variant_type == VariantType::EXPERIMENTAL) + .map(|(_, variant)| variant.overrides.iter().cloned().collect::>()) + .unwrap_or_default(); + FunctionEnvironment { context, overrides } + }); + + let handle_variant_form_change = + move |updated_variants: Vec<(String, VariantFormT)>| { + variants_ws.set_untracked(updated_variants); + }; + + let on_previous = Callback::new(move |_| { + current_step.update(|step| { + *step = match step { + StepType::OverrideStep => StepType::ContextStep, + StepType::ContextStep => StepType::ContextStep, + }; + }); + }); + + let on_next = Callback::new(move |_| { + current_step.update(|step| { + *step = match step { + StepType::ContextStep => StepType::OverrideStep, + StepType::OverrideStep => StepType::OverrideStep, + }; + }); + }); + + let redirect_url_success = StoredValue::new(redirect_url_success); + let on_submit = Callback::new(move |_: ()| { + req_inprogress_ws.set(true); + spawn_local(async move { + let f_variants = variants_rs + .get_untracked() + .into_iter() + .map(|(_, variant)| variant) + .collect::>(); + let workspace = workspace.get_untracked(); + let org = org.get_untracked(); + let redirect_url = redirect_url_success.get_value(); + let edit_id = edit_id.get_value(); + let f_experiment_name = experiment_name.get_untracked(); + + let experiment_group_id = if let Some(experiment_group_id) = + experiment_group_id_rs.get_untracked() + { + Value::String(experiment_group_id) + } else { + Value::Null + }; + + let result = match (edit_id.clone(), update_request_rws.get_untracked()) { + (Some(ref experiment_id), Some((_, payload))) => { + let future = + update_experiment(experiment_id, payload, &workspace.0, &org.0); + update_request_rws.set(None); + future.await.map(ResponseType::Response) + } + (Some(experiment_id), None) => { + let request_payload = try_update_payload( + f_variants, + Some(metrics_rws.get_untracked()), + description_rs.get_untracked(), + change_reason_rs.get_untracked(), + experiment_group_id, + ); + match request_payload { + Ok(payload) => { + update_request_rws.set(Some((experiment_id, payload))); + Ok(ResponseType::UpdatePrecheck) + } + Err(e) => Err(e), + } + } + _ => create_experiment( + context_rs.get_untracked(), + f_variants, + Some(metrics_rws.get_untracked()), + f_experiment_name, + ExperimentType::from(experiment_form_type.get_value()), + description_rs.get_untracked(), + change_reason_rs.get_untracked(), + experiment_group_id, + &workspace.0, + &org.0, + ) + .await + .map(ResponseType::Response), + }; + + let is_edit = edit_id.is_some(); + + req_inprogress_ws.set(false); + match result { + Ok(ResponseType::UpdatePrecheck) => (), + Ok(ResponseType::Response(res)) => { + let navigate = use_navigate(); + let final_redirect_url = match is_edit { + true => redirect_url.clone(), + false => format!( + "/admin/{}/{}/experiments/{}", + org.0, workspace.0, res.id + ), + }; + navigate(&final_redirect_url, Default::default()); + let success_message = if is_edit { + "Experiment updated successfully!" + } else { + "New Experiment created successfully!" + }; + enqueue_alert( + String::from(success_message), + AlertType::Success, + 5000, + ); + } + Err(e) => { + enqueue_alert(e, AlertType::Error, 5000); + } + } + }); + }); + + view! { +
+
+

+ {if edit { "Edit Experiment" } else { "Create Experiment" }} +

+ +
+ + + + +
+
+ +
+
+
+ + + + + + } + }> + {move || { + let experiment_groups = experiment_groups_resource + .get() + .unwrap_or_default(); + let mut experiment_options: Vec> = experiment_groups + .iter() + .map(|group| Some(group.id.to_string())) + .collect(); + experiment_options.insert(0, None); + view! { +
+
+ } + }} +
+ + + + +
+
+ + +
+

"Context"

+
+ + + +
+
+ {move || { + let variants = variants_rs.get(); + match experiment_form_type.get_value() { + ExperimentFormType::Default => { + view! { + + } + } + ExperimentFormType::Delete(data) => { + view! { + + } + } + } + }} +
+
+
+ + +
+
+
+
+ + {move || match update_request_rws.get() { + None => ().into_view(), + Some((experiment_id, update_request)) => { + view! { + + } + } + }} + } +} + +#[component] +fn ChangeLogSummary( + experiment_id: String, + update_request: OverrideKeysUpdateRequest, + #[prop(into)] on_confirm: Callback<()>, + #[prop(into)] on_close: Callback<()>, +) -> impl IntoView { + let workspace = use_context::>().unwrap(); + let org = use_context::>().unwrap(); + + let experiment = create_local_resource( + move || (experiment_id.clone(), workspace.get().0, org.get().0), + |(experiment_id, workspace, org)| async move { + fetch_experiment(experiment_id, &workspace, &org).await + }, + ); + + let disabled_rws = RwSignal::new(true); + let update_request = StoredValue::new(update_request); + + view! { + + } + }> + { + Effect::new(move |_| { + let experiment = experiment.get(); + if let Some(Ok(_)) = experiment { + disabled_rws.set(false); + } else if let Some(Err(e)) = experiment { + logging::error!("Error fetching experiment: {}", e); + } + }); + } + {move || match experiment.get() { + Some(Ok(experiment)) => { + let update_request = update_request.get_value(); + let description = update_request + .description + .unwrap_or_else(|| experiment.description.clone()) + .to_string(); + let experiment_group_id = update_request + .experiment_group_id + .map(|r| r.get_value().map(|v| v.to_string())) + .unwrap_or_else(|| experiment.experiment_group_id.clone()); + let variant_ids = experiment + .variants + .iter() + .map(|v| v.id.clone()) + .chain(update_request.variants.iter().map(|v| v.id.clone())) + .collect::>(); + let variant_data = variant_ids + .iter() + .map(|id| { + ( + id.clone(), + experiment + .variants + .iter() + .find(|v| v.id == *id) + .map(|v| { + (*v.overrides.clone().into_inner().clone()).clone() + }) + .unwrap_or_default(), + update_request + .variants + .iter() + .find(|v| v.id == *id) + .map(|v| { + (*v.overrides.clone().into_inner().clone()).clone() + }) + .unwrap_or_default(), + ) + }) + .collect::>(); + view! { + {variant_data + .into_iter() + .map(|(variant_id, old_overrides, new_overrides)| { + view! { + + } + }) + .collect_view()} + + + } + .into_view() + } + Some(Err(e)) => { + logging::error!("Error fetching experiment: {}", e); + view! {
Error fetching experiment
}.into_view() + } + None => view! {
Loading...
}.into_view(), + }} +
+
+ } +} diff --git a/crates/frontend/src/components/variant_form.rs b/crates/frontend/src/components/variant_form.rs index fa5d83d85..75db2df03 100644 --- a/crates/frontend/src/components/variant_form.rs +++ b/crates/frontend/src/components/variant_form.rs @@ -52,6 +52,7 @@ pub fn VariantForm( default_config: Vec, handle_change: HC, fn_environment: Memo, + #[prop(default = false)] vertical_split: bool, ) -> impl IntoView where HC: Fn(Vec<(String, VariantFormT)>) + 'static + Clone, @@ -62,6 +63,9 @@ where let init_override_keys = get_init_state(&variants); let (f_variants, set_variants) = create_signal(variants); let (override_keys, set_override_keys) = create_signal(init_override_keys); + let (show_add_override_menu, set_show_add_override_menu) = create_signal(false); + let (add_override_search_term, set_add_override_search_term) = + create_signal(String::new()); let key_to_type = StoredValue::new( default_config @@ -83,6 +87,14 @@ where .filter(|config| !override_keys.get().contains(&config.key)) .collect::>() }); + let filtered_unused_config_keys = Signal::derive(move || { + let term = add_override_search_term.get().to_lowercase(); + unused_config_keys + .get() + .into_iter() + .filter(|config| config.key.to_lowercase().contains(&term)) + .collect::>() + }); let resolved_config_resource = create_blocking_resource( move || { @@ -167,7 +179,7 @@ where } else { def_value.clone() }; - variant.overrides.push((config_key.clone(), value)); + variant.overrides.insert(0, (config_key.clone(), value)); } }, ); @@ -180,15 +192,33 @@ where let auto_populate_control = Callback::new(move |(resolved_config, idx): (Map, usize)| { - let control_overrides = override_keys + let override_keys = override_keys.get(); + let existing_control_overrides = f_variants .get() - .iter() - .filter_map(|k| { - resolved_config - .get(k) - .map(|default_value| (k.clone(), default_value.clone())) - }) - .collect::>(); + .get(idx) + .map(|(_, variant)| variant.overrides.clone()) + .unwrap_or_default(); + let mut control_overrides = Vec::new(); + + for (key, _) in existing_control_overrides { + if override_keys.contains(&key) { + if let Some(default_value) = resolved_config.get(&key) { + control_overrides.push((key, default_value.clone())); + } + } + } + + for key in &override_keys { + if control_overrides + .iter() + .any(|(existing_key, _)| existing_key == key) + { + continue; + } + if let Some(default_value) = resolved_config.get(key) { + control_overrides.push((key.clone(), default_value.clone())); + } + } if control_overrides != f_variants.get()[idx].1.overrides { set_variants.update(|curr_variants: &mut Vec<(String, VariantFormT)>| { @@ -230,217 +260,284 @@ where handle_change.get_value()(f_variants.clone()); }); + let vertical_split = StoredValue::new(vertical_split); + view! {
-
+
- +
+ + +
+
+ +
+ +
+
+
} }> - {move || { - f_variants - .get() - .iter() - .cloned() - .enumerate() - .map(move |(idx, (key, variant))| { - let is_control_variant = variant.variant_type == VariantType::CONTROL; - let handle_change = on_override_change(idx); - let variant_type_label = match variant.variant_type { - VariantType::CONTROL => "Control".to_string(), - VariantType::EXPERIMENTAL => format!("Variant {idx}"), - }; - let show_remove_btn = key != "control-variant" - && key != "experimental-variant" && !is_control_variant; - let key = StoredValue::new(key); - let overrides = StoredValue::new(variant.overrides); - let resolved_config = StoredValue::new( - if is_control_variant { - resolved_config_resource.get().unwrap_or_default() - } else { - Map::new() - }, - ); - Effect::new(move |_| { - let resolved_config = resolved_config_resource - .get() - .unwrap_or_default(); - if !edit && is_control_variant && !resolved_config.is_empty() { - auto_populate_control.call((resolved_config, idx)); - } - }); - - view! { -
-
- -
- +
+ {move || { + f_variants + .get() + .iter() + .cloned() + .enumerate() + .map(move |(idx, (key, variant))| { + let is_control_variant = variant.variant_type + == VariantType::CONTROL; + let handle_change = on_override_change(idx); + let variant_type_label = match variant.variant_type { + VariantType::CONTROL => "Control".to_string(), + VariantType::EXPERIMENTAL => format!("Variant {idx}"), + }; + let show_remove_btn = key != "control-variant" + && key != "experimental-variant" && !is_control_variant; + let key = StoredValue::new(key); + let overrides = StoredValue::new(variant.overrides); + let resolved_config = StoredValue::new( + if is_control_variant { + resolved_config_resource.get().unwrap_or_default() + } else { + Map::new() + }, + ); + Effect::new(move |_| { + let resolved_config = resolved_config_resource + .get() + .unwrap_or_default(); + if !edit && is_control_variant && !resolved_config.is_empty() { + auto_populate_control.call((resolved_config, idx)); + } + }); + let card_class = move || { + if vertical_split.get_value() { + "my-1 p-2 rounded bg-gray-50 flex-shrink-0 w-[calc(50%-2rem)] overflow-visible" + .to_string() + } else { + "my-1 p-2 rounded bg-gray-50 overflow-visible".to_string() + } + }; + + view! { +
+
+ +
+ + + +
+ - - - -
- - - -
-
-
-
-
- | - { - let variant_to_be_updated = current_variants.get_mut(idx); - match variant_to_be_updated { - Some((_, variant)) => { - variant.id = variant_id; - } - None => { - logging::log!( - "variant not found to update with id: {:?}", variant_id - ) +
+
+ +
+
+ | + { + let variant_to_be_updated = current_variants.get_mut(idx); + match variant_to_be_updated { + Some((_, variant)) => { + variant.id = variant_id; + } + None => { + logging::log!( + "variant not found to update with id: {:?}", variant_id + ) + } } - } - }); - } - /> - -
-
-
- -
- - - "Add keys from your config that you want to override in this experiment" - +
-
- - - - "Keys added in CONTROL will appear here as well for override" - - - - - {move || { - if is_control_variant { - view! { - - } - } else { - view! { - +
+
+ + + "Add keys from your config using the Add Override button above" + + + + + + "Keys added in CONTROL will appear here as well for override" + + + + + {move || { + if is_control_variant { + view! { + + } + } else { + view! { + + } } - } - }} - + }} + +
-
- } - }) - .collect_view() - }} + } + }) + .collect_view() + }} +
@@ -465,6 +562,7 @@ pub fn DeleteVariant( key: String, variant: VariantFormT, default_config: Vec, + original_override_keys: HashSet, label: String, #[prop(default = true)] show_remove_btn: bool, #[prop(into)] on_variant_change: Callback, @@ -486,10 +584,14 @@ pub fn DeleteVariant( let default_config = StoredValue::new(default_config); let unused_config_keys = Signal::derive(move || { + let keys = original_override_keys + .difference(&override_keys.get()) + .map(String::to_owned) + .collect::>(); default_config .get_value() .into_iter() - .filter(|config| !override_keys.get().contains(&config.key)) + .filter(|d| keys.contains(&d.key)) .collect::>() }); @@ -502,7 +604,7 @@ pub fn DeleteVariant( {label}
- +
- -
- - - "Add keys from your override that you want to delete from the context's overrides" - -
-
- @@ -607,7 +686,7 @@ pub fn DeleteVariant( #[derive(Serialize, Deserialize, Clone, Default)] struct CombinedResource { overrides: Map, - default_config: Vec, + delete_override_default_config: Vec, } #[component] @@ -664,7 +743,7 @@ where .unwrap_or_default(); let override_keys = overrides.keys().cloned().collect::>(); - let default_config = default_config + let delete_override_default_config = default_config .iter() .filter(|&d| override_keys.contains(&d.key)) .filter_map(|d| { @@ -677,7 +756,7 @@ where Ok(CombinedResource { overrides, - default_config, + delete_override_default_config, }) } Err(e) => Err(e), @@ -687,20 +766,6 @@ where let handle_change = StoredValue::new(handle_change); - let on_add_variant = move |_: web_sys::MouseEvent| { - variants_rws.update(|curr_variants| { - let key = Local::now().timestamp().to_string(); - curr_variants.push(( - key, - VariantFormT { - id: String::new(), - variant_type: VariantType::EXPERIMENTAL, - overrides: vec![], - }, - )) - }); - }; - Effect::new(move |_| handle_change.get_value()(variants_rws.get())); let update_control_variant = @@ -739,14 +804,16 @@ where view! { }>
-
- Experiment Variants - - "These are the override sets that would apply on the above context, for deleting the keys from the existing context's overrides. The value of the keys will be picked from the immediate-parent context." - +
+
+ Experiment Variants + + "These are the override sets that would apply on the above context, for deleting the keys from the existing context's overrides. The value of the keys will be picked from the immediate-parent context." + +
{move || { - let default_config = match combined_resource.get() { + let (original_override_keys, default_config) = match combined_resource.get() { None => { return view! {
Loading...
}.into_view(); } @@ -758,7 +825,14 @@ where } .into_view(); } - Some(Ok(combined_resource)) => combined_resource.default_config, + Some(Ok(combined_resource)) => { + ( + HashSet::from_iter( + combined_resource.overrides.keys().map(String::to_owned), + ), + combined_resource.delete_override_default_config, + ) + } }; view! { - - Add Variant - -
} diff --git a/crates/frontend/src/pages.rs b/crates/frontend/src/pages.rs index 378cf3498..9c29c16db 100644 --- a/crates/frontend/src/pages.rs +++ b/crates/frontend/src/pages.rs @@ -12,6 +12,7 @@ pub mod experiment; pub mod experiment_group_listing; pub mod experiment_groups; pub mod experiment_list; +pub mod experiment_page; pub mod function; pub mod home; pub mod not_found; diff --git a/crates/frontend/src/pages/context_override.rs b/crates/frontend/src/pages/context_override.rs index 72dcb0da9..76abce759 100644 --- a/crates/frontend/src/pages/context_override.rs +++ b/crates/frontend/src/pages/context_override.rs @@ -17,13 +17,9 @@ use superposition_types::{ default_config::DefaultConfigFilters, dimension::DimensionResponse, functions::FunctionEnvironment, - workspace::WorkspaceResponse, }, custom_query::{CustomQuery, DimensionQuery, PaginationParams, Query, QueryMap}, - database::models::{ - cac::{Context, DefaultConfig}, - experimentation::ExperimentType, - }, + database::models::cac::{Context, DefaultConfig}, }; use utils::{create_context, try_update_context_payload, update_context}; use wasm_bindgen::JsCast; @@ -38,9 +34,8 @@ use crate::{ change_summary::{ChangeLogPopup, ChangeSummary}, context_card::ContextCard, context_form::ContextForm, - drawer::{Drawer, DrawerBtn, close_drawer, open_drawer}, + drawer::DrawerBtn, dropdown::{Dropdown, DropdownBtnType}, - experiment_form::{ExperimentForm, ExperimentFormType}, override_form::OverrideForm, pagination::Pagination, skeleton::{Skeleton, SkeletonVariant}, @@ -50,10 +45,9 @@ use crate::{ providers::{ alert_provider::enqueue_alert, condition_collapse_provider::ConditionCollapseProvider, - editor_provider::EditorProvider, }, query_updater::use_signal_from_query, - types::{OrganisationId, VariantFormTs, Workspace}, + types::{OrganisationId, Workspace}, }; #[derive(Clone, Debug, Default)] @@ -73,7 +67,6 @@ struct PageResource { #[derive(Debug, Clone)] enum FormMode { - Experiment(ExperimentType, String), Delete(String), } @@ -312,69 +305,6 @@ fn AutofillForm( } } -#[component] -fn AutofillExperimentForm( - context_id: String, - #[prop(into)] handle_submit: Callback, - dimensions: Vec, - default_config: Vec, - experiment_type: ExperimentType, -) -> impl IntoView { - let workspace_settings = use_context::>().unwrap(); - let default_config = StoredValue::new(default_config); - let dimensions = StoredValue::new(dimensions); - - let context_data = use_context_data(context_id); - - view! { - } - }> - {move || { - match context_data.get() { - Some(Ok((context, condition))) => { - match experiment_type { - ExperimentType::Default => { - let overrides = context.override_.into_iter().collect::>(); - view! { - - } - } - ExperimentType::DeleteOverrides => { - view! { - - } - } - } - .into_view() - } - Some(Err(e)) => { - logging::error!("Error fetching context: {}", e); - view! {
Error fetching context
}.into_view() - } - None => view! {
Loading...
}.into_view(), - } - }} -
- } -} - #[component] pub fn ContextOverride() -> impl IntoView { let workspace = use_context::>().unwrap(); @@ -446,32 +376,26 @@ pub fn ContextOverride() -> impl IntoView { navigate(&url, Default::default()); }; - let handle_submit_experiment_form = move |experiment_id: String| { - page_resource.refetch(); - close_drawer("create_exp_drawer"); - + let handle_create_experiment = move |context_id| { + let navigate = use_navigate(); let workspace = workspace.get().0; let org = org.get().0; - let navigate = use_navigate(); - let redirect_url = - format!("/admin/{org}/{workspace}/experiments/{experiment_id}"); - navigate(redirect_url.as_str(), Default::default()) - }; - - let handle_create_experiment = move |context_id| { - set_form_mode.set(Some(FormMode::Experiment( - ExperimentType::Default, - context_id, - ))); - open_drawer("context_and_override_drawer"); + let url = format!( + "/admin/{}/{}/experiments/action/create?context_id={}", + org, workspace, context_id + ); + navigate(&url, Default::default()); }; let handle_delete_experiment = move |context_id| { - set_form_mode.set(Some(FormMode::Experiment( - ExperimentType::DeleteOverrides, - context_id, - ))); - open_drawer("context_and_override_drawer"); + let navigate = use_navigate(); + let workspace = workspace.get().0; + let org = org.get().0; + let url = format!( + "/admin/{}/{}/experiments/action/create?context_id={}&delete=true", + org, workspace, context_id + ); + navigate(&url, Default::default()); }; let on_context_clone = move |context_id| { @@ -683,11 +607,8 @@ pub fn ContextOverride() -> impl IntoView { }}
{move || { - let PageResource { dimensions, default_config, .. } = page_resource - .get() - .unwrap_or_default(); if let Some(FormMode::Delete(context_id)) = form_mode.get() { - return view! { + view! { impl IntoView { inprogress=delete_inprogress_rws /> } - .into_view(); - } - let drawer_header = match form_mode.get() { - Some(FormMode::Experiment(ExperimentType::Default, _)) => { - "Update Override via Experiment" - } - Some(FormMode::Experiment(ExperimentType::DeleteOverrides, _)) => { - "Delete Override via Experiment" - } - _ => "", - }; - view! { - - - {match form_mode.get_untracked() { - Some(FormMode::Experiment(experiment_type, context_id)) => { - view! { - - } - .into_view() - } - _ => ().into_view(), - }} - - + .into_view() + } else { + ().into_view() } }} {move || { diff --git a/crates/frontend/src/pages/experiment.rs b/crates/frontend/src/pages/experiment.rs index e50a20f06..566ff4c13 100644 --- a/crates/frontend/src/pages/experiment.rs +++ b/crates/frontend/src/pages/experiment.rs @@ -1,28 +1,17 @@ -use futures::join; use leptos::*; -use leptos_router::use_params_map; +use leptos_router::{use_navigate, use_params_map}; use serde::{Deserialize, Serialize}; -use superposition_types::{ - api::{ - default_config::DefaultConfigFilters, dimension::DimensionResponse, - experiments::ExperimentResponse, - }, - custom_query::PaginationParams, - database::models::cac::DefaultConfig, -}; +use superposition_types::api::experiments::ExperimentResponse; use crate::{ - api::{default_configs, dimensions, fetch_experiment}, + api::fetch_experiment, components::{ experiment::Experiment, experiment_action_form::ExperimentActionForm, experiment_conclude_form::ExperimentConcludeForm, - experiment_form::{ExperimentForm, ExperimentFormType}, - modal::{Modal, PortalModal}, + modal::Modal, skeleton::{Skeleton, SkeletonVariant}, }, - logic::Conditions, - providers::editor_provider::EditorProvider, types::{OrganisationId, Workspace}, utils::{close_modal, show_modal}, }; @@ -32,14 +21,11 @@ use crate::components::experiment_ramp_form::ExperimentRampForm; #[derive(Serialize, Deserialize, Clone, Debug)] struct CombinedResource { experiment: Option, - dimensions: Vec, - default_config: Vec, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub enum PopupType { ExperimentStart, - ExperimentEdit, ExperimentPause, ExperimentResume, ExperimentDiscard, @@ -63,33 +49,24 @@ pub fn ExperimentPage() -> impl IntoView { let combined_resource: Resource<(String, String, String), CombinedResource> = create_blocking_resource(source, |(exp_id, workspace, org_id)| async move { // Perform all fetch operations concurrently - let default_config_filters = DefaultConfigFilters::default(); - let experiments_future = fetch_experiment(exp_id, &workspace, &org_id); - let empty_list_filters = PaginationParams::all_entries(); - let dimensions_future = - dimensions::list(&empty_list_filters, &workspace, &org_id); - let config_future = default_configs::list( - &empty_list_filters, - &default_config_filters, - &workspace, - &org_id, - ); - - let (experiments_result, dimensions_result, config_result) = - join!(experiments_future, dimensions_future, config_future); - - // Construct the combined result, handling errors as needed + let experiments_result = fetch_experiment(exp_id, &workspace, &org_id).await; CombinedResource { experiment: experiments_result.ok(), - dimensions: dimensions_result.unwrap_or_default().data, - default_config: config_result.unwrap_or_default().data, } }); let handle_start = move || set_show_popup.set(PopupType::ExperimentStart); let handle_ramp = move || show_modal("ramp_form_modal"); let handle_conclude = move || show_modal("conclude_form_modal"); - let handle_edit = move || set_show_popup.set(PopupType::ExperimentEdit); + let handle_edit = move || { + let navigate = use_navigate(); + let workspace = workspace.get().0; + let org = org.get().0; + let exp_id = + exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + let url = format!("/admin/{}/{}/experiments/{}/edit", org, workspace, exp_id); + navigate(&url, Default::default()); + }; let handle_discard = move || set_show_popup.set(PopupType::ExperimentDiscard); let handle_pause = move || set_show_popup.set(PopupType::ExperimentPause); let handle_resume = move || set_show_popup.set(PopupType::ExperimentResume); @@ -104,8 +81,6 @@ pub fn ExperimentPage() -> impl IntoView { None => return view! {

Error fetching experiment

}.into_view(), }; let experiment = resource.experiment; - let default_config = resource.default_config; - let dimensions = resource.dimensions; match experiment { Some(experiment) => { let experiment_rf = experiment.clone(); @@ -133,44 +108,6 @@ pub fn ExperimentPage() -> impl IntoView { {match show_popup.get() { - PopupType::ExperimentEdit => { - view! { - - { - let experiment_ef = experiment.clone(); - let default_config = default_config.clone(); - let dimensions = dimensions.clone(); - view! { - - - - } - } - - } - } PopupType::None => ().into_view(), popup_type => { let experiment_id = experiment.id.clone(); diff --git a/crates/frontend/src/pages/experiment_list.rs b/crates/frontend/src/pages/experiment_list.rs index 41002feae..840b43c24 100644 --- a/crates/frontend/src/pages/experiment_list.rs +++ b/crates/frontend/src/pages/experiment_list.rs @@ -12,7 +12,6 @@ use superposition_types::{ default_config::DefaultConfigFilters, dimension::DimensionResponse, experiments::{ExperimentListFilters, ExperimentResponse}, - workspace::WorkspaceResponse, }, custom_query::{CustomQuery, DimensionQuery, PaginationParams, Query, QueryMap}, database::models::cac::DefaultConfig, @@ -22,19 +21,14 @@ use utils::experiment_table_columns; use crate::{ api::{default_configs, dimensions, fetch_experiments}, components::{ - drawer::{Drawer, DrawerBtn, close_drawer}, - experiment_form::ExperimentForm, + button::ButtonAnchor, skeleton::Skeleton, stat::Stat, table::{Table, types::TablePaginationProps}, }, - logic::Conditions, - providers::{ - condition_collapse_provider::ConditionCollapseProvider, - editor_provider::EditorProvider, - }, + providers::condition_collapse_provider::ConditionCollapseProvider, query_updater::use_signal_from_query, - types::{OrganisationId, VariantFormTs, Workspace}, + types::{OrganisationId, Workspace}, }; #[derive(Serialize, Deserialize, Clone, Default)] @@ -46,10 +40,8 @@ struct CombinedResource { #[component] pub fn ExperimentList() -> impl IntoView { - let workspace_settings = use_context::>().unwrap(); let workspace = use_context::>().unwrap(); let org = use_context::>().unwrap(); - let (reset_exp_form, set_exp_form) = create_signal(0); let (filters_rws, pagination_params_rws, dimension_params_rws) = use_signal_from_query(move |query_string| { ( @@ -71,7 +63,6 @@ pub fn ExperimentList() -> impl IntoView { ) }, |(workspace, filters, dimension_params, pagination_params, org_id)| async move { - // Perform all fetch operations concurrently let fetch_all_filters = PaginationParams::all_entries(); let default_config_filters = DefaultConfigFilters::default(); let experiments_future = fetch_experiments( @@ -91,7 +82,6 @@ pub fn ExperimentList() -> impl IntoView { ); let (experiments_result, dimensions_result, config_result) = join!(experiments_future, dimensions_future, config_future,); - // Construct the combined result, handling errors as needed CombinedResource { experiments: experiments_result.unwrap_or_default(), dimensions: dimensions_result.unwrap_or_default().data, @@ -100,16 +90,6 @@ pub fn ExperimentList() -> impl IntoView { }, ); - let handle_submit_experiment_form = move |_| { - filters_rws.set(ExperimentListFilters::default()); - pagination_params_rws.update(|f| f.reset_page()); - combined_resource.refetch(); - set_exp_form.update(|val| { - *val += 1; - }); - close_drawer("create_exp_drawer"); - }; - let handle_page_change = Callback::new(move |page: i64| { pagination_params_rws.update(|f| f.page = Some(page)); }); @@ -136,8 +116,9 @@ pub fn ExperimentList() -> impl IntoView { filters_rws combined_resource=combined_resource.get().unwrap_or_default() /> - @@ -190,37 +171,6 @@ pub fn ExperimentList() -> impl IntoView {
- {move || { - let CombinedResource { - dimensions: dim, - default_config: def_conf, - experiments: _, - } = combined_resource.get().unwrap_or_default(); - let _ = reset_exp_form.get(); - view! { - - - - - - - } - }} } diff --git a/crates/frontend/src/pages/experiment_page.rs b/crates/frontend/src/pages/experiment_page.rs new file mode 100644 index 000000000..ced311371 --- /dev/null +++ b/crates/frontend/src/pages/experiment_page.rs @@ -0,0 +1,212 @@ +use futures::join; +use leptos::*; +use leptos_router::{use_params_map, use_query_map}; +use serde::{Deserialize, Serialize}; +use superposition_types::{ + api::{default_config::DefaultConfigFilters, dimension::DimensionResponse}, + custom_query::PaginationParams, + database::models::cac::DefaultConfig, +}; + +use crate::{ + api::{default_configs, dimensions, fetch_experiment, get_context}, + components::skeleton::{Skeleton, SkeletonVariant}, + logic::Conditions, + types::{OrganisationId, VariantFormTs, Workspace}, +}; + +use crate::components::experiment_form::ExperimentFormType; + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +struct FormPageResource { + dimensions: Vec, + default_config: Vec, +} + +#[component] +pub fn EditExperiment() -> impl IntoView { + let path_params = use_params_map(); + let workspace = use_context::>().unwrap(); + let org = use_context::>().unwrap(); + let experiment_id = Memo::new(move |_| { + path_params.with(|params| params.get("id").cloned().unwrap_or("1".into())) + }); + + let experiment_resource = create_blocking_resource( + move || (experiment_id.get(), workspace.get().0, org.get().0), + |(experiment_id, workspace, org_id)| async move { + fetch_experiment(experiment_id, &workspace, &org_id) + .await + .ok() + }, + ); + + let page_resource: Resource<(String, String), FormPageResource> = + create_blocking_resource( + move || (workspace.get().0, org.get().0), + |(workspace, org_id)| async move { + let empty_list_filters = PaginationParams::all_entries(); + let default_config_filters = DefaultConfigFilters::default(); + let (dimensions_result, default_config_result) = join!( + dimensions::list(&empty_list_filters, &workspace, &org_id), + default_configs::list( + &empty_list_filters, + &default_config_filters, + &workspace, + &org_id + ), + ); + FormPageResource { + dimensions: dimensions_result.unwrap_or_default().data, + default_config: default_config_result.unwrap_or_default().data, + } + }, + ); + + view! { + } + }> + {move || { + let experiment = match experiment_resource.get() { + Some(Some(exp)) => exp, + _ => return view! {

"Error fetching experiment"

}.into_view(), + }; + let exp_id = experiment.id.clone(); + let conditions = Conditions::from_iter(experiment.context.clone().into_inner()); + let variants = VariantFormTs::from_iter(experiment.variants.clone()); + let description = experiment.description.to_string(); + let metrics = experiment.metrics.clone(); + let experiment_group_id = experiment.experiment_group_id.clone(); + let experiment_form_type = ExperimentFormType::from(experiment.experiment_type); + let FormPageResource { dimensions, default_config } = page_resource + .get() + .unwrap_or_default(); + let redirect_url = format!( + "/admin/{}/{}/experiments/{}", + org.get().0, + workspace.get().0, + exp_id, + ); + + view! { + + } + .into_view() + }} +
+ } +} + +#[component] +pub fn CreateExperiment() -> impl IntoView { + let workspace = use_context::>().unwrap(); + let org = use_context::>().unwrap(); + let query = use_query_map(); + let context_id = Memo::new(move |_| query.with(|q| q.get("context_id").cloned())); + let is_delete = Memo::new(move |_| { + query.with(|q| q.get("delete").cloned().unwrap_or_default() == "true") + }); + + let context_resource = create_blocking_resource( + move || (context_id.get(), workspace.get().0, org.get().0), + |(context_id, workspace, org_id)| async move { + let id = context_id?; + get_context(&id, &workspace, &org_id).await.ok() + }, + ); + + let page_resource: Resource<(String, String), FormPageResource> = + create_blocking_resource( + move || (workspace.get().0, org.get().0), + |(workspace, org_id)| async move { + let empty_list_filters = PaginationParams::all_entries(); + let default_config_filters = DefaultConfigFilters::default(); + let (dimensions_result, default_config_result) = join!( + dimensions::list(&empty_list_filters, &workspace, &org_id), + default_configs::list( + &empty_list_filters, + &default_config_filters, + &workspace, + &org_id + ), + ); + FormPageResource { + dimensions: dimensions_result.unwrap_or_default().data, + default_config: default_config_result.unwrap_or_default().data, + } + }, + ); + + let redirect_url = store_value(format!( + "/admin/{}/{}/experiments", + org.get().0, + workspace.get().0 + )); + + let workspace_settings = use_context::< + StoredValue, + >() + .unwrap(); + + view! { + } + }> + {move || { + let FormPageResource { dimensions, default_config } = page_resource + .get() + .unwrap_or_default(); + let cloned_context = context_resource.get().flatten(); + let (context, variants, experiment_form_type) = match cloned_context { + Some(ctx) => { + let conditions = Conditions::from_iter(ctx.value.clone().into_inner()); + let overrides = ctx.override_.clone().into_iter().collect::>(); + let form_type = if is_delete.get() { + ExperimentFormType::Delete(Some((ctx.id, ctx.override_.into()))) + } else { + ExperimentFormType::Default + }; + (conditions, VariantFormTs::default_with_overrides(overrides), form_type) + } + None => { + ( + Conditions::default(), + VariantFormTs::default(), + ExperimentFormType::Default, + ) + } + }; + + view! { + + } + .into_view() + }} + + } +} diff --git a/crates/superposition_core/src/format/toml/helpers.rs b/crates/superposition_core/src/format/toml/helpers.rs index 471850f6d..e440a5309 100644 --- a/crates/superposition_core/src/format/toml/helpers.rs +++ b/crates/superposition_core/src/format/toml/helpers.rs @@ -59,9 +59,7 @@ pub fn format_toml_value(value: &TomlValue) -> String { keys.sort(); let entries: Vec = keys .into_iter() - .map(|k| { - format!("{} = {}", format_key(k), format_toml_value(&table[k])) - }) + .map(|k| format!("{} = {}", format_key(k), format_toml_value(&table[k]))) .collect(); format!("{{ {} }}", entries.join(", ")) } diff --git a/crates/superposition_types/src/api/config.rs b/crates/superposition_types/src/api/config.rs index f0ed4ff52..2e96acf23 100644 --- a/crates/superposition_types/src/api/config.rs +++ b/crates/superposition_types/src/api/config.rs @@ -32,6 +32,8 @@ pub struct ResolveConfigQuery { pub version: Option, pub show_reasoning: Option, pub resolve_remote: Option, + /// if provided, Resolves upto the context ID provided in the sorted resolution step + // TODO: change this to exclude_context_id with an alias for backwards compatibility #[query_param(skip_if_empty)] pub context_id: Option, #[query_param(skip_if_empty, iterable)]