From 561c31965eb790afcbb6bc13c20e1c58467d1522 Mon Sep 17 00:00:00 2001 From: dal Date: Tue, 18 Feb 2025 10:22:27 -0700 Subject: [PATCH] get dashboards still need to tweak the metrics dashboard rel --- api/Cargo.toml | 1 + api/libs/handlers/Cargo.toml | 1 + .../dashboard_files/helpers/get_dashboard.rs | 171 ++++++++++++++++++ .../src/files/dashboard_files/helpers/mod.rs | 3 + .../handlers/src/files/dashboard_files/mod.rs | 5 + .../src/files/dashboard_files/types.rs | 76 ++++++++ .../files/metric_files/helpers/get_metric.rs | 78 ++++---- .../handlers/src/files/metric_files/types.rs | 2 + api/libs/handlers/src/files/mod.rs | 3 +- .../src/threads/helpers/get_thread.rs | 1 + .../rest/routes/dashboards/get_dashboard.rs | 23 +++ api/src/routes/rest/routes/dashboards/mod.rs | 12 ++ .../metrics/{get_metrics.rs => get_metric.rs} | 2 +- api/src/routes/rest/routes/metrics/mod.rs | 4 +- api/src/routes/rest/routes/mod.rs | 2 + 15 files changed, 336 insertions(+), 48 deletions(-) create mode 100644 api/libs/handlers/src/files/dashboard_files/helpers/get_dashboard.rs create mode 100644 api/libs/handlers/src/files/dashboard_files/helpers/mod.rs create mode 100644 api/libs/handlers/src/files/dashboard_files/types.rs create mode 100644 api/src/routes/rest/routes/dashboards/get_dashboard.rs create mode 100644 api/src/routes/rest/routes/dashboards/mod.rs rename api/src/routes/rest/routes/metrics/{get_metrics.rs => get_metric.rs} (94%) diff --git a/api/Cargo.toml b/api/Cargo.toml index 574de4739..417a2dd98 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -18,6 +18,7 @@ tracing = "0.1.40" uuid = { version = "1.8", features = ["serde", "v4"] } diesel = { version = "2", features = ["uuid", "chrono", "serde_json", "postgres"] } diesel-async = { version = "0.5.2", features = ["postgres", "bb8"] } +futures = "0.3.30" [package] name = "bi_api" diff --git a/api/libs/handlers/Cargo.toml b/api/libs/handlers/Cargo.toml index 544f78196..f4e08a3c7 100644 --- a/api/libs/handlers/Cargo.toml +++ b/api/libs/handlers/Cargo.toml @@ -15,6 +15,7 @@ tracing = { workspace = true } uuid = { workspace = true } diesel = { workspace = true } diesel-async = { workspace = true } +futures = { workspace = true } # Local dependencies database = { path = "../database" } diff --git a/api/libs/handlers/src/files/dashboard_files/helpers/get_dashboard.rs b/api/libs/handlers/src/files/dashboard_files/helpers/get_dashboard.rs new file mode 100644 index 000000000..069121f8e --- /dev/null +++ b/api/libs/handlers/src/files/dashboard_files/helpers/get_dashboard.rs @@ -0,0 +1,171 @@ +use anyhow::{anyhow, Result}; +use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable}; +use diesel_async::RunQueryDsl; +use serde_json::{json, Value}; +use uuid::Uuid; +use futures::future::{try_join_all, join_all}; +use chrono::{DateTime, Utc}; +use serde_yaml; + +use crate::files::dashboard_files::types::{ + BusterDashboard, BusterDashboardResponse, DashboardConfig, DashboardRow, DashboardRowItem, +}; +use crate::files::metric_files::helpers::get_metric; +use database::enums::{AssetPermissionRole, Verification}; +use database::pool::get_pg_pool; +use database::schema::dashboard_files; + +#[derive(Queryable, Selectable)] +#[diesel(table_name = dashboard_files)] +struct QueryableDashboardFile { + id: Uuid, + name: String, + file_name: String, + content: Value, + filter: Option, + organization_id: Uuid, + created_by: Uuid, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, +} + +pub async fn get_dashboard(dashboard_id: &Uuid, user_id: &Uuid) -> Result { + let mut conn = match get_pg_pool().get().await { + Ok(conn) => conn, + Err(e) => return Err(anyhow!("Failed to get database connection: {}", e)), + }; + + // Query the dashboard file + let dashboard_file = dashboard_files::table + .filter(dashboard_files::id.eq(dashboard_id)) + .filter(dashboard_files::deleted_at.is_null()) + .select(( + dashboard_files::id, + dashboard_files::name, + dashboard_files::file_name, + dashboard_files::content, + dashboard_files::filter, + dashboard_files::organization_id, + dashboard_files::created_by, + dashboard_files::created_at, + dashboard_files::updated_at, + )) + .first::(&mut conn) + .await + .map_err(|e| match e { + diesel::result::Error::NotFound => anyhow!("Dashboard file not found"), + _ => anyhow!("Database error: {}", e), + })?; + + // Parse the content to get metric IDs and other dashboard info + let content = dashboard_file.content.clone(); + let config = parse_dashboard_config(&content)?; + + // Get updated_at from content if available, otherwise use the database value + let updated_at = content + .get("updated_at") + .and_then(Value::as_str) + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or(dashboard_file.updated_at); + + // Get name from content if available, otherwise use the database value + let name = content + .get("name") + .and_then(Value::as_str) + .unwrap_or(&dashboard_file.name) + .to_string(); + + // Collect all metric IDs from the rows + let metric_ids: Vec = config + .rows + .iter() + .flat_map(|row| { + row.items.iter().filter_map(|item| { + Uuid::parse_str(&item.id).ok() + }) + }) + .collect(); + + // Fetch all metrics concurrently + let metric_futures: Vec<_> = metric_ids + .iter() + .map(|metric_id| get_metric(metric_id, user_id)) + .collect(); + + let metric_results = join_all(metric_futures).await; + let metrics: Vec<_> = metric_results + .into_iter() + .filter_map(|result| result.ok()) + .collect(); + + // Construct the dashboard using content values where available + let dashboard = BusterDashboard { + config, + created_at: dashboard_file.created_at.to_string(), + created_by: dashboard_file.created_by.to_string(), + deleted_at: None, + description: content.get("description").and_then(|v| v.as_str().map(String::from)), + id: content + .get("id") + .and_then(Value::as_str) + .unwrap_or(&dashboard_file.id.to_string()) + .to_string(), + name, + updated_at: Some(updated_at.to_string()), + updated_by: dashboard_file.created_by.to_string(), + status: Verification::Verified, + version_number: content + .get("version_number") + .and_then(Value::as_i64) + .unwrap_or(1) as i32, + file: serde_yaml::to_string(&dashboard_file.content)?, + file_name: dashboard_file.file_name, + }; + + Ok(BusterDashboardResponse { + access: AssetPermissionRole::Owner, + metrics, + dashboard, + permission: AssetPermissionRole::Owner, + public_password: None, + collections: vec![], + }) +} + +fn parse_dashboard_config(content: &Value) -> Result { + let rows = content + .get("rows") + .ok_or_else(|| anyhow!("Missing rows in dashboard content"))? + .as_array() + .ok_or_else(|| anyhow!("Rows is not an array"))? + .iter() + .map(|row| { + let items = row + .get("items") + .ok_or_else(|| anyhow!("Missing items in row"))? + .as_array() + .ok_or_else(|| anyhow!("Items is not an array"))? + .iter() + .map(|item| { + Ok(DashboardRowItem { + id: item + .get("id") + .ok_or_else(|| anyhow!("Missing id in item"))? + .as_str() + .ok_or_else(|| anyhow!("Id is not a string"))? + .to_string(), + }) + }) + .collect::>>()?; + + Ok(DashboardRow { + items, + row_height: None, + column_sizes: None, + }) + }) + .collect::>>()?; + + Ok(DashboardConfig { rows }) +} diff --git a/api/libs/handlers/src/files/dashboard_files/helpers/mod.rs b/api/libs/handlers/src/files/dashboard_files/helpers/mod.rs new file mode 100644 index 000000000..64efb6531 --- /dev/null +++ b/api/libs/handlers/src/files/dashboard_files/helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod get_dashboard; + +pub use get_dashboard::*; \ No newline at end of file diff --git a/api/libs/handlers/src/files/dashboard_files/mod.rs b/api/libs/handlers/src/files/dashboard_files/mod.rs index e69de29bb..d288280e1 100644 --- a/api/libs/handlers/src/files/dashboard_files/mod.rs +++ b/api/libs/handlers/src/files/dashboard_files/mod.rs @@ -0,0 +1,5 @@ +mod types; +mod helpers; + +pub use types::*; +pub use helpers::*; \ No newline at end of file diff --git a/api/libs/handlers/src/files/dashboard_files/types.rs b/api/libs/handlers/src/files/dashboard_files/types.rs new file mode 100644 index 000000000..8af52f574 --- /dev/null +++ b/api/libs/handlers/src/files/dashboard_files/types.rs @@ -0,0 +1,76 @@ +use database::enums::{AssetPermissionRole, Verification}; +use serde::{Deserialize, Serialize}; + +use crate::files::BusterMetric; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BusterDashboardListItem { + pub created_at: String, + pub id: String, + pub last_edited: String, + pub members: Vec, + pub name: String, + pub owner: DashboardMember, + pub status: Verification, + pub is_shared: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DashboardMember { + pub avatar_url: Option, + pub id: String, + pub name: String, +} + +// Note: This extends BusterShare which needs to be defined +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BusterDashboardResponse { + pub access: AssetPermissionRole, + pub metrics: Vec, + pub dashboard: BusterDashboard, + pub permission: AssetPermissionRole, + pub public_password: Option, + pub collections: Vec, +} + +// Note: This extends BusterShare but omits certain fields +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BusterDashboard { + pub config: DashboardConfig, + pub created_at: String, + pub created_by: String, + pub deleted_at: Option, + pub description: Option, + pub id: String, + pub name: String, + pub updated_at: Option, + pub updated_by: String, + pub status: Verification, + pub version_number: i32, + pub file: String, // yaml file + pub file_name: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Collection { + pub id: String, + pub name: String, +} + +// Note: This is a placeholder for DashboardConfig which needs to be defined based on your specific needs +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DashboardConfig { + pub rows: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DashboardRow { + pub items: Vec, + pub row_height: Option, + pub column_sizes: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DashboardRowItem { + pub id: String, +} diff --git a/api/libs/handlers/src/files/metric_files/helpers/get_metric.rs b/api/libs/handlers/src/files/metric_files/helpers/get_metric.rs index d0313c836..11979fe6b 100644 --- a/api/libs/handlers/src/files/metric_files/helpers/get_metric.rs +++ b/api/libs/handlers/src/files/metric_files/helpers/get_metric.rs @@ -58,53 +58,37 @@ pub async fn get_metric(metric_id: &Uuid, user_id: &Uuid) -> Result anyhow!("Database error: {}", e), })?; - // Join the content lines into a single YAML string - let yaml_content = metric_file - .content - .as_array() - .ok_or_else(|| anyhow!("Invalid content format"))? - .iter() - .filter_map(|line| line.get("text").and_then(Value::as_str)) - .collect::>() - .join("\n"); - - // Parse the YAML content into a Value - let yaml_value: Value = serde_yaml::from_str(&yaml_content) - .map_err(|e| anyhow!("Failed to parse YAML content: {}", e))?; - - // Extract fields from the YAML - let title = yaml_value + // Extract fields directly from the JSON content + let content = &metric_file.content; + + let title = content .get("title") .and_then(Value::as_str) .unwrap_or("Untitled") .to_string(); - let description = yaml_value + let description = content .get("description") .and_then(|v| match v { Value::Null => None, v => v.as_str().map(String::from), }); - let sql = yaml_value + let sql = content .get("sql") .and_then(Value::as_str) .unwrap_or_default() .to_string(); - // Parse chart config - let chart_config = yaml_value.get("chart_config").cloned().unwrap_or(json!({})); + // Get chart config directly + let chart_config = content.get("chart_config").cloned().unwrap_or(json!({})); // Parse data metadata if it exists - let data_metadata = yaml_value.get("data_metadata").map(|metadata| { + let data_metadata = content.get("data_metadata").map(|metadata| { DataMetadata { - column_count: metadata - .get("column_count") - .and_then(Value::as_i64) - .unwrap_or(1) as i32, + column_count: metadata.as_array().map(|arr| arr.len() as i32).unwrap_or(1), column_metadata: metadata - .get("column_metadata") - .and_then(Value::as_array) + .as_array() .map(|columns| { columns .iter() @@ -114,22 +98,28 @@ pub async fn get_metric(metric_id: &Uuid, user_id: &Uuid) -> Result SimpleType::Text, + Some("number") => SimpleType::Number, + Some("boolean") => SimpleType::Boolean, + Some("date") => SimpleType::Date, + _ => SimpleType::Number, + }, + column_type: match col.get("data_type").and_then(Value::as_str) { + Some("string") => ColumnType::Text, + Some("number") => ColumnType::Number, + Some("boolean") => ColumnType::Boolean, + Some("date") => ColumnType::Date, + _ => ColumnType::Number, + }, }) .collect() }) .unwrap_or_default(), - row_count: metadata - .get("row_count") - .and_then(Value::as_i64) - .unwrap_or(1) as i32, + row_count: 1, // Default value since it's not in your JSON structure } }); @@ -141,9 +131,9 @@ pub async fn get_metric(metric_id: &Uuid, user_id: &Uuid) -> Result Result, + Path(id): Path, +) -> Result, (StatusCode, &'static str)> { + let dashboard = match get_dashboard(&id, &user.id).await { + Ok(response) => response, + Err(e) => { + tracing::error!("Error getting dashboard: {}", e); + return Err((StatusCode::INTERNAL_SERVER_ERROR, "Failed to get dashboard")); + } + }; + + Ok(ApiResponse::JsonData(dashboard)) +} diff --git a/api/src/routes/rest/routes/dashboards/mod.rs b/api/src/routes/rest/routes/dashboards/mod.rs new file mode 100644 index 000000000..7833b1764 --- /dev/null +++ b/api/src/routes/rest/routes/dashboards/mod.rs @@ -0,0 +1,12 @@ +use axum::{ + routing::get, + Router, +}; + +// Placeholder modules that you'll need to create +mod get_dashboard; + +pub fn router() -> Router { + Router::new() + .route("/:id", get(get_dashboard::get_dashboard_rest_handler)) +} diff --git a/api/src/routes/rest/routes/metrics/get_metrics.rs b/api/src/routes/rest/routes/metrics/get_metric.rs similarity index 94% rename from api/src/routes/rest/routes/metrics/get_metrics.rs rename to api/src/routes/rest/routes/metrics/get_metric.rs index 4b8cd2f54..06a3af2fb 100644 --- a/api/src/routes/rest/routes/metrics/get_metrics.rs +++ b/api/src/routes/rest/routes/metrics/get_metric.rs @@ -7,7 +7,7 @@ use handlers::files::metric_files::types::BusterMetric; use handlers::files::metric_files::helpers::get_metric::get_metric; use uuid::Uuid; -pub async fn get_metrics_rest_handler( +pub async fn get_metric_rest_handler( Extension(user): Extension, Path(id): Path, ) -> Result, (StatusCode, &'static str)> { diff --git a/api/src/routes/rest/routes/metrics/mod.rs b/api/src/routes/rest/routes/metrics/mod.rs index 0ae5c411c..087e4b118 100644 --- a/api/src/routes/rest/routes/metrics/mod.rs +++ b/api/src/routes/rest/routes/metrics/mod.rs @@ -4,9 +4,9 @@ use axum::{ }; // Placeholder modules that you'll need to create -mod get_metrics; +mod get_metric; pub fn router() -> Router { Router::new() - .route("/:id", get(get_metrics::get_metrics_rest_handler)) + .route("/:id", get(get_metric::get_metric_rest_handler)) } diff --git a/api/src/routes/rest/routes/mod.rs b/api/src/routes/rest/routes/mod.rs index f955a7611..b573c4a96 100644 --- a/api/src/routes/rest/routes/mod.rs +++ b/api/src/routes/rest/routes/mod.rs @@ -9,6 +9,7 @@ mod organizations; mod permission_groups; mod sql; mod users; +mod dashboards; use axum::{middleware, Router}; @@ -29,6 +30,7 @@ pub fn router() -> Router { .nest("/organizations", organizations::router()) .nest("/chats", chats::router()) .nest("/metrics", metrics::router()) + .nest("/dashboards", dashboards::router()) .route_layer(middleware::from_fn(auth)), ) }