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(¤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:
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
- Create the business logic handler
- Create the REST endpoint handler
- Update module files
- Add unit tests
- Add integration tests
- Manual testing
Success Criteria
- The endpoint successfully updates a dashboard with the provided values
- The endpoint handles file content updates correctly
- The endpoint returns a properly formatted response
- All tests pass
- The endpoint is properly documented
- The endpoint is secured with authentication