--- title: Update Dashboard REST Endpoint author: Cascade date: 2025-03-19 status: Draft --- # Update Dashboard REST Endpoint ## Problem Statement Currently, our application lacks a REST API endpoint for updating dashboards. Users need to be able to programmatically update existing dashboards through the API to support automation and integration scenarios. ## Proposed Solution Implement a PUT /dashboards/:id endpoint that updates an existing dashboard. The endpoint will accept a `DashboardUpdateRequest` object that can update the dashboard's name, description, configuration, status, metrics, and file content. If the file content is provided, it will override all other parameters. ## Technical Design ### REST Endpoint ``` PUT /dashboards/:id ``` #### Request ```typescript { /** The unique identifier of the dashboard */ id: string; /** New name for the dashboard */ name?: string; /** New description for the dashboard */ description?: string | null; /** Updated dashboard configuration */ config?: DashboardConfig; /** Updated verification status */ status?: VerificationStatus; metrics?: string[]; /** The file content of the dashboard */ file?: string; /** Sharing properties */ public?: boolean; publicExpiryDate?: string; publicPassword?: string; } ``` #### Response ```json { "dashboard": { "id": "uuid", "name": "Updated Dashboard Name", "description": "Updated description", "config": { "rows": [ { "items": [ { "id": "metric_uuid" } ], "row_height": 300, "column_sizes": [12] } ] }, "created_at": "timestamp", "created_by": "user_uuid", "updated_at": "timestamp", "updated_by": "user_uuid", "status": "Verified", "version_number": 2, "file": "yaml_content", "file_name": "dashboard_filename.yml" }, "access": "Owner", "permission": "Owner", "metrics": {}, "public_password": null, "collections": [] } ``` ### Handler Implementation #### REST Handler Create a new file at `src/routes/rest/routes/dashboards/update_dashboard.rs`: ```rust use axum::{ extract::{Path, State}, Json, }; use handlers::dashboards::update_dashboard_handler; use handlers::types::User; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::routes::rest::ApiResponse; use crate::AppState; use database::enums::Verification; use handlers::dashboards::{DashboardConfig, DashboardUpdateRequest}; pub async fn update_dashboard_rest_handler( State(state): State, Path(id): Path, user: User, Json(request): Json, ) -> ApiResponse { match update_dashboard_handler(id, request, &user.id).await { Ok(response) => ApiResponse::success(response), Err(e) => { tracing::error!("Failed to update dashboard: {}", e); ApiResponse::error(e) } } } ``` #### Business Logic Handler Create a new file at `libs/handlers/src/dashboards/update_dashboard_handler.rs`: ```rust use anyhow::{anyhow, Result}; use chrono::Utc; use database::pool::get_pg_pool; use database::schema::dashboard_files; use database::types::dashboard_yml::DashboardYml; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use serde_yaml; use uuid::Uuid; use super::{ get_dashboard_handler, BusterDashboard, BusterDashboardResponse, DashboardConfig, DashboardRow, DashboardRowItem, }; use database::enums::{AssetPermissionRole, Verification}; #[derive(Debug, Serialize, Deserialize)] pub struct DashboardUpdateRequest { /// The unique identifier of the dashboard pub id: String, /// New name for the dashboard pub name: Option, /// New description for the dashboard pub description: Option>, /// Updated dashboard configuration pub config: Option, /// Updated verification status pub status: Option, pub metrics: Option>, /// The file content of the dashboard pub file: Option, /// Sharing properties pub public: Option, pub public_expiry_date: Option, pub public_password: Option, } pub async fn update_dashboard_handler( dashboard_id: Uuid, request: DashboardUpdateRequest, user_id: &Uuid, ) -> Result { let mut conn = get_pg_pool().get().await?; // First, get the current dashboard to ensure it exists and to get its current state let current_dashboard = get_dashboard_handler(&dashboard_id, user_id).await?; // If file content is provided, parse it and use it instead of other fields if let Some(file_content) = request.file { // Parse the YAML file content let dashboard_yml: DashboardYml = serde_yaml::from_str(&file_content)?; // Update the dashboard file with the new content diesel::update(dashboard_files::table) .filter(dashboard_files::id.eq(dashboard_id)) .filter(dashboard_files::deleted_at.is_null()) .set(( dashboard_files::name.eq(&dashboard_yml.name), dashboard_files::content.eq(serde_json::to_value(&dashboard_yml)?), dashboard_files::updated_at.eq(Utc::now()), )) .execute(&mut conn) .await?; } else { // Otherwise, update individual fields // Start building the update values let mut update_values = vec![]; // Update name if provided if let Some(name) = request.name { update_values.push(dashboard_files::name.eq(name)); } // Get the current content as a Value let mut content: Value = serde_json::to_value(¤t_dashboard.dashboard.config)?; // Update description if provided if let Some(description) = request.description { content["description"] = serde_json::to_value(description)?; } // Update config if provided if let Some(config) = request.config { content["rows"] = serde_json::to_value(config.rows)?; } // Update metrics if provided if let Some(metrics) = request.metrics { // This would require additional logic to map metrics to the dashboard config // For now, we'll just log that metrics were provided tracing::info!("Metrics provided for dashboard update: {:?}", metrics); } // Update the dashboard file diesel::update(dashboard_files::table) .filter(dashboard_files::id.eq(dashboard_id)) .filter(dashboard_files::deleted_at.is_null()) .set(( dashboard_files::content.eq(content), dashboard_files::updated_at.eq(Utc::now()), )) .execute(&mut conn) .await?; // Update sharing properties if provided if request.public.is_some() || request.public_expiry_date.is_some() || request.public_password.is_some() { // This would require additional logic to update sharing properties // For now, we'll just log that sharing properties were provided tracing::info!("Sharing properties provided for dashboard update"); } } // Return the updated dashboard get_dashboard_handler(&dashboard_id, user_id).await } ``` ### Update Module Files Update `libs/handlers/src/dashboards/mod.rs` to include the new handler and type: ```rust mod create_dashboard_handler; mod get_dashboard_handler; mod list_dashboard_handler; mod update_dashboard_handler; mod types; pub mod sharing; pub use create_dashboard_handler::*; pub use get_dashboard_handler::*; pub use list_dashboard_handler::*; pub use update_dashboard_handler::*; pub use types::*; ``` Update `src/routes/rest/routes/dashboards/mod.rs` to include the new route: ```rust use axum::{ routing::delete, routing::{get, post, put}, Router, }; // Modules for dashboard endpoints mod create_dashboard; mod get_dashboard; mod list_dashboards; mod update_dashboard; mod sharing; pub fn router() -> Router { Router::new() .route("/", post(create_dashboard::create_dashboard_rest_handler)) .route("/:id", put(update_dashboard::update_dashboard_rest_handler)) .route("/:id", get(get_dashboard::get_dashboard_rest_handler)) .route("/", get(list_dashboards::list_dashboard_rest_handler)) .route( "/:id/sharing", get(sharing::list_dashboard_sharing_rest_handler), ) .route( "/:id/sharing", post(sharing::create_dashboard_sharing_rest_handler), ) .route( "/:id/sharing", put(sharing::update_dashboard_sharing_rest_handler), ) .route( "/:id/sharing", delete(sharing::delete_dashboard_sharing_rest_handler), ) } ``` ## Testing Strategy ### Unit Tests Create unit tests for the `update_dashboard_handler` function: ```rust #[cfg(test)] mod tests { use super::*; use mockito; use uuid::Uuid; #[tokio::test] async fn test_update_dashboard_handler_with_name() { // Setup test environment let dashboard_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); // Create a test dashboard first // This would require setting up a test database // Create update request with just a name change let request = DashboardUpdateRequest { id: dashboard_id.to_string(), name: Some("Updated Dashboard Name".to_string()), description: None, config: None, status: None, metrics: None, file: None, public: None, public_expiry_date: None, public_password: None, }; // Call the handler let result = update_dashboard_handler(dashboard_id, request, &user_id).await; // Verify the result assert!(result.is_ok()); let response = result.unwrap(); // Check dashboard properties assert_eq!(response.dashboard.name, "Updated Dashboard Name"); } #[tokio::test] async fn test_update_dashboard_handler_with_file() { // Setup test environment let dashboard_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); // Create a test dashboard first // This would require setting up a test database // Create update request with file content let yaml_content = r#" name: File Updated Dashboard description: Updated from file rows: [] "#; let request = DashboardUpdateRequest { id: dashboard_id.to_string(), name: None, description: None, config: None, status: None, metrics: None, file: Some(yaml_content.to_string()), public: None, public_expiry_date: None, public_password: None, }; // Call the handler let result = update_dashboard_handler(dashboard_id, request, &user_id).await; // Verify the result assert!(result.is_ok()); let response = result.unwrap(); // Check dashboard properties assert_eq!(response.dashboard.name, "File Updated Dashboard"); assert_eq!(response.dashboard.description, Some("Updated from file".to_string())); } } ``` ### Integration Tests Create integration tests in `tests/routes/rest/dashboards/update_dashboard_test.rs`: ```rust use anyhow::Result; use axum::http::StatusCode; use serde_json::json; use uuid::Uuid; use crate::common::test_app::TestApp; #[tokio::test] async fn test_update_dashboard_endpoint() -> Result<()> { // Setup test app let app = TestApp::new().await?; // Create a test dashboard first let create_response = app .client .post("/api/v1/dashboards") .bearer_auth(&app.test_user.token) .send() .await?; let create_body: serde_json::Value = create_response.json().await?; let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); // Make request to update dashboard let update_response = app .client .put(&format!("/api/v1/dashboards/{}", dashboard_id)) .bearer_auth(&app.test_user.token) .json(&json!({ "id": dashboard_id, "name": "Updated Dashboard Name", "description": "Updated description" })) .send() .await?; // Verify response assert_eq!(update_response.status(), StatusCode::OK); // Parse response body let update_body: serde_json::Value = update_response.json().await?; // Verify dashboard properties assert_eq!(update_body["dashboard"]["name"], "Updated Dashboard Name"); assert_eq!(update_body["dashboard"]["description"], "Updated description"); Ok(()) } #[tokio::test] async fn test_update_dashboard_with_file_endpoint() -> Result<()> { // Setup test app let app = TestApp::new().await?; // Create a test dashboard first let create_response = app .client .post("/api/v1/dashboards") .bearer_auth(&app.test_user.token) .send() .await?; let create_body: serde_json::Value = create_response.json().await?; let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap(); // YAML content for update let yaml_content = r#" name: File Updated Dashboard description: Updated from file rows: [] "#; // Make request to update dashboard with file let update_response = app .client .put(&format!("/api/v1/dashboards/{}", dashboard_id)) .bearer_auth(&app.test_user.token) .json(&json!({ "id": dashboard_id, "file": yaml_content })) .send() .await?; // Verify response assert_eq!(update_response.status(), StatusCode::OK); // Parse response body let update_body: serde_json::Value = update_response.json().await?; // Verify dashboard properties assert_eq!(update_body["dashboard"]["name"], "File Updated Dashboard"); assert_eq!(update_body["dashboard"]["description"], "Updated from file"); Ok(()) } ``` ## Dependencies - Database schema for dashboard_files - DashboardYml structure - Authentication middleware - Existing get_dashboard_handler function ## File Changes ### New Files - `src/routes/rest/routes/dashboards/update_dashboard.rs` - `libs/handlers/src/dashboards/update_dashboard_handler.rs` ### Modified Files - `src/routes/rest/routes/dashboards/mod.rs` - `libs/handlers/src/dashboards/mod.rs` ## Implementation Plan 1. Create the business logic handler 2. Create the REST endpoint handler 3. Update module files 4. Add unit tests 5. Add integration tests 6. Manual testing ## Success Criteria 1. The endpoint successfully updates a dashboard with the provided values 2. The endpoint handles file content updates correctly 3. The endpoint returns a properly formatted response 4. All tests pass 5. The endpoint is properly documented 6. The endpoint is secured with authentication