buster/api/prds/active/api_dashboard_update_endpoi...

15 KiB

title author date status
Update Dashboard REST Endpoint Cascade 2025-03-19 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

{
  /** 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

{
  "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:

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<AppState>,
    Path(id): Path<Uuid>,
    user: User,
    Json(request): Json<DashboardUpdateRequest>,
) -> 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:

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<String>,
    /// New description for the dashboard
    pub description: Option<Option<String>>,
    /// Updated dashboard configuration
    pub config: Option<DashboardConfig>,
    /// Updated verification status
    pub status: Option<Verification>,
    pub metrics: Option<Vec<String>>,
    /// The file content of the dashboard
    pub file: Option<String>,
    /// Sharing properties
    pub public: Option<bool>,
    pub public_expiry_date: Option<String>,
    pub public_password: Option<String>,
}

pub async fn update_dashboard_handler(
    dashboard_id: Uuid,
    request: DashboardUpdateRequest,
    user_id: &Uuid,
) -> Result<BusterDashboardResponse> {
    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(&current_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:

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:

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:

#[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:

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