Skip to content

Commit f7ad187

Browse files
Dan opensource webapp layout (#778)
1 parent a3490e1 commit f7ad187

File tree

5 files changed

+318
-11
lines changed

5 files changed

+318
-11
lines changed

pgml-dashboard/src/templates/components.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,24 @@ pub struct Nav<'a> {
6262
pub links: Vec<NavLink<'a>>,
6363
}
6464

65+
impl<'a> Nav<'a> {
66+
pub fn render(links: Vec<NavLink<'a>>) -> String {
67+
Nav { links }.render_once().unwrap()
68+
}
69+
}
70+
6571
#[derive(TemplateOnce)]
6672
#[template(path = "components/breadcrumbs.html")]
6773
pub struct Breadcrumbs<'a> {
6874
pub links: Vec<NavLink<'a>>,
6975
}
7076

77+
impl<'a> Breadcrumbs<'a> {
78+
pub fn render(links: Vec<NavLink<'a>>) -> String {
79+
Breadcrumbs { links }.render_once().unwrap()
80+
}
81+
}
82+
7183
#[derive(TemplateOnce)]
7284
#[template(path = "components/boxes.html")]
7385
pub struct Boxes<'a> {

pgml-dashboard/src/templates/head.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1+
use sailfish::TemplateOnce;
2+
13
#[derive(Clone, Default)]
24
pub struct Head {
35
pub title: String,
46
pub description: Option<String>,
57
pub image: Option<String>,
8+
pub preloads: Vec<String>,
69
}
710

