get dashboards still need to tweak the metrics dashboard rel

This commit is contained in:
dal 2025-02-18 10:22:27 -07:00
parent 7ab35b7f22
commit 561c31965e
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
15 changed files with 336 additions and 48 deletions

View File

@ -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"

View File

@ -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" }

View File

@ -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<String>,
organization_id: Uuid,
created_by: Uuid,
created_at: chrono::DateTime<chrono::Utc>,
updated_at: chrono::DateTime<chrono::Utc>,
}
pub async fn get_dashboard(dashboard_id: &Uuid, user_id: &Uuid) -> Result<BusterDashboardResponse> {
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::<QueryableDashboardFile>(&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<Uuid> = 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<DashboardConfig> {
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::<Result<Vec<_>>>()?;
Ok(DashboardRow {
items,
row_height: None,
column_sizes: None,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(DashboardConfig { rows })
}

View File

@ -0,0 +1,3 @@
pub mod get_dashboard;
pub use get_dashboard::*;

View File

@ -0,0 +1,5 @@
mod types;
mod helpers;
pub use types::*;
pub use helpers::*;

View File

@ -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<DashboardMember>,
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<String>,
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<BusterMetric>,
pub dashboard: BusterDashboard,
pub permission: AssetPermissionRole,
pub public_password: Option<String>,
pub collections: Vec<Collection>,
}
// 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<String>,
pub description: Option<String>,
pub id: String,
pub name: String,
pub updated_at: Option<String>,
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<DashboardRow>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DashboardRow {
pub items: Vec<DashboardRowItem>,
pub row_height: Option<u32>,
pub column_sizes: Option<Vec<u32>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DashboardRowItem {
pub id: String,
}

View File

@ -58,53 +58,37 @@ pub async fn get_metric(metric_id: &Uuid, user_id: &Uuid) -> Result<BusterMetric
_ => 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::<Vec<&str>>()
.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<BusterMetric
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
min_value: MinMaxValue::Number(0.0), // You might want to parse this from the YAML
max_value: MinMaxValue::Number(0.0), // You might want to parse this from the YAML
unique_values: col
.get("unique_values")
.and_then(Value::as_i64)
.unwrap_or(0) as i32,
simple_type: SimpleType::Number, // You might want to parse this from the YAML
column_type: ColumnType::Number, // You might want to parse this from the YAML
min_value: MinMaxValue::Number(0.0), // Default value
max_value: MinMaxValue::Number(0.0), // Default value
unique_values: 0, // Default value
simple_type: match col.get("data_type").and_then(Value::as_str) {
Some("string") => 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<BusterMetric
version_number: 1,
description,
file_name: metric_file.file_name,
time_frame: "all".to_string(), // Default value
dataset_id: "".to_string(), // Would need to be populated if required
data_source_id: "".to_string(), // Would need to be populated if required
time_frame: "TODO".to_string(),
dataset_id: "TODO".to_string(),
data_source_id: "TODO".to_string(),
dataset_name: None,
error: None,
chart_config: Some(chart_config),
@ -151,11 +141,11 @@ pub async fn get_metric(metric_id: &Uuid, user_id: &Uuid) -> Result<BusterMetric
status: metric_file.verification,
evaluation_score: metric_file.evaluation_score.map(|score| score.to_string()),
evaluation_summary: metric_file.evaluation_summary.unwrap_or_default(),
file: yaml_content, // Store the original YAML content
file: serde_json::to_string(&content).unwrap_or_default(),
created_at: metric_file.created_at.to_string(),
updated_at: metric_file.updated_at.to_string(),
sent_by_id: metric_file.created_by.to_string(),
sent_by_name: "".to_string(), // Would need to join with users table to get this
sent_by_name: "".to_string(),
sent_by_avatar_url: None,
code: None,
dashboards: vec![],

View File

@ -94,6 +94,8 @@ pub enum SimpleType {
Number,
#[serde(rename = "date")]
Date,
#[serde(rename = "boolean")]
Boolean,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View File

@ -1,4 +1,5 @@
pub mod metric_files;
pub mod dashboard_files;
pub use metric_files::*;
pub use metric_files::*;
pub use dashboard_files::*;

View File

@ -6,6 +6,7 @@ use diesel_async::RunQueryDsl;
use serde_json::Value;
use tokio;
use uuid::Uuid;
use serde_yaml;
use crate::messages::types::ThreadMessage;
use crate::threads::types::ThreadWithMessages;

View File

@ -0,0 +1,23 @@
use crate::database_dep::models::User;
use crate::routes::rest::ApiResponse;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::Extension;
use handlers::files::dashboard_files::get_dashboard::get_dashboard;
use handlers::files::dashboard_files::BusterDashboardResponse;
use uuid::Uuid;
pub async fn get_dashboard_rest_handler(
Extension(user): Extension<User>,
Path(id): Path<Uuid>,
) -> Result<ApiResponse<BusterDashboardResponse>, (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))
}

View File

@ -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))
}

View File

@ -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<User>,
Path(id): Path<Uuid>,
) -> Result<ApiResponse<BusterMetric>, (StatusCode, &'static str)> {

View File

@ -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))
}

View File

@ -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)),
)
}