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

537 lines
15 KiB
Markdown

---
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<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`:
```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<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:
```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