811
impl Head {
912
pub fn new() -> Head {
1013
Head::default()
1114
}
1215

16+
pub fn add_preload(&mut self, preload: &str) -> &mut Self {
17+
self.preloads.push(preload.to_owned());
18+
self
19+
}
20+
1321
pub fn title(mut self, title: &str) -> Head {
1422
self.title = title.to_owned();
1523
self
@@ -29,3 +37,115 @@ impl Head {
2937
Head::new().title("404 - Not Found")
3038
}
3139
}
40+
41+
#[derive(TemplateOnce, Default, Clone)]
42+
#[template(path = "layout/head.html")]
43+
pub struct DefaultHeadTemplate {
44+
pub head: Head,
45+
}
46+
47+
impl DefaultHeadTemplate {
48+
pub fn new(head: Option<Head>) -> DefaultHeadTemplate {
49+
let head = match head {
50+
Some(head) => head,
51+
None => Head::new(),
52+
};
53+
54+
DefaultHeadTemplate { head }
55+
}
56+
}
57+
58+
impl From<DefaultHeadTemplate> for String {
59+
fn from(layout: DefaultHeadTemplate) -> String {
60+
layout.render_once().unwrap()
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod head_tests {
66+
use crate::templates::Head;
67+
68+
#[test]
69+
fn new_head() {
70+
let head = Head::new();
71+
assert_eq!(
72+
(head.title, head.description, head.image, head.preloads),
73+
("".to_string(), None, None, vec![])
74+
);
75+
}
76+
77+
#[test]
78+
fn add_preload() {
79+
let mut head = Head::new();
80+
let mut preloads: Vec<String> = vec![];
81+
for i in 0..5 {
82+
preloads.push(format!("image/test_preload_{}.test", i).to_string());
83+
}
84+
for preload in preloads.clone() {
85+
head.add_preload(&preload);
86+
}
87+
assert!(head.preloads.eq(&preloads));
88+
}
89+
90+
#[test]
91+
fn add_title() {
92+
let head = Head::new().title("test title");
93+
assert_eq!(head.title, "test title");
94+
}
95+
96+
#[test]
97+
fn add_description() {
98+
let head = Head::new().description("test description");
99+
assert_eq!(head.description, Some("test description".to_string()));
100+
}
101+
102+
#[test]
103+
fn add_image() {
104+
let head = Head::new().image("images/image_file_path.jpg");
105+
assert_eq!(head.image, Some("images/image_file_path.jpg".to_string()));
106+
}
107+
108+
#[test]
109+
fn not_found() {
110+
let head = Head::not_found();
111+
assert_eq!(head.title, "404 - Not Found")
112+
}
113+
}
114+
115+
#[cfg(test)]
116+
mod default_head_template_test {
117+
use super::{DefaultHeadTemplate, Head};
118+
use sailfish::TemplateOnce;
119+
120+
#[test]
121+
fn default() {
122+
let head = DefaultHeadTemplate::new(None);
123+
let rendered = head.render_once().unwrap();
124+
assert!(
125+
rendered.contains(r#"<head>"#) &&
126+
rendered.contains(r#"<title> – PostgresML</title>"#) &&
127+
rendered.contains(r#"<meta name="description" content="Train and deploy models to make online predictions using only SQL, with an open source Postgres extension.">"#) &&
128+
!rendered.contains("preload") &&
129+
rendered.contains(r#"<script type="importmap-shim" data-turbo-track="reload">"#) &&
130+
rendered.contains("</head>")
131+
)
132+
}
133+
134+
#[test]
135+
fn set_head() {
136+
let mut head_info = Head::new()
137+
.title("test title")
138+
.description("test description")
139+
.image("image/test_image.jpg");
140+
head_info.add_preload("image/test_preload.webp");
141+
142+
let head = DefaultHeadTemplate::new(Some(head_info));
143+
let rendered = head.render_once().unwrap();
144+
assert!(
145+
rendered.contains("<title>test title – PostgresML</title>") &&
146+
rendered.contains(r#"<meta name="description" content="test description">"#) &&
147+
rendered.contains(r#"<meta property="og:image" content="image/test_image.jpg">"#) &&
148+
!rendered.contains(r#"<link rel="preload" fetchpriority="high" as="image" href="image/test_preload.webp" type="image/webp">"#)
149+
);
150+
}
151+
}

pgml-dashboard/src/templates/mod.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::collections::HashMap;
22

3+
use components::{Nav, NavLink};
4+
35
use sailfish::TemplateOnce;
46
use sqlx::postgres::types::PgMoney;
57
use sqlx::types::time::PrimitiveDateTime;
@@ -93,6 +95,136 @@ impl From<Layout> for String {
9395
}
9496
}
9597

98+
#[derive(TemplateOnce, Clone, Default)]
99+
#[template(path = "layout/web_app_base.html")]
100+
pub struct WebAppBase<'a> {
101+
pub content: Option<String>,
102+
pub visible_clusters: HashMap<String, String>,
103+
pub breadcrumbs: Vec<NavLink<'a>>,
104+
pub nav: Vec<NavLink<'a>>,
105+
pub head: String,
106+
}
107+
108+
impl<'a> WebAppBase<'a> {
109+
pub fn new(title: &str) -> Self {
110+
WebAppBase {
111+
head: crate::templates::head::DefaultHeadTemplate::new(Some(
112+
crate::templates::head::Head {
113+
title: title.to_owned(),
114+
description: None,
115+
image: None,
116+
preloads: vec![],
117+
},
118+
))
119+
.render_once()
120+
.unwrap(),
121+
..Default::default()
122+
}
123+
}
124+
125+
pub fn head(&mut self, head: String) -> &mut Self {
126+
self.head = head.to_owned();
127+
self
128+
}
129+
130+
pub fn clusters(&mut self, clusters: HashMap<String, String>) -> &mut Self {
131+
self.visible_clusters = clusters.to_owned();
132+
self
133+
}
134+
135+
pub fn breadcrumbs(&mut self, breadcrumbs: Vec<NavLink<'a>>) -> &mut Self {
136+
self.breadcrumbs = breadcrumbs.to_owned();
137+
self
138+
}
139+
140+
pub fn nav(&mut self, active: &str) -> &mut Self {
141+
let mut nav_links = vec![NavLink::new("Create new cluster", "/clusters/new").icon("add")];
142+
143+
// Adds the spesific cluster to a sublist.
144+
if self.visible_clusters.len() > 0 {
145+
let mut sorted_clusters: Vec<(String, String)> = self
146+
.visible_clusters
147+
.iter()
148+
.map(|(name, id)| (name.to_string(), id.to_string()))
149+
.collect();
150+
sorted_clusters.sort_by_key(|k| k.1.to_owned());
151+
152+
let cluster_links = sorted_clusters
153+
.iter()
154+
.map(|(name, id)| {
155+
NavLink::new(name, &format!("/clusters/{}", id)).icon("developer_board")
156+
})
157+
.collect();
158+
159+
let cluster_nav = Nav {
160+
links: cluster_links,
161+
};
162+
163+
nav_links.push(
164+
NavLink::new("Clusters", "/clusters")
165+
.icon("lan")
166+
.nav(cluster_nav),
167+
)
168+
} else {
169+
nav_links.push(NavLink::new("Clusters", "/clusters").icon("lan"))
170+
}
171+
172+
nav_links.push(NavLink::new("Payments", "/payments").icon("payments"));
173+
174+
// Sets the active left nav item.
175+
let nav_with_active: Vec<NavLink> = nav_links
176+
.into_iter()
177+
.map(|item| {
178+
if item.name.eq(active) {
179+
return item.active();
180+
}
181+
match item.nav {
182+
Some(sub_nav) => {
183+
let sub_links: Vec<NavLink> = sub_nav
184+
.links
185+
.into_iter()
186+
.map(|sub_item| {
187+
if sub_item.name.eq(active) {
188+
sub_item.active()
189+
} else {
190+
sub_item
191+
}
192+
})
193+
.collect();
194+
NavLink {
195+
nav: Some(Nav { links: sub_links }),
196+
..item
197+
}
198+
}
199+
None => item,
200+
}
201+
})
202+
.collect();
203+
204+
self.nav = nav_with_active;
205+
self
206+
}
207+
208+
pub fn content(&mut self, content: &str) -> &mut Self {
209+
self.content = Some(content.to_owned());
210+
self
211+
}
212+
213+
pub fn render<T>(&mut self, template: T) -> String
214+
where
215+
T: sailfish::TemplateOnce,
216+
{
217+
self.content = Some(template.render_once().unwrap());
218+
(*self).clone().into()
219+
}
220+
}
221+
222+
impl<'a> From<WebAppBase<'a>> for String {
223+
fn from(layout: WebAppBase) -> String {
224+
layout.render_once().unwrap()
225+
}
226+
}
227+
96228
#[derive(TemplateOnce)]
97229
#[template(path = "content/article.html")]
98230
pub struct Article {

pgml-dashboard/templates/components/breadcrumbs.html

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,31 @@
2424
</ol>
2525
</nav>
2626

27-
<nav class="d-flex gap-3 align-items-center">
28-
<button type="text" class="btn-search d-flex gap-2" name="search" data-bs-toggle="modal" data-bs-target="#search" autocomplete="off" data-search-target="searchTrigger" data-action="search#openSearch">
29-
<span class="material-symbols-outlined">
30-
search
31-
</span>
32-
<span>search</span>
33-
</button>
34-
<a href="/logout" class="btn btn-secondary" data-controller="btn-secondary" data-btn-secondary-target="btnSecondary">Logout</a>
35-
<a href="/support" class="btn btn-tertiary p-0 pe-2">
36-
<img loading="lazy" src="/dashboard/static/images/icons/help.svg" width="30" height="30" alt="help" />
37-
</a>
27+
<nav class="horizontal">
28+
<ul class="navbar-nav flex-row gap-3 mb-2 mb-lg-0">
29+
30+
<li class="nav-item d-flex align-items-center">
31+
<button type="text" class="btn-search nav-link p-0" name="search" data-bs-toggle="modal" data-bs-target="#search" autocomplete="off" data-search-target="searchTrigger" data-action="search#openSearch">
32+
Search
33+
</button>
34+
</li>
35+
<li class="nav-item d-flex align-items-center">
36+
<a class="nav-link p-0" href="/docs/guides/setup/quick_start_with_docker/">Docs</a>
37+
</li>
38+
<li class="nav-item d-flex align-items-center">
39+
<a class="nav-link p-0" href="/blog/mindsdb-vs-postgresml">Blog</a>
40+
</li>
41+
<li class="nav-item d-flex align-items-center">
42+
<a href="/logout" class="btn btn-secondary" data-controller="btn-secondary" data-btn-secondary-target="btnSecondary">Logout</a>
43+
</li>
44+
<li class="nav-item d-flex align-items-center">
45+
<a href="/support" class="btn btn-tertiary p-0 pe-2">
46+
<span class="material-symbols-outlined">
47+
help
48+
</span>
49+
</a>
50+
</li>
51+
</ul>
3852
</nav>
3953
<% include!("search_modal.html"); %>
4054
</nav>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<% use crate::templates::components::{Nav, Breadcrumbs}; %>
2+
3+
<!DOCTYPE html>
4+
<html lang="en-US" data-bs-theme="dark">
5+
<%- head %>
6+
<body>
7+
<main>
8+
<div class="container-fluid p-lg-0 min-vh-lg-100">
9+
<div class="row gx-0 min-vh-lg-100">
10+
<div class="sidenav-container col-12 col-lg-3 col-xxl-2 pt-3 pt-lg-0" >
11+
<%- Nav::render( nav ) %>
12+
</div>
13+
14+
<div class="col-12 col-lg-9 col-xxl-10">
15+
<div>
16+
<%- Breadcrumbs::render( breadcrumbs ) %>
17+
</div>
18+
19+
<div>
20+
<%- content.unwrap_or_default() %>
21+
</div>
22+
</div>
23+
</div>
24+
</div>
25+
</main>
26+
27+
<div id="toast-container" class="toast-container position-fixed top-0 end-0 p-3"></div>
28+
</body>
29+
</html>

0 commit comments

Comments
 (0)