Merge pull request #555 from buster-so/devin/BUS-1453-1752871654

BUS-1453: Implement color fallback logic for metric handlers
This commit is contained in:
dal 2025-07-18 15:12:12 -06:00 committed by GitHub
commit c42a358c7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 343 additions and 3 deletions

View File

@ -0,0 +1,327 @@
use anyhow::{anyhow, Result};
use database::pool::get_pg_pool;
use database::schema::organizations;
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", "#6B5B95"
];
// Define all default palettes to match TypeScript
const SOFT_THEME: [&str; 10] = [
"#36A2EB", "#4BC0C0", "#9966FF", "#FF9F40", "#C9CBCF",
"#66FF66", "#FF66B2", "#66B2FF", "#FF6384", "#5C6BC0"
];
const RAINBOW_THEME: [&str; 10] = [
"#ff595e", "#ff924c", "#ffca3a", "#c5ca30", "#8ac926",
"#36949d", "#1982c4", "#4267ac", "#565aa0", "#6a4c93"
];
const BOLD_RAINBOW_THEME: [&str; 9] = [
"#ffcd00", "#faff98", "#bbdd00", "#5a8900", "#009c89",
"#e31d04", "#ff7b83", "#ffa6a0", "#320e00"
];
const VIBRANT_RAINBOW_THEME: [&str; 9] = [
"#9b5de5", "#c65ccd", "#f15bb5", "#f8a07b", "#fee440",
"#7fd09d", "#00bbf9", "#00d8e7", "#00f5d4"
];
const CORPORATE_THEME: [&str; 10] = [
"#73b7b8", "#52a1a3", "#76c8b1", "#50b99b", "#dc244b",
"#af1d3c", "#f6cb52", "#f3b816", "#f05a29", "#d23f0f"
];
const EMERALD_SPECTRUM_THEME: [&str; 10] = [
"#75F0A0", "#6FD5EC", "#6B69E8", "#D064E3", "#DD5F87",
"#D7995B", "#A8D157", "#53CA67", "#50C3C3", "#4E60BC"
];
const VIBRANT_JEWEL_TONES_THEME: [&str; 10] = [
"#F07589", "#EB6FEB", "#7F6BE6", "#67B7E0", "#63D99E",
"#86D260", "#CBB85D", "#C25B5B", "#BA5AA8", "#7759B1"
];
const RED_YELLOW_BLUE_THEME: [&str; 10] = [
"#f94144", "#f3722c", "#f8961e", "#f9844a", "#f9c74f",
"#90be6d", "#43aa8b", "#4d908e", "#577590", "#277da1"
];
const PASTEL_RAINBOW_THEME: [&str; 9] = [
"#ffadad", "#ffd6a5", "#fdffb6", "#caffbf", "#9bf6ff",
"#a0c4ff", "#bdb2ff", "#ffc6ff", "#ffd1ff"
];
const DIVERSE_DARK_PALETTE_GREEN_THEME: [&str; 10] = [
"#669900", "#99cc33", "#ccee66", "#006699", "#3399cc",
"#990066", "#cc3399", "#ff6600", "#ff9900", "#ffcc00"
];
const DIVERSE_DARK_PALETTE_BLACK_THEME: [&str; 9] = [
"#303638", "#f0c808", "#5d4b20", "#469374", "#9341b3",
"#e3427d", "#e68653", "#ebe0b0", "#edfbba"
];
const VIBRANT_PASTEL_THEME: [&str; 10] = [
"#F07575", "#ECD76F", "#93E869", "#64E3A3", "#5FB3DD",
"#705BD7", "#D157D1", "#CA5367", "#C39B50", "#86BC4E"
];
const BLUE_TO_ORANGE_GRADIENT: [&str; 10] = [
"#8ecae6", "#73bfdc", "#58b4d1", "#219ebc", "#126782",
"#023047", "#ffb703", "#fd9e02", "#fb8500", "#fb9017"
];
const VIBRANT_RAINBOW: [&str; 10] = [
"#F0DC75", "#98EC6F", "#69E8A8", "#64B8E3", "#745FDD",
"#D75BD7", "#D1576B", "#CAA153", "#8CC350", "#4EBC70"
];
const FOREST_LAKE_GRADIENT: [&str; 10] = [
"#40916c", "#52b788", "#74c69d", "#95d5b2", "#b7e4c7",
"#89c2d9", "#61a5c2", "#468faf", "#2a6f97", "#013a63"
];
const GREENS_THEME: [&str; 10] = [
"#99e2b4", "#88d4ab", "#78c6a3", "#67b99a", "#56ab91",
"#469d89", "#358f80", "#248277", "#14746f", "#036666"
];
const MORE_BLUES_DARK_TO_LIGHT_THEME: [&str; 10] = [
"#03045e", "#023e8a", "#0077b6", "#0096c7", "#00b4d8",
"#48cae4", "#90e0ef", "#ade8f4", "#caf0f8", "#d5f3f9"
];
const PURPLE_THEME: [&str; 10] = [
"#4a148c", "#6a1b9a", "#7b1fa2", "#8e24aa", "#9c27b0",
"#ab47bc", "#ba68c8", "#ce93d8", "#e1bee7", "#f3e5f5"
];
const ORANGE_THEME: [&str; 10] = [
"#e65100", "#ef6c00", "#f57c00", "#fb8c00", "#ff9800",
"#ffa726", "#ffb74d", "#ffcc80", "#ffe0b2", "#fff3e0"
];
const RED_THEME: [&str; 10] = [
"#b71c1c", "#c62828", "#d32f2f", "#e53935", "#f44336",
"#ef5350", "#e57373", "#e35555", "#d13030", "#b71c1c"
];
const TEAL_THEME: [&str; 10] = [
"#004d4d", "#006666", "#008080", "#009999", "#00b3b3",
"#00cccc", "#00b3b3", "#009999", "#008080", "#006666"
];
const BLUE_THEME: [&str; 10] = [
"#0d47a1", "#1565c0", "#1976d2", "#1e88e5", "#2196f3",
"#42a5f5", "#2196f3", "#1976d2", "#1565c0", "#0d47a1"
];
const BROWN_THEME: [&str; 10] = [
"#3e2723", "#4e342e", "#5d4037", "#6d4c41", "#795548",
"#8d6e63", "#795548", "#6d4c41", "#5d4037", "#4e342e"
];
const PINK_THEME: [&str; 10] = [
"#880e4f", "#ad1457", "#c2185b", "#d81b60", "#e91e63",
"#ec407a", "#e91e63", "#d81b60", "#c2185b", "#ad1457"
];
// Map default palette IDs to their color arrays
fn get_default_palette_by_id(palette_id: &str) -> Option<Vec<String>> {
// Remove the __DEFAULT__ prefix and parse the palette name
if !palette_id.starts_with("__DEFAULT__") {
return None;
}
let palette_suffix = &palette_id[11..]; // Skip "__DEFAULT__"
match palette_suffix {
"buster-0" => Some(DEFAULT_COLOR_PALETTE.iter().map(|&s| s.to_string()).collect()),
"rainbow-1" => Some(RAINBOW_THEME.iter().map(|&s| s.to_string()).collect()),
"soft-2" => Some(SOFT_THEME.iter().map(|&s| s.to_string()).collect()),
"red-yellow-blue-3" => Some(RED_YELLOW_BLUE_THEME.iter().map(|&s| s.to_string()).collect()),
"pastel-rainbow-4" => Some(PASTEL_RAINBOW_THEME.iter().map(|&s| s.to_string()).collect()),
"bold-rainbow-5" => Some(BOLD_RAINBOW_THEME.iter().map(|&s| s.to_string()).collect()),
"modern-6" => Some(VIBRANT_RAINBOW_THEME.iter().map(|&s| s.to_string()).collect()),
"corporate-7" => Some(CORPORATE_THEME.iter().map(|&s| s.to_string()).collect()),
"jewel-tones-8" => Some(VIBRANT_JEWEL_TONES_THEME.iter().map(|&s| s.to_string()).collect()),
"soft-pastel-9" => Some(VIBRANT_PASTEL_THEME.iter().map(|&s| s.to_string()).collect()),
"diverse-dark-10" => Some(DIVERSE_DARK_PALETTE_BLACK_THEME.iter().map(|&s| s.to_string()).collect()),
"emerald-spectrum-11" => Some(EMERALD_SPECTRUM_THEME.iter().map(|&s| s.to_string()).collect()),
"deep-forest-12" => Some(DIVERSE_DARK_PALETTE_GREEN_THEME.iter().map(|&s| s.to_string()).collect()),
"vibrant-rainbow-13" => Some(VIBRANT_RAINBOW.iter().map(|&s| s.to_string()).collect()),
// Monochrome themes
"greens-0" => Some(GREENS_THEME.iter().map(|&s| s.to_string()).collect()),
"blue---orange-1" => Some(BLUE_TO_ORANGE_GRADIENT.iter().map(|&s| s.to_string()).collect()),
"forest-sunset-2" => Some(FOREST_LAKE_GRADIENT.iter().map(|&s| s.to_string()).collect()),
"more-blues-3" => Some(MORE_BLUES_DARK_TO_LIGHT_THEME.iter().map(|&s| s.to_string()).collect()),
"purple-4" => Some(PURPLE_THEME.iter().map(|&s| s.to_string()).collect()),
"orange-5" => Some(ORANGE_THEME.iter().map(|&s| s.to_string()).collect()),
"red-6" => Some(RED_THEME.iter().map(|&s| s.to_string()).collect()),
"teal-7" => Some(TEAL_THEME.iter().map(|&s| s.to_string()).collect()),
"brown-8" => Some(BROWN_THEME.iter().map(|&s| s.to_string()).collect()),
"pink-9" => Some(PINK_THEME.iter().map(|&s| s.to_string()).collect()),
"blue-10" => Some(BLUE_THEME.iter().map(|&s| s.to_string()).collect()),
_ => None,
}
}
pub async fn get_organization_color_palette(organization_id: &Uuid) -> Result<Option<Vec<String>>> {
let mut conn = get_pg_pool().get().await?;
// Query the organizations table directly using Diesel query builder
let org_result = organizations::table
.select(organizations::organization_color_palettes)
.filter(organizations::id.eq(organization_id))
.filter(organizations::deleted_at.is_null())
.first::<Value>(&mut conn)
.await;
match org_result {
Ok(color_palettes_value) => {
// Check if there's a selectedId field
if let Some(selected_id) = color_palettes_value.get("selectedId") {
if let Some(selected_id_str) = selected_id.as_str() {
// Check if it's a default palette ID
if selected_id_str.starts_with("__DEFAULT__") {
// Return the corresponding default palette
if let Some(default_palette) = get_default_palette_by_id(selected_id_str) {
return Ok(Some(default_palette));
}
} else {
// It's a custom palette ID, look for it in the palettes array
if let Some(palettes) = color_palettes_value.get("palettes") {
if let Some(palettes_array) = palettes.as_array() {
// Find the palette with matching ID
for palette in palettes_array {
if let Some(id) = palette.get("id") {
if let Some(id_str) = id.as_str() {
if id_str == selected_id_str {
// Found the selected palette
if let Some(colors) = palette.get("colors") {
if let Some(colors_array) = colors.as_array() {
let palette: Vec<String> = colors_array
.iter()
.filter_map(|c| c.as_str().map(|s| s.to_string()))
.collect();
if !palette.is_empty() {
return Ok(Some(palette));
}
}
}
}
}
}
}
}
}
}
}
}
// Legacy fallbacks for older data structures
// Check for selectedDictionaryPalette
if let Some(selected_palette) = color_palettes_value.get("selectedDictionaryPalette") {
if let Some(colors) = selected_palette.get("colors") {
if let Some(colors_array) = colors.as_array() {
let palette: Vec<String> = colors_array
.iter()
.filter_map(|c| c.as_str().map(|s| s.to_string()))
.collect();
if !palette.is_empty() {
return Ok(Some(palette));
}
}
}
}
// Fall back to last_used_color_palette if nothing else is available
if let Some(last_used) = color_palettes_value.get("last_used_color_palette") {
if let Some(colors_array) = last_used.as_array() {
let palette: Vec<String> = 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(_) => {
// Organization not found or no color palettes set
Ok(None)
}
}
}
pub fn get_default_color_palette() -> Vec<String> {
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<Value>,
}

View File

@ -9,6 +9,7 @@ use serde_yaml;
use sharing::asset_access_checks::check_metric_collection_access; use sharing::asset_access_checks::check_metric_collection_access;
use uuid::Uuid; use uuid::Uuid;
use crate::metrics::color_palette_helpers::apply_color_fallback;
use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, BusterShareIndividual, Dataset}; use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, BusterShareIndividual, Dataset};
use crate::utils::workspace::count_workspace_members; use crate::utils::workspace::count_workspace_members;
use database::enums::{AssetPermissionRole, AssetType, IdentityType}; 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 // 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 { let workspace_sharing_enabled_by = if let Some(enabled_by_id) = metric_file.workspace_sharing_enabled_by {
users::table users::table
@ -488,7 +494,7 @@ pub async fn get_metric_for_dashboard_handler(
datasets, datasets,
data_source_id: metric_file.data_source_id, data_source_id: metric_file.data_source_id,
error: None, error: None,
chart_config: Some(resolved_chart_config), chart_config: Some(final_chart_config), // Use chart config with color fallback applied
data_metadata, data_metadata,
status: metric_file.verification, status: metric_file.verification,
evaluation_score, evaluation_score,
@ -517,4 +523,4 @@ pub async fn get_metric_for_dashboard_handler(
// Workspace member count // Workspace member count
workspace_member_count, workspace_member_count,
}) })
} }

