diff --git a/src/cli/basic/cli.rs b/src/cli/basic/cli.rs
index 4f64470ac8..edec397bbf 100644
--- a/src/cli/basic/cli.rs
+++ b/src/cli/basic/cli.rs
@@ -14,7 +14,7 @@
// along with this program. If not, see .
use config::utils::file::set_permission;
-use infra::file_list as infra_file_list;
+use infra::{file_list as infra_file_list, table};
use crate::{
cli::data::{
@@ -172,7 +172,7 @@ pub async fn cli() -> Result {
db::alerts::alert::reset().await?;
}
"dashboard" => {
- db::dashboards::reset().await?;
+ table::dashboards::delete_all().await?;
}
"report" => {
db::dashboards::reports::reset().await?;
diff --git a/src/common/migration/dashboards.rs b/src/common/migration/dashboards.rs
index 2d5da4bfb8..5f58f68c4b 100644
--- a/src/common/migration/dashboards.rs
+++ b/src/common/migration/dashboards.rs
@@ -27,7 +27,7 @@ use infra::{
},
};
-use crate::service::{db::dashboards::get_query_variables, stream::save_stream_settings};
+use crate::service::stream::save_stream_settings;
type SettingsCache = HashMap<(String, String, StreamType), StreamSettings>;
@@ -143,3 +143,54 @@ pub async fn run() -> Result<(), anyhow::Error> {
Ok(())
}
+
+macro_rules! _get_variables {
+ ($map:ident, $dash:ident) => {
+ if let Some(vars) = &$dash.variables {
+ for v in vars.list.iter() {
+ if let Some(ref qd) = v.query_data {
+ $map.entry((qd.stream.clone(), qd.stream_type))
+ .or_default()
+ .push(qd.field.clone());
+ }
+ }
+ }
+ };
+}
+
+fn get_query_variables(
+ dashboard: Option<&Dashboard>,
+) -> HashMap<(String, StreamType), Vec> {
+ let mut map: HashMap<(String, StreamType), Vec> = HashMap::new();
+ let dashboard = if let Some(d) = dashboard {
+ d
+ } else {
+ return map;
+ };
+ match dashboard.version {
+ 1 => {
+ let dash = dashboard.v1.as_ref().unwrap();
+ _get_variables!(map, dash);
+ }
+ 2 => {
+ let dash = dashboard.v2.as_ref().unwrap();
+ _get_variables!(map, dash);
+ }
+ 3 => {
+ let dash = dashboard.v3.as_ref().unwrap();
+ _get_variables!(map, dash);
+ }
+ 4 => {
+ let dash = dashboard.v4.as_ref().unwrap();
+ _get_variables!(map, dash);
+ }
+ 5 => {
+ let dash = dashboard.v5.as_ref().unwrap();
+ _get_variables!(map, dash);
+ }
+ _ => {
+ unreachable!("we only have 5 dashboard versions")
+ }
+ }
+ map
+}
diff --git a/src/handler/http/request/dashboards/mod.rs b/src/handler/http/request/dashboards/mod.rs
index 5e4009b11f..8b49e39a0e 100644
--- a/src/handler/http/request/dashboards/mod.rs
+++ b/src/handler/http/request/dashboards/mod.rs
@@ -13,16 +13,38 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::{collections::HashMap, io::Error};
+use std::collections::HashMap;
use actix_web::{delete, get, http, post, put, web, HttpRequest, HttpResponse, Responder};
-use config::meta::dashboards::{ListDashboardsParams, MoveDashboard};
+use config::{
+ meta::dashboards::{v1, v2, v3, v4, v5, Dashboard, ListDashboardsParams, MoveDashboard},
+ utils::json,
+};
use serde::Deserialize;
-use crate::{common::meta::http::HttpResponse as MetaHttpResponse, service::dashboards};
+use crate::{
+ common::meta::http::HttpResponse as MetaHttpResponse,
+ service::dashboards::{self, DashboardError},
+};
pub mod reports;
+impl From for HttpResponse {
+ fn from(value: DashboardError) -> Self {
+ match value {
+ DashboardError::InfraError(err) => MetaHttpResponse::internal_error(err),
+ DashboardError::DashboardNotFound => MetaHttpResponse::not_found("Dashboard not found"),
+ DashboardError::UpdateMissingHash => MetaHttpResponse::internal_error("Request to update existing dashboard with missing or invalid hash value. BUG"),
+ DashboardError::UpdateConflictingHash => MetaHttpResponse::conflict("Conflict: Failed to save due to concurrent changes. Please refresh the page after backing up your work to avoid losing changes."),
+ DashboardError::PutMissingTitle => MetaHttpResponse::internal_error("Dashboard should have title"),
+ DashboardError::MoveMissingFolderParam => MetaHttpResponse::bad_request("Please specify from & to folder from dashboard movement"),
+ DashboardError::MoveDestinationFolderNotFound => MetaHttpResponse::not_found("Folder not found"),
+ DashboardError::CreateFolderNotFound => MetaHttpResponse::not_found("Folder not found"),
+ DashboardError::CreateDefaultFolder => MetaHttpResponse::internal_error("Error saving default folder"),
+ }
+ }
+}
+
/// CreateDashboard
#[utoipa::path(
context_path = "/api",
@@ -52,14 +74,17 @@ pub async fn create_dashboard(
path: web::Path,
body: web::Bytes,
req: HttpRequest,
-) -> Result {
+) -> impl Responder {
let org_id = path.into_inner();
let folder = get_folder(req);
- let resp = match dashboards::create_dashboard(&org_id, &folder, body).await {
- Ok(resp) => resp,
- Err(_) => HttpResponse::InternalServerError().into(),
+ let dashboard = match parse_dashboard(body) {
+ Ok(dashboard) => dashboard,
+ Err(err) => return MetaHttpResponse::bad_request(err),
};
- Ok(resp)
+ match dashboards::create_dashboard(&org_id, &folder, dashboard).await {
+ Ok(dashboard) => HttpResponse::Ok().json(dashboard),
+ Err(err) => err.into(),
+ }
}
/// UpdateDashboard
@@ -94,7 +119,14 @@ async fn update_dashboard(
let query = web::Query::>::from_query(req.query_string()).unwrap();
let folder = crate::common::utils::http::get_folder(&query);
let hash = query.get("hash").map(|h| h.as_str());
- dashboards::update_dashboard(&org_id, &dashboard_id, &folder, body, hash).await
+ let dashboard = match parse_dashboard(body) {
+ Ok(dashboard) => dashboard,
+ Err(err) => return MetaHttpResponse::bad_request(err),
+ };
+ match dashboards::update_dashboard(&org_id, &dashboard_id, &folder, dashboard, hash).await {
+ Ok(dashboard) => HttpResponse::Ok().json(dashboard),
+ Err(err) => err.into(),
+ }
}
/// HTTP URL query component that contains parameters for listing dashboards.
@@ -164,11 +196,16 @@ impl ListQuery {
#[get("/{org_id}/dashboards")]
async fn list_dashboards(org_id: web::Path, req: HttpRequest) -> impl Responder {
let Ok(query) = web::Query::::from_query(req.query_string()) else {
- return Ok(HttpResponse::BadRequest().into());
+ return MetaHttpResponse::bad_request("Error parsing query parameters");
};
-
let params = query.into_inner().into(&org_id.into_inner());
- dashboards::list_dashboards(params).await
+ match dashboards::list_dashboards(params).await {
+ Ok(dashboards) => {
+ let list = config::meta::dashboards::Dashboards { dashboards };
+ HttpResponse::Ok().json(list)
+ }
+ Err(err) => err.into(),
+ }
}
/// GetDashboard
@@ -192,7 +229,10 @@ async fn list_dashboards(org_id: web::Path, req: HttpRequest) -> impl Re
async fn get_dashboard(path: web::Path<(String, String)>, req: HttpRequest) -> impl Responder {
let (org_id, dashboard_id) = path.into_inner();
let folder = get_folder(req);
- dashboards::get_dashboard(&org_id, &dashboard_id, &folder).await
+ match dashboards::get_dashboard(&org_id, &dashboard_id, &folder).await {
+ Ok(dashboard) => HttpResponse::Ok().json(dashboard),
+ Err(err) => err.into(),
+ }
}
/// DeleteDashboard
@@ -217,7 +257,13 @@ async fn get_dashboard(path: web::Path<(String, String)>, req: HttpRequest) -> i
async fn delete_dashboard(path: web::Path<(String, String)>, req: HttpRequest) -> impl Responder {
let (org_id, dashboard_id) = path.into_inner();
let folder_id = get_folder(req);
- dashboards::delete_dashboard(&org_id, &dashboard_id, &folder_id).await
+ match dashboards::delete_dashboard(&org_id, &dashboard_id, &folder_id).await {
+ Ok(()) => HttpResponse::Ok().json(MetaHttpResponse::message(
+ http::StatusCode::OK.into(),
+ "Dashboard deleted".to_string(),
+ )),
+ Err(err) => err.into(),
+ }
}
/// MoveDashboard
@@ -249,26 +295,58 @@ async fn delete_dashboard(path: web::Path<(String, String)>, req: HttpRequest) -
async fn move_dashboard(
path: web::Path<(String, String)>,
folder: web::Json,
-) -> Result {
+) -> impl Responder {
let (org_id, dashboard_id) = path.into_inner();
- if folder.from.is_empty() || folder.to.is_empty() {
- return Ok(
- HttpResponse::InternalServerError().json(MetaHttpResponse::message(
- http::StatusCode::BAD_REQUEST.into(),
- "Please specify from & to folder from dashboard movement".to_string(),
- )),
- );
- };
-
- let resp =
- match dashboards::move_dashboard(&org_id, &dashboard_id, &folder.from, &folder.to).await {
- Ok(resp) => resp,
- Err(_) => HttpResponse::InternalServerError().into(),
- };
- Ok(resp)
+ match dashboards::move_dashboard(&org_id, &dashboard_id, &folder.from, &folder.to).await {
+ Ok(()) => HttpResponse::Ok().json(MetaHttpResponse::message(
+ http::StatusCode::OK.into(),
+ "Dashboard moved".to_string(),
+ )),
+ Err(err) => err.into(),
+ }
}
fn get_folder(req: HttpRequest) -> String {
let query = web::Query::>::from_query(req.query_string()).unwrap();
crate::common::utils::http::get_folder(&query)
}
+
+/// Parses the bytes into a dashboard with the given version.
+fn parse_dashboard(bytes: web::Bytes) -> Result {
+ let d_version: DashboardVersion = json::from_slice(&bytes)?;
+ let version = d_version.version;
+
+ let dash = match version {
+ 1 => {
+ let inner: v1::Dashboard = json::from_slice(&bytes)?;
+ inner.into()
+ }
+ 2 => {
+ let inner: v2::Dashboard = json::from_slice(&bytes)?;
+ inner.into()
+ }
+ 3 => {
+ let inner: v3::Dashboard = json::from_slice(&bytes)?;
+ inner.into()
+ }
+ 4 => {
+ let inner: v4::Dashboard = json::from_slice(&bytes)?;
+ inner.into()
+ }
+ _ => {
+ let inner: v5::Dashboard = json::from_slice(&bytes)?;
+ inner.into()
+ }
+ };
+ Ok(dash)
+}
+
+#[derive(Clone, Debug, Deserialize)]
+struct DashboardVersion {
+ #[serde(default = "default_version")]
+ pub version: i32,
+}
+
+fn default_version() -> i32 {
+ 1
+}
diff --git a/src/service/dashboards/mod.rs b/src/service/dashboards/mod.rs
index ec3255fb67..aab05245bb 100644
--- a/src/service/dashboards/mod.rs
+++ b/src/service/dashboards/mod.rs
@@ -13,120 +13,139 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use std::io;
-
-use actix_web::{http, web, HttpResponse};
use config::{
ider,
meta::{
- dashboards::{Dashboards, ListDashboardsParams},
+ dashboards::{Dashboard, ListDashboardsParams},
folder::{Folder, DEFAULT_FOLDER},
},
- utils::json,
};
use infra::table;
use super::folders;
-use crate::{
- common::{
- meta::{authz::Authz, http::HttpResponse as MetaHttpResponse},
- utils::auth::{remove_ownership, set_ownership},
- },
- service::db::{self},
+use crate::common::{
+ meta::authz::Authz,
+ utils::auth::{remove_ownership, set_ownership},
};
pub mod reports;
-#[tracing::instrument(skip(body))]
+/// An error that occurs interacting with dashboards.
+#[derive(Debug, thiserror::Error)]
+pub enum DashboardError {
+ /// An error that occurs while interacting with the database through the
+ /// [infra] crate.
+ #[error("InfraError# {0}")]
+ InfraError(#[from] infra::errors::Error),
+
+ /// Error that occurs when trying to access a dashboard that cannot be
+ /// found.
+ #[error("dashboard not found")]
+ DashboardNotFound,
+
+ /// Error that occurs when trying to update a dashboard using a missing hash
+ /// value or a hash value that cannot be parsed as an [i64].
+ #[error("tried to update dashboard using hash that cannot be parsed")]
+ UpdateMissingHash,
+
+ /// Error that occurs when trying to update a dashboard using a stale hash.
+ /// This occurs when the two clients attempt to update the dashboard
+ /// concurrently.
+ #[error("tried to update dashboard using conflicting hash")]
+ UpdateConflictingHash,
+
+ /// Error that occurs when trying to create an update a dashboard but not
+ /// title is provided.
+ #[error("dashboard cannot have empty title")]
+ PutMissingTitle,
+
+ /// Error that occurs when trying to move a dashboard but either the source
+ /// or destination folder is not specified.
+ #[error("missing source or destination folder for dashboard move")]
+ MoveMissingFolderParam,
+
+ /// Error that occurs when trying to move a dashboard to a destination
+ /// folder that cannot be found.
+ #[error("error moving dashboard to folder that cannot be found")]
+ MoveDestinationFolderNotFound,
+
+ /// Error that occurs when trying to create a dashboard in a folder that
+ /// cannot be found.
+ #[error("error creating dashboard in folder that cannot be found")]
+ CreateFolderNotFound,
+
+ /// Error that occurs when trying to create a destination folder for a new
+ /// dashboard.
+ #[error("error creating default folder")]
+ CreateDefaultFolder,
+}
+
+#[tracing::instrument(skip(dashboard))]
pub async fn create_dashboard(
org_id: &str,
folder_id: &str,
- body: web::Bytes,
-) -> Result {
+ dashboard: Dashboard,
+) -> Result {
// NOTE: Overwrite whatever `dashboard_id` the client has sent us
// If folder is default folder & doesn't exist then create it
if table::folders::exists(org_id, folder_id).await? {
let dashboard_id = ider::generate();
- match save_dashboard(org_id, &dashboard_id, folder_id, body, None).await {
- Ok(res) => {
- set_ownership(
- org_id,
- "dashboards",
- Authz {
- obj_id: dashboard_id,
- parent_type: "folders".to_owned(),
- parent: folder_id.to_owned(),
- },
- )
- .await;
- Ok(res)
- }
- Err(error) => Ok(
- HttpResponse::InternalServerError().json(MetaHttpResponse::message(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- error.to_string(),
- )),
- ),
- }
+ let saved = put(org_id, &dashboard_id, folder_id, dashboard, None).await?;
+ set_ownership(
+ org_id,
+ "dashboards",
+ Authz {
+ obj_id: dashboard_id,
+ parent_type: "folders".to_owned(),
+ parent: folder_id.to_owned(),
+ },
+ )
+ .await;
+ Ok(saved)
} else if folder_id == DEFAULT_FOLDER {
let folder = Folder {
folder_id: DEFAULT_FOLDER.to_string(),
name: DEFAULT_FOLDER.to_string(),
description: DEFAULT_FOLDER.to_string(),
};
- folders::save_folder(org_id, folder, true).await?;
+ folders::save_folder(org_id, folder, true)
+ .await
+ .map_err(|_| DashboardError::CreateDefaultFolder)?;
let dashboard_id = ider::generate();
- match save_dashboard(org_id, &dashboard_id, folder_id, body, None).await {
- Ok(res) => {
- set_ownership(
- org_id,
- "dashboards",
- Authz {
- obj_id: dashboard_id,
- parent_type: "folders".to_owned(),
- parent: folder_id.to_owned(),
- },
- )
- .await;
- Ok(res)
- }
- Err(error) => Ok(
- HttpResponse::InternalServerError().json(MetaHttpResponse::message(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- error.to_string(),
- )),
- ),
- }
+ let saved = put(org_id, &dashboard_id, folder_id, dashboard, None).await?;
+ set_ownership(
+ org_id,
+ "dashboards",
+ Authz {
+ obj_id: dashboard_id,
+ parent_type: "folders".to_owned(),
+ parent: folder_id.to_owned(),
+ },
+ )
+ .await;
+ Ok(saved)
} else {
- Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
- http::StatusCode::NOT_FOUND.into(),
- "folder not found".to_string(),
- )))
+ Err(DashboardError::CreateFolderNotFound)
}
}
-#[tracing::instrument(skip(body))]
+#[tracing::instrument(skip(dashboard))]
pub async fn update_dashboard(
org_id: &str,
dashboard_id: &str,
folder_id: &str,
- body: web::Bytes,
+ dashboard: Dashboard,
hash: Option<&str>,
-) -> Result {
- // Store new dashboard in the database
- save_dashboard(org_id, dashboard_id, folder_id, body, hash).await
+) -> Result {
+ put(org_id, dashboard_id, folder_id, dashboard, hash).await
}
#[tracing::instrument]
-pub async fn list_dashboards(params: ListDashboardsParams) -> Result {
- let resp = match db::dashboards::list(params).await {
- Ok(dashboards) => HttpResponse::Ok().json(Dashboards { dashboards }),
- Err(error) => HttpResponse::InternalServerError().json(MetaHttpResponse::error(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- error.to_string(),
- )),
- };
- Ok(resp)
+pub async fn list_dashboards(
+ params: ListDashboardsParams,
+) -> Result, DashboardError> {
+ let dashboards = table::dashboards::list(params).await?;
+ Ok(dashboards)
}
#[tracing::instrument]
@@ -134,16 +153,10 @@ pub async fn get_dashboard(
org_id: &str,
dashboard_id: &str,
folder_id: &str,
-) -> Result {
- let resp = match db::dashboards::get(org_id, dashboard_id, folder_id).await {
- Ok(Some(dash)) => HttpResponse::Ok().json(dash),
- Ok(None) => Response::NotFound("Dashboard".to_string()).into(),
- Err(error) => HttpResponse::InternalServerError().json(MetaHttpResponse::error(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- error.to_string(),
- )),
- };
- Ok(resp)
+) -> Result {
+ table::dashboards::get(org_id, folder_id, dashboard_id)
+ .await?
+ .ok_or(DashboardError::DashboardNotFound)
}
#[tracing::instrument]
@@ -151,72 +164,23 @@ pub async fn delete_dashboard(
org_id: &str,
dashboard_id: &str,
folder_id: &str,
-) -> Result {
- match db::dashboards::get(org_id, dashboard_id, folder_id).await {
- Ok(Some(_)) => {} // Dashboard exists. Continue with deleting.
- Ok(None) => {
- return Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
- http::StatusCode::NOT_FOUND.into(),
- "Dashboard not found".to_string(),
- )));
- }
- Err(error) => {
- return Ok(
- HttpResponse::InternalServerError().json(MetaHttpResponse::error(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- error.to_string(),
- )),
- )
- }
+) -> Result<(), DashboardError> {
+ match table::dashboards::get(org_id, folder_id, dashboard_id).await? {
+ Some(_) => {} // Dashboard exists. Continue with deleting.
+ None => return Err(DashboardError::DashboardNotFound),
};
-
- match db::dashboards::delete(org_id, dashboard_id, folder_id).await {
- Ok(_) => {
- remove_ownership(
- org_id,
- "dashboards",
- Authz {
- obj_id: dashboard_id.to_owned(),
- parent_type: "folders".to_owned(),
- parent: folder_id.to_owned(),
- },
- )
- .await;
- Ok(HttpResponse::Ok().json(MetaHttpResponse::message(
- http::StatusCode::OK.into(),
- "Dashboard deleted".to_string(),
- )))
- }
- Err(error) => Ok(
- HttpResponse::InternalServerError().json(MetaHttpResponse::error(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- error.to_string(),
- )),
- ),
- }
-}
-
-async fn save_dashboard(
- org_id: &str,
- dashboard_id: &str,
- folder_id: &str,
- body: web::Bytes,
- hash: Option<&str>,
-) -> Result {
- match db::dashboards::put(org_id, dashboard_id, folder_id, body, hash).await {
- Ok(dashboard) => {
- tracing::info!(dashboard_id, "Dashboard updated");
- Ok(HttpResponse::Ok().json(dashboard))
- }
- Err(error) => {
- tracing::error!(%error, dashboard_id, "Failed to store the dashboard");
- if error.to_string().contains("Conflict") {
- Ok(Response::Conflict(error).into())
- } else {
- Ok(Response::InternalServerError(error).into())
- }
- }
- }
+ table::dashboards::delete(org_id, folder_id, dashboard_id).await?;
+ remove_ownership(
+ org_id,
+ "dashboards",
+ Authz {
+ obj_id: dashboard_id.to_owned(),
+ parent_type: "folders".to_owned(),
+ parent: folder_id.to_owned(),
+ },
+ )
+ .await;
+ Ok(())
}
#[tracing::instrument]
@@ -225,69 +189,56 @@ pub async fn move_dashboard(
dashboard_id: &str,
from_folder: &str,
to_folder: &str,
-) -> Result {
- if let Ok(Some(dashboard)) = db::dashboards::get(org_id, dashboard_id, from_folder).await {
- // make sure the destination folder exists
- if !table::folders::exists(org_id, to_folder).await? {
- return Ok(Response::NotFound("Destination Folder".to_string()).into());
- }
+) -> Result<(), DashboardError> {
+ if from_folder.is_empty() || to_folder.is_empty() {
+ return Err(DashboardError::MoveMissingFolderParam);
+ };
- let dash = if dashboard.version == 1 {
- json::to_vec(&dashboard.v1.unwrap()).unwrap()
- } else if dashboard.version == 2 {
- json::to_vec(&dashboard.v2.unwrap()).unwrap()
- } else if dashboard.version == 3 {
- json::to_vec(&dashboard.v3.unwrap()).unwrap()
- } else if dashboard.version == 4 {
- json::to_vec(&dashboard.v4.unwrap()).unwrap()
- } else {
- json::to_vec(&dashboard.v5.unwrap()).unwrap()
+ let Some(dashboard) = table::dashboards::get(org_id, from_folder, dashboard_id).await? else {
+ return Err(DashboardError::DashboardNotFound);
+ };
+
+ // make sure the destination folder exists
+ if !table::folders::exists(org_id, to_folder).await? {
+ return Err(DashboardError::MoveDestinationFolderNotFound);
+ };
+
+ // add the dashboard to the destination folder
+ put(org_id, dashboard_id, to_folder, dashboard, None).await?;
+
+ // delete the dashboard from the source folder
+ let _ = table::dashboards::delete(org_id, from_folder, dashboard_id).await;
+
+ Ok(())
+}
+
+#[tracing::instrument(skip(dashboard))]
+async fn put(
+ org_id: &str,
+ dashboard_id: &str,
+ folder_id: &str,
+ mut dashboard: Dashboard,
+ hash: Option<&str>,
+) -> Result {
+ if let Some(existing_dash) = table::dashboards::get(org_id, folder_id, dashboard_id).await? {
+ let existing_dash_hash = existing_dash.hash;
+
+ let Some(Ok(hash_val)) = hash.map(|hash_str| hash_str.parse::()) else {
+ return Err(DashboardError::UpdateMissingHash);
};
-
- // add the dashboard to the destination folder
- if let Err(error) =
- db::dashboards::put(org_id, dashboard_id, to_folder, dash.into(), None).await
- {
- return Ok(Response::InternalServerError(error).into());
+ if hash_val.to_string() != existing_dash_hash {
+ return Err(DashboardError::UpdateConflictingHash);
}
+ };
- // delete the dashboard from the source folder
- let _ = db::dashboards::delete(org_id, dashboard_id, from_folder).await;
- Ok(Response::OkMessage("Dashboard moved successfully".to_string()).into())
- } else {
- Ok(Response::NotFound("Dashboard".to_string()).into())
- }
-}
+ let title = dashboard
+ .title()
+ .map(|t| t.trim().to_string())
+ .and_then(|t| if t.is_empty() { None } else { Some(t) })
+ .ok_or_else(|| DashboardError::PutMissingTitle)?;
+ dashboard.set_title(title);
-#[derive(Debug)]
-enum Response {
- OkMessage(String),
- NotFound(String),
- Conflict(anyhow::Error),
- InternalServerError(anyhow::Error),
-}
-
-impl From for HttpResponse {
- fn from(resp: Response) -> Self {
- match resp {
- Response::OkMessage(message) => Self::Ok().json(MetaHttpResponse::message(
- http::StatusCode::OK.into(),
- message,
- )),
- Response::NotFound(entity) => Self::NotFound().json(MetaHttpResponse::error(
- http::StatusCode::NOT_FOUND.into(),
- format!("{entity} not found"),
- )),
- Response::Conflict(err) => Self::Conflict().json(MetaHttpResponse::error(
- http::StatusCode::CONFLICT.into(),
- err.to_string(),
- )),
- Response::InternalServerError(err) => {
- Self::InternalServerError().json(MetaHttpResponse::error(
- http::StatusCode::INTERNAL_SERVER_ERROR.into(),
- err.to_string(),
- ))
- }
- }
- }
+ dashboard.set_dashboard_id(dashboard_id.to_owned());
+ let dash = table::dashboards::put(org_id, folder_id, dashboard).await?;
+ Ok(dash)
}
diff --git a/src/service/dashboards/reports.rs b/src/service/dashboards/reports.rs
index 5d9e65e6a4..20fcbff3f7 100644
--- a/src/service/dashboards/reports.rs
+++ b/src/service/dashboards/reports.rs
@@ -32,6 +32,7 @@ use config::{
};
use cron::Schedule;
use futures::{future::try_join_all, StreamExt};
+use infra::table;
use lettre::{
message::{header::ContentType, MultiPart, SinglePart},
AsyncTransport, Message,
@@ -141,7 +142,7 @@ pub async fn save(
// Supports only one tab for now
let tab_id = &dashboard.tabs[0];
tasks.push(async move {
- let maybe_dashboard = db::dashboards::get(org_id, dash_id, folder).await?;
+ let maybe_dashboard = table::dashboards::get(org_id, folder, dash_id).await?;
// Check if the tab_id exists
if let Some(dashboard) = maybe_dashboard.and_then(|d| d.v3) {
let mut tab_found = false;
diff --git a/src/service/db/dashboards/mod.rs b/src/service/db/dashboards/mod.rs
index 6318e625c7..136e7fa4fa 100644
--- a/src/service/db/dashboards/mod.rs
+++ b/src/service/db/dashboards/mod.rs
@@ -13,294 +13,4 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-use actix_web::web;
-use config::{
- meta::{
- dashboards::{v1, v2, v3, v4, v5, Dashboard, DashboardVersion, ListDashboardsParams},
- stream::{DistinctField, StreamType},
- },
- utils::json,
-};
-use hashbrown::HashMap;
-use infra::table::{
- dashboards,
- distinct_values::{self, DistinctFieldRecord, OriginType},
-};
-
-use crate::service::stream::save_stream_settings;
-
pub mod reports;
-
-async fn add_distinct_field_entry(
- dashboard_id: &str,
- org: &str,
- stream: &str,
- stype: String,
- field: &str,
-) -> Result<(), anyhow::Error> {
- let record = DistinctFieldRecord::new(
- OriginType::Dashboard,
- dashboard_id,
- org,
- stream,
- stype,
- field,
- );
- distinct_values::add(record)
- .await
- .map_err(|e| anyhow::anyhow!("error in adding distinct value record : {e}"))
-}
-
-async fn remove_distinct_field_entry(
- dashboard_id: &str,
- org: &str,
- stream: &str,
- stype: String,
- field: &str,
-) -> Result<(), anyhow::Error> {
- let record = DistinctFieldRecord::new(
- OriginType::Dashboard,
- dashboard_id,
- org,
- stream,
- stype,
- field,
- );
- distinct_values::remove(record)
- .await
- .map_err(|e| anyhow::anyhow!("error in removing distinct value record : {e}"))
-}
-
-async fn update_distinct_variables(
- org_id: &str,
- old_dash: Option,
- new_dash: &Dashboard,
-) -> Result<(), anyhow::Error> {
- let mut old_variables = get_query_variables(old_dash.as_ref());
- let new_variables = get_query_variables(Some(new_dash));
-
- let dashboard_id = new_dash.dashboard_id().unwrap();
-
- if !new_variables.is_empty() {
- for ((name, typ), fields) in new_variables.into_iter() {
- let mut stream_settings = infra::schema::get_settings(org_id, &name, typ)
- .await
- .unwrap_or_default();
- // we only store distinct values for logs and traces -
- // if anything else, we can ignore.
- if !matches!(typ, StreamType::Logs | StreamType::Traces) {
- continue;
- }
- // get entry from previous variables corresponding to this stream
- let old_fields = old_variables
- .remove(&(name.to_owned(), typ))
- .unwrap_or_default();
- let mut _new_added = false;
-
- for f in fields.iter() {
- // we ignore full text search no matter what
- if stream_settings.full_text_search_keys.contains(f) {
- continue;
- }
- // we add entry for all the fields, because we need mappings for each individual
- // origin-stream-field mapping. The duplicates are handled in add function, so
- // we can call it for each field without issues
- add_distinct_field_entry(dashboard_id, org_id, &name, typ.to_string(), f).await?;
- let _temp = DistinctField {
- name: f.to_owned(),
- added_ts: chrono::Utc::now().timestamp_micros(),
- };
- if !stream_settings.distinct_value_fields.contains(&_temp) {
- stream_settings.distinct_value_fields.push(_temp);
- _new_added = true;
- }
- }
- // here we check if any of the fields used in previous version are no longer used
- // if so, remove their entry.
- for f in old_fields {
- if !fields.contains(&f) {
- remove_distinct_field_entry(dashboard_id, org_id, &name, typ.to_string(), &f)
- .await?;
- }
- }
- if _new_added {
- save_stream_settings(org_id, &name, typ, stream_settings).await?;
- }
- }
-
- // finally, whatever stream remains in the old variables
- // it has all corresponding fields removed, so remove those as well
- for ((name, typ), fields) in old_variables.into_iter() {
- for f in fields {
- remove_distinct_field_entry(dashboard_id, org_id, &name, typ.to_string(), &f)
- .await?;
- }
- }
- } else {
- // I guess all the variables were removed from the dashboard.
- // we can batch remove all entries belonging to this dashboard.
- distinct_values::batch_remove(OriginType::Dashboard, dashboard_id).await?
- }
- Ok(())
-}
-
-macro_rules! _get_variables {
- ($map:ident, $dash:ident) => {
- if let Some(vars) = &$dash.variables {
- for v in vars.list.iter() {
- if let Some(ref qd) = v.query_data {
- $map.entry((qd.stream.clone(), qd.stream_type))
- .or_default()
- .push(qd.field.clone());
- }
- }
- }
- };
-}
-
-pub fn get_query_variables(
- dashboard: Option<&Dashboard>,
-) -> HashMap<(String, StreamType), Vec> {
- let mut map: HashMap<(String, StreamType), Vec> = HashMap::new();
- let dashboard = if let Some(d) = dashboard {
- d
- } else {
- return map;
- };
- match dashboard.version {
- 1 => {
- let dash = dashboard.v1.as_ref().unwrap();
- _get_variables!(map, dash);
- }
- 2 => {
- let dash = dashboard.v2.as_ref().unwrap();
- _get_variables!(map, dash);
- }
- 3 => {
- let dash = dashboard.v3.as_ref().unwrap();
- _get_variables!(map, dash);
- }
- 4 => {
- let dash = dashboard.v4.as_ref().unwrap();
- _get_variables!(map, dash);
- }
- 5 => {
- let dash = dashboard.v5.as_ref().unwrap();
- _get_variables!(map, dash);
- }
- _ => {
- unreachable!("we only have 5 dashboard versions")
- }
- }
- map
-}
-
-#[tracing::instrument]
-pub(crate) async fn get(
- org_id: &str,
- dashboard_id: &str,
- folder_id: &str,
-) -> Result