2025-03-20 12:32:06 +08:00
---
2025-03-20 13:33:54 +08:00
title: Remove Dashboard from Collections REST Endpoint
2025-03-20 12:32:06 +08:00
author: Cascade
date: 2025-03-19
status: Draft
---
2025-03-20 13:33:54 +08:00
# Remove Dashboard from Collections REST Endpoint
2025-03-20 12:32:06 +08:00
## Problem Statement
2025-03-20 13:33:54 +08:00
Users need the ability to programmatically remove a dashboard from multiple collections via a REST API. Currently, this functionality is not available, limiting the ability to manage collections through the API.
2025-03-20 12:32:06 +08:00
## Goals
2025-03-20 13:33:54 +08:00
1. Create a REST endpoint to remove a dashboard from multiple collections
2025-03-20 12:32:06 +08:00
2. Implement proper permission validation
3. Ensure data integrity with proper error handling
4. Follow established patterns for REST endpoints and handlers
## Non-Goals
1. Modifying the existing collections functionality
2. Creating UI components for this endpoint
## Technical Design
### REST Endpoint
2025-03-20 13:33:54 +08:00
**Endpoint:** `DELETE /dashboards/:id/collections`
2025-03-20 12:32:06 +08:00
**Request Body:**
```json
{
2025-03-20 13:33:54 +08:00
"collection_ids": ["uuid1", "uuid2", "uuid3"]
2025-03-20 12:32:06 +08:00
}
```
**Response:**
- `200 OK` - Success
```json
{
2025-03-20 13:33:54 +08:00
"message": "Dashboard removed from collections successfully",
"removed_count": 3,
"failed_count": 0,
"failed_ids": []
2025-03-20 12:32:06 +08:00
}
```
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
- `400 Bad Request` - Invalid input
- `403 Forbidden` - Insufficient permissions
2025-03-20 13:33:54 +08:00
- `404 Not Found` - Dashboard not found
2025-03-20 12:32:06 +08:00
- `500 Internal Server Error` - Server error
### Handler Implementation
The handler will:
2025-03-20 13:33:54 +08:00
1. Validate that the dashboard exists
2. Check if the user has appropriate permissions for the dashboard (Owner, FullAccess, or CanEdit)
3. For each collection in the request:
a. Check if the user has permission to modify the collection
b. Mark the dashboard association as deleted in the `collections_to_assets` table
2025-03-20 12:32:06 +08:00
### File Changes
#### New Files
2025-03-20 13:33:54 +08:00
1. `libs/handlers/src/dashboards/remove_dashboard_from_collections_handler.rs`
2025-03-20 12:32:06 +08:00
```rust
use anyhow::{anyhow, Result};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
2025-03-20 13:33:54 +08:00
helpers::dashboard_files::get_dashboard_file_by_id,
2025-03-20 12:32:06 +08:00
pool::get_pg_pool,
schema::{collections, collections_to_assets},
};
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
2025-03-20 13:33:54 +08:00
use futures::future::join_all;
2025-03-20 12:32:06 +08:00
use sharing::check_asset_permission::has_permission;
2025-03-20 13:33:54 +08:00
use std::collections::HashMap;
2025-03-20 12:32:06 +08:00
use tracing::{error, info};
use uuid::Uuid;
2025-03-20 13:33:54 +08:00
/// Response for removing a dashboard from collections
#[derive(Debug)]
pub struct RemoveDashboardFromCollectionsResponse {
pub removed_count: usize,
pub failed_count: usize,
pub failed_ids: Vec< Uuid > ,
}
/// Removes a dashboard from multiple collections
2025-03-20 12:32:06 +08:00
///
/// # Arguments
///
2025-03-20 13:33:54 +08:00
/// * `dashboard_id` - The unique identifier of the dashboard
/// * `collection_ids` - Vector of collection IDs to remove the dashboard from
2025-03-20 12:32:06 +08:00
/// * `user_id` - The unique identifier of the user performing the action
///
/// # Returns
///
2025-03-20 13:33:54 +08:00
/// RemoveDashboardFromCollectionsResponse on success, or an error if the operation fails
pub async fn remove_dashboard_from_collections_handler(
dashboard_id: & Uuid,
collection_ids: Vec< Uuid > ,
2025-03-20 12:32:06 +08:00
user_id: & Uuid,
2025-03-20 13:33:54 +08:00
) -> Result< RemoveDashboardFromCollectionsResponse > {
2025-03-20 12:32:06 +08:00
info!(
2025-03-20 13:33:54 +08:00
dashboard_id = %dashboard_id,
2025-03-20 12:32:06 +08:00
user_id = %user_id,
2025-03-20 13:33:54 +08:00
collection_count = collection_ids.len(),
"Removing dashboard from collections"
2025-03-20 12:32:06 +08:00
);
2025-03-20 13:33:54 +08:00
if collection_ids.is_empty() {
return Ok(RemoveDashboardFromCollectionsResponse {
removed_count: 0,
failed_count: 0,
failed_ids: vec![],
});
2025-03-20 12:32:06 +08:00
}
2025-03-20 13:33:54 +08:00
// 1. Validate the dashboard exists
let dashboard = match get_dashboard_file_by_id(dashboard_id).await {
Ok(Some(dashboard)) => dashboard,
Ok(None) => {
error!(
dashboard_id = %dashboard_id,
"Dashboard not found"
);
return Err(anyhow!("Dashboard not found"));
}
Err(e) => {
error!("Error checking if dashboard exists: {}", e);
return Err(anyhow!("Database error: {}", e));
}
};
2025-03-20 12:32:06 +08:00
2025-03-20 13:33:54 +08:00
// 2. Check if user has permission to modify the dashboard
let has_dashboard_permission = has_permission(
*dashboard_id,
AssetType::DashboardFile,
2025-03-20 12:32:06 +08:00
*user_id,
IdentityType::User,
AssetPermissionRole::CanEdit, // This will pass for Owner and FullAccess too
)
.await
.map_err(|e| {
error!(
2025-03-20 13:33:54 +08:00
dashboard_id = %dashboard_id,
2025-03-20 12:32:06 +08:00
user_id = %user_id,
2025-03-20 13:33:54 +08:00
"Error checking dashboard permission: {}", e
2025-03-20 12:32:06 +08:00
);
anyhow!("Error checking permissions: {}", e)
})?;
2025-03-20 13:33:54 +08:00
if !has_dashboard_permission {
2025-03-20 12:32:06 +08:00
error!(
2025-03-20 13:33:54 +08:00
dashboard_id = %dashboard_id,
2025-03-20 12:32:06 +08:00
user_id = %user_id,
2025-03-20 13:33:54 +08:00
"User does not have permission to modify this dashboard"
2025-03-20 12:32:06 +08:00
);
2025-03-20 13:33:54 +08:00
return Err(anyhow!("User does not have permission to modify this dashboard"));
2025-03-20 12:32:06 +08:00
}
2025-03-20 13:33:54 +08:00
// 3. Get database connection
let mut conn = get_pg_pool().get().await.map_err(|e| {
error!("Database connection error: {}", e);
anyhow!("Failed to get database connection: {}", e)
})?;
// 4. Process each collection
let mut failed_ids = Vec::new();
let mut removed_count = 0;
2025-03-20 12:32:06 +08:00
let now = chrono::Utc::now();
2025-03-20 13:33:54 +08:00
for collection_id in & collection_ids {
// Check if collection exists
let collection_exists = collections::table
.filter(collections::id.eq(collection_id))
.filter(collections::deleted_at.is_null())
.count()
.get_result::< i64 > (& mut conn)
.await;
if let Err(e) = collection_exists {
error!(
collection_id = %collection_id,
"Error checking if collection exists: {}", e
);
failed_ids.push(*collection_id);
continue;
}
if collection_exists.unwrap() == 0 {
error!(
collection_id = %collection_id,
"Collection not found"
);
failed_ids.push(*collection_id);
continue;
}
// Check if user has permission to modify the collection
let has_collection_permission = has_permission(
*collection_id,
AssetType::Collection,
*user_id,
IdentityType::User,
AssetPermissionRole::CanEdit,
)
.await;
if let Err(e) = has_collection_permission {
error!(
collection_id = %collection_id,
user_id = %user_id,
"Error checking collection permission: {}", e
);
failed_ids.push(*collection_id);
continue;
}
if !has_collection_permission.unwrap() {
error!(
collection_id = %collection_id,
user_id = %user_id,
"User does not have permission to modify this collection"
);
failed_ids.push(*collection_id);
continue;
}
// Mark dashboard as deleted from this collection
match diesel::update(collections_to_assets::table)
.filter(collections_to_assets::collection_id.eq(collection_id))
.filter(collections_to_assets::asset_id.eq(dashboard_id))
.filter(collections_to_assets::asset_type.eq(AssetType::DashboardFile))
.filter(collections_to_assets::deleted_at.is_null())
.set((
collections_to_assets::deleted_at.eq(now),
collections_to_assets::updated_at.eq(now),
collections_to_assets::updated_by.eq(user_id),
))
.execute(& mut conn)
.await
{
Ok(updated) => {
if updated > 0 {
removed_count += 1;
info!(
dashboard_id = %dashboard_id,
collection_id = %collection_id,
"Successfully removed dashboard from collection"
);
} else {
error!(
dashboard_id = %dashboard_id,
collection_id = %collection_id,
"Dashboard not found in collection"
);
failed_ids.push(*collection_id);
}
}
Err(e) => {
error!(
dashboard_id = %dashboard_id,
collection_id = %collection_id,
"Error removing dashboard from collection: {}", e
);
failed_ids.push(*collection_id);
}
}
}
let failed_count = failed_ids.len();
2025-03-20 12:32:06 +08:00
info!(
2025-03-20 13:33:54 +08:00
dashboard_id = %dashboard_id,
2025-03-20 12:32:06 +08:00
user_id = %user_id,
2025-03-20 13:33:54 +08:00
collection_count = collection_ids.len(),
removed_count = removed_count,
failed_count = failed_count,
"Finished removing dashboard from collections"
2025-03-20 12:32:06 +08:00
);
2025-03-20 13:33:54 +08:00
Ok(RemoveDashboardFromCollectionsResponse {
removed_count,
failed_count,
failed_ids,
})
2025-03-20 12:32:06 +08:00
}
#[cfg(test)]
mod tests {
use super::*;
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use uuid::Uuid;
#[tokio::test]
2025-03-20 13:33:54 +08:00
async fn test_remove_dashboard_from_collections_handler() {
2025-03-20 12:32:06 +08:00
// This is a placeholder for the actual test
// In a real implementation, we would use test fixtures and a test database
assert!(true);
}
}
```
2025-03-20 13:33:54 +08:00
2. `src/routes/rest/routes/dashboards/remove_dashboard_from_collections.rs`
2025-03-20 12:32:06 +08:00
```rust
use axum::{
extract::{Extension, Json, Path},
http::StatusCode,
};
2025-03-20 13:33:54 +08:00
use handlers::dashboards::remove_dashboard_from_collections_handler;
2025-03-20 12:32:06 +08:00
use middleware::AuthenticatedUser;
2025-03-20 13:33:54 +08:00
use serde::{Deserialize, Serialize};
2025-03-20 12:32:06 +08:00
use tracing::info;
use uuid::Uuid;
use crate::routes::rest::ApiResponse;
#[derive(Debug, Deserialize)]
2025-03-20 13:33:54 +08:00
pub struct RemoveFromCollectionsRequest {
pub collection_ids: Vec< Uuid > ,
2025-03-20 12:32:06 +08:00
}
2025-03-20 13:33:54 +08:00
#[derive(Debug, Serialize)]
pub struct RemoveFromCollectionsResponse {
pub message: String,
pub removed_count: usize,
pub failed_count: usize,
pub failed_ids: Vec< Uuid > ,
}
/// REST handler for removing a dashboard from multiple collections
2025-03-20 12:32:06 +08:00
///
/// # Arguments
///
/// * `user` - The authenticated user making the request
2025-03-20 13:33:54 +08:00
/// * `id` - The unique identifier of the dashboard
/// * `request` - The collection IDs to remove the dashboard from
2025-03-20 12:32:06 +08:00
///
/// # Returns
///
/// A success message on success, or an appropriate error response
2025-03-20 13:33:54 +08:00
pub async fn remove_dashboard_from_collections_rest_handler(
2025-03-20 12:32:06 +08:00
Extension(user): Extension< AuthenticatedUser > ,
Path(id): Path< Uuid > ,
2025-03-20 13:33:54 +08:00
Json(request): Json< RemoveFromCollectionsRequest > ,
) -> Result< ApiResponse < RemoveFromCollectionsResponse > , (StatusCode, String)> {
2025-03-20 12:32:06 +08:00
info!(
2025-03-20 13:33:54 +08:00
dashboard_id = %id,
2025-03-20 12:32:06 +08:00
user_id = %user.id,
2025-03-20 13:33:54 +08:00
collection_count = request.collection_ids.len(),
"Processing DELETE request to remove dashboard from collections"
2025-03-20 12:32:06 +08:00
);
2025-03-20 13:33:54 +08:00
match remove_dashboard_from_collections_handler(& id, request.collection_ids, & user.id).await {
Ok(result) => {
let response = RemoveFromCollectionsResponse {
message: "Dashboard removed from collections successfully".to_string(),
removed_count: result.removed_count,
failed_count: result.failed_count,
failed_ids: result.failed_ids,
};
Ok(ApiResponse::JsonData(response))
}
2025-03-20 12:32:06 +08:00
Err(e) => {
2025-03-20 13:33:54 +08:00
tracing::error!("Error removing dashboard from collections: {}", e);
2025-03-20 12:32:06 +08:00
// Map specific errors to appropriate status codes
let error_message = e.to_string();
2025-03-20 13:33:54 +08:00
if error_message.contains("Dashboard not found") {
return Err((StatusCode::NOT_FOUND, format!("Dashboard not found: {}", e)));
2025-03-20 12:32:06 +08:00
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
}
2025-03-20 13:33:54 +08:00
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to remove dashboard from collections: {}", e)))
2025-03-20 12:32:06 +08:00
}
}
}
```
2025-03-20 13:33:54 +08:00
3. Update `libs/handlers/src/dashboards/mod.rs` to include the new handler
4. Update `src/routes/rest/routes/dashboards/mod.rs` to include the new endpoint
2025-03-20 12:32:06 +08:00
### Database Operations
2025-03-20 13:33:54 +08:00
The implementation will use the following database operations:
1. SELECT to check if the dashboard exists
2. SELECT to check if the collections exist
3. UPDATE to mark dashboard associations as deleted
2025-03-20 12:32:06 +08:00
## Testing Strategy
### Unit Tests
1. Test the handler with mocked database connections
2025-03-20 13:33:54 +08:00
- Test removing a dashboard from collections
- Test error cases (dashboard not found, collection not found, insufficient permissions)
- Test removing a dashboard that isn't in a collection
2025-03-20 12:32:06 +08:00
2. Test the REST endpoint
- Test successful request
- Test error responses for various scenarios
### Integration Tests
1. Test the endpoint with a test database
2025-03-20 13:33:54 +08:00
- Create collections and add a dashboard to them
- Remove the dashboard from the collections
2025-03-20 12:32:06 +08:00
- Verify the database state
- Test with different user roles
## Security Considerations
- The endpoint requires authentication
2025-03-20 13:33:54 +08:00
- Permission checks ensure users can only modify collections and dashboards they have access to
2025-03-20 12:32:06 +08:00
- Input validation prevents malicious data
## Monitoring and Logging
- All operations are logged with appropriate context
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
- Errors are logged with detailed information
## Dependencies
- `libs/sharing` - For permission checking
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
- `libs/database` - For database operations
2025-03-20 13:33:54 +08:00
- Using existing helpers in `dashboard_files.rs` for dashboard existence checks
2025-03-20 12:32:06 +08:00
## Rollout Plan
1. Implement the handler and endpoint
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
2. Write tests
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
3. Code review
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
4. Deploy to staging
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
5. Test in staging
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
6. Deploy to production
2025-03-20 13:33:54 +08:00
2025-03-20 12:32:06 +08:00
7. Monitor for issues