From 4a7155e006170677adb9e98bcc6d09995ab3c722 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Tue, 29 Apr 2025 22:21:49 -0400 Subject: [PATCH 01/13] feat: dashboard implementation --- src/handlers/http/users/dashboards.rs | 77 ++++----- src/prism/home/mod.rs | 15 +- src/users/dashboards.rs | 232 +++----------------------- 3 files changed, 63 insertions(+), 261 deletions(-) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 16a885969..fe031c9e0 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -21,7 +21,7 @@ use crate::{ parseable::PARSEABLE, storage::{object_storage::dashboard_path, ObjectStorageError}, users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS}, - utils::{actix::extract_session_key_from_req, get_hash, get_user_from_request}, + utils::{get_hash, get_user_from_request}, }; use actix_web::{ http::header::ContentType, @@ -29,31 +29,25 @@ use actix_web::{ HttpRequest, HttpResponse, Responder, }; use bytes::Bytes; -use rand::distributions::DistString; -use chrono::Utc; use http::StatusCode; use serde_json::Error as SerdeError; +use ulid::Ulid; -pub async fn list(req: HttpRequest) -> Result { - let key = - extract_session_key_from_req(&req).map_err(|e| DashboardError::Custom(e.to_string()))?; - let dashboards = DASHBOARDS.list_dashboards(&key).await; +pub async fn list() -> Result { + let dashboards = DASHBOARDS.list_dashboards().await; Ok((web::Json(dashboards), StatusCode::OK)) } -pub async fn get( - req: HttpRequest, - dashboard_id: Path, -) -> Result { - let user_id = get_user_from_request(&req)?; - let dashboard_id = dashboard_id.into_inner(); +pub async fn get(dashboard_id: Path) -> Result { + let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { + dashboard_id + } else { + return Err(DashboardError::Metadata("Invalid dashboard ID")); + }; - if let Some(dashboard) = DASHBOARDS - .get_dashboard(&dashboard_id, &get_hash(&user_id)) - .await - { + if let Some(dashboard) = DASHBOARDS.get_dashboard(dashboard_id).await { return Ok((web::Json(dashboard), StatusCode::OK)); } @@ -66,20 +60,13 @@ pub async fn post( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = get_hash(Utc::now().timestamp_micros().to_string().as_str()); - dashboard.dashboard_id = Some(dashboard_id.clone()); + let dashboard_id = Ulid::new(); + dashboard.dashboard_id = dashboard_id; dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - dashboard.user_id = Some(user_id.clone()); + dashboard.author = user_id.clone(); for tile in dashboard.tiles.iter_mut() { - tile.tile_id = Some(get_hash( - format!( - "{}{}", - rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 8), - Utc::now().timestamp_micros() - ) - .as_str(), - )); + tile.tile_id = Ulid::new(); } DASHBOARDS.update(&dashboard).await; @@ -101,21 +88,21 @@ pub async fn update( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = dashboard_id.into_inner(); + let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { + dashboard_id + } else { + return Err(DashboardError::Metadata("Invalid dashboard ID")); + }; - if DASHBOARDS - .get_dashboard(&dashboard_id, &user_id) - .await - .is_none() - { + if DASHBOARDS.get_dashboard(dashboard_id).await.is_none() { return Err(DashboardError::Metadata("Dashboard does not exist")); } - dashboard.dashboard_id = Some(dashboard_id.to_string()); - dashboard.user_id = Some(user_id.clone()); + dashboard.dashboard_id = dashboard_id; + dashboard.author = user_id.clone(); dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); for tile in dashboard.tiles.iter_mut() { - if tile.tile_id.is_none() { - tile.tile_id = Some(get_hash(Utc::now().timestamp_micros().to_string().as_str())); + if tile.tile_id.is_nil() { + tile.tile_id = Ulid::new(); } } DASHBOARDS.update(&dashboard).await; @@ -137,19 +124,19 @@ pub async fn delete( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = dashboard_id.into_inner(); - if DASHBOARDS - .get_dashboard(&dashboard_id, &user_id) - .await - .is_none() - { + let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { + dashboard_id + } else { + return Err(DashboardError::Metadata("Invalid dashboard ID")); + }; + if DASHBOARDS.get_dashboard(dashboard_id).await.is_none() { return Err(DashboardError::Metadata("Dashboard does not exist")); } let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); store.delete_object(&path).await?; - DASHBOARDS.delete_dashboard(&dashboard_id).await; + DASHBOARDS.delete_dashboard(dashboard_id).await; Ok(HttpResponse::Ok().finish()) } diff --git a/src/prism/home/mod.rs b/src/prism/home/mod.rs index 39bd5c4e3..bc067f622 100644 --- a/src/prism/home/mod.rs +++ b/src/prism/home/mod.rs @@ -103,7 +103,7 @@ pub async fn generate_home_response(key: &SessionKey) -> Result Result, Pri Ok(correlation_titles) } -async fn get_dashboard_titles(key: &SessionKey) -> Result, PrismHomeError> { +async fn get_dashboard_titles() -> Result, PrismHomeError> { let dashboard_titles = DASHBOARDS - .list_dashboards(key) + .list_dashboards() .await .iter() .map(|dashboard| TitleAndId { - title: dashboard.name.clone(), - id: dashboard - .dashboard_id - .as_ref() - .ok_or_else(|| anyhow::Error::msg("Dashboard ID is null")) - .unwrap() - .clone(), + title: dashboard.title.clone(), + id: dashboard.dashboard_id.to_string(), }) .collect_vec(); diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 8da9f71c0..1d0dfb9a4 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -16,97 +16,40 @@ * */ +use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::sync::RwLock; +use ulid::Ulid; -use crate::{ - migration::to_bytes, - parseable::PARSEABLE, - rbac::map::SessionKey, - storage::object_storage::dashboard_path, - utils::{get_hash, user_auth_for_query}, -}; - -use super::TimeFilter; +use crate::parseable::PARSEABLE; pub static DASHBOARDS: Lazy = Lazy::new(Dashboards::default); -pub const CURRENT_DASHBOARD_VERSION: &str = "v3"; - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct Tiles { - name: String, - pub tile_id: Option, - description: String, - query: String, - order: Option, - visualization: Visualization, -} - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct Visualization { - visualization_type: String, - circular_chart_config: Option, - graph_config: Option, - size: String, - color_config: Vec, - tick_config: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct CircularChartConfig { - name_key: String, - value_key: String, -} - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct GraphConfig { - x_key: String, - y_keys: Vec, - graph_type: Option, - orientation: Option, -} - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -#[serde(rename_all = "lowercase")] -pub enum GraphType { - #[default] - Default, - Stacked, - Percent, -} - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Orientation { - #[default] - Horizontal, - Vertical, -} +pub const CURRENT_DASHBOARD_VERSION: &str = "v1"; #[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct ColorConfig { - field_name: String, - color_palette: String, +pub struct Tile { + pub tile_id: Ulid, + #[serde(flatten)] + pub other_fields: Option>, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct TickConfig { - key: String, - unit: String, +pub struct Layout { + pub tile_id: Option, + #[serde(flatten)] + pub other_fields: Option>, } - #[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Dashboard { pub version: Option, - pub name: String, - description: String, - pub dashboard_id: Option, - pub user_id: Option, - pub time_filter: Option, - refresh_interval: u64, - pub tiles: Vec, + pub title: String, + pub author: String, + pub dashboard_id: Ulid, + pub modified: DateTime, + pub tiles: Vec, + pub layout: Vec, } #[derive(Default, Debug)] @@ -117,56 +60,12 @@ impl Dashboards { let mut this = vec![]; let store = PARSEABLE.storage.get_object_store(); let all_dashboards = store.get_all_dashboards().await.unwrap_or_default(); - for (dashboard_relative_path, dashboards) in all_dashboards { + for (_, dashboards) in all_dashboards { for dashboard in dashboards { if dashboard.is_empty() { continue; } - let mut dashboard_value = serde_json::from_slice::(&dashboard)?; - if let Some(meta) = dashboard_value.clone().as_object() { - let version = meta.get("version").and_then(|version| version.as_str()); - let dashboard_id = meta - .get("dashboard_id") - .and_then(|dashboard_id| dashboard_id.as_str()); - match version { - Some("v1") => { - //delete older version of the dashboard - store.delete_object(&dashboard_relative_path).await?; - - dashboard_value = migrate_v1_v2(dashboard_value); - dashboard_value = migrate_v2_v3(dashboard_value); - let user_id = dashboard_value - .as_object() - .unwrap() - .get("user_id") - .and_then(|user_id| user_id.as_str()); - let path = dashboard_path( - user_id.unwrap(), - &format!("{}.json", dashboard_id.unwrap()), - ); - let dashboard_bytes = to_bytes(&dashboard_value); - store.put_object(&path, dashboard_bytes.clone()).await?; - } - Some("v2") => { - //delete older version of the dashboard - store.delete_object(&dashboard_relative_path).await?; - - dashboard_value = migrate_v2_v3(dashboard_value); - let user_id = dashboard_value - .as_object() - .unwrap() - .get("user_id") - .and_then(|user_id| user_id.as_str()); - let path = dashboard_path( - user_id.unwrap(), - &format!("{}.json", dashboard_id.unwrap()), - ); - let dashboard_bytes = to_bytes(&dashboard_value); - store.put_object(&path, dashboard_bytes.clone()).await?; - } - _ => {} - } - } + let dashboard_value = serde_json::from_slice::(&dashboard)?; if let Ok(dashboard) = serde_json::from_value::(dashboard_value) { this.retain(|d: &Dashboard| d.dashboard_id != dashboard.dashboard_id); this.push(dashboard); @@ -186,107 +85,28 @@ impl Dashboards { s.push(dashboard.clone()); } - pub async fn delete_dashboard(&self, dashboard_id: &str) { + pub async fn delete_dashboard(&self, dashboard_id: Ulid) { let mut s = self.0.write().await; - s.retain(|d| d.dashboard_id != Some(dashboard_id.to_string())); + s.retain(|d| d.dashboard_id != dashboard_id); } - pub async fn get_dashboard(&self, dashboard_id: &str, user_id: &str) -> Option { + pub async fn get_dashboard(&self, dashboard_id: Ulid) -> Option { self.0 .read() .await .iter() - .find(|d| { - d.dashboard_id == Some(dashboard_id.to_string()) - && d.user_id == Some(user_id.to_string()) - }) + .find(|d| d.dashboard_id == dashboard_id) .cloned() } - pub async fn list_dashboards(&self, key: &SessionKey) -> Vec { + pub async fn list_dashboards(&self) -> Vec { let read = self.0.read().await; let mut dashboards = Vec::new(); for d in read.iter() { - let mut skip_dashboard = false; - for tile in d.tiles.iter() { - let query = &tile.query; - match user_auth_for_query(key, query).await { - Ok(_) => {} - Err(_) => { - skip_dashboard = true; - break; - } - } - } - if !skip_dashboard { - dashboards.push(d.clone()); - } + dashboards.push(d.clone()); } dashboards } } - -fn migrate_v1_v2(mut dashboard_meta: Value) -> Value { - let dashboard_meta_map = dashboard_meta.as_object_mut().unwrap(); - let user_id = dashboard_meta_map.get("user_id").unwrap().clone(); - let str_user_id = user_id.as_str().unwrap(); - let user_id_hash = get_hash(str_user_id); - dashboard_meta_map.insert("user_id".to_owned(), Value::String(user_id_hash)); - dashboard_meta_map.insert( - "version".to_owned(), - Value::String(CURRENT_DASHBOARD_VERSION.into()), - ); - let tiles = dashboard_meta_map - .get_mut("tiles") - .unwrap() - .as_array_mut() - .unwrap(); - for tile in tiles.iter_mut() { - let tile_map = tile.as_object_mut().unwrap(); - let visualization = tile_map - .get_mut("visualization") - .unwrap() - .as_object_mut() - .unwrap(); - visualization.insert("tick_config".to_owned(), Value::Array(vec![])); - } - - dashboard_meta -} - -fn migrate_v2_v3(mut dashboard_meta: Value) -> Value { - let dashboard_meta_map = dashboard_meta.as_object_mut().unwrap(); - - dashboard_meta_map.insert( - "version".to_owned(), - Value::String(CURRENT_DASHBOARD_VERSION.into()), - ); - let tiles = dashboard_meta_map - .get_mut("tiles") - .unwrap() - .as_array_mut() - .unwrap(); - for tile in tiles { - let tile_map = tile.as_object_mut().unwrap(); - let visualization = tile_map - .get_mut("visualization") - .unwrap() - .as_object_mut() - .unwrap(); - if visualization.get("graph_config").is_some() - && !visualization.get("graph_config").unwrap().is_null() - { - let graph_config = visualization - .get_mut("graph_config") - .unwrap() - .as_object_mut() - .unwrap(); - graph_config.insert("orientation".to_owned(), Value::String("horizontal".into())); - graph_config.insert("graph_type".to_owned(), Value::String("default".into())); - } - } - - dashboard_meta -} From 8c4c41556f8790610a17dd6a71f8e7823c998c62 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Tue, 29 Apr 2025 22:51:21 -0400 Subject: [PATCH 02/13] add endpoint for add tile to existing dashboard update list dashboards --- src/handlers/http/modal/server.rs | 7 +++ src/handlers/http/users/dashboards.rs | 65 +++++++++++++++++++++++++-- src/users/dashboards.rs | 2 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index 1674c3ee2..014276292 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -275,6 +275,13 @@ impl Server { .authorize(Action::CreateDashboard), ), ) + .service( + web::resource("/add_tile").route( + web::post() + .to(dashboards::add_tile) + .authorize(Action::CreateDashboard), + ), + ) } // get the filters web scope diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index fe031c9e0..ca9f59bf5 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -20,7 +20,7 @@ use crate::{ handlers::http::rbac::RBACError, parseable::PARSEABLE, storage::{object_storage::dashboard_path, ObjectStorageError}, - users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS}, + users::dashboards::{Dashboard, Tile, CURRENT_DASHBOARD_VERSION, DASHBOARDS}, utils::{get_hash, get_user_from_request}, }; use actix_web::{ @@ -30,13 +30,33 @@ use actix_web::{ }; use bytes::Bytes; +use chrono::Utc; use http::StatusCode; -use serde_json::Error as SerdeError; +use serde_json::{Error as SerdeError, Map}; use ulid::Ulid; pub async fn list() -> Result { let dashboards = DASHBOARDS.list_dashboards().await; - + //dashboards list should contain the title, author and modified date + let dashboards: Vec> = dashboards + .iter() + .map(|dashboard| { + let mut map = Map::new(); + map.insert( + "title".to_string(), + serde_json::Value::String(dashboard.title.clone()), + ); + map.insert( + "author".to_string(), + serde_json::Value::String(dashboard.author.clone()), + ); + map.insert( + "modified".to_string(), + serde_json::Value::String(dashboard.modified.unwrap().to_string()), + ); + map + }) + .collect(); Ok((web::Json(dashboards), StatusCode::OK)) } @@ -63,7 +83,7 @@ pub async fn post( let dashboard_id = Ulid::new(); dashboard.dashboard_id = dashboard_id; dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - + dashboard.modified = Some(Utc::now()); dashboard.author = user_id.clone(); for tile in dashboard.tiles.iter_mut() { tile.tile_id = Ulid::new(); @@ -99,6 +119,7 @@ pub async fn update( } dashboard.dashboard_id = dashboard_id; dashboard.author = user_id.clone(); + dashboard.modified = Some(Utc::now()); dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); for tile in dashboard.tiles.iter_mut() { if tile.tile_id.is_nil() { @@ -141,6 +162,42 @@ pub async fn delete( Ok(HttpResponse::Ok().finish()) } +pub async fn add_tile( + req: HttpRequest, + dashboard_id: Path, + Json(mut tile): Json, +) -> Result { + let mut user_id = get_user_from_request(&req)?; + user_id = get_hash(&user_id); + let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { + dashboard_id + } else { + return Err(DashboardError::Metadata("Invalid dashboard ID")); + }; + + let mut dashboard = DASHBOARDS + .get_dashboard(dashboard_id) + .await + .ok_or(DashboardError::Metadata("Dashboard does not exist"))?; + if tile.tile_id.is_nil() { + tile.tile_id = Ulid::new(); + } + dashboard.tiles.push(tile.clone()); + dashboard.modified = Some(Utc::now()); + dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); + DASHBOARDS.update(&dashboard).await; + + let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); + + let store = PARSEABLE.storage.get_object_store(); + let dashboard_bytes = serde_json::to_vec(&dashboard)?; + store + .put_object(&path, Bytes::from(dashboard_bytes)) + .await?; + + Ok((web::Json(dashboard), StatusCode::OK)) +} + #[derive(Debug, thiserror::Error)] pub enum DashboardError { #[error("Failed to connect to storage: {0}")] diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 1d0dfb9a4..f3debe88b 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -47,7 +47,7 @@ pub struct Dashboard { pub title: String, pub author: String, pub dashboard_id: Ulid, - pub modified: DateTime, + pub modified: Option>, pub tiles: Vec, pub layout: Vec, } From 4e4b7608a9c1eb768cb81a480fa12f98c1587313 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 30 Apr 2025 01:31:13 -0400 Subject: [PATCH 03/13] remove layout from dashboard, make fields optional --- src/handlers/http/users/dashboards.rs | 10 +++++----- src/prism/home/mod.rs | 2 +- src/users/dashboards.rs | 16 ++++------------ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index ca9f59bf5..66bb4468b 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -48,7 +48,7 @@ pub async fn list() -> Result { ); map.insert( "author".to_string(), - serde_json::Value::String(dashboard.author.clone()), + serde_json::Value::String(dashboard.author.as_ref().unwrap().clone()), ); map.insert( "modified".to_string(), @@ -81,10 +81,10 @@ pub async fn post( let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); let dashboard_id = Ulid::new(); - dashboard.dashboard_id = dashboard_id; + dashboard.dashboard_id = Some(dashboard_id); dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); dashboard.modified = Some(Utc::now()); - dashboard.author = user_id.clone(); + dashboard.author = Some(user_id.clone()); for tile in dashboard.tiles.iter_mut() { tile.tile_id = Ulid::new(); } @@ -117,8 +117,8 @@ pub async fn update( if DASHBOARDS.get_dashboard(dashboard_id).await.is_none() { return Err(DashboardError::Metadata("Dashboard does not exist")); } - dashboard.dashboard_id = dashboard_id; - dashboard.author = user_id.clone(); + dashboard.dashboard_id = Some(dashboard_id); + dashboard.author = Some(user_id.clone()); dashboard.modified = Some(Utc::now()); dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); for tile in dashboard.tiles.iter_mut() { diff --git a/src/prism/home/mod.rs b/src/prism/home/mod.rs index bc067f622..3f61365d9 100644 --- a/src/prism/home/mod.rs +++ b/src/prism/home/mod.rs @@ -246,7 +246,7 @@ async fn get_dashboard_titles() -> Result, PrismHomeError> { .iter() .map(|dashboard| TitleAndId { title: dashboard.title.clone(), - id: dashboard.dashboard_id.to_string(), + id: dashboard.dashboard_id.as_ref().unwrap().to_string(), }) .collect_vec(); diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index f3debe88b..549197925 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -34,22 +34,14 @@ pub struct Tile { #[serde(flatten)] pub other_fields: Option>, } - -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct Layout { - pub tile_id: Option, - #[serde(flatten)] - pub other_fields: Option>, -} #[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Dashboard { pub version: Option, pub title: String, - pub author: String, - pub dashboard_id: Ulid, + pub author: Option, + pub dashboard_id: Option, pub modified: Option>, pub tiles: Vec, - pub layout: Vec, } #[derive(Default, Debug)] @@ -87,7 +79,7 @@ impl Dashboards { pub async fn delete_dashboard(&self, dashboard_id: Ulid) { let mut s = self.0.write().await; - s.retain(|d| d.dashboard_id != dashboard_id); + s.retain(|d| *d.dashboard_id.as_ref().unwrap() != dashboard_id); } pub async fn get_dashboard(&self, dashboard_id: Ulid) -> Option { @@ -95,7 +87,7 @@ impl Dashboards { .read() .await .iter() - .find(|d| d.dashboard_id == dashboard_id) + .find(|d| *d.dashboard_id.as_ref().unwrap() == dashboard_id) .cloned() } From cd58a466f07229e7e7147813ae474232b1aac161 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 30 Apr 2025 02:37:58 -0400 Subject: [PATCH 04/13] add dashboard type, refactor --- src/handlers/http/users/dashboards.rs | 68 +++------------------- src/users/dashboards.rs | 83 ++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 63 deletions(-) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 66bb4468b..598f10ab7 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -18,9 +18,8 @@ use crate::{ handlers::http::rbac::RBACError, - parseable::PARSEABLE, - storage::{object_storage::dashboard_path, ObjectStorageError}, - users::dashboards::{Dashboard, Tile, CURRENT_DASHBOARD_VERSION, DASHBOARDS}, + storage::ObjectStorageError, + users::dashboards::{Dashboard, Tile, DASHBOARDS}, utils::{get_hash, get_user_from_request}, }; use actix_web::{ @@ -28,9 +27,6 @@ use actix_web::{ web::{self, Json, Path}, HttpRequest, HttpResponse, Responder, }; -use bytes::Bytes; - -use chrono::Utc; use http::StatusCode; use serde_json::{Error as SerdeError, Map}; use ulid::Ulid; @@ -80,24 +76,9 @@ pub async fn post( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = Ulid::new(); - dashboard.dashboard_id = Some(dashboard_id); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - dashboard.modified = Some(Utc::now()); dashboard.author = Some(user_id.clone()); - for tile in dashboard.tiles.iter_mut() { - tile.tile_id = Ulid::new(); - } - DASHBOARDS.update(&dashboard).await; - - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); - - let store = PARSEABLE.storage.get_object_store(); - let dashboard_bytes = serde_json::to_vec(&dashboard)?; - store - .put_object(&path, Bytes::from(dashboard_bytes)) - .await?; + DASHBOARDS.create(&user_id, &mut dashboard).await?; Ok((web::Json(dashboard), StatusCode::OK)) } @@ -113,29 +94,11 @@ pub async fn update( } else { return Err(DashboardError::Metadata("Invalid dashboard ID")); }; - - if DASHBOARDS.get_dashboard(dashboard_id).await.is_none() { - return Err(DashboardError::Metadata("Dashboard does not exist")); - } - dashboard.dashboard_id = Some(dashboard_id); dashboard.author = Some(user_id.clone()); - dashboard.modified = Some(Utc::now()); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - for tile in dashboard.tiles.iter_mut() { - if tile.tile_id.is_nil() { - tile.tile_id = Ulid::new(); - } - } - DASHBOARDS.update(&dashboard).await; - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); - - let store = PARSEABLE.storage.get_object_store(); - let dashboard_bytes = serde_json::to_vec(&dashboard)?; - store - .put_object(&path, Bytes::from(dashboard_bytes)) + DASHBOARDS + .update(&user_id, dashboard_id, &mut dashboard) .await?; - Ok((web::Json(dashboard), StatusCode::OK)) } @@ -150,14 +113,7 @@ pub async fn delete( } else { return Err(DashboardError::Metadata("Invalid dashboard ID")); }; - if DASHBOARDS.get_dashboard(dashboard_id).await.is_none() { - return Err(DashboardError::Metadata("Dashboard does not exist")); - } - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); - let store = PARSEABLE.storage.get_object_store(); - store.delete_object(&path).await?; - - DASHBOARDS.delete_dashboard(dashboard_id).await; + DASHBOARDS.delete_dashboard(&user_id, dashboard_id).await?; Ok(HttpResponse::Ok().finish()) } @@ -183,16 +139,8 @@ pub async fn add_tile( tile.tile_id = Ulid::new(); } dashboard.tiles.push(tile.clone()); - dashboard.modified = Some(Utc::now()); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - DASHBOARDS.update(&dashboard).await; - - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); - - let store = PARSEABLE.storage.get_object_store(); - let dashboard_bytes = serde_json::to_vec(&dashboard)?; - store - .put_object(&path, Bytes::from(dashboard_bytes)) + DASHBOARDS + .update(&user_id, dashboard_id, &mut dashboard) .await?; Ok((web::Json(dashboard), StatusCode::OK)) diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 549197925..fd4471619 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -16,6 +16,7 @@ * */ +use bytes::Bytes; use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -23,11 +24,21 @@ use serde_json::Value; use tokio::sync::RwLock; use ulid::Ulid; -use crate::parseable::PARSEABLE; +use crate::{ + handlers::http::users::dashboards::DashboardError, parseable::PARSEABLE, + storage::object_storage::dashboard_path, +}; pub static DASHBOARDS: Lazy = Lazy::new(Dashboards::default); pub const CURRENT_DASHBOARD_VERSION: &str = "v1"; +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +pub enum DashboardType { + #[default] + Dashboard, + Report, +} + #[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Tile { pub tile_id: Ulid, @@ -41,6 +52,7 @@ pub struct Dashboard { pub author: Option, pub dashboard_id: Option, pub modified: Option>, + dashboard_type: Option, pub tiles: Vec, } @@ -71,15 +83,80 @@ impl Dashboards { Ok(()) } - pub async fn update(&self, dashboard: &Dashboard) { + pub async fn create( + &self, + user_id: &str, + dashboard: &mut Dashboard, + ) -> Result<(), DashboardError> { + let mut s = self.0.write().await; + let dashboard_id = Ulid::new(); + dashboard.author = Some(user_id.to_string()); + dashboard.dashboard_id = Some(dashboard_id); + dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); + dashboard.modified = Some(Utc::now()); + dashboard.dashboard_type = Some(DashboardType::Dashboard); + for tile in dashboard.tiles.iter_mut() { + tile.tile_id = Ulid::new(); + } + s.push(dashboard.clone()); + + let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); + + let store = PARSEABLE.storage.get_object_store(); + let dashboard_bytes = serde_json::to_vec(&dashboard)?; + store + .put_object(&path, Bytes::from(dashboard_bytes)) + .await?; + Ok(()) + } + + pub async fn update( + &self, + user_id: &str, + dashboard_id: Ulid, + dashboard: &mut Dashboard, + ) -> Result<(), DashboardError> { let mut s = self.0.write().await; + if self.get_dashboard(dashboard_id).await.is_none() { + return Err(DashboardError::Metadata("Dashboard does not exist")); + } + dashboard.dashboard_id = Some(dashboard_id); + dashboard.modified = Some(Utc::now()); + dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); + for tile in dashboard.tiles.iter_mut() { + if tile.tile_id.is_nil() { + tile.tile_id = Ulid::new(); + } + } s.retain(|d| d.dashboard_id != dashboard.dashboard_id); s.push(dashboard.clone()); + + let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); + + let store = PARSEABLE.storage.get_object_store(); + let dashboard_bytes = serde_json::to_vec(&dashboard)?; + store + .put_object(&path, Bytes::from(dashboard_bytes)) + .await?; + Ok(()) } - pub async fn delete_dashboard(&self, dashboard_id: Ulid) { + pub async fn delete_dashboard( + &self, + user_id: &str, + dashboard_id: Ulid, + ) -> Result<(), DashboardError> { let mut s = self.0.write().await; + + if self.get_dashboard(dashboard_id).await.is_none() { + return Err(DashboardError::Metadata("Dashboard does not exist")); + } s.retain(|d| *d.dashboard_id.as_ref().unwrap() != dashboard_id); + let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); + let store = PARSEABLE.storage.get_object_store(); + store.delete_object(&path).await?; + + Ok(()) } pub async fn get_dashboard(&self, dashboard_id: Ulid) -> Option { From 4f3d432694f34148a5ad6b614cbb0d2bd3c33513 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 30 Apr 2025 02:59:06 -0400 Subject: [PATCH 05/13] dashboard id in list response --- src/handlers/http/users/dashboards.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 598f10ab7..40ba66ab2 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -50,6 +50,10 @@ pub async fn list() -> Result { "modified".to_string(), serde_json::Value::String(dashboard.modified.unwrap().to_string()), ); + map.insert( + "dashboard_id".to_string(), + serde_json::Value::String(dashboard.dashboard_id.unwrap().to_string()), + ); map }) .collect(); From 886c1e082753e44a58fb2b9d0c6909d78ffd5a9a Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 30 Apr 2025 06:21:43 -0400 Subject: [PATCH 06/13] fix write lock issue, remove tile id generation --- src/handlers/http/modal/server.rs | 46 ++++++++++--------- src/handlers/http/users/dashboards.rs | 7 +-- src/users/dashboards.rs | 66 ++++++++++++++++++--------- 3 files changed, 71 insertions(+), 48 deletions(-) diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index 014276292..dff23d990 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -241,7 +241,6 @@ impl Server { ) } - // get the dashboards web scope pub fn get_dashboards_webscope() -> Scope { web::scope("/dashboards") .service( @@ -258,30 +257,33 @@ impl Server { ), ) .service( - web::resource("/{dashboard_id}") - .route( - web::get() - .to(dashboards::get) - .authorize(Action::GetDashboard), - ) - .route( - web::delete() - .to(dashboards::delete) - .authorize(Action::DeleteDashboard), + web::scope("/{dashboard_id}") + .service( + web::resource("") + .route( + web::get() + .to(dashboards::get) + .authorize(Action::GetDashboard), + ) + .route( + web::delete() + .to(dashboards::delete) + .authorize(Action::DeleteDashboard), + ) + .route( + web::put() + .to(dashboards::update) + .authorize(Action::CreateDashboard), + ), ) - .route( - web::put() - .to(dashboards::update) - .authorize(Action::CreateDashboard), + .service( + web::resource("/add_tile").route( + web::put() + .to(dashboards::add_tile) + .authorize(Action::CreateDashboard), + ), ), ) - .service( - web::resource("/add_tile").route( - web::post() - .to(dashboards::add_tile) - .authorize(Action::CreateDashboard), - ), - ) } // get the filters web scope diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 40ba66ab2..52b12576f 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -125,7 +125,7 @@ pub async fn delete( pub async fn add_tile( req: HttpRequest, dashboard_id: Path, - Json(mut tile): Json, + Json(tile): Json, ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); @@ -139,10 +139,7 @@ pub async fn add_tile( .get_dashboard(dashboard_id) .await .ok_or(DashboardError::Metadata("Dashboard does not exist"))?; - if tile.tile_id.is_nil() { - tile.tile_id = Ulid::new(); - } - dashboard.tiles.push(tile.clone()); + dashboard.tiles.as_mut().unwrap().push(tile.clone()); DASHBOARDS .update(&user_id, dashboard_id, &mut dashboard) .await?; diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index fd4471619..ec1c87448 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -53,7 +53,7 @@ pub struct Dashboard { pub dashboard_id: Option, pub modified: Option>, dashboard_type: Option, - pub tiles: Vec, + pub tiles: Option>, } #[derive(Default, Debug)] @@ -88,18 +88,12 @@ impl Dashboards { user_id: &str, dashboard: &mut Dashboard, ) -> Result<(), DashboardError> { - let mut s = self.0.write().await; let dashboard_id = Ulid::new(); dashboard.author = Some(user_id.to_string()); dashboard.dashboard_id = Some(dashboard_id); dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); dashboard.modified = Some(Utc::now()); dashboard.dashboard_type = Some(DashboardType::Dashboard); - for tile in dashboard.tiles.iter_mut() { - tile.tile_id = Ulid::new(); - } - s.push(dashboard.clone()); - let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); @@ -107,6 +101,9 @@ impl Dashboards { store .put_object(&path, Bytes::from(dashboard_bytes)) .await?; + + self.0.write().await.push(dashboard.clone()); + Ok(()) } @@ -116,20 +113,18 @@ impl Dashboards { dashboard_id: Ulid, dashboard: &mut Dashboard, ) -> Result<(), DashboardError> { - let mut s = self.0.write().await; - if self.get_dashboard(dashboard_id).await.is_none() { + if self + .get_dashboard_by_user(dashboard_id, user_id) + .await + .is_none() + { return Err(DashboardError::Metadata("Dashboard does not exist")); } + dashboard.author = Some(user_id.to_string()); dashboard.dashboard_id = Some(dashboard_id); - dashboard.modified = Some(Utc::now()); dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - for tile in dashboard.tiles.iter_mut() { - if tile.tile_id.is_nil() { - tile.tile_id = Ulid::new(); - } - } - s.retain(|d| d.dashboard_id != dashboard.dashboard_id); - s.push(dashboard.clone()); + dashboard.modified = Some(Utc::now()); + dashboard.dashboard_type = Some(DashboardType::Dashboard); let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); @@ -138,6 +133,13 @@ impl Dashboards { store .put_object(&path, Bytes::from(dashboard_bytes)) .await?; + + self.0 + .write() + .await + .retain(|d| d.dashboard_id != dashboard.dashboard_id); + self.0.write().await.push(dashboard.clone()); + Ok(()) } @@ -146,15 +148,21 @@ impl Dashboards { user_id: &str, dashboard_id: Ulid, ) -> Result<(), DashboardError> { - let mut s = self.0.write().await; - - if self.get_dashboard(dashboard_id).await.is_none() { + if self + .get_dashboard_by_user(dashboard_id, user_id) + .await + .is_none() + { return Err(DashboardError::Metadata("Dashboard does not exist")); } - s.retain(|d| *d.dashboard_id.as_ref().unwrap() != dashboard_id); + let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); store.delete_object(&path).await?; + self.0 + .write() + .await + .retain(|d| *d.dashboard_id.as_ref().unwrap() != dashboard_id); Ok(()) } @@ -168,6 +176,22 @@ impl Dashboards { .cloned() } + pub async fn get_dashboard_by_user( + &self, + dashboard_id: Ulid, + user_id: &str, + ) -> Option { + self.0 + .read() + .await + .iter() + .find(|d| { + *d.dashboard_id.as_ref().unwrap() == dashboard_id + && d.author == Some(user_id.to_string()) + }) + .cloned() + } + pub async fn list_dashboards(&self) -> Vec { let read = self.0.read().await; From 9d1419cd28c448d83df77076397b9207e96814e1 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Thu, 1 May 2025 06:26:24 -0400 Subject: [PATCH 07/13] get dashboard by user to add tile --- src/handlers/http/users/dashboards.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 52b12576f..fe4d79e51 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -136,7 +136,7 @@ pub async fn add_tile( }; let mut dashboard = DASHBOARDS - .get_dashboard(dashboard_id) + .get_dashboard_by_user(dashboard_id, &user_id) .await .ok_or(DashboardError::Metadata("Dashboard does not exist"))?; dashboard.tiles.as_mut().unwrap().push(tile.clone()); From 40fd7a141765fe390f00c70d7fe25123a07f0cf1 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Thu, 1 May 2025 07:24:49 -0400 Subject: [PATCH 08/13] refactor --- src/handlers/http/users/dashboards.rs | 82 +++++++++++++++------------ src/users/dashboards.rs | 27 ++++++--- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index fe4d79e51..39567e65a 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -19,7 +19,7 @@ use crate::{ handlers::http::rbac::RBACError, storage::ObjectStorageError, - users::dashboards::{Dashboard, Tile, DASHBOARDS}, + users::dashboards::{validate_dashboard_id, Dashboard, Tile, DASHBOARDS}, utils::{get_hash, get_user_from_request}, }; use actix_web::{ @@ -29,7 +29,6 @@ use actix_web::{ }; use http::StatusCode; use serde_json::{Error as SerdeError, Map}; -use ulid::Ulid; pub async fn list() -> Result { let dashboards = DASHBOARDS.list_dashboards().await; @@ -42,18 +41,24 @@ pub async fn list() -> Result { "title".to_string(), serde_json::Value::String(dashboard.title.clone()), ); - map.insert( - "author".to_string(), - serde_json::Value::String(dashboard.author.as_ref().unwrap().clone()), - ); - map.insert( - "modified".to_string(), - serde_json::Value::String(dashboard.modified.unwrap().to_string()), - ); - map.insert( - "dashboard_id".to_string(), - serde_json::Value::String(dashboard.dashboard_id.unwrap().to_string()), - ); + if let Some(author) = &dashboard.author { + map.insert( + "author".to_string(), + serde_json::Value::String(author.to_string()), + ); + } + if let Some(modified) = &dashboard.modified { + map.insert( + "modified".to_string(), + serde_json::Value::String(modified.to_string()), + ); + } + if let Some(dashboard_id) = &dashboard.dashboard_id { + map.insert( + "dashboard_id".to_string(), + serde_json::Value::String(dashboard_id.to_string()), + ); + } map }) .collect(); @@ -61,11 +66,7 @@ pub async fn list() -> Result { } pub async fn get(dashboard_id: Path) -> Result { - let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { - dashboard_id - } else { - return Err(DashboardError::Metadata("Invalid dashboard ID")); - }; + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; if let Some(dashboard) = DASHBOARDS.get_dashboard(dashboard_id).await { return Ok((web::Json(dashboard), StatusCode::OK)); @@ -78,6 +79,9 @@ pub async fn post( req: HttpRequest, Json(mut dashboard): Json, ) -> Result { + if dashboard.title.is_empty() { + return Err(DashboardError::Metadata("Title must be provided")); + } let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); dashboard.author = Some(user_id.clone()); @@ -93,11 +97,15 @@ pub async fn update( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { - dashboard_id - } else { - return Err(DashboardError::Metadata("Invalid dashboard ID")); - }; + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; + + for tile in dashboard.tiles.as_ref().unwrap_or(&Vec::new()) { + if tile.tile_id.is_nil() { + return Err(DashboardError::Metadata( + "Tile ID must be provided by the client", + )); + } + } dashboard.author = Some(user_id.clone()); DASHBOARDS @@ -112,11 +120,7 @@ pub async fn delete( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { - dashboard_id - } else { - return Err(DashboardError::Metadata("Invalid dashboard ID")); - }; + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; DASHBOARDS.delete_dashboard(&user_id, dashboard_id).await?; Ok(HttpResponse::Ok().finish()) @@ -129,17 +133,20 @@ pub async fn add_tile( ) -> Result { let mut user_id = get_user_from_request(&req)?; user_id = get_hash(&user_id); - let dashboard_id = if let Ok(dashboard_id) = Ulid::from_string(&dashboard_id.into_inner()) { - dashboard_id - } else { - return Err(DashboardError::Metadata("Invalid dashboard ID")); - }; + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; + + if tile.tile_id.is_nil() { + return Err(DashboardError::Metadata( + "Tile ID must be provided by the client", + )); + } let mut dashboard = DASHBOARDS .get_dashboard_by_user(dashboard_id, &user_id) .await - .ok_or(DashboardError::Metadata("Dashboard does not exist"))?; - dashboard.tiles.as_mut().unwrap().push(tile.clone()); + .ok_or(DashboardError::Unauthorized)?; + let tiles = dashboard.tiles.get_or_insert_with(Vec::new); + tiles.push(tile.clone()); DASHBOARDS .update(&user_id, dashboard_id, &mut dashboard) .await?; @@ -159,6 +166,8 @@ pub enum DashboardError { UserDoesNotExist(#[from] RBACError), #[error("Error: {0}")] Custom(String), + #[error("Unauthorized to access resource")] + Unauthorized, } impl actix_web::ResponseError for DashboardError { @@ -169,6 +178,7 @@ impl actix_web::ResponseError for DashboardError { Self::Metadata(_) => StatusCode::BAD_REQUEST, Self::UserDoesNotExist(_) => StatusCode::NOT_FOUND, Self::Custom(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::Unauthorized => StatusCode::UNAUTHORIZED, } } diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index ec1c87448..074ffcd78 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -118,7 +118,7 @@ impl Dashboards { .await .is_none() { - return Err(DashboardError::Metadata("Dashboard does not exist")); + return Err(DashboardError::Unauthorized); } dashboard.author = Some(user_id.to_string()); dashboard.dashboard_id = Some(dashboard_id); @@ -153,16 +153,17 @@ impl Dashboards { .await .is_none() { - return Err(DashboardError::Metadata("Dashboard does not exist")); + return Err(DashboardError::Unauthorized); } let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); store.delete_object(&path).await?; - self.0 - .write() - .await - .retain(|d| *d.dashboard_id.as_ref().unwrap() != dashboard_id); + self.0.write().await.retain(|d| { + d.dashboard_id + .as_ref() + .map_or(false, |id| *id == dashboard_id) + }); Ok(()) } @@ -172,7 +173,11 @@ impl Dashboards { .read() .await .iter() - .find(|d| *d.dashboard_id.as_ref().unwrap() == dashboard_id) + .find(|d| { + d.dashboard_id + .as_ref() + .map_or(false, |id| *id == dashboard_id) + }) .cloned() } @@ -186,7 +191,9 @@ impl Dashboards { .await .iter() .find(|d| { - *d.dashboard_id.as_ref().unwrap() == dashboard_id + d.dashboard_id + .as_ref() + .map_or(false, |id| *id == dashboard_id) && d.author == Some(user_id.to_string()) }) .cloned() @@ -203,3 +210,7 @@ impl Dashboards { dashboards } } + +pub fn validate_dashboard_id(dashboard_id: String) -> Result { + Ulid::from_string(&dashboard_id).map_err(|_| DashboardError::Metadata("Invalid dashboard ID")) +} From 9a2d5f53c024aaf425e013216963960a482b696e Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Thu, 1 May 2025 08:04:01 -0400 Subject: [PATCH 09/13] refactor --- src/handlers/http/modal/server.rs | 10 +-- src/handlers/http/users/dashboards.rs | 115 ++++++++++++-------------- src/users/dashboards.rs | 111 ++++++++++++++++--------- 3 files changed, 131 insertions(+), 105 deletions(-) diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index dff23d990..c5a584c9e 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -247,12 +247,12 @@ impl Server { web::resource("") .route( web::post() - .to(dashboards::post) + .to(dashboards::create_dashboard) .authorize(Action::CreateDashboard), ) .route( web::get() - .to(dashboards::list) + .to(dashboards::list_dashboards) .authorize(Action::ListDashboard), ), ) @@ -262,17 +262,17 @@ impl Server { web::resource("") .route( web::get() - .to(dashboards::get) + .to(dashboards::get_dashboard) .authorize(Action::GetDashboard), ) .route( web::delete() - .to(dashboards::delete) + .to(dashboards::delete_dashboard) .authorize(Action::DeleteDashboard), ) .route( web::put() - .to(dashboards::update) + .to(dashboards::update_dashboard) .authorize(Action::CreateDashboard), ), ) diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 39567e65a..93dc4fd7d 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -28,99 +28,86 @@ use actix_web::{ HttpRequest, HttpResponse, Responder, }; use http::StatusCode; -use serde_json::{Error as SerdeError, Map}; +use serde_json::Error as SerdeError; -pub async fn list() -> Result { +pub async fn list_dashboards() -> Result { let dashboards = DASHBOARDS.list_dashboards().await; - //dashboards list should contain the title, author and modified date - let dashboards: Vec> = dashboards + let dashboard_summaries = dashboards .iter() - .map(|dashboard| { - let mut map = Map::new(); - map.insert( - "title".to_string(), - serde_json::Value::String(dashboard.title.clone()), - ); - if let Some(author) = &dashboard.author { - map.insert( - "author".to_string(), - serde_json::Value::String(author.to_string()), - ); - } - if let Some(modified) = &dashboard.modified { - map.insert( - "modified".to_string(), - serde_json::Value::String(modified.to_string()), - ); - } - if let Some(dashboard_id) = &dashboard.dashboard_id { - map.insert( - "dashboard_id".to_string(), - serde_json::Value::String(dashboard_id.to_string()), - ); - } - map - }) - .collect(); - Ok((web::Json(dashboards), StatusCode::OK)) + .map(|dashboard| dashboard.to_summary()) + .collect::>(); + + Ok((web::Json(dashboard_summaries), StatusCode::OK)) } -pub async fn get(dashboard_id: Path) -> Result { +pub async fn get_dashboard(dashboard_id: Path) -> Result { let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; - if let Some(dashboard) = DASHBOARDS.get_dashboard(dashboard_id).await { - return Ok((web::Json(dashboard), StatusCode::OK)); - } + let dashboard = DASHBOARDS + .get_dashboard(dashboard_id) + .await + .ok_or_else(|| DashboardError::Metadata("Dashboard does not exist"))?; - Err(DashboardError::Metadata("Dashboard does not exist")) + Ok((web::Json(dashboard), StatusCode::OK)) } -pub async fn post( +pub async fn create_dashboard( req: HttpRequest, Json(mut dashboard): Json, ) -> Result { if dashboard.title.is_empty() { return Err(DashboardError::Metadata("Title must be provided")); } - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); - dashboard.author = Some(user_id.clone()); + + let user_id = get_hash(&get_user_from_request(&req)?); DASHBOARDS.create(&user_id, &mut dashboard).await?; Ok((web::Json(dashboard), StatusCode::OK)) } -pub async fn update( +pub async fn update_dashboard( req: HttpRequest, dashboard_id: Path, Json(mut dashboard): Json, ) -> Result { - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); + let user_id = get_hash(&get_user_from_request(&req)?); let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; - for tile in dashboard.tiles.as_ref().unwrap_or(&Vec::new()) { - if tile.tile_id.is_nil() { - return Err(DashboardError::Metadata( - "Tile ID must be provided by the client", - )); + // Validate all tiles have valid IDs + if let Some(tiles) = &dashboard.tiles { + if tiles.iter().any(|tile| tile.tile_id.is_nil()) { + return Err(DashboardError::Metadata("Tile ID must be provided")); + } + } + + // Check if tile_id are unique + if let Some(tiles) = &dashboard.tiles { + let unique_tiles: Vec<_> = tiles + .iter() + .map(|tile| tile.tile_id) + .collect::>() + .into_iter() + .collect(); + + if unique_tiles.len() != tiles.len() { + return Err(DashboardError::Metadata("Tile IDs must be unique")); } } - dashboard.author = Some(user_id.clone()); DASHBOARDS .update(&user_id, dashboard_id, &mut dashboard) .await?; + Ok((web::Json(dashboard), StatusCode::OK)) } -pub async fn delete( +pub async fn delete_dashboard( req: HttpRequest, dashboard_id: Path, ) -> Result { - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); + let user_id = get_hash(&get_user_from_request(&req)?); let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; + DASHBOARDS.delete_dashboard(&user_id, dashboard_id).await?; Ok(HttpResponse::Ok().finish()) @@ -131,22 +118,26 @@ pub async fn add_tile( dashboard_id: Path, Json(tile): Json, ) -> Result { - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); - let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; - if tile.tile_id.is_nil() { - return Err(DashboardError::Metadata( - "Tile ID must be provided by the client", - )); + return Err(DashboardError::Metadata("Tile ID must be provided")); } + let user_id = get_hash(&get_user_from_request(&req)?); + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; + let mut dashboard = DASHBOARDS .get_dashboard_by_user(dashboard_id, &user_id) .await .ok_or(DashboardError::Unauthorized)?; + let tiles = dashboard.tiles.get_or_insert_with(Vec::new); - tiles.push(tile.clone()); + + // check if the tile already exists + if tiles.iter().any(|t| t.tile_id == tile.tile_id) { + return Err(DashboardError::Metadata("Tile already exists")); + } + tiles.push(tile); + DASHBOARDS .update(&user_id, dashboard_id, &mut dashboard) .await?; @@ -166,7 +157,7 @@ pub enum DashboardError { UserDoesNotExist(#[from] RBACError), #[error("Error: {0}")] Custom(String), - #[error("Unauthorized to access resource")] + #[error("Dashboard does not exist or is not accessible")] Unauthorized, } diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 074ffcd78..73e5908ec 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -56,6 +56,53 @@ pub struct Dashboard { pub tiles: Option>, } +impl Dashboard { + pub fn set_metadata(&mut self, user_id: &str, dashboard_id: Option) { + self.author = Some(user_id.to_string()); + self.dashboard_id = dashboard_id.or_else(|| Some(Ulid::new())); + self.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); + self.modified = Some(Utc::now()); + self.dashboard_type = Some(DashboardType::Dashboard); + } + + pub fn to_summary(&self) -> serde_json::Map { + let mut map = serde_json::Map::new(); + + map.insert( + "title".to_string(), + serde_json::Value::String(self.title.clone()), + ); + + if let Some(author) = &self.author { + map.insert( + "author".to_string(), + serde_json::Value::String(author.to_string()), + ); + } + + if let Some(modified) = &self.modified { + map.insert( + "modified".to_string(), + serde_json::Value::String(modified.to_string()), + ); + } + + if let Some(dashboard_id) = &self.dashboard_id { + map.insert( + "dashboard_id".to_string(), + serde_json::Value::String(dashboard_id.to_string()), + ); + } + + map + } +} + +pub fn validate_dashboard_id(dashboard_id: String) -> Result { + Ulid::from_string(&dashboard_id) + .map_err(|_| DashboardError::Metadata("Dashboard ID must be provided")) +} + #[derive(Default, Debug)] pub struct Dashboards(RwLock>); @@ -64,11 +111,13 @@ impl Dashboards { let mut this = vec![]; let store = PARSEABLE.storage.get_object_store(); let all_dashboards = store.get_all_dashboards().await.unwrap_or_default(); + for (_, dashboards) in all_dashboards { for dashboard in dashboards { if dashboard.is_empty() { continue; } + let dashboard_value = serde_json::from_slice::(&dashboard)?; if let Ok(dashboard) = serde_json::from_value::(dashboard_value) { this.retain(|d: &Dashboard| d.dashboard_id != dashboard.dashboard_id); @@ -83,17 +132,14 @@ impl Dashboards { Ok(()) } - pub async fn create( + async fn save_dashboard( &self, user_id: &str, - dashboard: &mut Dashboard, + dashboard: &Dashboard, ) -> Result<(), DashboardError> { - let dashboard_id = Ulid::new(); - dashboard.author = Some(user_id.to_string()); - dashboard.dashboard_id = Some(dashboard_id); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - dashboard.modified = Some(Utc::now()); - dashboard.dashboard_type = Some(DashboardType::Dashboard); + let dashboard_id = dashboard + .dashboard_id + .ok_or(DashboardError::Metadata("Dashboard ID must be provided"))?; let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); @@ -102,6 +148,17 @@ impl Dashboards { .put_object(&path, Bytes::from(dashboard_bytes)) .await?; + Ok(()) + } + + pub async fn create( + &self, + user_id: &str, + dashboard: &mut Dashboard, + ) -> Result<(), DashboardError> { + dashboard.set_metadata(user_id, None); + + self.save_dashboard(user_id, dashboard).await?; self.0.write().await.push(dashboard.clone()); Ok(()) @@ -120,25 +177,13 @@ impl Dashboards { { return Err(DashboardError::Unauthorized); } - dashboard.author = Some(user_id.to_string()); - dashboard.dashboard_id = Some(dashboard_id); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - dashboard.modified = Some(Utc::now()); - dashboard.dashboard_type = Some(DashboardType::Dashboard); - let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); + dashboard.set_metadata(user_id, Some(dashboard_id)); + self.save_dashboard(user_id, dashboard).await?; - let store = PARSEABLE.storage.get_object_store(); - let dashboard_bytes = serde_json::to_vec(&dashboard)?; - store - .put_object(&path, Bytes::from(dashboard_bytes)) - .await?; - - self.0 - .write() - .await - .retain(|d| d.dashboard_id != dashboard.dashboard_id); - self.0.write().await.push(dashboard.clone()); + let mut dashboards = self.0.write().await; + dashboards.retain(|d| d.dashboard_id != dashboard.dashboard_id); + dashboards.push(dashboard.clone()); Ok(()) } @@ -159,10 +204,11 @@ impl Dashboards { let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); store.delete_object(&path).await?; + self.0.write().await.retain(|d| { d.dashboard_id .as_ref() - .map_or(false, |id| *id == dashboard_id) + .map_or(true, |id| *id != dashboard_id) }); Ok(()) @@ -200,17 +246,6 @@ impl Dashboards { } pub async fn list_dashboards(&self) -> Vec { - let read = self.0.read().await; - - let mut dashboards = Vec::new(); - - for d in read.iter() { - dashboards.push(d.clone()); - } - dashboards + self.0.read().await.clone() } } - -pub fn validate_dashboard_id(dashboard_id: String) -> Result { - Ulid::from_string(&dashboard_id).map_err(|_| DashboardError::Metadata("Invalid dashboard ID")) -} From cb9d135ab03172d4193ba809fce4c5fe2baa9638 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Thu, 1 May 2025 08:15:52 -0400 Subject: [PATCH 10/13] clippy fix --- src/users/dashboards.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 73e5908ec..2ee021161 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -208,7 +208,7 @@ impl Dashboards { self.0.write().await.retain(|d| { d.dashboard_id .as_ref() - .map_or(true, |id| *id != dashboard_id) + .is_some_and(|id| *id == dashboard_id) }); Ok(()) @@ -222,7 +222,7 @@ impl Dashboards { .find(|d| { d.dashboard_id .as_ref() - .map_or(false, |id| *id == dashboard_id) + .is_some_and(|id| *id == dashboard_id) }) .cloned() } @@ -239,7 +239,7 @@ impl Dashboards { .find(|d| { d.dashboard_id .as_ref() - .map_or(false, |id| *id == dashboard_id) + .is_some_and(|id| *id == dashboard_id) && d.author == Some(user_id.to_string()) }) .cloned() From 94a54953dea50569ea2ab6db165dd5caa1f6d11d Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Sat, 3 May 2025 13:36:26 -0400 Subject: [PATCH 11/13] refactor and add comments --- src/users/dashboards.rs | 88 ++++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 2ee021161..9abc253a1 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -33,15 +33,21 @@ pub static DASHBOARDS: Lazy = Lazy::new(Dashboards::default); pub const CURRENT_DASHBOARD_VERSION: &str = "v1"; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +/// type of dashboard +/// Dashboard is the default type +/// Report is a type of dashboard that is used for reporting pub enum DashboardType { + /// Dashboard is the default type #[default] Dashboard, + /// Report is a type of dashboard that is used for reporting Report, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Tile { pub tile_id: Ulid, + /// all other fields are variable and can be added as needed #[serde(flatten)] pub other_fields: Option>, } @@ -57,14 +63,22 @@ pub struct Dashboard { } impl Dashboard { + /// set metadata for the dashboard + /// add author, dashboard_id, version, modified, and dashboard_type + /// if dashboard_id is None, generate a new one pub fn set_metadata(&mut self, user_id: &str, dashboard_id: Option) { self.author = Some(user_id.to_string()); self.dashboard_id = dashboard_id.or_else(|| Some(Ulid::new())); self.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); self.modified = Some(Utc::now()); self.dashboard_type = Some(DashboardType::Dashboard); + if self.tiles.is_none() { + self.tiles = Some(Vec::new()); + } } + /// create a summary of the dashboard + /// used for listing dashboards pub fn to_summary(&self) -> serde_json::Map { let mut map = serde_json::Map::new(); @@ -98,15 +112,21 @@ impl Dashboard { } } +/// Validate the dashboard ID +/// Check if the dashboard ID is a valid ULID +/// If the dashboard ID is not valid, return an error pub fn validate_dashboard_id(dashboard_id: String) -> Result { Ulid::from_string(&dashboard_id) - .map_err(|_| DashboardError::Metadata("Dashboard ID must be provided")) + .map_err(|_| DashboardError::Metadata("Invalid dashboard ID format - must be a valid ULID")) } #[derive(Default, Debug)] pub struct Dashboards(RwLock>); impl Dashboards { + /// Load all dashboards from the object store + /// and store them in memory + /// This function is called on server start pub async fn load(&self) -> anyhow::Result<()> { let mut this = vec![]; let store = PARSEABLE.storage.get_object_store(); @@ -118,10 +138,21 @@ impl Dashboards { continue; } - let dashboard_value = serde_json::from_slice::(&dashboard)?; - if let Ok(dashboard) = serde_json::from_value::(dashboard_value) { + let dashboard_value = match serde_json::from_slice::(&dashboard) + { + Ok(value) => value, + Err(err) => { + tracing::warn!("Failed to parse dashboard JSON: {}", err); + continue; + } + }; + + if let Ok(dashboard) = serde_json::from_value::(dashboard_value.clone()) + { this.retain(|d: &Dashboard| d.dashboard_id != dashboard.dashboard_id); this.push(dashboard); + } else { + tracing::warn!("Failed to deserialize dashboard: {:?}", dashboard_value); } } } @@ -132,6 +163,8 @@ impl Dashboards { Ok(()) } + /// Save the dashboard to the object store + /// This function is called when creating or updating a dashboard async fn save_dashboard( &self, user_id: &str, @@ -151,6 +184,9 @@ impl Dashboards { Ok(()) } + /// Create a new dashboard + /// This function is called when creating a new dashboard + /// add dashboard in memory and save it to the object store pub async fn create( &self, user_id: &str, @@ -164,19 +200,17 @@ impl Dashboards { Ok(()) } + /// Update an existing dashboard + /// This function is called when updating a dashboard + /// update dashboard in memory and save it to the object store pub async fn update( &self, user_id: &str, dashboard_id: Ulid, dashboard: &mut Dashboard, ) -> Result<(), DashboardError> { - if self - .get_dashboard_by_user(dashboard_id, user_id) - .await - .is_none() - { - return Err(DashboardError::Unauthorized); - } + self.ensure_dashboard_ownership(dashboard_id, user_id) + .await?; dashboard.set_metadata(user_id, Some(dashboard_id)); self.save_dashboard(user_id, dashboard).await?; @@ -188,18 +222,16 @@ impl Dashboards { Ok(()) } + /// Delete a dashboard + /// This function is called when deleting a dashboard + /// delete dashboard in memory and from the object store pub async fn delete_dashboard( &self, user_id: &str, dashboard_id: Ulid, ) -> Result<(), DashboardError> { - if self - .get_dashboard_by_user(dashboard_id, user_id) - .await - .is_none() - { - return Err(DashboardError::Unauthorized); - } + self.ensure_dashboard_ownership(dashboard_id, user_id) + .await?; let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); let store = PARSEABLE.storage.get_object_store(); @@ -208,12 +240,14 @@ impl Dashboards { self.0.write().await.retain(|d| { d.dashboard_id .as_ref() - .is_some_and(|id| *id == dashboard_id) + .map_or(true, |id| *id != dashboard_id) }); Ok(()) } + /// Get a dashboard by ID + /// fetch dashboard from memory pub async fn get_dashboard(&self, dashboard_id: Ulid) -> Option { self.0 .read() @@ -227,6 +261,8 @@ impl Dashboards { .cloned() } + /// Get a dashboard by ID and user ID + /// fetch dashboard from memory pub async fn get_dashboard_by_user( &self, dashboard_id: Ulid, @@ -245,7 +281,23 @@ impl Dashboards { .cloned() } + /// List all dashboards + /// fetch all dashboards from memory pub async fn list_dashboards(&self) -> Vec { self.0.read().await.clone() } + + /// Ensure the user is the owner of the dashboard + /// This function is called when updating or deleting a dashboard + /// check if the user is the owner of the dashboard + /// if the user is not the owner, return an error + async fn ensure_dashboard_ownership( + &self, + dashboard_id: Ulid, + user_id: &str, + ) -> Result { + self.get_dashboard_by_user(dashboard_id, user_id) + .await + .ok_or(DashboardError::Unauthorized) + } } From 1ea935b98db91788971e758facfd89b2c1f6d593 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Sat, 3 May 2025 13:44:23 -0400 Subject: [PATCH 12/13] update delete dashboard from memory --- src/users/dashboards.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 9abc253a1..f02511f3b 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -237,11 +237,10 @@ impl Dashboards { let store = PARSEABLE.storage.get_object_store(); store.delete_object(&path).await?; - self.0.write().await.retain(|d| { - d.dashboard_id - .as_ref() - .map_or(true, |id| *id != dashboard_id) - }); + self.0 + .write() + .await + .retain(|d| d.dashboard_id.as_ref().is_none_or(|id| *id != dashboard_id)); Ok(()) } From 57587f63b6b7c2ad65814001d36cd207bee3caaa Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Sun, 11 May 2025 10:39:21 -0400 Subject: [PATCH 13/13] merge from main --- src/handlers/http/modal/server.rs | 6 +- src/handlers/http/prism_home.rs | 27 ++- src/prism/home/mod.rs | 317 +++++++++++++++++------------- 3 files changed, 214 insertions(+), 136 deletions(-) diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index c5a584c9e..8283be750 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -148,8 +148,10 @@ impl ParseableServer for Server { } impl Server { - pub fn get_prism_home() -> Resource { - web::resource("/home").route(web::get().to(http::prism_home::home_api)) + pub fn get_prism_home() -> Scope { + web::scope("/home") + .service(web::resource("").route(web::get().to(http::prism_home::home_api))) + .service(web::resource("/search").route(web::get().to(http::prism_home::home_search))) } pub fn get_prism_logstream() -> Scope { diff --git a/src/handlers/http/prism_home.rs b/src/handlers/http/prism_home.rs index a8a0328a6..10ff0f04c 100644 --- a/src/handlers/http/prism_home.rs +++ b/src/handlers/http/prism_home.rs @@ -16,13 +16,17 @@ * */ +use std::collections::HashMap; + use actix_web::{web, HttpRequest, Responder}; use crate::{ - prism::home::{generate_home_response, PrismHomeError}, + prism::home::{generate_home_response, generate_home_search_response, PrismHomeError}, utils::actix::extract_session_key_from_req, }; +const HOME_SEARCH_QUERY_PARAM: &str = "key"; + /// Fetches the data to populate Prism's home /// /// @@ -37,3 +41,24 @@ pub async fn home_api(req: HttpRequest) -> Result Result { + let key = extract_session_key_from_req(&req) + .map_err(|err| PrismHomeError::Anyhow(anyhow::Error::msg(err.to_string())))?; + let query_map = web::Query::>::from_query(req.query_string()) + .map_err(|_| PrismHomeError::InvalidQueryParameter(HOME_SEARCH_QUERY_PARAM.to_string()))?; + + if query_map.is_empty() { + return Ok(web::Json(serde_json::json!({}))); + } + + let query_value = query_map + .get(HOME_SEARCH_QUERY_PARAM) + .ok_or_else(|| PrismHomeError::InvalidQueryParameter(HOME_SEARCH_QUERY_PARAM.to_string()))? + .to_lowercase(); + let res = generate_home_search_response(&key, &query_value).await?; + let json_res = serde_json::to_value(res) + .map_err(|err| PrismHomeError::Anyhow(anyhow::Error::msg(err.to_string())))?; + + Ok(web::Json(json_res)) +} diff --git a/src/prism/home/mod.rs b/src/prism/home/mod.rs index 3f61365d9..7d7d4b3ad 100644 --- a/src/prism/home/mod.rs +++ b/src/prism/home/mod.rs @@ -36,20 +36,12 @@ use crate::{ }, parseable::PARSEABLE, rbac::{map::SessionKey, role::Action, Users}, - stats::Stats, storage::{ObjectStorageError, ObjectStoreFormat, STREAM_ROOT_DIRECTORY}, users::{dashboards::DASHBOARDS, filters::FILTERS}, }; type StreamMetadataResponse = Result<(String, Vec, DataSetType), PrismHomeError>; -#[derive(Debug, Serialize, Default)] -struct StreamInfo { - // stream_count: u32, - // log_source_count: u32, - stats_summary: Stats, -} - #[derive(Debug, Serialize, Default)] struct DatedStats { date: String, @@ -58,12 +50,6 @@ struct DatedStats { storage_size: u64, } -#[derive(Debug, Serialize)] -struct TitleAndId { - title: String, - id: String, -} - #[derive(Debug, Serialize)] enum DataSetType { Logs, @@ -79,40 +65,38 @@ struct DataSet { #[derive(Debug, Serialize)] pub struct HomeResponse { - alert_titles: Vec, alerts_info: AlertsInfo, - correlation_titles: Vec, - stream_info: StreamInfo, stats_details: Vec, - stream_titles: Vec, datasets: Vec, - dashboard_titles: Vec, - filter_titles: Vec, +} + +#[derive(Debug, Serialize)] +pub enum ResourceType { + Alert, + Correlation, + Dashboard, + Filter, + DataSet, +} + +#[derive(Debug, Serialize)] +pub struct Resource { + id: String, + name: String, + resource_type: ResourceType, +} + +#[derive(Debug, Serialize)] +pub struct HomeSearchResponse { + resources: Vec, } pub async fn generate_home_response(key: &SessionKey) -> Result { // Execute these operations concurrently - let ( - stream_titles_result, - alert_titles_result, - correlation_titles_result, - dashboards_result, - filters_result, - alerts_info_result, - ) = tokio::join!( - get_stream_titles(key), - get_alert_titles(key), - get_correlation_titles(key), - get_dashboard_titles(), - get_filter_titles(key), - get_alerts_info() - ); + let (stream_titles_result, alerts_info_result) = + tokio::join!(get_stream_titles(key), get_alerts_info()); let stream_titles = stream_titles_result?; - let alert_titles = alert_titles_result?; - let correlation_titles = correlation_titles_result?; - let dashboard_titles = dashboards_result?; - let filter_titles = filters_result?; let alerts_info = alerts_info_result?; // Generate dates for date-wise stats @@ -120,8 +104,7 @@ pub async fn generate_home_response(key: &SessionKey) -> Result Result { - summary.stats_summary.events += dated_stats.events; - summary.stats_summary.ingestion += dated_stats.ingestion_size; - summary.stats_summary.storage += dated_stats.storage_size; stream_details.push(dated_stats); } Err(e) => { @@ -179,99 +158,11 @@ pub async fn generate_home_response(key: &SessionKey) -> Result Result, PrismHomeError> { - let stream_titles: Vec = PARSEABLE - .storage - .get_object_store() - .list_streams() - .await - .map_err(|e| PrismHomeError::Anyhow(anyhow::Error::new(e)))? - .into_iter() - .filter(|logstream| { - Users.authorize(key.clone(), Action::ListStream, Some(logstream), None) - == crate::rbac::Response::Authorized - }) - .sorted() - .collect_vec(); - - Ok(stream_titles) -} - -async fn get_alert_titles(key: &SessionKey) -> Result, PrismHomeError> { - let alert_titles = ALERTS - .list_alerts_for_user(key.clone()) - .await? - .iter() - .map(|alert| TitleAndId { - title: alert.title.clone(), - id: alert.id.to_string(), - }) - .collect_vec(); - - Ok(alert_titles) -} - -async fn get_correlation_titles(key: &SessionKey) -> Result, PrismHomeError> { - let correlation_titles = CORRELATIONS - .list_correlations(key) - .await? - .iter() - .map(|corr| TitleAndId { - title: corr.title.clone(), - id: corr.id.clone(), - }) - .collect_vec(); - - Ok(correlation_titles) -} - -async fn get_dashboard_titles() -> Result, PrismHomeError> { - let dashboard_titles = DASHBOARDS - .list_dashboards() - .await - .iter() - .map(|dashboard| TitleAndId { - title: dashboard.title.clone(), - id: dashboard.dashboard_id.as_ref().unwrap().to_string(), - }) - .collect_vec(); - - Ok(dashboard_titles) -} - -async fn get_filter_titles(key: &SessionKey) -> Result, PrismHomeError> { - let filter_titles = FILTERS - .list_filters(key) - .await - .iter() - .map(|filter| TitleAndId { - title: filter.filter_name.clone(), - id: filter - .filter_id - .as_ref() - .ok_or_else(|| anyhow::Error::msg("Filter ID is null")) - .unwrap() - .clone(), - }) - .collect_vec(); - - Ok(filter_titles) -} - async fn get_stream_metadata( stream: String, ) -> Result<(String, Vec, DataSetType), PrismHomeError> { @@ -369,6 +260,163 @@ async fn get_stream_stats_for_date( )) } +pub async fn generate_home_search_response( + key: &SessionKey, + query_value: &str, +) -> Result { + let mut resources = Vec::new(); + let (alert_titles, correlation_titles, dashboard_titles, filter_titles, stream_titles) = tokio::join!( + get_alert_titles(key, query_value), + get_correlation_titles(key, query_value), + get_dashboard_titles(query_value), + get_filter_titles(key, query_value), + get_stream_titles(key) + ); + + let alerts = alert_titles?; + resources.extend(alerts); + let correlations = correlation_titles?; + resources.extend(correlations); + let dashboards = dashboard_titles?; + resources.extend(dashboards); + let filters = filter_titles?; + resources.extend(filters); + let stream_titles = stream_titles?; + + for title in stream_titles { + if title.to_lowercase().contains(query_value) { + resources.push(Resource { + id: title.clone(), + name: title, + resource_type: ResourceType::DataSet, + }); + } + } + Ok(HomeSearchResponse { resources }) +} + +// Helper functions to split the work +async fn get_stream_titles(key: &SessionKey) -> Result, PrismHomeError> { + let stream_titles: Vec = PARSEABLE + .storage + .get_object_store() + .list_streams() + .await + .map_err(|e| PrismHomeError::Anyhow(anyhow::Error::new(e)))? + .into_iter() + .filter(|logstream| { + Users.authorize(key.clone(), Action::ListStream, Some(logstream), None) + == crate::rbac::Response::Authorized + }) + .sorted() + .collect_vec(); + + Ok(stream_titles) +} + +async fn get_alert_titles( + key: &SessionKey, + query_value: &str, +) -> Result, PrismHomeError> { + let alerts = ALERTS + .list_alerts_for_user(key.clone()) + .await? + .iter() + .filter_map(|alert| { + if alert.title.to_lowercase().contains(query_value) + || alert.id.to_string().to_lowercase().contains(query_value) + { + Some(Resource { + id: alert.id.to_string(), + name: alert.title.clone(), + resource_type: ResourceType::Alert, + }) + } else { + None + } + }) + .collect_vec(); + + Ok(alerts) +} + +async fn get_correlation_titles( + key: &SessionKey, + query_value: &str, +) -> Result, PrismHomeError> { + let correlations = CORRELATIONS + .list_correlations(key) + .await? + .iter() + .filter_map(|correlation| { + if correlation.title.to_lowercase().contains(query_value) + || correlation.id.to_lowercase().contains(query_value) + { + Some(Resource { + id: correlation.id.to_string(), + name: correlation.title.clone(), + resource_type: ResourceType::Correlation, + }) + } else { + None + } + }) + .collect_vec(); + + Ok(correlations) +} + +async fn get_dashboard_titles(query_value: &str) -> Result, PrismHomeError> { + let dashboard_titles = DASHBOARDS + .list_dashboards() + .await + .iter() + .filter_map(|dashboard| { + let dashboard_id = *dashboard.dashboard_id.as_ref().unwrap(); + let dashboard_id = dashboard_id.to_string(); + if dashboard.title.to_lowercase().contains(query_value) + || dashboard_id.to_lowercase().contains(query_value) + { + Some(Resource { + id: dashboard_id, + name: dashboard.title.clone(), + resource_type: ResourceType::Dashboard, + }) + } else { + None + } + }) + .collect_vec(); + + Ok(dashboard_titles) +} + +async fn get_filter_titles( + key: &SessionKey, + query_value: &str, +) -> Result, PrismHomeError> { + let filter_titles = FILTERS + .list_filters(key) + .await + .iter() + .filter_map(|filter| { + let filter_id = filter.filter_id.as_ref().unwrap().clone(); + if filter.filter_name.to_lowercase().contains(query_value) + || filter_id.to_lowercase().contains(query_value) + { + Some(Resource { + id: filter_id, + name: filter.filter_name.clone(), + resource_type: ResourceType::Filter, + }) + } else { + None + } + }) + .collect_vec(); + Ok(filter_titles) +} + #[derive(Debug, thiserror::Error)] pub enum PrismHomeError { #[error("Error: {0}")] @@ -381,6 +429,8 @@ pub enum PrismHomeError { StreamError(#[from] StreamError), #[error("ObjectStorageError: {0}")] ObjectStorageError(#[from] ObjectStorageError), + #[error("Invalid query parameter: {0}")] + InvalidQueryParameter(String), } impl actix_web::ResponseError for PrismHomeError { @@ -391,6 +441,7 @@ impl actix_web::ResponseError for PrismHomeError { PrismHomeError::CorrelationError(e) => e.status_code(), PrismHomeError::StreamError(e) => e.status_code(), PrismHomeError::ObjectStorageError(_) => StatusCode::INTERNAL_SERVER_ERROR, + PrismHomeError::InvalidQueryParameter(_) => StatusCode::BAD_REQUEST, } }