mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
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.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -45,6 +45,14 @@ pub struct ListFoldersResponseBody {
|
||||
pub list: Vec<Folder>,
|
||||
}
|
||||
|
||||
/// 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<UpdateFolderRequestBody> for config::meta::folder::Folder {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FolderType> for infra::table::folders::FolderType {
|
||||
fn from(value: FolderType) -> Self {
|
||||
match value {
|
||||
FolderType::Dashboards => Self::Dashboards,
|
||||
FolderType::Alerts => Self::Alerts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<config::meta::folder::Folder>> for ListFoldersResponseBody {
|
||||
fn from(value: Vec<config::meta::folder::Folder>) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<FolderError> 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<FolderError> 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<String>,
|
||||
path: web::Path<(String, FolderType)>,
|
||||
body: web::Json<CreateFolderRequestBody>,
|
||||
) -> 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<UpdateFolderRequestBody>,
|
||||
) -> 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<String>, 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<String>, 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<String>, 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<String>, 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<String>,
|
||||
body: web::Json<CreateFolderRequestBody>,
|
||||
) -> 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<UpdateFolderRequestBody>,
|
||||
) -> 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<String>, 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user