mirror of https://github.com/buster-so/buster.git
537 lines
15 KiB
Markdown
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(¤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
|