diff --git a/src/config/src/meta/dashboards/mod.rs b/src/config/src/meta/dashboards/mod.rs index 40a010cc9e..54cadaa936 100644 --- a/src/config/src/meta/dashboards/mod.rs +++ b/src/config/src/meta/dashboards/mod.rs @@ -201,6 +201,9 @@ pub struct ListDashboardsParams { /// The optional case-insensitive title substring with which to filter /// dashboards. pub title_pat: Option, + + /// The optional page size and page index of results to retrieve. + pub page_size_and_idx: Option<(u64, u64)>, } impl ListDashboardsParams { @@ -211,6 +214,7 @@ impl ListDashboardsParams { org_id: org_id.to_string(), folder_id: None, title_pat: None, + page_size_and_idx: None, } } @@ -228,4 +232,10 @@ impl ListDashboardsParams { self.title_pat = Some(title_pat.to_string()); self } + + /// Paginate the results by the given page size and page index. + pub fn paginate(mut self, page_size: u64, page_idx: u64) -> Self { + self.page_size_and_idx = Some((page_size, page_idx)); + self + } } diff --git a/src/handler/http/models/dashboards.rs b/src/handler/http/models/dashboards.rs index bc636be8e3..7607d29f53 100644 --- a/src/handler/http/models/dashboards.rs +++ b/src/handler/http/models/dashboards.rs @@ -30,6 +30,7 @@ pub struct UpdateDashboardResponseBody(DashboardDetails); /// HTTP URL query component that contains parameters for listing dashboards. #[derive(Debug, Deserialize, utoipa::IntoParams)] #[into_params(style = Form, parameter_in = Query)] +#[serde(rename_all = "camelCase")] pub struct ListDashboardsQuery { /// Optional folder ID filter parameter /// @@ -43,6 +44,13 @@ pub struct ListDashboardsQuery { /// The optional case-insensitive title substring with which to filter /// dashboards. title: Option, + + /// The optional number of dashboards to retrieve. If not set then all + /// dashboards that match the query parameters will be returned. + /// + /// Currently this parameter is only untilized by the API when the `title` + /// parameter is also set. + page_size: Option, } /// HTTP response body for `ListDashboards` endpoint. @@ -137,33 +145,51 @@ impl From for UpdateDashboardResponseBody { impl ListDashboardsQuery { pub fn into(self, org_id: &str) -> config::meta::dashboards::ListDashboardsParams { - match self { + let mut query = match &self { Self { folder: Some(f), title: Some(t), + .. } => config::meta::dashboards::ListDashboardsParams::new(org_id) - .with_folder_id(&f) - .where_title_contains(&t), + .with_folder_id(f) + .where_title_contains(t), Self { folder: None, title: Some(t), + .. } => { - config::meta::dashboards::ListDashboardsParams::new(org_id).where_title_contains(&t) + config::meta::dashboards::ListDashboardsParams::new(org_id).where_title_contains(t) } Self { folder: Some(f), title: None, - } => config::meta::dashboards::ListDashboardsParams::new(org_id).with_folder_id(&f), + .. + } => config::meta::dashboards::ListDashboardsParams::new(org_id).with_folder_id(f), Self { folder: None, title: None, + .. } => { // To preserve backwards-compatability when no filter parameters // are given we will list the contents of the default folder. config::meta::dashboards::ListDashboardsParams::new(org_id) .with_folder_id(config::meta::folder::DEFAULT_FOLDER) } + }; + + // The API currently only supports using page_size to limit the output + // to the top results. And the page_size parameter is only used when the + // title parameter is provided to search dashboards by title pattern. + // When the title parameter is not set we simply want to return all + // dashboards that match the selected folder so we ignore the page_size + // parameter. + if self.title.is_some_and(|t| !t.is_empty()) { + if let Some(page_size) = self.page_size { + query = query.paginate(page_size, 0) + } } + + query } } diff --git a/src/infra/src/table/dashboards.rs b/src/infra/src/table/dashboards.rs index 25ea0f4c92..403d2fb69f 100644 --- a/src/infra/src/table/dashboards.rs +++ b/src/infra/src/table/dashboards.rs @@ -23,7 +23,8 @@ use config::meta::{ }; use sea_orm::{ prelude::Expr, sea_query::Func, ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, - DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder, Set, TryIntoModel, + DatabaseConnection, EntityTrait, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, Set, + TryIntoModel, }; use serde_json::Value as JsonValue; @@ -277,7 +278,8 @@ async fn get_model( Ok(Some((folder, maybe_dashboard))) } -/// Lists all dashboard ORM models that belong to the given org and folder. +/// Lists dashboard ORM models using the given parameters. Returns each +/// dashboard and its parent folder. async fn list_models( db: &DatabaseConnection, params: ListDashboardsParams, @@ -310,13 +312,20 @@ async fn list_models( .order_by_asc(dashboards::Column::Title) .order_by_asc(folders::Column::Name); - let dashboards_and_folders = query - .all(db) - .await? + // Execute the query, either getting all results or a specific page of results. + let results = if let Some((page_size, page_idx)) = params.page_size_and_idx { + query.paginate(db, page_size).fetch_page(page_idx).await? + } else { + query.all(db).await? + }; + + // Flatten the results so that each dashboard is returned alongside its + // parent folder. + let folders_and_dashboards = results .into_iter() .filter_map(|(d, maybe_f)| maybe_f.map(|f| (f, d))) .collect(); - Ok(dashboards_and_folders) + Ok(folders_and_dashboards) } /// Lists all dashboard ORM models that belong to the given org and folder. @@ -410,18 +419,21 @@ mod tests { org_id: "orgId".to_owned(), folder_id: Some("folderId".to_owned()), title_pat: Some("tItLePat".to_owned()), + page_size_and_idx: Some((100, 2)), }; list_models(&db, params).await?; assert_eq!( db.into_transaction_log(), vec![Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "dashboards"."id" AS "A_id", "dashboards"."dashboard_id" AS "A_dashboard_id", "dashboards"."folder_id" AS "A_folder_id", "dashboards"."owner" AS "A_owner", "dashboards"."role" AS "A_role", "dashboards"."title" AS "A_title", "dashboards"."description" AS "A_description", "dashboards"."data" AS "A_data", "dashboards"."version" AS "A_version", "dashboards"."created_at" AS "A_created_at", "folders"."id" AS "B_id", "folders"."org" AS "B_org", "folders"."folder_id" AS "B_folder_id", "folders"."name" AS "B_name", "folders"."description" AS "B_description", "folders"."type" AS "B_type" FROM "dashboards" LEFT JOIN "folders" ON "dashboards"."folder_id" = "folders"."id" WHERE "folders"."org" = $1 AND "folders"."type" = $2 AND "folders"."folder_id" = $3 AND LOWER("title") LIKE $4 ORDER BY "dashboards"."title" ASC, "folders"."name" ASC"#, + r#"SELECT "dashboards"."id" AS "A_id", "dashboards"."dashboard_id" AS "A_dashboard_id", "dashboards"."folder_id" AS "A_folder_id", "dashboards"."owner" AS "A_owner", "dashboards"."role" AS "A_role", "dashboards"."title" AS "A_title", "dashboards"."description" AS "A_description", "dashboards"."data" AS "A_data", "dashboards"."version" AS "A_version", "dashboards"."created_at" AS "A_created_at", "folders"."id" AS "B_id", "folders"."org" AS "B_org", "folders"."folder_id" AS "B_folder_id", "folders"."name" AS "B_name", "folders"."description" AS "B_description", "folders"."type" AS "B_type" FROM "dashboards" LEFT JOIN "folders" ON "dashboards"."folder_id" = "folders"."id" WHERE "folders"."org" = $1 AND "folders"."type" = $2 AND "folders"."folder_id" = $3 AND LOWER("title") LIKE $4 ORDER BY "dashboards"."title" ASC, "folders"."name" ASC LIMIT $5 OFFSET $6"#, [ "orgId".into(), 0i16.into(), "folderId".into(), - "%titlepat%".into() + "%titlepat%".into(), + 100u64.into(), + 200u64.into() ] )] ); @@ -437,18 +449,21 @@ mod tests { org_id: "orgId".to_owned(), folder_id: Some("folderId".to_owned()), title_pat: Some("tItLePat".to_owned()), + page_size_and_idx: Some((100, 2)), }; list_models(&db, params).await?; assert_eq!( db.into_transaction_log(), vec![Transaction::from_sql_and_values( DatabaseBackend::MySql, - r#"SELECT `dashboards`.`id` AS `A_id`, `dashboards`.`dashboard_id` AS `A_dashboard_id`, `dashboards`.`folder_id` AS `A_folder_id`, `dashboards`.`owner` AS `A_owner`, `dashboards`.`role` AS `A_role`, `dashboards`.`title` AS `A_title`, `dashboards`.`description` AS `A_description`, `dashboards`.`data` AS `A_data`, `dashboards`.`version` AS `A_version`, `dashboards`.`created_at` AS `A_created_at`, `folders`.`id` AS `B_id`, `folders`.`org` AS `B_org`, `folders`.`folder_id` AS `B_folder_id`, `folders`.`name` AS `B_name`, `folders`.`description` AS `B_description`, `folders`.`type` AS `B_type` FROM `dashboards` LEFT JOIN `folders` ON `dashboards`.`folder_id` = `folders`.`id` WHERE `folders`.`org` = ? AND `folders`.`type` = ? AND `folders`.`folder_id` = ? AND LOWER(`title`) LIKE ? ORDER BY `dashboards`.`title` ASC, `folders`.`name` ASC"#, + r#"SELECT `dashboards`.`id` AS `A_id`, `dashboards`.`dashboard_id` AS `A_dashboard_id`, `dashboards`.`folder_id` AS `A_folder_id`, `dashboards`.`owner` AS `A_owner`, `dashboards`.`role` AS `A_role`, `dashboards`.`title` AS `A_title`, `dashboards`.`description` AS `A_description`, `dashboards`.`data` AS `A_data`, `dashboards`.`version` AS `A_version`, `dashboards`.`created_at` AS `A_created_at`, `folders`.`id` AS `B_id`, `folders`.`org` AS `B_org`, `folders`.`folder_id` AS `B_folder_id`, `folders`.`name` AS `B_name`, `folders`.`description` AS `B_description`, `folders`.`type` AS `B_type` FROM `dashboards` LEFT JOIN `folders` ON `dashboards`.`folder_id` = `folders`.`id` WHERE `folders`.`org` = ? AND `folders`.`type` = ? AND `folders`.`folder_id` = ? AND LOWER(`title`) LIKE ? ORDER BY `dashboards`.`title` ASC, `folders`.`name` ASC LIMIT ? OFFSET ?"#, [ "orgId".into(), 0i16.into(), "folderId".into(), - "%titlepat%".into() + "%titlepat%".into(), + 100u64.into(), + 200u64.into() ] )] ); @@ -464,18 +479,21 @@ mod tests { org_id: "orgId".to_owned(), folder_id: Some("folderId".to_owned()), title_pat: Some("tItLePat".to_owned()), + page_size_and_idx: Some((100, 2)), }; list_models(&db, params).await?; assert_eq!( db.into_transaction_log(), vec![Transaction::from_sql_and_values( DatabaseBackend::Sqlite, - r#"SELECT "dashboards"."id" AS "A_id", "dashboards"."dashboard_id" AS "A_dashboard_id", "dashboards"."folder_id" AS "A_folder_id", "dashboards"."owner" AS "A_owner", "dashboards"."role" AS "A_role", "dashboards"."title" AS "A_title", "dashboards"."description" AS "A_description", "dashboards"."data" AS "A_data", "dashboards"."version" AS "A_version", "dashboards"."created_at" AS "A_created_at", "folders"."id" AS "B_id", "folders"."org" AS "B_org", "folders"."folder_id" AS "B_folder_id", "folders"."name" AS "B_name", "folders"."description" AS "B_description", "folders"."type" AS "B_type" FROM "dashboards" LEFT JOIN "folders" ON "dashboards"."folder_id" = "folders"."id" WHERE "folders"."org" = ? AND "folders"."type" = ? AND "folders"."folder_id" = ? AND LOWER("title") LIKE ? ORDER BY "dashboards"."title" ASC, "folders"."name" ASC"#, + r#"SELECT "dashboards"."id" AS "A_id", "dashboards"."dashboard_id" AS "A_dashboard_id", "dashboards"."folder_id" AS "A_folder_id", "dashboards"."owner" AS "A_owner", "dashboards"."role" AS "A_role", "dashboards"."title" AS "A_title", "dashboards"."description" AS "A_description", "dashboards"."data" AS "A_data", "dashboards"."version" AS "A_version", "dashboards"."created_at" AS "A_created_at", "folders"."id" AS "B_id", "folders"."org" AS "B_org", "folders"."folder_id" AS "B_folder_id", "folders"."name" AS "B_name", "folders"."description" AS "B_description", "folders"."type" AS "B_type" FROM "dashboards" LEFT JOIN "folders" ON "dashboards"."folder_id" = "folders"."id" WHERE "folders"."org" = ? AND "folders"."type" = ? AND "folders"."folder_id" = ? AND LOWER("title") LIKE ? ORDER BY "dashboards"."title" ASC, "folders"."name" ASC LIMIT ? OFFSET ?"#, [ "orgId".into(), 0i16.into(), "folderId".into(), - "%titlepat%".into() + "%titlepat%".into(), + 100u64.into(), + 200u64.into() ] )] );