From d236fbf3682e9bdad00a2f480fa90bf55b822d49 Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Sat, 21 Dec 2024 14:01:25 -0600 Subject: [PATCH] feat: New folders endpoints that accept `folder_type` URL parameter (#5405) ## Overview This adds new CRUDL folder endpoints that include `folder_type` as URL parameter. These new endpoints can be used to create/read/update/delete/list folders of the type specified in the `folder_type` URL parameter. The older CRUDL folder endpoints only worked on dashboard folders. They are being deprecated in this PR. Once the UI stops calling these endpoints they can be removed entirely. ## URL path design he URLs for the new endpoints generally follow the pattern `{org_id}/folders/{folder_type}/{folder_id}`. The position of the `{folder_type}` parameter in these URLs is specifically intended to represent the place of "folder types" within the hierarchy of resources/concepts in the URL. Specifically the hierarchy is as follows... - `{org_id}` - represents an org which owns many folders - `folders` - represents all folders - `{folder_type}` - represents a class or subset of folders - `{folder_id}` - identifies a single folder; this ID is only uniquely identifying in the context of all concepts higher in the hierarchy, including `{folder_type}` ## Alternatives An alternative approach to this PR would be to update the existing endpoints so that either - `{folder_type}` is an optional final URL parameter (after `{folder_id}`) that defaults to `dashboards` - `{folder_type}` is an optional query parameter that defaults to `dashboards` Although pursuing one of these approaches would have reduced the need to add new routes and deprecate old ones, I think the design of the URL path would suffer and be less clear with either of these approaches since in either of these approaches the path would not actually be formatted to represent the resource hierarchy described above. ## Summary by CodeRabbit - **New Features** - Introduced a new `FolderType` enum for folder data categorization. - Enhanced folder management API with new functions that include a `folder_type` parameter. - **Bug Fixes** - Deprecated old folder-related functions while maintaining backward compatibility. - **Documentation** - Updated API documentation to reflect changes in folder management functions, including new deprecated endpoints and the `FolderType` schema. - **Chores** - Added deprecation warnings for the `folders` module to inform users about outdated methods. --- src/handler/http/models/folders.rs | 17 ++ src/handler/http/request/folders.rs | 241 +++++++++++++++++++++++++--- src/handler/http/request/mod.rs | 1 + src/handler/http/router/mod.rs | 15 +- src/handler/http/router/openapi.rs | 6 + 5 files changed, 249 insertions(+), 31 deletions(-) diff --git a/src/handler/http/models/folders.rs b/src/handler/http/models/folders.rs index e342296ae8..03d6e9be70 100644 --- a/src/handler/http/models/folders.rs +++ b/src/handler/http/models/folders.rs @@ -45,6 +45,14 @@ pub struct ListFoldersResponseBody { pub list: Vec, } +/// Indicates the type of data that the folder can contain. +#[derive(Clone, Debug, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum FolderType { + Dashboards, + Alerts, +} + /// Common folder fields used in HTTP request and response bodies. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -83,6 +91,15 @@ impl From for config::meta::folder::Folder { } } +impl From for infra::table::folders::FolderType { + fn from(value: FolderType) -> Self { + match value { + FolderType::Dashboards => Self::Dashboards, + FolderType::Alerts => Self::Alerts, + } + } +} + impl From> for ListFoldersResponseBody { fn from(value: Vec) -> Self { Self { diff --git a/src/handler/http/request/folders.rs b/src/handler/http/request/folders.rs index fbbd467e21..6e3d44404c 100644 --- a/src/handler/http/request/folders.rs +++ b/src/handler/http/request/folders.rs @@ -14,12 +14,11 @@ // along with this program. If not, see . use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, Responder}; -use infra::table::folders::FolderType; use crate::{ common::meta::http::HttpResponse as MetaHttpResponse, handler::http::models::folders::{ - CreateFolderRequestBody, CreateFolderResponseBody, ListFoldersResponseBody, + CreateFolderRequestBody, CreateFolderResponseBody, FolderType, ListFoldersResponseBody, UpdateFolderRequestBody, }, service::folders::{self, FolderError}, @@ -58,6 +57,7 @@ impl From for HttpResponse { ), params( ("org_id" = String, Path, description = "Organization name"), + ("folder_type" = FolderType, Path, description = "Type of data the folder can contain"), ), request_body( content = CreateFolderRequestBody, @@ -72,15 +72,14 @@ impl From for HttpResponse { (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Internal Server Error", body = HttpResponse), ), )] -#[post("/{org_id}/folders")] +#[post("/v2/{org_id}/folders/{folder_type}")] pub async fn create_folder( - path: web::Path, + path: web::Path<(String, FolderType)>, body: web::Json, ) -> impl Responder { - let org_id = path.into_inner(); + let (org_id, folder_type) = path.into_inner(); let folder = body.into_inner().into(); - let folder_type = FolderType::Dashboards; - match folders::save_folder(&org_id, folder, folder_type, false).await { + match folders::save_folder(&org_id, folder, folder_type.into(), false).await { Ok(folder) => { let body: CreateFolderResponseBody = folder.into(); HttpResponse::Ok().json(body) @@ -99,6 +98,7 @@ pub async fn create_folder( ), params( ("org_id" = String, Path, description = "Organization name"), + ("folder_type" = FolderType, Path, description = "Type of data the folder can contain"), ("folder_id" = String, Path, description = "Folder name"), ), request_body( @@ -114,15 +114,14 @@ pub async fn create_folder( (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Internal Server Error", body = HttpResponse), ), )] -#[put("/{org_id}/folders/{folder_id}")] +#[put("/v2/{org_id}/folders/{folder_type}/{folder_id}")] pub async fn update_folder( - path: web::Path<(String, String)>, + path: web::Path<(String, FolderType, String)>, body: web::Json, ) -> impl Responder { - let (org_id, folder_id) = path.into_inner(); + let (org_id, folder_type, folder_id) = path.into_inner(); let folder = body.into_inner().into(); - let folder_type = FolderType::Dashboards; - match folders::update_folder(&org_id, &folder_id, folder_type, folder).await { + match folders::update_folder(&org_id, &folder_id, folder_type.into(), folder).await { Ok(_) => HttpResponse::Ok().body("Folder updated"), Err(err) => err.into(), } @@ -138,15 +137,19 @@ pub async fn update_folder( ), params( ("org_id" = String, Path, description = "Organization name"), + ("folder_type" = FolderType, Path, description = "Type of data the folder can contain"), ), responses( (status = StatusCode::OK, body = ListFoldersResponseBody), ), )] -#[get("/{org_id}/folders")] +#[get("/v2/{org_id}/folders/{folder_type}")] #[allow(unused_variables)] -pub async fn list_folders(path: web::Path, req: HttpRequest) -> impl Responder { - let org_id = path.into_inner(); +pub async fn list_folders( + path: web::Path<(String, FolderType)>, + req: HttpRequest, +) -> impl Responder { + let (org_id, folder_type) = path.into_inner(); #[cfg(not(feature = "enterprise"))] let user_id = None; @@ -157,7 +160,7 @@ pub async fn list_folders(path: web::Path, req: HttpRequest) -> impl Res }; let folder_type = FolderType::Dashboards; - match folders::list_folders(&org_id, user_id, folder_type).await { + match folders::list_folders(&org_id, user_id, folder_type.into()).await { Ok(folders) => { let body: ListFoldersResponseBody = folders.into(); HttpResponse::Ok().json(body) @@ -176,6 +179,7 @@ pub async fn list_folders(path: web::Path, req: HttpRequest) -> impl Res ), params( ("org_id" = String, Path, description = "Organization name"), + ("folder_type" = FolderType, Path, description = "Type of data the folder can contain"), ("folder_id" = String, Path, description = "Folder ID"), ), responses( @@ -183,11 +187,10 @@ pub async fn list_folders(path: web::Path, req: HttpRequest) -> impl Res (status = StatusCode::NOT_FOUND, description = "Folder not found", body = HttpResponse), ), )] -#[get("/{org_id}/folders/{folder_id}")] -pub async fn get_folder(path: web::Path<(String, String)>) -> impl Responder { - let (org_id, folder_id) = path.into_inner(); - let folder_type = FolderType::Dashboards; - match folders::get_folder(&org_id, &folder_id, folder_type).await { +#[get("/v2/{org_id}/folders/{folder_type}/{folder_id}")] +pub async fn get_folder(path: web::Path<(String, FolderType, String)>) -> impl Responder { + let (org_id, folder_type, folder_id) = path.into_inner(); + match folders::get_folder(&org_id, &folder_id, folder_type.into()).await { Ok(folder) => { let body: CreateFolderResponseBody = folder.into(); HttpResponse::Ok().json(body) @@ -206,6 +209,7 @@ pub async fn get_folder(path: web::Path<(String, String)>) -> impl Responder { ), params( ("org_id" = String, Path, description = "Organization name"), + ("folder_type" = FolderType, Path, description = "Type of data the folder can contain"), ("folder_id" = String, Path, description = "Folder ID"), ), responses( @@ -214,12 +218,197 @@ pub async fn get_folder(path: web::Path<(String, String)>) -> impl Responder { (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Error", body = HttpResponse), ), )] -#[delete("/{org_id}/folders/{folder_id}")] -async fn delete_folder(path: web::Path<(String, String)>) -> impl Responder { - let (org_id, folder_id) = path.into_inner(); - let folder_type = FolderType::Dashboards; - match folders::delete_folder(&org_id, &folder_id, folder_type).await { +#[delete("/v2/{org_id}/folders/{folder_type}/{folder_id}")] +async fn delete_folder(path: web::Path<(String, FolderType, String)>) -> impl Responder { + let (org_id, folder_type, folder_id) = path.into_inner(); + match folders::delete_folder(&org_id, &folder_id, folder_type.into()).await { Ok(()) => HttpResponse::Ok().body("Folder deleted"), Err(err) => err.into(), } } + +/// Deprecated folder endpoints. +pub mod deprecated { + use super::*; + + /// CreateFolder + #[deprecated] + #[utoipa::path( + context_path = "/api", + tag = "Folders", + operation_id = "CreateFolder", + security( + ("Authorization" = []) + ), + params( + ("org_id" = String, Path, description = "Organization name"), + ), + request_body( + content = CreateFolderRequestBody, + description = "Folder details", + example = json!({ + "name": "Infrastructure", + "description": "Traffic patterns and network performance of the infrastructure", + }), + ), + responses( + (status = StatusCode::OK, description = "Folder created", body = CreateFolderResponseBody), + (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Internal Server Error", body = HttpResponse), + ), + )] + #[post("/{org_id}/folders")] + pub async fn create_folder( + path: web::Path, + body: web::Json, + ) -> impl Responder { + let org_id = path.into_inner(); + let folder = body.into_inner().into(); + let folder_type = infra::table::folders::FolderType::Dashboards; + match folders::save_folder(&org_id, folder, folder_type, false).await { + Ok(folder) => { + let body: CreateFolderResponseBody = folder.into(); + HttpResponse::Ok().json(body) + } + Err(err) => err.into(), + } + } + + /// UpdateFolder + #[deprecated] + #[utoipa::path( + context_path = "/api", + tag = "Folders", + operation_id = "UpdateFolder", + security( + ("Authorization" = []) + ), + params( + ("org_id" = String, Path, description = "Organization name"), + ("folder_id" = String, Path, description = "Folder name"), + ), + request_body( + content = Folder, + description = "Folder details", + example = json!({ + "title": "Infra", + "description": "Traffic patterns and network performance of the infrastructure", + }), + ), + responses( + (status = StatusCode::OK, description = "Folder updated", body = HttpResponse), + (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Internal Server Error", body = HttpResponse), + ), + )] + #[put("/{org_id}/folders/{folder_id}")] + pub async fn update_folder( + path: web::Path<(String, String)>, + body: web::Json, + ) -> impl Responder { + let (org_id, folder_id) = path.into_inner(); + let folder = body.into_inner().into(); + let folder_type = infra::table::folders::FolderType::Dashboards; + match folders::update_folder(&org_id, &folder_id, folder_type, folder).await { + Ok(_) => HttpResponse::Ok().body("Folder updated"), + Err(err) => err.into(), + } + } + + /// ListFolders + #[deprecated] + #[utoipa::path( + context_path = "/api", + tag = "Folders", + operation_id = "ListFolders", + security( + ("Authorization" = []) + ), + params( + ("org_id" = String, Path, description = "Organization name"), + ), + responses( + (status = StatusCode::OK, body = ListFoldersResponseBody), + ), + )] + #[get("/{org_id}/folders")] + #[allow(unused_variables)] + pub async fn list_folders(path: web::Path, req: HttpRequest) -> impl Responder { + let org_id = path.into_inner(); + + #[cfg(not(feature = "enterprise"))] + let user_id = None; + + #[cfg(feature = "enterprise")] + let Ok(user_id) = req.headers().get("user_id").map(|v| v.to_str()).transpose() else { + return HttpResponse::Forbidden().finish(); + }; + + let folder_type = infra::table::folders::FolderType::Dashboards; + match folders::list_folders(&org_id, user_id, folder_type).await { + Ok(folders) => { + let body: ListFoldersResponseBody = folders.into(); + HttpResponse::Ok().json(body) + } + Err(err) => err.into(), + } + } + + /// GetFolder + #[deprecated] + #[utoipa::path( + context_path = "/api", + tag = "Folders", + operation_id = "GetFolder", + security( + ("Authorization" = []) + ), + params( + ("org_id" = String, Path, description = "Organization name"), + ("folder_id" = String, Path, description = "Folder ID"), + ), + responses( + (status = StatusCode::OK, body = GetFolderResponseBody), + (status = StatusCode::NOT_FOUND, description = "Folder not found", body = HttpResponse), + ), + )] + #[get("/{org_id}/folders/{folder_id}")] + pub async fn get_folder(path: web::Path<(String, String)>) -> impl Responder { + let (org_id, folder_id) = path.into_inner(); + let folder_type = infra::table::folders::FolderType::Dashboards; + match folders::get_folder(&org_id, &folder_id, folder_type).await { + Ok(folder) => { + let body: CreateFolderResponseBody = folder.into(); + HttpResponse::Ok().json(body) + } + Err(err) => err.into(), + } + } + + /// DeleteFolder + #[deprecated] + #[utoipa::path( + context_path = "/api", + tag = "Folders", + operation_id = "DeleteFolder", + security( + ("Authorization" = []) + ), + params( + ("org_id" = String, Path, description = "Organization name"), + ("folder_id" = String, Path, description = "Folder ID"), + ), + responses( + (status = StatusCode::OK, description = "Success", body = HttpResponse), + (status = StatusCode::NOT_FOUND, description = "NotFound", body = HttpResponse), + (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Error", body = HttpResponse), + ), + )] + #[delete("/{org_id}/folders/{folder_id}")] + async fn delete_folder(path: web::Path<(String, String)>) -> impl Responder { + let (org_id, folder_id) = path.into_inner(); + let folder_type = infra::table::folders::FolderType::Dashboards; + match folders::delete_folder(&org_id, &folder_id, folder_type).await { + Ok(()) => HttpResponse::Ok().body("Folder deleted"), + Err(err) => err.into(), + } + } +} diff --git a/src/handler/http/request/mod.rs b/src/handler/http/request/mod.rs index 6982de31da..3ec7d76730 100644 --- a/src/handler/http/request/mod.rs +++ b/src/handler/http/request/mod.rs @@ -18,6 +18,7 @@ pub mod authz; pub mod clusters; pub mod dashboards; pub mod enrichment_table; +#[allow(deprecated)] pub mod folders; pub mod functions; pub mod kv; diff --git a/src/handler/http/router/mod.rs b/src/handler/http/router/mod.rs index 1ebf2afbbc..097a13de2c 100644 --- a/src/handler/http/router/mod.rs +++ b/src/handler/http/router/mod.rs @@ -403,11 +403,6 @@ pub fn get_service_routes(cfg: &mut web::ServiceConfig) { .service(search::saved_view::get_view) .service(search::saved_view::get_views) .service(search::saved_view::delete_view) - .service(folders::create_folder) - .service(folders::list_folders) - .service(folders::update_folder) - .service(folders::get_folder) - .service(folders::delete_folder) .service(functions::save_function) .service(functions::list_functions) .service(functions::delete_function) @@ -426,6 +421,16 @@ pub fn get_service_routes(cfg: &mut web::ServiceConfig) { .service(dashboards::reports::delete_report) .service(dashboards::reports::enable_report) .service(dashboards::reports::trigger_report) + .service(folders::create_folder) + .service(folders::list_folders) + .service(folders::update_folder) + .service(folders::get_folder) + .service(folders::delete_folder) + .service(folders::deprecated::create_folder) + .service(folders::deprecated::list_folders) + .service(folders::deprecated::update_folder) + .service(folders::deprecated::get_folder) + .service(folders::deprecated::delete_folder) .service(alerts::alert::save_alert) .service(alerts::alert::update_alert) .service(alerts::alert::get_alert) diff --git a/src/handler/http/router/openapi.rs b/src/handler/http/router/openapi.rs index dca7982c04..942da58148 100644 --- a/src/handler/http/router/openapi.rs +++ b/src/handler/http/router/openapi.rs @@ -75,6 +75,11 @@ use crate::{common::meta, handler::http::request}; request::folders::list_folders, request::folders::get_folder, request::folders::update_folder, + request::folders::deprecated::delete_folder, + request::folders::deprecated::create_folder, + request::folders::deprecated::list_folders, + request::folders::deprecated::get_folder, + request::folders::deprecated::update_folder, request::functions::list_functions, request::functions::update_function, request::functions::save_function, @@ -171,6 +176,7 @@ use crate::{common::meta, handler::http::request}; crate::handler::http::models::folders::GetFolderResponseBody, crate::handler::http::models::folders::ListFoldersResponseBody, crate::handler::http::models::folders::UpdateFolderRequestBody, + crate::handler::http::models::folders::FolderType, config::meta::function::Transform, config::meta::function::FunctionList, config::meta::function::StreamOrder,