Skip to content

Commit 07290d1

Browse files
Dan notification banner (#1197)
1 parent 73f24dd commit 07290d1

File tree

16 files changed

+298
-13
lines changed

16 files changed

+298
-13
lines changed

pgml-dashboard/src/api/cms.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl Collection {
266266
Some(cluster.context.user.clone())
267267
};
268268

269-
let mut layout = crate::templates::Layout::new(&title);
269+
let mut layout = crate::templates::Layout::new(&title, Some(cluster.clone()));
270270
if let Some(image) = image {
271271
// translate relative url into absolute for head social sharing
272272
let parts = image.split(".gitbook/assets/").collect::<Vec<&str>>();

pgml-dashboard/src/components/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ pub use nav_link::NavLink;
5353
// src/components/navigation
5454
pub mod navigation;
5555

56+
// src/components/notifications
57+
pub mod notifications;
58+
5659
// src/components/postgres_logo
5760
pub mod postgres_logo;
5861
pub use postgres_logo::PostgresLogo;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#notifications-banner {
2+
margin-left: calc(var(--bs-gutter-x) * -0.5);
3+
margin-right: calc(var(--bs-gutter-x) * -0.5);
4+
}
5+
6+
div[data-controller="notifications-banner"] {
7+
.btn-tertiary {
8+
border: 0px;
9+
}
10+
.news {
11+
background-color: #{$gray-100};
12+
color: #{$gray-900};
13+
.btn-tertiary:hover {
14+
filter: brightness(0.9);
15+
}
16+
}
17+
.blog {
18+
background-color: #{$neon-shade-100};
19+
.btn-tertiary {
20+
filter: brightness(1.5);
21+
}
22+
}
23+
.launch {
24+
background-color: #{$magenta-shade-200};
25+
.btn-tertiary {
26+
filter: brightness(1.5);
27+
}
28+
}
29+
.tip {
30+
background-color: #{$gray-900};
31+
}
32+
.level1 {
33+
background-color: #FFFF00;
34+
color: #{$gray-900};
35+
}
36+
.level2 {
37+
background-color: #FF6929;
38+
color: #{$gray-900};
39+
}
40+
.level3 {
41+
background-color: #{$peach-shade-200};
42+
}
43+
44+
.close-dark {
45+
color: #{$gray-900};
46+
}
47+
.close-light {
48+
color: #{$gray-100};
49+
}
50+
.close-dark, .close-light {
51+
margin-left: -100%;
52+
}
53+
54+
.message-area {
55+
max-width: 75vw;
56+
}
57+
58+
.banner {
59+
min-height: 2rem;
60+
}
61+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Controller } from '@hotwired/stimulus'
2+
3+
export default class extends Controller {}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use crate::{Notification, NotificationLevel};
2+
use pgml_components::component;
3+
use sailfish::TemplateOnce;
4+
5+
#[derive(TemplateOnce, Default, Clone)]
6+
#[template(path = "notifications/banner/template.html")]
7+
pub struct Banner {
8+
pub notification: Notification,
9+
pub remove_banner: bool,
10+
}
11+
12+
impl Banner {
13+
pub fn new() -> Banner {
14+
Banner {
15+
notification: Notification::default(),
16+
remove_banner: false,
17+
}
18+
}
19+
20+
pub fn from_notification(notification: Notification) -> Banner {
21+
Banner {
22+
notification,
23+
remove_banner: false,
24+
}
25+
}
26+
27+
pub fn remove_banner(mut self, remove_banner: bool) -> Banner {
28+
self.remove_banner = remove_banner;
29+
self
30+
}
31+
}
32+
33+
component!(Banner);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<% use crate::NotificationLevel; %>
2+
<turbo-frame id="notifications-banner" class="position-relative d-block">
3+
<% if !remove_banner {%>
4+
<div data-controller="notifications-banner">
5+
<div class="<%- notification.level.to_string() %> W-100">
6+
<div class="banner d-flex container p-1">
7+
<div class="flex-grow-1 d-flex flex-column flex-md-row justify-content-center align-items-center row-gap-0 column-gap-3 fw-semibold overflow-hidden">
8+
<div class="mx-3 overflow-hidden" style="max-width: 80%;">
9+
<p class="m-0 text-center"><%- notification.message %></p>
10+
</div>
11+
<% if notification.link.is_some() {%>
12+
<a class="btn btn-tertiary fw-semibold p-0" href="<%- notification.link.unwrap() %>" data-turbo="false">
13+
Learn More
14+
<span class="material-symbols-outlined">arrow_forward</span>
15+
</a>
16+
<% } %>
17+
</div>
18+
<% if notification.dismissible {%>
19+
<a class="w-0 overflow-visible d-flex align-items-center" style="right: 4vw" href="/dashboard/notifications/remove_banner?id=<%- notification.id%>">
20+
<span class="material-symbols-outlined <% if notification.level == NotificationLevel::Tip {%>close-light<% } else {%>close-dark<% } %>">
21+
close
22+
</span></a>
23+
<% } %>
24+
</div>
25+
</div>
26+
</div>
27+
<% } %>
28+
</turbo-frame>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// This file is automatically generated.
2+
// You shouldn't modify it manually.
3+
4+
// src/components/notifications/banner
5+
pub mod banner;
6+
pub use banner::Banner;

pgml-dashboard/src/guards.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ use sqlx::{postgres::PgPoolOptions, Executor, PgPool};
88

99
static POOL: OnceCell<PgPool> = OnceCell::new();
1010

11-
use crate::{models, utils::config, Context};
11+
use crate::{models, utils::config, Context, Notification};
1212

13-
#[derive(Debug)]
13+
#[derive(Debug, Clone, Default)]
1414
pub struct Cluster {
1515
pub pool: Option<PgPool>,
1616
pub context: Context,
17+
pub notifications: Option<Vec<Notification>>,
1718
}
1819

1920
impl Cluster {
@@ -132,6 +133,7 @@ impl Cluster {
132133
lower_left_nav: StaticNav::default(),
133134
marketing_footer: MarketingFooter::new().render_once().unwrap(),
134135
},
136+
notifications: None,
135137
}
136138
}
137139
}

