buster/api/prds/active/api_dashboard_create_endpoi...

9.7 KiB

title author date status
Create Dashboard REST Endpoint Cascade 2025-03-19 Draft

Create Dashboard REST Endpoint

Problem Statement

Currently, our application lacks a REST API endpoint for creating dashboards. Users need to be able to programmatically create new dashboards through the API to support automation and integration scenarios.

Proposed Solution

Implement a POST /dashboards endpoint that creates a new dashboard with default values. The endpoint will create a DashboardFile object with a DashboardYml containing a default name "Untitled Dashboard", no description, and an empty array of rows.

Technical Design

REST Endpoint

POST /dashboards

Request

The request body will be empty. All default values will be used for the new dashboard.

Response

{
  "dashboard": {
    "id": "uuid",
    "name": "Untitled Dashboard",
    "description": null,
    "config": {
      "rows": []
    },
    "created_at": "timestamp",
    "created_by": "user_uuid",
    "updated_at": "timestamp",
    "updated_by": "user_uuid",
    "status": "Verified",
    "version_number": 1,
    "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/create_dashboard.rs:

use axum::{extract::State, Json};
use handlers::dashboards::create_dashboard_handler;
use handlers::types::User;
use uuid::Uuid;

use crate::routes::rest::ApiResponse;
use crate::AppState;

pub async fn create_dashboard_rest_handler(
    State(state): State<AppState>,
    user: User,
) -> ApiResponse {
    match create_dashboard_handler(&user.id).await {
        Ok(response) => ApiResponse::success(response),
        Err(e) => {
            tracing::error!("Failed to create dashboard: {}", e);
            ApiResponse::error(e)
        }
    }
}

Business Logic Handler

Create a new file at libs/handlers/src/dashboards/create_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::insert_into;
use diesel_async::RunQueryDsl;
use serde_yaml;
use uuid::Uuid;

use super::{BusterDashboard, BusterDashboardResponse, DashboardConfig};
use database::enums::{AssetPermissionRole, Verification};
use std::collections::HashMap;

pub async fn create_dashboard_handler(user_id: &Uuid) -> Result<BusterDashboardResponse> {
    let mut conn = get_pg_pool().get().await?;
    
    // Create a default dashboard YAML
    let dashboard_yml = DashboardYml {
        name: "Untitled Dashboard".to_string(),
        description: None,
        rows: vec![],
    };
    
    // Convert to YAML string
    let yaml_content = serde_yaml::to_string(&dashboard_yml)?;
    
    // Generate a unique ID and filename
    let dashboard_id = Uuid::new_v4();
    let file_name = format!("dashboard_{}.yml", dashboard_id);
    
    // Get user's organization ID
    let organization_id = get_user_organization_id(user_id).await?;
    
    // Current timestamp
    let now = Utc::now();
    
    // Insert the dashboard file
    let dashboard_file = insert_into(dashboard_files::table)
        .values((
            dashboard_files::id.eq(dashboard_id),
            dashboard_files::name.eq("Untitled Dashboard"),
            dashboard_files::file_name.eq(&file_name),
            dashboard_files::content.eq(serde_json::to_value(&dashboard_yml)?),
            dashboard_files::organization_id.eq(organization_id),
            dashboard_files::created_by.eq(user_id),
            dashboard_files::created_at.eq(now),
            dashboard_files::updated_at.eq(now),
            dashboard_files::publicly_accessible.eq(false),
        ))
        .returning((
            dashboard_files::id,
            dashboard_files::name,
            dashboard_files::file_name,
            dashboard_files::created_by,
            dashboard_files::created_at,
            dashboard_files::updated_at,
        ))
        .get_result::<(Uuid, String, String, Uuid, chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>(&mut conn)
        .await?;
    
    // Construct the dashboard
    let dashboard = BusterDashboard {
        config: DashboardConfig { rows: vec![] },
        created_at: dashboard_file.4,
        created_by: dashboard_file.3,
        description: None,
        id: dashboard_file.0,
        name: dashboard_file.1,
        updated_at: Some(dashboard_file.5),
        updated_by: dashboard_file.3,
        status: Verification::Verified,
        version_number: 1,
        file: yaml_content,
        file_name: dashboard_file.2,
    };
    
    Ok(BusterDashboardResponse {
        access: AssetPermissionRole::Owner,
        metrics: HashMap::new(),
        dashboard,
        permission: AssetPermissionRole::Owner,
        public_password: None,
        collections: vec![],
    })
}

// Helper function to get user's organization ID
async fn get_user_organization_id(user_id: &Uuid) -> Result<Uuid> {
    // Implementation will depend on your user/organization model
    // For now, we'll return a placeholder
    // In a real implementation, you would query the database to get the user's organization
    
    let mut conn = get_pg_pool().get().await?;
    
    // Query to get the user's organization ID
    // This is a placeholder - replace with your actual query
    let organization_id = Uuid::new_v4(); // Replace with actual query
    
    Ok(organization_id)
}

Update Module Files

Update libs/handlers/src/dashboards/mod.rs to include the new handler:

mod create_dashboard_handler;
mod get_dashboard_handler;
mod list_dashboard_handler;
mod types;
pub mod sharing;

pub use create_dashboard_handler::*;
pub use get_dashboard_handler::*;
pub use list_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 sharing;

pub fn router() -> Router {
    Router::new()
        .route("/", post(create_dashboard::create_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 create_dashboard_handler function:

#[cfg(test)]
mod tests {
    use super::*;
    use mockito;
    use uuid::Uuid;

    #[tokio::test]
    async fn test_create_dashboard_handler() {
        // Setup test environment
        let user_id = Uuid::new_v4();
        
        // Call the handler
        let result = create_dashboard_handler(&user_id).await;
        
        // Verify the result
        assert!(result.is_ok());
        let response = result.unwrap();
        
        // Check dashboard properties
        assert_eq!(response.dashboard.name, "Untitled Dashboard");
        assert!(response.dashboard.description.is_none());
        assert_eq!(response.dashboard.config.rows.len(), 0);
        assert_eq!(response.access, AssetPermissionRole::Owner);
        assert_eq!(response.permission, AssetPermissionRole::Owner);
        assert!(response.public_password.is_none());
        assert_eq!(response.collections.len(), 0);
    }
}

Integration Tests

Create integration tests in tests/routes/rest/dashboards/create_dashboard_test.rs:

use anyhow::Result;
use axum::http::StatusCode;
use uuid::Uuid;

use crate::common::test_app::TestApp;

#[tokio::test]
async fn test_create_dashboard_endpoint() -> Result<()> {
    // Setup test app
    let app = TestApp::new().await?;
    
    // Make request to create dashboard
    let response = app
        .client
        .post("/api/v1/dashboards")
        .bearer_auth(&app.test_user.token)
        .send()
        .await?;
    
    // Verify response
    assert_eq!(response.status(), StatusCode::OK);
    
    // Parse response body
    let body: serde_json::Value = response.json().await?;
    
    // Verify dashboard properties
    assert_eq!(body["dashboard"]["name"], "Untitled Dashboard");
    assert!(body["dashboard"]["description"].is_null());
    assert_eq!(body["dashboard"]["config"]["rows"].as_array().unwrap().len(), 0);
    
    Ok(())
}

Dependencies

  • Database schema for dashboard_files
  • DashboardYml structure
  • Authentication middleware
  • User organization lookup functionality

File Changes

New Files

  • src/routes/rest/routes/dashboards/create_dashboard.rs
  • libs/handlers/src/dashboards/create_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 creates a new dashboard with default values
  2. The endpoint returns a properly formatted response
  3. All tests pass
  4. The endpoint is properly documented
  5. The endpoint is secured with authentication