diff --git a/pgml-dashboard/src/api/cms.rs b/pgml-dashboard/src/api/cms.rs index d39994e89..0c703b661 100644 --- a/pgml-dashboard/src/api/cms.rs +++ b/pgml-dashboard/src/api/cms.rs @@ -266,7 +266,7 @@ impl Collection { Some(cluster.context.user.clone()) }; - let mut layout = crate::templates::Layout::new(&title); + let mut layout = crate::templates::Layout::new(&title, Some(cluster.clone())); if let Some(image) = image { // translate relative url into absolute for head social sharing let parts = image.split(".gitbook/assets/").collect::>(); diff --git a/pgml-dashboard/src/components/mod.rs b/pgml-dashboard/src/components/mod.rs index e165ec1a5..1054f2d8a 100644 --- a/pgml-dashboard/src/components/mod.rs +++ b/pgml-dashboard/src/components/mod.rs @@ -53,6 +53,9 @@ pub use nav_link::NavLink; // src/components/navigation pub mod navigation; +// src/components/notifications +pub mod notifications; + // src/components/postgres_logo pub mod postgres_logo; pub use postgres_logo::PostgresLogo; diff --git a/pgml-dashboard/src/components/notifications/banner/banner.scss b/pgml-dashboard/src/components/notifications/banner/banner.scss new file mode 100644 index 000000000..2fbeca37b --- /dev/null +++ b/pgml-dashboard/src/components/notifications/banner/banner.scss @@ -0,0 +1,61 @@ +#notifications-banner { + margin-left: calc(var(--bs-gutter-x) * -0.5); + margin-right: calc(var(--bs-gutter-x) * -0.5); +} + +div[data-controller="notifications-banner"] { + .btn-tertiary { + border: 0px; + } + .news { + background-color: #{$gray-100}; + color: #{$gray-900}; + .btn-tertiary:hover { + filter: brightness(0.9); + } + } + .blog { + background-color: #{$neon-shade-100}; + .btn-tertiary { + filter: brightness(1.5); + } + } + .launch { + background-color: #{$magenta-shade-200}; + .btn-tertiary { + filter: brightness(1.5); + } + } + .tip { + background-color: #{$gray-900}; + } + .level1 { + background-color: #FFFF00; + color: #{$gray-900}; + } + .level2 { + background-color: #FF6929; + color: #{$gray-900}; + } + .level3 { + background-color: #{$peach-shade-200}; + } + + .close-dark { + color: #{$gray-900}; + } + .close-light { + color: #{$gray-100}; + } + .close-dark, .close-light { + margin-left: -100%; + } + + .message-area { + max-width: 75vw; + } + + .banner { + min-height: 2rem; + } +} diff --git a/pgml-dashboard/src/components/notifications/banner/banner_controller.js b/pgml-dashboard/src/components/notifications/banner/banner_controller.js new file mode 100644 index 000000000..a4e516972 --- /dev/null +++ b/pgml-dashboard/src/components/notifications/banner/banner_controller.js @@ -0,0 +1,3 @@ +import { Controller } from '@hotwired/stimulus' + +export default class extends Controller {} diff --git a/pgml-dashboard/src/components/notifications/banner/mod.rs b/pgml-dashboard/src/components/notifications/banner/mod.rs new file mode 100644 index 000000000..94477389c --- /dev/null +++ b/pgml-dashboard/src/components/notifications/banner/mod.rs @@ -0,0 +1,33 @@ +use crate::{Notification, NotificationLevel}; +use pgml_components::component; +use sailfish::TemplateOnce; + +#[derive(TemplateOnce, Default, Clone)] +#[template(path = "notifications/banner/template.html")] +pub struct Banner { + pub notification: Notification, + pub remove_banner: bool, +} + +impl Banner { + pub fn new() -> Banner { + Banner { + notification: Notification::default(), + remove_banner: false, + } + } + + pub fn from_notification(notification: Notification) -> Banner { + Banner { + notification, + remove_banner: false, + } + } + + pub fn remove_banner(mut self, remove_banner: bool) -> Banner { + self.remove_banner = remove_banner; + self + } +} + +component!(Banner); diff --git a/pgml-dashboard/src/components/notifications/banner/template.html b/pgml-dashboard/src/components/notifications/banner/template.html new file mode 100644 index 000000000..c1c23262a --- /dev/null +++ b/pgml-dashboard/src/components/notifications/banner/template.html @@ -0,0 +1,28 @@ +<% use crate::NotificationLevel; %> + + <% if !remove_banner {%> +
+
+ +
+
+ <% } %> +
diff --git a/pgml-dashboard/src/components/notifications/mod.rs b/pgml-dashboard/src/components/notifications/mod.rs new file mode 100644 index 000000000..81d73efd5 --- /dev/null +++ b/pgml-dashboard/src/components/notifications/mod.rs @@ -0,0 +1,6 @@ +// This file is automatically generated. +// You shouldn't modify it manually. + +// src/components/notifications/banner +pub mod banner; +pub use banner::Banner; diff --git a/pgml-dashboard/src/guards.rs b/pgml-dashboard/src/guards.rs index 47cef69fa..14fd3e1d5 100644 --- a/pgml-dashboard/src/guards.rs +++ b/pgml-dashboard/src/guards.rs @@ -8,12 +8,13 @@ use sqlx::{postgres::PgPoolOptions, Executor, PgPool}; static POOL: OnceCell = OnceCell::new(); -use crate::{models, utils::config, Context}; +use crate::{models, utils::config, Context, Notification}; -#[derive(Debug)] +#[derive(Debug, Clone, Default)] pub struct Cluster { pub pool: Option, pub context: Context, + pub notifications: Option>, } impl Cluster { @@ -132,6 +133,7 @@ impl Cluster { lower_left_nav: StaticNav::default(), marketing_footer: MarketingFooter::new().render_once().unwrap(), }, + notifications: None, } } } diff --git a/pgml-dashboard/src/lib.rs b/pgml-dashboard/src/lib.rs index 0761cc5c4..f96717045 100644 --- a/pgml-dashboard/src/lib.rs +++ b/pgml-dashboard/src/lib.rs @@ -2,6 +2,7 @@ extern crate rocket; use rocket::form::Form; +use rocket::http::{Cookie, CookieJar}; use rocket::response::Redirect; use rocket::route::Route; use rocket::serde::json::Json; @@ -20,6 +21,7 @@ pub mod templates; pub mod types; pub mod utils; +use components::notifications::banner::Banner; use guards::{Cluster, ConnectedCluster}; use responses::{BadRequest, Error, ResponseOk}; use templates::{ @@ -28,6 +30,10 @@ use templates::{ }; use utils::tabs; +use crate::utils::cookies::Notifications; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + #[derive(Debug, Default, Clone)] pub struct ClustersSettings { pub max_connections: u32, @@ -50,6 +56,77 @@ pub struct Context { pub marketing_footer: String, } +#[derive(Debug, Clone, Default)] +pub struct Notification { + pub message: String, + pub level: NotificationLevel, + pub id: String, + pub dismissible: bool, + pub viewed: bool, + pub link: Option, +} +impl Notification { + pub fn new(message: &str) -> Notification { + let mut s = DefaultHasher::new(); + message.hash(&mut s); + + Notification { + message: message.to_string(), + level: NotificationLevel::News, + id: s.finish().to_string(), + dismissible: true, + viewed: false, + link: None, + } + } + + pub fn level(mut self, level: &NotificationLevel) -> Notification { + self.level = level.clone(); + self + } + + pub fn dismissible(mut self, dismissible: bool) -> Notification { + self.dismissible = dismissible; + self + } + + pub fn link(mut self, link: &str) -> Notification { + self.link = Some(link.into()); + self + } + + pub fn viewed(mut self, viewed: bool) -> Notification { + self.viewed = viewed; + self + } +} + +impl std::fmt::Display for NotificationLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NotificationLevel::News => write!(f, "news"), + NotificationLevel::Blog => write!(f, "blog"), + NotificationLevel::Launch => write!(f, "launch"), + NotificationLevel::Tip => write!(f, "tip"), + NotificationLevel::Level1 => write!(f, "level1"), + NotificationLevel::Level2 => write!(f, "level2"), + NotificationLevel::Level3 => write!(f, "level3"), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub enum NotificationLevel { + #[default] + News, + Blog, + Launch, + Tip, + Level1, + Level2, + Level3, +} + #[get("/projects")] pub async fn project_index(cluster: ConnectedCluster<'_>) -> Result { Ok(ResponseOk( @@ -672,6 +749,30 @@ pub async fn playground(cluster: &Cluster) -> Result { Ok(ResponseOk(layout.render(templates::Playground {}))) } +#[get("/notifications/remove_banner?")] +pub fn remove_banner(id: String, cookies: &CookieJar<'_>, context: &Cluster) -> ResponseOk { + let mut viewed = Notifications::get_viewed(cookies); + + viewed.push(id); + Notifications::update_viewed(&viewed, cookies); + + match context.notifications.as_ref() { + Some(notifications) => { + for notification in notifications { + if !viewed.contains(¬ification.id) { + return ResponseOk( + Banner::from_notification(notification.clone()) + .render_once() + .unwrap(), + ); + } + } + return ResponseOk(Banner::new().remove_banner(true).render_once().unwrap()); + } + None => return ResponseOk(Banner::new().remove_banner(true).render_once().unwrap()), + } +} + pub fn routes() -> Vec { routes![ notebook_index, @@ -699,6 +800,7 @@ pub fn routes() -> Vec { uploaded_index, dashboard, notebook_reorder, + remove_banner, ] } diff --git a/pgml-dashboard/src/responses.rs b/pgml-dashboard/src/responses.rs index 8fc5d5186..fe7574124 100644 --- a/pgml-dashboard/src/responses.rs +++ b/pgml-dashboard/src/responses.rs @@ -81,9 +81,8 @@ impl<'r> response::Responder<'r, 'r> for Response { let body = match self.body { Some(body) => body, None => match self.status.code { - 404 => { - templates::Layout::new("Internal Server Error").render(templates::NotFound {}) - } + 404 => templates::Layout::new("Internal Server Error", None) + .render(templates::NotFound {}), _ => "".into(), }, }; @@ -134,8 +133,8 @@ impl<'r> response::Responder<'r, 'r> for Error { "".into() }; - let body = - templates::Layout::new("Internal Server Error").render(templates::Error { error }); + let body = templates::Layout::new("Internal Server Error", None) + .render(templates::Error { error }); response::Response::build_from(body.respond_to(request)?) .header(ContentType::new("text", "html")) diff --git a/pgml-dashboard/src/templates/mod.rs b/pgml-dashboard/src/templates/mod.rs index b2173be0c..4cd880700 100644 --- a/pgml-dashboard/src/templates/mod.rs +++ b/pgml-dashboard/src/templates/mod.rs @@ -2,6 +2,7 @@ use pgml_components::Component; use std::collections::HashMap; pub use crate::components::{self, cms::index_link::IndexLink, NavLink, StaticNav, StaticNavLink}; +use components::notifications::banner::Banner; use sailfish::TemplateOnce; use sqlx::postgres::types::PgMoney; @@ -36,12 +37,22 @@ pub struct Layout { pub nav_links: Vec, pub toc_links: Vec, pub footer: String, + pub banner: Option, } impl Layout { - pub fn new(title: &str) -> Self { + pub fn new(title: &str, context: Option) -> Self { + let banner = match context.as_ref() { + Some(context) => match &context.notifications { + Some(notification) => Some(Banner::from_notification(notification[0].clone())), + None => None, + }, + None => None, + }; + Layout { head: Head::new().title(title), + banner, ..Default::default() } } diff --git a/pgml-dashboard/src/utils/cookies.rs b/pgml-dashboard/src/utils/cookies.rs new file mode 100644 index 000000000..49c4aafe0 --- /dev/null +++ b/pgml-dashboard/src/utils/cookies.rs @@ -0,0 +1,32 @@ +use rocket::http::{Cookie, CookieJar}; +use rocket::serde::json::Json; + +pub struct Notifications {} + +impl Notifications { + pub fn update_viewed(new: &Vec, cookies: &CookieJar<'_>) { + let mut cookie = Cookie::new("session", format!(r#"{{"notifications": {:?}}}"#, new)); + cookie.set_max_age(::time::Duration::weeks(4)); + cookies.add_private(cookie); + } + + pub fn get_viewed(cookies: &CookieJar<'_>) -> Vec { + let mut viewed = 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| x.as_str().unwrap().to_string()) + .collect::>(), + _ => vec![], + } + } + None => vec![], + }; + + viewed + } +} diff --git a/pgml-dashboard/src/utils/mod.rs b/pgml-dashboard/src/utils/mod.rs index 78a8a9c72..44e25011d 100644 --- a/pgml-dashboard/src/utils/mod.rs +++ b/pgml-dashboard/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod cookies; pub mod datadog; pub mod markdown; pub mod tabs; diff --git a/pgml-dashboard/static/css/modules.scss b/pgml-dashboard/static/css/modules.scss index ea6aadd69..c592e741f 100644 --- a/pgml-dashboard/static/css/modules.scss +++ b/pgml-dashboard/static/css/modules.scss @@ -18,6 +18,7 @@ @import "../../src/components/navigation/navbar/web_app/web_app.scss"; @import "../../src/components/navigation/tabs/tab/tab.scss"; @import "../../src/components/navigation/tabs/tabs/tabs.scss"; +@import "../../src/components/notifications/banner/banner.scss"; @import "../../src/components/postgres_logo/postgres_logo.scss"; @import "../../src/components/sections/footers/marketing_footer/marketing_footer.scss"; @import "../../src/components/star/star.scss"; diff --git a/pgml-dashboard/static/css/scss/components/_buttons.scss b/pgml-dashboard/static/css/scss/components/_buttons.scss index 45db891a5..c32f9cf5c 100644 --- a/pgml-dashboard/static/css/scss/components/_buttons.scss +++ b/pgml-dashboard/static/css/scss/components/_buttons.scss @@ -84,11 +84,11 @@ --bs-btn-border-color: transparent; --bs-btn-hover-bg: transparent; - --bs-btn-hover-color: #{$gray-100}; + --bs-btn-hover-color: #{$slate-tint-400}; --bs-btn-hover-border-color: transparent; --bs-btn-active-bg: transparent; - --bs-btn-active-color: #{$gray-100}; + --bs-btn-active-color: #{$slate-tint-700}; --bs-btn-active-border-color: transparent; span { diff --git a/pgml-dashboard/templates/layout/base.html b/pgml-dashboard/templates/layout/base.html index d46c2e9bd..c917decf7 100644 --- a/pgml-dashboard/templates/layout/base.html +++ b/pgml-dashboard/templates/layout/base.html @@ -1,4 +1,7 @@ -<% use crate::components::navigation::navbar::marketing::Marketing as MarketingNavbar; %> +<% + use crate::components::navigation::navbar::marketing::Marketing as MarketingNavbar; + use crate::components::notifications::Banner; +%> @@ -10,7 +13,7 @@
- + <% if banner.is_some() {%><%+ banner.unwrap() %><% } %> <%+ MarketingNavbar::new( user ) %>