pgml-dashboard/src/lib.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
extern crate rocket;
33

44
use rocket::form::Form;
5+
use rocket::http::{Cookie, CookieJar};
56
use rocket::response::Redirect;
67
use rocket::route::Route;
78
use rocket::serde::json::Json;
@@ -20,6 +21,7 @@ pub mod templates;
2021
pub mod types;
2122
pub mod utils;
2223

24+
use components::notifications::banner::Banner;
2325
use guards::{Cluster, ConnectedCluster};
2426
use responses::{BadRequest, Error, ResponseOk};
2527
use templates::{
@@ -28,6 +30,10 @@ use templates::{
2830
};
2931
use utils::tabs;
3032

33+
use crate::utils::cookies::Notifications;
34+
use std::collections::hash_map::DefaultHasher;
35+
use std::hash::{Hash, Hasher};
36+
3137
#[derive(Debug, Default, Clone)]
3238
pub struct ClustersSettings {
3339
pub max_connections: u32,
@@ -50,6 +56,77 @@ pub struct Context {
5056
pub marketing_footer: String,
5157
}
5258

59+
#[derive(Debug, Clone, Default)]
60+
pub struct Notification {
61+
pub message: String,
62+
pub level: NotificationLevel,
63+
pub id: String,
64+
pub dismissible: bool,
65+
pub viewed: bool,
66+
pub link: Option<String>,
67+
}
68+
impl Notification {
69+
pub fn new(message: &str) -> Notification {
70+
let mut s = DefaultHasher::new();
71+
message.hash(&mut s);
72+
73+
Notification {
74+
message: message.to_string(),
75+
level: NotificationLevel::News,
76+
id: s.finish().to_string(),
77+
dismissible: true,
78+
viewed: false,
79+
link: None,
80+
}
81+
}
82+
83+
pub fn level(mut self, level: &NotificationLevel) -> Notification {
84+
self.level = level.clone();
85+
self
86+
}
87+
88+
pub fn dismissible(mut self, dismissible: bool) -> Notification {
89+
self.dismissible = dismissible;
90+
self
91+
}
92+
93+
pub fn link(mut self, link: &str) -> Notification {
94+
self.link = Some(link.into());
95+
self
96+
}
97+
98+
pub fn viewed(mut self, viewed: bool) -> Notification {
99+
self.viewed = viewed;
100+
self
101+
}
102+
}
103+
104+
impl std::fmt::Display for NotificationLevel {
105+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106+
match self {
107+
NotificationLevel::News => write!(f, "news"),
108+
NotificationLevel::Blog => write!(f, "blog"),
109+
NotificationLevel::Launch => write!(f, "launch"),
110+
NotificationLevel::Tip => write!(f, "tip"),
111+
NotificationLevel::Level1 => write!(f, "level1"),
112+
NotificationLevel::Level2 => write!(f, "level2"),
113+
NotificationLevel::Level3 => write!(f, "level3"),
114+
}
115+
}
116+
}
117+
118+
#[derive(Debug, Clone, Default, PartialEq)]
119+
pub enum NotificationLevel {
120+
#[default]
121+
News,
122+
Blog,
123+
Launch,
124+
Tip,
125+
Level1,
126+
Level2,
127+
Level3,
128+
}
129+
53130
#[get("/projects")]
54131
pub async fn project_index(cluster: ConnectedCluster<'_>) -> Result<ResponseOk, Error> {
55132
Ok(ResponseOk(
@@ -672,6 +749,30 @@ pub async fn playground(cluster: &Cluster) -> Result<ResponseOk, Error> {
672749
Ok(ResponseOk(layout.render(templates::Playground {})))
673750
}
674751

752+
#[get("/notifications/remove_banner?<id>")]
753+
pub fn remove_banner(id: String, cookies: &CookieJar<'_>, context: &Cluster) -> ResponseOk {
754+
let mut viewed = Notifications::get_viewed(cookies);
755+
756+
viewed.push(id);
757+
Notifications::update_viewed(&viewed, cookies);
758+
759+
match context.notifications.as_ref() {
760+
Some(notifications) => {
761+
for notification in notifications {
762+
if !viewed.contains(&notification.id) {
763+
return ResponseOk(
764+
Banner::from_notification(notification.clone())
765+
.render_once()
766+
.unwrap(),
767+
);
768+
}
769+
}
770+
return ResponseOk(Banner::new().remove_banner(true).render_once().unwrap());
771+
}
772+
None => return ResponseOk(Banner::new().remove_banner(true).render_once().unwrap()),
773+
}
774+
}
775+
675776
pub fn routes() -> Vec<Route> {
676777
routes![
677778
notebook_index,
@@ -699,6 +800,7 @@ pub fn routes() -> Vec<Route> {
699800
uploaded_index,
700801
dashboard,
701802
notebook_reorder,
803+
remove_banner,
702804
]
703805
}
704806

pgml-dashboard/src/responses.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,8 @@ impl<'r> response::Responder<'r, 'r> for Response {
8181
let body = match self.body {
8282
Some(body) => body,
8383
None => match self.status.code {
84-
404 => {
85-
templates::Layout::new("Internal Server Error").render(templates::NotFound {})
86-
}
84+
404 => templates::Layout::new("Internal Server Error", None)
85+
.render(templates::NotFound {}),
8786
_ => "".into(),
8887
},
8988
};
@@ -134,8 +133,8 @@ impl<'r> response::Responder<'r, 'r> for Error {
134133
"".into()
135134
};
136135

137-
let body =
138-
templates::Layout::new("Internal Server Error").render(templates::Error { error });
136+
let body = templates::Layout::new("Internal Server Error", None)
137+
.render(templates::Error { error });
139138

140139
response::Response::build_from(body.respond_to(request)?)
141140
.header(ContentType::new("text", "html"))

pgml-dashboard/src/templates/mod.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use pgml_components::Component;
22
use std::collections::HashMap;
33

44
pub use crate::components::{self, cms::index_link::IndexLink, NavLink, StaticNav, StaticNavLink};
5+
use components::notifications::banner::Banner;
56

67
use sailfish::TemplateOnce;
78
use sqlx::postgres::types::PgMoney;
@@ -36,12 +37,22 @@ pub struct Layout {
3637
pub nav_links: Vec<IndexLink>,
3738
pub toc_links: Vec<docs::TocLink>,
3839
pub footer: String,
40+
pub banner: Option<Banner>,
3941
}
4042

4143
impl Layout {
42-
pub fn new(title: &str) -> Self {
44+
pub fn new(title: &str, context: Option<crate::guards::Cluster>) -> Self {
45+
let banner = match context.as_ref() {
46+
Some(context) => match &context.notifications {
47+
Some(notification) => Some(Banner::from_notification(notification[0].clone())),
48+
None => None,
49+
},
50+
None => None,
51+
};
52+
4353
Layout {
4454
head: Head::new().title(title),
55+
banner,
4556
..Default::default()
4657
}
4758
}

0 commit comments

Comments
 (0)