View File

@ -7,6 +7,7 @@ use serde_yaml;
use sharing::asset_access_checks::check_metric_collection_access; use sharing::asset_access_checks::check_metric_collection_access;
use uuid::Uuid; use uuid::Uuid;
use crate::metrics::color_palette_helpers::apply_color_fallback;
use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, Dataset}; use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, Dataset};
use crate::utils::workspace::count_workspace_members; use crate::utils::workspace::count_workspace_members;
use database::enums::{AssetPermissionRole, AssetType, IdentityType}; 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"); 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 // Convert the selected content to pretty YAML for the 'file' field
let file = match serde_yaml::to_string(&resolved_content_for_yaml) { let file = match serde_yaml::to_string(&resolved_content_for_yaml) {
Ok(yaml) => 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) 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 data_source_id: metric_file.data_source_id, // Use canonical ID (Uuid) from main record
error: None, // Assume ok 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 data_metadata, // Not versioned
status: metric_file.verification, // Not versioned status: metric_file.verification, // Not versioned
evaluation_score, // Not versioned evaluation_score, // Not versioned

View File

@ -1,4 +1,5 @@
pub mod bulk_update_metrics_handler; pub mod bulk_update_metrics_handler;
pub mod color_palette_helpers;
pub mod delete_metric_handler; pub mod delete_metric_handler;
pub mod get_metric_data_handler; pub mod get_metric_data_handler;
pub mod get_metric_handler; pub mod get_metric_handler;