From 4d44a1766c372f9af70746056db9c1741f9980a9 Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 23 Apr 2025 06:10:54 -0600 Subject: [PATCH] status filtering on metrics, public dashboard functionality restored --- api/libs/database/src/enums.rs | 11 +- .../src/dashboards/get_dashboard_handler.rs | 11 +- .../metrics/bulk_update_metrics_handler.rs | 1 + .../src/metrics/get_metric_data_handler.rs | 182 +++++++++-- .../get_metric_for_dashboard_handler.rs | 295 ++++++++++++++++++ .../src/metrics/list_metrics_handler.rs | 9 +- api/libs/handlers/src/metrics/mod.rs | 2 + .../rest/routes/metrics/list_metrics.rs | 64 ++++ 8 files changed, 537 insertions(+), 38 deletions(-) create mode 100644 api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs diff --git a/api/libs/database/src/enums.rs b/api/libs/database/src/enums.rs index 0bc41308d..9fc01361d 100644 --- a/api/libs/database/src/enums.rs +++ b/api/libs/database/src/enums.rs @@ -417,20 +417,18 @@ impl FromSql for DatasetType { } #[derive( - Serialize, - Deserialize, Debug, - Clone, - Copy, PartialEq, Eq, diesel::AsExpression, diesel::FromSqlRow, + Serialize, + Deserialize, + Clone, + Copy, )] #[diesel(sql_type = sql_types::VerificationEnum)] #[serde(rename_all = "camelCase")] -#[derive(sqlx::Type)] -#[sqlx(type_name = "verification_enum", rename_all = "camelCase")] pub enum Verification { Verified, Backlogged, @@ -473,7 +471,6 @@ impl FromSql for Verification { Copy, PartialEq, Eq, - Hash, diesel::AsExpression, diesel::FromSqlRow, )] diff --git a/api/libs/handlers/src/dashboards/get_dashboard_handler.rs b/api/libs/handlers/src/dashboards/get_dashboard_handler.rs index c038f9c27..e0baea354 100644 --- a/api/libs/handlers/src/dashboards/get_dashboard_handler.rs +++ b/api/libs/handlers/src/dashboards/get_dashboard_handler.rs @@ -12,7 +12,7 @@ use tokio::task::JoinHandle; use uuid::Uuid; use crate::dashboards::types::{BusterShareIndividual, DashboardCollection}; -use crate::metrics::get_metric_handler; +use crate::metrics::{get_metric_for_dashboard_handler, get_metric_handler}; use crate::metrics::{BusterMetric, Dataset, Version}; use database::enums::{AssetPermissionRole, AssetType, IdentityType, Verification}; use database::helpers::dashboard_files::fetch_dashboard_file_with_permission; @@ -249,12 +249,11 @@ pub async fn get_dashboard_handler( // Fetch metrics concurrently using get_metric_handler let mut metric_fetch_handles = Vec::new(); for metric_id in metric_ids { - let user_clone = user.clone(); // Clone user for the spawned task - // Spawn a task for each metric fetch. - // Pass None for version_number and password as the dashboard view uses the latest metric - // and access is primarily determined by dashboard permissions. + // Spawn a task for each metric fetch using the dashboard-specific handler. + // Pass only the metric_id and None for version_number. let handle = tokio::spawn(async move { - get_metric_handler(&metric_id, &user_clone, None, None).await + // Call the new handler, no user or password needed + get_metric_for_dashboard_handler(&metric_id, None).await }); metric_fetch_handles.push((metric_id, handle)); } diff --git a/api/libs/handlers/src/metrics/bulk_update_metrics_handler.rs b/api/libs/handlers/src/metrics/bulk_update_metrics_handler.rs index b85cdbe15..26ec80296 100644 --- a/api/libs/handlers/src/metrics/bulk_update_metrics_handler.rs +++ b/api/libs/handlers/src/metrics/bulk_update_metrics_handler.rs @@ -59,6 +59,7 @@ async fn process_single_update( // Create an update request with just the verification status let request = UpdateMetricRequest { verification: Some(update.verification), + update_version: Some(false), ..UpdateMetricRequest::default() }; diff --git a/api/libs/handlers/src/metrics/get_metric_data_handler.rs b/api/libs/handlers/src/metrics/get_metric_data_handler.rs index 2ac4ca31e..22a054065 100644 --- a/api/libs/handlers/src/metrics/get_metric_data_handler.rs +++ b/api/libs/handlers/src/metrics/get_metric_data_handler.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, Result}; +use chrono::Utc; use database::{ + models::DashboardFile, pool::get_pg_pool, - schema::metric_files, - types::{MetricYml, data_metadata::DataMetadata}, + schema::{dashboard_files, metric_files, metric_files_to_dashboard_files}, + types::{data_metadata::DataMetadata, MetricYml}, }; -use diesel::{ExpressionMethods, QueryDsl}; +use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, OptionalExtension, QueryDsl}; use diesel_async::RunQueryDsl; use indexmap::IndexMap; use middleware::AuthenticatedUser; @@ -14,7 +16,7 @@ use uuid::Uuid; use query_engine::data_source_helpers; use query_engine::data_types::DataType; -use crate::metrics::get_metric_handler; +use crate::metrics::{get_metric_for_dashboard_handler, get_metric_handler, BusterMetric}; /// Request structure for the get_metric_data handler #[derive(Debug, Deserialize)] @@ -44,55 +46,164 @@ pub async fn get_metric_data_handler( user.id ); - // Retrieve the metric definition based on version, if none, use latest. - let metric = get_metric_handler( - &request.metric_id, - &user, - request.version_number, - request.password - ).await?; + // --- Step 1: Try retrieving metric with standard permission checks --- + let metric_result = get_metric_handler( + &request.metric_id, + &user, + request.version_number, + request.password.clone(), // Clone password for potential reuse/logging + ) + .await; + let metric: BusterMetric = match metric_result { + Ok(metric) => { + tracing::debug!("Successfully retrieved metric via standard permissions."); + metric + } + Err(e) => { + // --- Step 2: Handle potential permission error --- + let error_string = e.to_string().to_lowercase(); + let is_permission_error = error_string.contains("permission") + || error_string.contains("expired") + || error_string.contains("password"); + + if is_permission_error { + tracing::warn!( + "Initial metric access failed due to potential permission issue: {}. Checking public dashboard access.", + e + ); + + // --- Step 3: Check if metric belongs to a valid public dashboard --- + let mut conn_check = get_pg_pool().get().await?; + let now = Utc::now(); + + let public_dashboard_exists = match metric_files_to_dashboard_files::table + .inner_join(dashboard_files::table.on( + dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id), + )) + .filter(metric_files_to_dashboard_files::metric_file_id.eq(request.metric_id)) + .filter(dashboard_files::publicly_accessible.eq(true)) + .filter(dashboard_files::deleted_at.is_null()) + .filter( + dashboard_files::public_expiry_date + .is_null() + .or(dashboard_files::public_expiry_date.gt(now)), + ) + .select(dashboard_files::id) // Select any column to check existence + .first::(&mut conn_check) // Try to get the first matching ID + .await + { + Ok(id) => Some(id), + Err(diesel::NotFound) => None, + Err(e) => { + tracing::error!("Error checking if public dashboard exists: {}", e); + return Err(anyhow!("Error checking if public dashboard exists: {}", e)); + } + }; + + if public_dashboard_exists.is_some() { + // --- Step 4: Public dashboard found, fetch metric bypassing permissions --- + tracing::info!("Found associated public dashboard. Fetching metric definition without direct permissions."); + match get_metric_for_dashboard_handler( + &request.metric_id, + request.version_number, + ) + .await + { + Ok(metric_via_dashboard) => { + tracing::debug!( + "Successfully retrieved metric via public dashboard association." + ); + metric_via_dashboard // Use this metric definition + } + Err(fetch_err) => { + // If fetching via dashboard fails unexpectedly, return that error + tracing::error!("Failed to fetch metric via dashboard context even though public dashboard exists: {}", fetch_err); + return Err(fetch_err); + } + } + } else { + // No public dashboard association found, return the original permission error + tracing::warn!("No valid public dashboard association found for metric. Returning original error."); + return Err(e); + } + } else { + // Error was not permission-related, return original error + tracing::error!("Metric retrieval failed for non-permission reason: {}", e); + return Err(e); + } + } + }; + + // --- Step 5: Proceed with data fetching using the obtained metric definition --- + tracing::debug!("Parsing metric definition from YAML to get SQL and dataset IDs."); // Parse the metric definition from YAML to get SQL and dataset IDs - let metric_yml = serde_yaml::from_str::(&metric.file)?; + let metric_yml: MetricYml = match serde_yaml::from_str(&metric.file) { + Ok(yml) => yml, + Err(parse_err) => { + tracing::error!("Failed to parse metric YAML: {}", parse_err); + return Err(anyhow!("Failed to parse metric definition: {}", parse_err)); + } + }; let sql = metric_yml.sql; let dataset_ids = metric_yml.dataset_ids; if dataset_ids.is_empty() { - return Err(anyhow!("No dataset IDs found in metric")); + tracing::error!( + "No dataset IDs found in metric definition for metric {}", + request.metric_id + ); + return Err(anyhow!("No dataset IDs found in metric definition")); } + tracing::debug!("Found dataset IDs: {:?}", dataset_ids); // Get the first dataset ID to use for querying let primary_dataset_id = dataset_ids[0]; // Get the data source ID for the dataset + tracing::debug!("Fetching data sources for datasets: {:?}", dataset_ids); let dataset_sources = data_source_helpers::get_data_sources_for_datasets(&dataset_ids).await?; if dataset_sources.is_empty() { + tracing::error!( + "Could not find data sources for the specified datasets: {:?}", + dataset_ids + ); return Err(anyhow!( "Could not find data sources for the specified datasets" )); } + tracing::debug!("Found data sources: {:?}", dataset_sources); // Find the data source for the primary dataset let data_source = dataset_sources .iter() .find(|ds| ds.dataset_id == primary_dataset_id) - .ok_or_else(|| anyhow!("Primary dataset not found"))?; + .ok_or_else(|| { + tracing::error!( + "Primary dataset ID {} not found among fetched data sources", + primary_dataset_id + ); + anyhow!("Primary dataset ID not found among associated data sources") + })?; tracing::info!( - "Querying data for metric. Dataset: {}, Data source: {}", + "Querying data for metric {}. Dataset: {}, Data source: {}, Limit: {:?}", + request.metric_id, data_source.name, - data_source.data_source_id + data_source.data_source_id, + request.limit ); // Try to get cached metadata first - let mut conn = get_pg_pool().get().await?; + let mut conn_meta = get_pg_pool().get().await?; let cached_metadata = metric_files::table .filter(metric_files::id.eq(request.metric_id)) .select(metric_files::data_metadata) - .first::>(&mut conn) + .first::>(&mut conn_meta) .await - .map_err(|e| anyhow!("Error retrieving metadata: {}", e))?; + .map_err(|e| anyhow!("Error retrieving cached metadata: {}", e))?; + tracing::debug!("Cached metadata found: {}", cached_metadata.is_some()); // Execute the query to get the metric data let query_result = match query_engine::data_source_query_routes::query_engine::query_engine( @@ -102,32 +213,55 @@ pub async fn get_metric_data_handler( ) .await { - Ok(result) => result, + Ok(result) => { + tracing::info!( + "Successfully executed metric query. Rows returned: {}", + result.data.len() + ); + result + } Err(e) => { - tracing::error!("Error executing metric query: {}", e); + tracing::error!( + "Error executing metric query for metric {}: {}", + request.metric_id, + e + ); return Err(anyhow!("Error executing metric query: {}", e)); } }; // Determine which metadata to use - let metadata = if let Some(metadata) = cached_metadata { - // Use cached metadata but update row count if it differs + let final_metadata = if let Some(metadata) = cached_metadata { + tracing::debug!( + "Using cached metadata. Cached rows: {}, Query rows: {}", + metadata.row_count, + query_result.data.len() + ); + // Use cached metadata but update row count if it differs significantly or if cached count is 0 + // (We update if different because the cache might be stale regarding row count) if metadata.row_count != query_result.data.len() as i64 { + tracing::debug!("Row count changed. Updating metadata row count."); let mut updated_metadata = metadata.clone(); updated_metadata.row_count = query_result.data.len() as i64; + // Potentially update updated_at? For now, just row count. updated_metadata } else { metadata } } else { + tracing::debug!("No cached metadata found. Using metadata from query result."); // No cached metadata, use the one from query_result query_result.metadata.clone() }; // Construct and return the response + tracing::info!( + "Successfully retrieved data for metric {}. Returning response.", + request.metric_id + ); Ok(MetricDataResponse { metric_id: request.metric_id, data: query_result.data, - data_metadata: metadata, + data_metadata: final_metadata, }) } diff --git a/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs b/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs new file mode 100644 index 000000000..cc8a46424 --- /dev/null +++ b/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs @@ -0,0 +1,295 @@ +use anyhow::{anyhow, Result}; +use chrono::Utc; +use database::models::MetricFile; +use diesel::prelude::Queryable; +use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use futures::future::join; +use serde_yaml; +use uuid::Uuid; + +use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, Dataset}; +use database::enums::AssetPermissionRole; // Keep for hardcoding permission +use database::pool::get_pg_pool; +use database::schema::{ + collections, collections_to_assets, dashboard_files, datasets, metric_files, + metric_files_to_dashboard_files, +}; + +use super::Version; + +#[derive(Queryable)] +struct DatasetInfo { + id: Uuid, + name: String, + data_source_id: Uuid, +} + +/// Fetch ALL dashboards associated with the given metric id (no user filtering) +async fn fetch_associated_dashboards_unfiltered( + metric_id: Uuid, + conn: &mut AsyncPgConnection, +) -> Result> { + let associated_dashboards = metric_files_to_dashboard_files::table + .inner_join( + dashboard_files::table + .on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)), + ) + .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) + .filter(dashboard_files::deleted_at.is_null()) + .filter(metric_files_to_dashboard_files::deleted_at.is_null()) + // REMOVED: User permission join/filters + .select((dashboard_files::id, dashboard_files::name)) + .load::<(Uuid, String)>(conn) + .await? + .into_iter() + .map(|(id, name)| AssociatedDashboard { id, name }) + .collect(); + Ok(associated_dashboards) +} + +/// Fetch ALL collections associated with the given metric id (no user filtering) +async fn fetch_associated_collections_unfiltered( + metric_id: Uuid, + conn: &mut AsyncPgConnection, +) -> Result> { + let associated_collections = collections_to_assets::table + .inner_join(collections::table.on(collections::id.eq(collections_to_assets::collection_id))) + // REMOVED: User permission join/filters + .filter(collections_to_assets::asset_id.eq(metric_id)) + .filter(collections_to_assets::asset_type.eq(database::enums::AssetType::MetricFile)) // Keep asset type filter + .filter(collections::deleted_at.is_null()) + .filter(collections_to_assets::deleted_at.is_null()) + .select((collections::id, collections::name)) + .load::<(Uuid, String)>(conn) + .await? + .into_iter() + .map(|(id, name)| AssociatedCollection { id, name }) + .collect(); + Ok(associated_collections) +} + +/// Handler to retrieve a metric by ID for display within a dashboard. +/// Assumes authentication/permission checks were done at the dashboard level. +/// Skips all metric-specific permission checks. +pub async fn get_metric_for_dashboard_handler( + metric_id: &Uuid, + version_number: Option, +) -> Result { + let mut conn = get_pg_pool().get().await?; + + // 1. Fetch metric file directly by ID - NO PERMISSION CHECK + let metric_file = metric_files::table + .find(metric_id) + .filter(metric_files::deleted_at.is_null()) + .first::(&mut conn) + .await + .map_err(|e| { + tracing::warn!(metric_id = %metric_id, "Metric file not found or DB error during direct fetch: {}", e); + anyhow!("Metric file not found") // Keep error generic + })?; + + // --- Permission is implicitly CanView because access is via dashboard --- + let permission = AssetPermissionRole::CanView; + + // Declare variables to hold potentially versioned data + let resolved_name: String; + let resolved_description: Option; + let resolved_time_frame: String; + let resolved_dataset_ids: Vec; + let resolved_chart_config: database::types::ChartConfig; + let resolved_sql: String; + let resolved_updated_at: chrono::DateTime; + let resolved_version_num: i32; + let resolved_content_for_yaml: database::types::MetricYml; + + // Data metadata always comes from the main table record (current state) + let data_metadata: Option = metric_file.data_metadata; + + if let Some(requested_version) = version_number { + // --- Specific version requested --- + tracing::debug!(metric_id = %metric_id, version = requested_version, "Attempting to retrieve specific version for dashboard context"); + if let Some(v) = metric_file.version_history.get_version(requested_version) { + match &v.content { + database::types::VersionContent::MetricYml(content) => { + let version_content = (**content).clone(); // Deref the Box and clone + resolved_name = version_content.name.clone(); + resolved_description = version_content.description.clone(); + resolved_time_frame = version_content.time_frame.clone(); + resolved_dataset_ids = version_content.dataset_ids.clone(); + resolved_chart_config = version_content.chart_config.clone(); + resolved_sql = version_content.sql.clone(); + resolved_updated_at = v.updated_at; + resolved_version_num = v.version_number; + resolved_content_for_yaml = version_content; // Use this content for YAML + + tracing::debug!(metric_id = %metric_id, version = requested_version, "Successfully retrieved specific version content for dashboard"); + } + _ => { + tracing::error!(metric_id = %metric_id, version = requested_version, "Invalid content type found for requested version"); + return Err(anyhow!( + "Invalid content type found for version {}", + requested_version + )); + } + } + } else { + tracing::warn!(metric_id = %metric_id, version = requested_version, "Requested version not found in history"); + return Err(anyhow!("Version {} not found", requested_version)); + } + } else { + // --- No specific version requested - use current state from the main table row --- + tracing::debug!(metric_id = %metric_id, "No specific version requested, using current metric file content for dashboard"); + let current_content = metric_file.content.clone(); // Use the content directly from the fetched MetricFile + resolved_name = metric_file.name.clone(); // Use main record name + resolved_description = current_content.description.clone(); + resolved_time_frame = current_content.time_frame.clone(); + resolved_dataset_ids = current_content.dataset_ids.clone(); + resolved_chart_config = current_content.chart_config.clone(); + resolved_sql = current_content.sql.clone(); + resolved_updated_at = metric_file.updated_at; // Use main record updated_at + resolved_version_num = metric_file.version_history.get_version_number(); + resolved_content_for_yaml = current_content; // Use this content for YAML + + tracing::debug!(metric_id = %metric_id, latest_version = resolved_version_num, "Determined latest version number for dashboard"); + } + + // 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, + Err(e) => { + tracing::error!(metric_id = %metric_id, error = %e, "Failed to serialize selected metric content to YAML for dashboard"); + return Err(anyhow!("Failed to convert metric content to YAML: {}", e)); + } + }; + + // Map evaluation score - this is not versioned + let evaluation_score = metric_file.evaluation_score.map(|score| { + if score >= 0.8 { + "High".to_string() + } else if score >= 0.5 { + "Moderate".to_string() + } else { + "Low".to_string() + } + }); + + // Get dataset information for the resolved dataset IDs + let mut datasets = Vec::new(); + let mut first_data_source_id = None; + if !resolved_dataset_ids.is_empty() { + // Fetch only if there are IDs to prevent unnecessary query + let dataset_infos = datasets::table + .filter(datasets::id.eq_any(&resolved_dataset_ids)) + .filter(datasets::deleted_at.is_null()) + .select((datasets::id, datasets::name, datasets::data_source_id)) + .load::(&mut conn) + .await + .map_err(|e| { + tracing::error!("Failed to fetch dataset info for metric {}: {}", metric_id, e); + anyhow!("Failed to fetch dataset info") + })?; + + for dataset_info in dataset_infos { + datasets.push(Dataset { + id: dataset_info.id.to_string(), + name: dataset_info.name, + }); + if first_data_source_id.is_none() { + first_data_source_id = Some(dataset_info.data_source_id); + } + } + } + + + let mut versions: Vec = metric_file + .version_history + .0 + .values() + .map(|v| Version { + version_number: v.version_number, + updated_at: v.updated_at, + }) + .collect(); + + // Sort versions by version_number in ascending order + versions.sort_by(|a, b| a.version_number.cmp(&b.version_number)); + + // Concurrently fetch associated dashboards and collections (unfiltered versions) + let metrics_id_clone = *metric_id; + + // Await both futures concurrently - NOTE: Need to handle connection borrowing carefully. + // It's safer to get the connection again or pass it differently if needed. + // For now, let's assume the initial `conn` can be reused or handle potential errors. + // A better approach might involve passing the pool and getting connections inside helpers. + // Re-getting connection for safety: + let mut conn_dash = get_pg_pool().get().await?; + let mut conn_coll = get_pg_pool().get().await?; + let dashboards_future = fetch_associated_dashboards_unfiltered(metrics_id_clone, &mut conn_dash); + let collections_future = fetch_associated_collections_unfiltered(metrics_id_clone, &mut conn_coll); + + let (dashboards_result, collections_result) = join(dashboards_future, collections_future).await; + + + // Handle results, logging errors but returning empty Vecs for failed tasks + let dashboards = match dashboards_result { + Ok(dashboards) => dashboards, + Err(e) => { + tracing::error!( + "Failed to fetch associated dashboards (unfiltered) for metric {}: {}", + metric_id, + e + ); + vec![] + } + }; + + let collections = match collections_result { + Ok(collections) => collections, + Err(e) => { + tracing::error!( + "Failed to fetch associated collections (unfiltered) for metric {}: {}", + metric_id, + e + ); + vec![] + } + }; + + // Construct BusterMetric using resolved values + Ok(BusterMetric { + id: metric_file.id, + metric_type: "metric".to_string(), + name: resolved_name, + version_number: resolved_version_num, + description: resolved_description, + file_name: metric_file.file_name, + time_frame: resolved_time_frame, + datasets, + data_source_id: first_data_source_id.map_or("".to_string(), |id| id.to_string()), + error: None, // Assume ok + chart_config: Some(resolved_chart_config), + data_metadata, + status: metric_file.verification, + evaluation_score, + evaluation_summary: metric_file.evaluation_summary.unwrap_or_default(), + file, // YAML based on resolved content + created_at: metric_file.created_at, + updated_at: resolved_updated_at, + sent_by_id: metric_file.created_by, + sent_by_name: "".to_string(), // Placeholder - user info not needed/fetched + sent_by_avatar_url: None, // Placeholder - user info not needed/fetched + code: None, // Placeholder + dashboards, // Unfiltered associations + collections, // Unfiltered associations + versions, // Full version history list + permission, // Hardcoded to CanView + sql: resolved_sql, + // Sharing fields are irrelevant/defaulted in this context + individual_permissions: None, + publicly_accessible: false, // Default value + public_expiry_date: None, // Default value + public_enabled_by: None, // Default value + public_password: None, // Default value + }) +} \ No newline at end of file diff --git a/api/libs/handlers/src/metrics/list_metrics_handler.rs b/api/libs/handlers/src/metrics/list_metrics_handler.rs index 704f66026..d28cb1d28 100644 --- a/api/libs/handlers/src/metrics/list_metrics_handler.rs +++ b/api/libs/handlers/src/metrics/list_metrics_handler.rs @@ -17,6 +17,7 @@ pub struct MetricsListRequest { pub page_size: i64, pub shared_with_me: Option, pub only_my_metrics: Option, + pub verification: Option>, } #[derive(Debug, Serialize, Deserialize)] @@ -73,13 +74,19 @@ pub async fn list_metrics_handler( ), )) .filter(metric_files::deleted_at.is_null()) - .distinct() .order((metric_files::updated_at.desc(), metric_files::id.asc())) .offset(offset) .limit(request.page_size) .into_boxed(); // Add filters based on request parameters + if let Some(verification_statuses) = request.verification { + // Only apply filter if the vec is not empty + if !verification_statuses.is_empty() { + metric_statement = metric_statement.filter(metric_files::verification.eq_any(verification_statuses)); + } + } + if let Some(true) = request.only_my_metrics { // Show only metrics created by the user metric_statement = metric_statement.filter(metric_files::created_by.eq(&user.id)); diff --git a/api/libs/handlers/src/metrics/mod.rs b/api/libs/handlers/src/metrics/mod.rs index ca8ca3761..e2dcddb3c 100644 --- a/api/libs/handlers/src/metrics/mod.rs +++ b/api/libs/handlers/src/metrics/mod.rs @@ -6,6 +6,7 @@ pub mod list_metrics_handler; pub mod sharing; pub mod types; pub mod update_metric_handler; +pub mod get_metric_for_dashboard_handler; // Re-export specific items from handlers pub use bulk_update_metrics_handler::*; @@ -13,6 +14,7 @@ pub use delete_metric_handler::*; pub use get_metric_handler::*; pub use list_metrics_handler::*; pub use update_metric_handler::*; +pub use get_metric_for_dashboard_handler::get_metric_for_dashboard_handler; // For get_metric_data_handler, only export the handler functions and request types // but not the types that conflict with types.rs diff --git a/api/server/src/routes/rest/routes/metrics/list_metrics.rs b/api/server/src/routes/rest/routes/metrics/list_metrics.rs index f52f28373..f60ca703d 100644 --- a/api/server/src/routes/rest/routes/metrics/list_metrics.rs +++ b/api/server/src/routes/rest/routes/metrics/list_metrics.rs @@ -5,6 +5,67 @@ use axum::Extension; use handlers::metrics::{list_metrics_handler, MetricsListRequest, BusterMetricListItem}; use middleware::AuthenticatedUser; use serde::Deserialize; +use database::enums::Verification; +use serde::de::{self, Deserializer, SeqAccess, Visitor}; +use std::fmt; + +// Helper function to deserialize Option or Option> into Option> +fn deserialize_optional_vec_or_single<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + struct OptionVecOrSingleVisitor; + + impl<'de> Visitor<'de> for OptionVecOrSingleVisitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a verification status string, a sequence of status strings, or null") + } + + // Handle a single string value + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + // Deserialize the single string into Verification + let verification = Verification::deserialize(de::value::StringDeserializer::new(value.to_string()))?; + Ok(Some(vec![verification])) + } + + // Handle a sequence of values + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + // Deserialize the sequence into Vec + let vec = Vec::::deserialize(de::value::SeqAccessDeserializer::new(seq))?; + if vec.is_empty() { + Ok(None) + } else { + Ok(Some(vec)) + } + } + + // Handle null or missing value + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + // Handle optional value + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(self) + } + } + + deserializer.deserialize_any(OptionVecOrSingleVisitor) +} #[derive(Deserialize)] pub struct ListMetricsQuery { @@ -12,6 +73,8 @@ pub struct ListMetricsQuery { page_size: Option, shared_with_me: Option, only_my_metrics: Option, + #[serde(rename = "status[]", deserialize_with = "deserialize_optional_vec_or_single", default)] + verification: Option>, } pub async fn list_metrics_rest_handler( @@ -23,6 +86,7 @@ pub async fn list_metrics_rest_handler( page_size: query.page_size.unwrap_or(25), shared_with_me: query.shared_with_me, only_my_metrics: query.only_my_metrics, + verification: query.verification, }; let metrics = match list_metrics_handler(&user, request).await {