mirror of https://github.com/buster-so/buster.git
get dashboards still need to tweak the metrics dashboard rel
This commit is contained in:
parent
7ab35b7f22
commit
561c31965e
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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 })
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod get_dashboard;
|
||||
|
||||
pub use get_dashboard::*;
|
|
@ -0,0 +1,5 @@
|
|||
mod types;
|
||||
mod helpers;
|
||||
|
||||
pub use types::*;
|
||||
pub use helpers::*;
|
|
@ -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,
|
||||
}
|
|
@ -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![],
|
||||
|
|
|
@ -94,6 +94,8 @@ pub enum SimpleType {
|
|||
Number,
|
||||
#[serde(rename = "date")]
|
||||
Date,
|
||||
#[serde(rename = "boolean")]
|
||||
Boolean,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod metric_files;
|
||||
pub mod dashboard_files;
|
||||
|
||||
pub use metric_files::*;
|
||||
pub use metric_files::*;
|
||||
pub use dashboard_files::*;
|
|
@ -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;
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)> {
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue