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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy