diff --git a/apps/api/libs/handlers/src/metrics/color_palette_helpers.rs b/apps/api/libs/handlers/src/metrics/color_palette_helpers.rs new file mode 100644 index 000000000..264eaa1cb --- /dev/null +++ b/apps/api/libs/handlers/src/metrics/color_palette_helpers.rs @@ -0,0 +1,125 @@ +use anyhow::{anyhow, Result}; +use database::pool::get_pg_pool; +use diesel::{ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use serde_json::Value; +use uuid::Uuid; + +pub const DEFAULT_COLOR_PALETTE: [&str; 10] = [ + "#B399FD", "#FC8497", "#FBBC30", "#279EFF", + "#E83562", "#41F8FF", "#F3864F", "#C82184", + "#31FCB4", "#E83562" +]; + +pub async fn get_organization_color_palette(organization_id: &Uuid) -> Result>> { + let mut conn = get_pg_pool().get().await?; + + let config_result = diesel::sql_query( + "SELECT config FROM users + INNER JOIN users_to_organizations ON users.id = users_to_organizations.user_id + WHERE users_to_organizations.organization_id = $1 + AND users_to_organizations.deleted_at IS NULL + LIMIT 1" + ) + .bind::(organization_id) + .get_result::(&mut conn) + .await; + + match config_result { + Ok(config_row) => { + if let Some(config_value) = config_row.config { + if let Some(selected_palette) = config_value.get("selectedDictionaryPalette") { + if let Some(colors) = selected_palette.get("colors") { + if let Some(colors_array) = colors.as_array() { + let palette: Vec = colors_array + .iter() + .filter_map(|c| c.as_str().map(|s| s.to_string())) + .collect(); + + if !palette.is_empty() { + return Ok(Some(palette)); + } + } + } + } + + if let Some(last_used) = config_value.get("last_used_color_palette") { + if let Some(colors_array) = last_used.as_array() { + let palette: Vec = colors_array + .iter() + .filter_map(|c| c.as_str().map(|s| s.to_string())) + .collect(); + + if !palette.is_empty() { + return Ok(Some(palette)); + } + } + } + } + + Ok(None) + }, + Err(_) => { + Ok(None) + } + } +} + +pub fn get_default_color_palette() -> Vec { + DEFAULT_COLOR_PALETTE.iter().map(|&s| s.to_string()).collect() +} + +pub async fn apply_color_fallback( + chart_config: &mut database::types::ChartConfig, + organization_id: &Uuid, +) -> Result<()> { + let colors_already_set = match chart_config { + database::types::ChartConfig::Bar(config) => config.base.colors.is_some(), + database::types::ChartConfig::Line(config) => config.base.colors.is_some(), + database::types::ChartConfig::Scatter(config) => config.base.colors.is_some(), + database::types::ChartConfig::Pie(config) => config.base.colors.is_some(), + database::types::ChartConfig::Combo(config) => config.base.colors.is_some(), + database::types::ChartConfig::Metric(_) => true, // Metric doesn't use colors + database::types::ChartConfig::Table(_) => true, // Table doesn't use colors + }; + + if colors_already_set { + return Ok(()); + } + + let org_palette = get_organization_color_palette(organization_id).await?; + + let palette = if let Some(palette) = org_palette { + palette + } else { + get_default_color_palette() + }; + + match chart_config { + database::types::ChartConfig::Bar(config) => { + config.base.colors = Some(palette); + }, + database::types::ChartConfig::Line(config) => { + config.base.colors = Some(palette); + }, + database::types::ChartConfig::Scatter(config) => { + config.base.colors = Some(palette); + }, + database::types::ChartConfig::Pie(config) => { + config.base.colors = Some(palette); + }, + database::types::ChartConfig::Combo(config) => { + config.base.colors = Some(palette); + }, + database::types::ChartConfig::Metric(_) => {}, // No colors to set + database::types::ChartConfig::Table(_) => {}, // No colors to set + } + + Ok(()) +} + +#[derive(diesel::QueryableByName, Debug)] +struct ConfigResult { + #[diesel(sql_type = diesel::sql_types::Jsonb)] + config: Option, +} diff --git a/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs index 1e70a9847..7f0b95ca6 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs @@ -9,6 +9,7 @@ use serde_yaml; use sharing::asset_access_checks::check_metric_collection_access; use uuid::Uuid; +use crate::metrics::color_palette_helpers::apply_color_fallback; use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, BusterShareIndividual, Dataset}; use crate::utils::workspace::count_workspace_members; use database::enums::{AssetPermissionRole, AssetType, IdentityType}; @@ -448,6 +449,11 @@ pub async fn get_metric_for_dashboard_handler( } }; + let mut final_chart_config = resolved_chart_config.clone(); + if let Err(e) = apply_color_fallback(&mut final_chart_config, &metric_file.organization_id).await { + tracing::warn!(metric_id = %metric_id, error = %e, "Failed to apply color fallback logic, continuing with original chart config"); + } + // Get workspace sharing enabled by email if set let workspace_sharing_enabled_by = if let Some(enabled_by_id) = metric_file.workspace_sharing_enabled_by { users::table @@ -488,7 +494,7 @@ pub async fn get_metric_for_dashboard_handler( datasets, data_source_id: metric_file.data_source_id, error: None, - chart_config: Some(resolved_chart_config), + chart_config: Some(final_chart_config), // Use chart config with color fallback applied data_metadata, status: metric_file.verification, evaluation_score, @@ -517,4 +523,4 @@ pub async fn get_metric_for_dashboard_handler( // Workspace member count workspace_member_count, }) -} \ No newline at end of file +} \ No newline at end of file diff --git a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs index 30c0f249a..40b4d75e1 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -7,6 +7,7 @@ use serde_yaml; use sharing::asset_access_checks::check_metric_collection_access; use uuid::Uuid; +use crate::metrics::color_palette_helpers::apply_color_fallback; use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, Dataset}; use crate::utils::workspace::count_workspace_members; use database::enums::{AssetPermissionRole, AssetType, IdentityType}; @@ -304,6 +305,11 @@ pub async fn get_metric_handler( tracing::debug!(metric_id = %metric_id, latest_version = resolved_version_num, "Determined latest version number"); } + let mut final_chart_config = resolved_chart_config.clone(); + if let Err(e) = apply_color_fallback(&mut final_chart_config, &metric_file.organization_id).await { + tracing::warn!(metric_id = %metric_id, error = %e, "Failed to apply color fallback logic, continuing with original chart config"); + } + // Convert the selected content to pretty YAML for the 'file' field let file = match serde_yaml::to_string(&resolved_content_for_yaml) { Ok(yaml) => yaml, @@ -494,7 +500,7 @@ pub async fn get_metric_handler( datasets, // Fetched based on resolved_dataset_ids (for display purposes only) data_source_id: metric_file.data_source_id, // Use canonical ID (Uuid) from main record error: None, // Assume ok - chart_config: Some(resolved_chart_config), // Use resolved chart config + chart_config: Some(final_chart_config), // Use chart config with color fallback applied data_metadata, // Not versioned status: metric_file.verification, // Not versioned evaluation_score, // Not versioned diff --git a/apps/api/libs/handlers/src/metrics/mod.rs b/apps/api/libs/handlers/src/metrics/mod.rs index e2dcddb3c..109d6c868 100644 --- a/apps/api/libs/handlers/src/metrics/mod.rs +++ b/apps/api/libs/handlers/src/metrics/mod.rs @@ -1,4 +1,5 @@ pub mod bulk_update_metrics_handler; +pub mod color_palette_helpers; pub mod delete_metric_handler; pub mod get_metric_data_handler; pub mod get_metric_handler;