diff --git a/pgml-dashboard/Cargo.lock b/pgml-dashboard/Cargo.lock index e14050a5b..59e710ba5 100644 --- a/pgml-dashboard/Cargo.lock +++ b/pgml-dashboard/Cargo.lock @@ -1924,6 +1924,16 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libloading" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libm" version = "0.2.8" @@ -2213,6 +2223,47 @@ dependencies = [ "tempfile", ] +[[package]] +name = "neon" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" +dependencies = [ + "neon-build", + "neon-macros", + "neon-runtime", + "semver 0.9.0", + "smallvec", +] + +[[package]] +name = "neon-build" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" + +[[package]] +name = "neon-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" +dependencies = [ + "quote", + "syn 1.0.109", + "syn-mid", +] + +[[package]] +name = "neon-runtime" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" +dependencies = [ + "cfg-if", + "libloading", + "smallvec", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -2535,7 +2586,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pgml" -version = "1.1.0" +version = "1.0.4" dependencies = [ "anyhow", "async-trait", @@ -2554,6 +2605,7 @@ dependencies = [ "parking_lot", "regex", "reqwest", + "rust_bridge", "sea-query", "sea-query-binder", "serde", @@ -3256,6 +3308,31 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rust_bridge" +version = "0.1.0" +dependencies = [ + "rust_bridge_macros", + "rust_bridge_traits", +] + +[[package]] +name = "rust_bridge_macros" +version = "0.1.0" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.32", +] + +[[package]] +name = "rust_bridge_traits" +version = "0.1.0" +dependencies = [ + "neon", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3274,7 +3351,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.18", ] [[package]] @@ -3539,12 +3616,27 @@ dependencies = [ "smallvec", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "sentry" version = "0.31.5" @@ -4240,6 +4332,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-mid" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "sync_wrapper" version = "0.1.2" diff --git a/pgml-dashboard/src/lib.rs b/pgml-dashboard/src/lib.rs index 3bdc0ecdd..6b5efceb2 100644 --- a/pgml-dashboard/src/lib.rs +++ b/pgml-dashboard/src/lib.rs @@ -390,6 +390,7 @@ pub fn replace_banner_product( context: &Cluster, ) -> Result { let mut all_notification_cookies = Notifications::get_viewed(cookies); + let current_notification_cookie = all_notification_cookies.iter().position(|x| x.id == id); match current_notification_cookie { @@ -407,8 +408,8 @@ pub fn replace_banner_product( Notifications::update_viewed(&all_notification_cookies, cookies); - // Get the notification that triggered this call.. - // unwrap notifications if fine since we should panic if this is missing. + // Get the notification that triggered this call. + // Guaranteed to exist since it built the component that called this, so this is safe to unwrap. let last_notification = context .notifications .as_ref() @@ -423,15 +424,11 @@ pub fn replace_banner_product( .into_iter() .filter(|n: &Notification| -> bool { let n = n.clone().set_viewed(n.id == id); - if last_notification.clone().is_none() { - return false; - } else { - Notification::product_filter( - &n, - last_notification.clone().unwrap().level.clone(), - deployment_id.clone(), - ) - } + Notification::product_filter( + &n, + last_notification.clone().unwrap().level.clone(), + deployment_id.clone(), + ) }) .next(), _ => None, @@ -522,188 +519,3 @@ pub fn routes() -> Vec { pub async fn migrate(pool: &PgPool) -> anyhow::Result<()> { Ok(sqlx::migrate!("./migrations").run(pool).await?) } - -#[cfg(test)] -mod test { - use super::*; - use crate::components::sections::footers::MarketingFooter; - use crate::guards::Cluster; - use rocket::fairing::AdHoc; - use rocket::http::Cookie; - use rocket::local::asynchronous::Client; - - #[sqlx::test] - async fn test_remove_modal() { - let rocket = rocket::build().mount("/", routes()); - let client = Client::untracked(rocket).await.unwrap(); - - let cookie = vec![ - NotificationCookie { - id: "1".to_string(), - time_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), - time_modal_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), - }, - NotificationCookie { - id: "2".to_string(), - time_viewed: None, - time_modal_viewed: None, - }, - ]; - - let response = client - .get("/notifications/product/modal/remove_modal?id=1") - .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) - .dispatch() - .await; - - let time_modal_viewed = Notifications::get_viewed(response.cookies()) - .get(0) - .unwrap() - .time_modal_viewed; - - // Update modal view time for existing notification cookie - assert_eq!(time_modal_viewed.is_some(), true); - - let response = client - .get("/notifications/product/modal/remove_modal?id=3") - .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) - .dispatch() - .await; - - let time_modal_viewed = Notifications::get_viewed(response.cookies()) - .get(0) - .unwrap() - .time_modal_viewed; - - // Update modal view time for new notification cookie - assert_eq!(time_modal_viewed.is_some(), true); - } - - #[sqlx::test] - async fn test_remove_banner_product() { - let rocket = rocket::build().mount("/", routes()); - let client = Client::untracked(rocket).await.unwrap(); - - let cookie = vec![ - NotificationCookie { - id: "1".to_string(), - time_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), - time_modal_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), - }, - NotificationCookie { - id: "2".to_string(), - time_viewed: None, - time_modal_viewed: Some(chrono::Utc::now() - chrono::Duration::days(1)), - }, - ]; - - let response = client - .get("/notifications/product/remove_banner?id=1&target=ajskghjfbs") - .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) - .dispatch() - .await; - - let time_viewed = Notifications::get_viewed(response.cookies()) - .get(0) - .unwrap() - .time_viewed; - - // Update view time for existing notification cookie - assert_eq!(time_viewed.is_some(), true); - - let response = client - .get("/notifications/product/remove_banner?id=3&target=ajfadghs") - .private_cookie(Cookie::new("session", Notifications::safe_serialize_session(&cookie))) - .dispatch() - .await; - - let time_viewed = Notifications::get_viewed(response.cookies()) - .get(0) - .unwrap() - .time_viewed; - - // Update view time for new notification cookie - assert_eq!(time_viewed.is_some(), true); - } - - #[sqlx::test] - async fn test_replace_banner_product() { - let notification1 = Notification::new("Test notification 1") - .set_level(&NotificationLevel::ProductMedium) - .set_deployment("1"); - let notification2 = Notification::new("Test notification 2") - .set_level(&NotificationLevel::ProductMedium) - .set_deployment("1"); - let _notification3 = Notification::new("Test notification 3") - .set_level(&NotificationLevel::ProductMedium) - .set_deployment("2"); - let _notification4 = Notification::new("Test notification 4").set_level(&NotificationLevel::ProductMedium); - let _notification5 = Notification::new("Test notification 5").set_level(&NotificationLevel::ProductMarketing); - - let rocket = rocket::build() - .attach(AdHoc::on_request("request", |req, _| { - Box::pin(async { - req.local_cache(|| Cluster { - pool: None, - context: Context { - user: models::User::default(), - cluster: models::Cluster::default(), - dropdown_nav: StaticNav { links: vec![] }, - product_left_nav: StaticNav { links: vec![] }, - marketing_footer: MarketingFooter::new().render_once().unwrap(), - head_items: None, - }, - notifications: Some(vec![ - Notification::new("Test notification 1") - .set_level(&NotificationLevel::ProductMedium) - .set_deployment("1"), - Notification::new("Test notification 2") - .set_level(&NotificationLevel::ProductMedium) - .set_deployment("1"), - Notification::new("Test notification 3") - .set_level(&NotificationLevel::ProductMedium) - .set_deployment("2"), - Notification::new("Test notification 4").set_level(&NotificationLevel::ProductMedium), - Notification::new("Test notification 5").set_level(&NotificationLevel::ProductMarketing), - ]), - }); - }) - })) - .mount("/", routes()); - - let client = Client::tracked(rocket).await.unwrap(); - - let response = client - .get(format!( - "/notifications/product/replace_banner?id={}&deployment_id=1", - notification1.id - )) - .dispatch() - .await; - - let body = response.into_string().await.unwrap(); - let rsp_contains_next_notification = body.contains("Test notification 2"); - - // Ensure the banner is replaced with next notification of same type - assert_eq!(rsp_contains_next_notification, true); - - let response = client - .get(format!( - "/notifications/product/replace_banner?id={}&deployment_id=1", - notification2.id - )) - .dispatch() - .await; - - let body = response.into_string().await.unwrap(); - let rsp_contains_next_notification_3 = body.contains("Test notification 3"); - let rsp_contains_next_notification_4 = body.contains("Test notification 4"); - let rsp_contains_next_notification_5 = body.contains("Test notification 5"); - - // Ensure the next notification is not found since none match deployment id or level - assert_eq!( - rsp_contains_next_notification_3 && rsp_contains_next_notification_4 && rsp_contains_next_notification_5, - false - ); - } -} diff --git a/pgml-dashboard/src/main.rs b/pgml-dashboard/src/main.rs index b31f488e4..a2e4fb90c 100644 --- a/pgml-dashboard/src/main.rs +++ b/pgml-dashboard/src/main.rs @@ -137,7 +137,7 @@ mod test { async fn rocket() -> Rocket { dotenv::dotenv().ok(); - pgml_dashboard::migrate(Cluster::default().pool()).await.unwrap(); + pgml_dashboard::migrate(Cluster::default(None).pool()).await.unwrap(); let mut site_search = markdown::SiteSearch::new() .await diff --git a/pgml-dashboard/src/utils/cookies.rs b/pgml-dashboard/src/utils/cookies.rs index 370e3d4b1..e7e5be1d5 100644 --- a/pgml-dashboard/src/utils/cookies.rs +++ b/pgml-dashboard/src/utils/cookies.rs @@ -2,123 +2,55 @@ use chrono; use rocket::http::{Cookie, CookieJar}; use rocket::serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct NotificationCookie { pub id: String, pub time_viewed: Option>, pub time_modal_viewed: Option>, } +impl std::fmt::Display for NotificationCookie { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut rsp = format!(r#"{{"id": "{}""#, self.id.clone()); + if self.time_viewed.is_some() { + rsp.push_str(&format!(r#", "time_viewed": "{}""#, self.time_viewed.clone().unwrap())); + } + if self.time_modal_viewed.is_some() { + rsp.push_str(&format!( + r#", "time_modal_viewed": "{}""#, + self.time_modal_viewed.clone().unwrap() + )); + } + rsp.push_str("}"); + return write!(f, "{}", rsp); + } +} + pub struct Notifications {} impl Notifications { - pub fn update_viewed(all_desired_notifications: &Vec, cookies: &CookieJar<'_>) { - let session = Notifications::safe_serialize_session(all_desired_notifications); + pub fn update_viewed(new: &Vec, cookies: &CookieJar<'_>) { + let serialized = new.iter().map(|x| x.to_string()).collect::>(); - let mut cookie = Cookie::new("session", session); + let mut cookie = Cookie::new("session", format!(r#"{{"notifications": [{}]}}"#, serialized.join(","))); cookie.set_max_age(::time::Duration::weeks(4)); cookies.add_private(cookie); } pub fn get_viewed(cookies: &CookieJar<'_>) -> Vec { - match cookies.get_private("session") { - Some(session) => Notifications::safe_deserialize_session(session.value()), + let viewed: Vec = match cookies.get_private("session") { + Some(session) => { + match serde_json::from_str::(session.value()).unwrap()["notifications"].as_array() { + Some(items) => items + .into_iter() + .map(|x| serde_json::from_str::(&x.to_string()).unwrap()) + .collect::>(), + _ => vec![], + } + } None => vec![], - } - } - - pub fn safe_deserialize_session(session: &str) -> Vec { - match serde_json::from_str::(session).unwrap_or_else(|_| { - serde_json::from_str::(&Notifications::safe_serialize_session(&vec![])).unwrap() - })["notifications"] - .as_array() - { - Some(items) => items - .into_iter() - .map(|notification| { - serde_json::from_str::(¬ification.to_string()).unwrap_or_else(|_| { - serde_json::from_str::(¬ification.to_string()) - .and_then(|id| { - Ok(NotificationCookie { - id, - time_viewed: None, - time_modal_viewed: None, - }) - }) - .unwrap_or_else(|_| NotificationCookie::default()) - }) - }) - .collect::>(), - _ => vec![], - } - } - - pub fn safe_serialize_session(cookies: &Vec) -> String { - let serialized = cookies - .iter() - .map(|x| serde_json::to_string(x)) - .filter(|x| x.is_ok()) - .map(|x| x.unwrap()) - .collect::>(); - - format!(r#"{{"notifications": [{}]}}"#, serialized.join(",")) - } -} - -#[cfg(test)] -mod test { - use super::*; - - // Test that we can safely deserialize expected session data. - #[test] - fn test_safe_deserialize_session() { - let session = r#"{"notifications": [{"id": "1", "time_viewed": null, "time_modal_viewed": null}, {"id": "1234567891234", "time_viewed": "2021-08-01T00:00:00Z"}]}"#; - let expected = vec![ - NotificationCookie { - id: "1".to_string(), - time_viewed: None, - time_modal_viewed: None, - }, - NotificationCookie { - id: "1234567891234".to_string(), - time_viewed: Some( - chrono::DateTime::parse_from_rfc3339("2021-08-01T00:00:00Z") - .unwrap() - .into(), - ), - time_modal_viewed: None, - }, - ]; - assert_eq!(Notifications::safe_deserialize_session(session), expected); - } - - // Test that new notification system is backwards compatible. - #[test] - fn test_safe_deserialize_session_old_form() { - let session = r#"{"notifications": ["123456789"]}"#; - let expected = vec![NotificationCookie { - id: "123456789".to_string(), - time_viewed: None, - time_modal_viewed: None, - }]; - assert_eq!(Notifications::safe_deserialize_session(session), expected); - } - - #[test] - fn test_safe_deserialize_session_empty() { - let session = r#"{}"#; - let expected: Vec = vec![]; - assert_eq!(Notifications::safe_deserialize_session(session), expected); - } + }; - #[test] - fn test_safe_serialize_session() { - let cookies = vec![NotificationCookie { - id: "1".to_string(), - time_viewed: None, - time_modal_viewed: None, - }]; - let expected = r#"{"notifications": [{"id":"1","time_viewed":null,"time_modal_viewed":null}]}"#; - assert_eq!(Notifications::safe_serialize_session(&cookies), expected); + viewed } }