Skip to content

Dan opensource webapp layout #778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pgml-dashboard/src/templates/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,24 @@ pub struct Nav<'a> {
pub links: Vec<NavLink<'a>>,
}

impl<'a> Nav<'a> {
pub fn render(links: Vec<NavLink<'a>>) -> String {
Nav { links }.render_once().unwrap()
}
}

#[derive(TemplateOnce)]
#[template(path = "components/breadcrumbs.html")]
pub struct Breadcrumbs<'a> {
pub links: Vec<NavLink<'a>>,
}

impl<'a> Breadcrumbs<'a> {
pub fn render(links: Vec<NavLink<'a>>) -> String {
Breadcrumbs { links }.render_once().unwrap()
}
}

#[derive(TemplateOnce)]
#[template(path = "components/boxes.html")]
pub struct Boxes<'a> {
Expand Down
120 changes: 120 additions & 0 deletions pgml-dashboard/src/templates/head.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
use sailfish::TemplateOnce;

#[derive(Clone, Default)]
pub struct Head {
pub title: String,
pub description: Option<String>,
pub image: Option<String>,
pub preloads: Vec<String>,
}

impl Head {
pub fn new() -> Head {
Head::default()
}

pub fn add_preload(&mut self, preload: &str) -> &mut Self {
self.preloads.push(preload.to_owned());
self
}

pub fn title(mut self, title: &str) -> Head {
self.title = title.to_owned();
self
Expand All @@ -29,3 +37,115 @@ impl Head {
Head::new().title("404 - Not Found")
}
}

#[derive(TemplateOnce, Default, Clone)]
#[template(path = "layout/head.html")]
pub struct DefaultHeadTemplate {
pub head: Head,
}

impl DefaultHeadTemplate {
pub fn new(head: Option<Head>) -> DefaultHeadTemplate {
let head = match head {
Some(head) => head,
None => Head::new(),
};

DefaultHeadTemplate { head }
}
}

impl From<DefaultHeadTemplate> for String {
fn from(layout: DefaultHeadTemplate) -> String {
layout.render_once().unwrap()
}
}

#[cfg(test)]
mod head_tests {
use crate::templates::Head;

#[test]
fn new_head() {
let head = Head::new();
assert_eq!(
(head.title, head.description, head.image, head.preloads),
("".to_string(), None, None, vec![])
);
}

#[test]
fn add_preload() {
let mut head = Head::new();
let mut preloads: Vec<String> = vec![];
for i in 0..5 {
preloads.push(format!("image/test_preload_{}.test", i).to_string());
}
for preload in preloads.clone() {
head.add_preload(&preload);
}
assert!(head.preloads.eq(&preloads));
}

#[test]
fn add_title() {
let head = Head::new().title("test title");
assert_eq!(head.title, "test title");
}

#[test]
fn add_description() {
let head = Head::new().description("test description");
assert_eq!(head.description, Some("test description".to_string()));
}

#[test]
fn add_image() {
let head = Head::new().image("images/image_file_path.jpg");
assert_eq!(head.image, Some("images/image_file_path.jpg".to_string()));
}

#[test]
fn not_found() {
let head = Head::not_found();
assert_eq!(head.title, "404 - Not Found")
}
}

#[cfg(test)]
mod default_head_template_test {
use super::{DefaultHeadTemplate, Head};
use sailfish::TemplateOnce;

#[test]
fn default() {
let head = DefaultHeadTemplate::new(None);
let rendered = head.render_once().unwrap();
assert!(
rendered.contains(r#"<head>"#) &&
rendered.contains(r#"<title> – PostgresML</title>"#) &&
rendered.contains(r#"<meta name="description" content="Train and deploy models to make online predictions using only SQL, with an open source Postgres extension.">"#) &&
!rendered.contains("preload") &&
rendered.contains(r#"<script type="importmap-shim" data-turbo-track="reload">"#) &&
rendered.contains("</head>")
)
}

#[test]
fn set_head() {
let mut head_info = Head::new()
.title("test title")
.description("test description")
.image("image/test_image.jpg");
head_info.add_preload("image/test_preload.webp");

let head = DefaultHeadTemplate::new(Some(head_info));
let rendered = head.render_once().unwrap();
assert!(
rendered.contains("<title>test title – PostgresML</title>") &&
rendered.contains(r#"<meta name="description" content="test description">"#) &&
rendered.contains(r#"<meta property="og:image" content="image/test_image.jpg">"#) &&
!rendered.contains(r#"<link rel="preload" fetchpriority="high" as="image" href="image/test_preload.webp" type="image/webp">"#)
);
}
}
132 changes: 132 additions & 0 deletions pgml-dashboard/src/templates/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::HashMap;

use components::{Nav, NavLink};

use sailfish::TemplateOnce;
use sqlx::postgres::types::PgMoney;
use sqlx::types::time::PrimitiveDateTime;
Expand Down Expand Up @@ -93,6 +95,136 @@ impl From<Layout> for String {
}
}

#[derive(TemplateOnce, Clone, Default)]
#[template(path = "layout/web_app_base.html")]
pub struct WebAppBase<'a> {
pub content: Option<String>,
pub visible_clusters: HashMap<String, String>,
pub breadcrumbs: Vec<NavLink<'a>>,
pub nav: Vec<NavLink<'a>>,
pub head: String,
}

impl<'a> WebAppBase<'a> {
pub fn new(title: &str) -> Self {
WebAppBase {
head: crate::templates::head::DefaultHeadTemplate::new(Some(
crate::templates::head::Head {
title: title.to_owned(),
description: None,
image: None,
preloads: vec![],
},
))
.render_once()
.unwrap(),
..Default::default()
}
}

pub fn head(&mut self, head: String) -> &mut Self {
self.head = head.to_owned();
self
}

pub fn clusters(&mut self, clusters: HashMap<String, String>) -> &mut Self {
self.visible_clusters = clusters.to_owned();
self
}

pub fn breadcrumbs(&mut self, breadcrumbs: Vec<NavLink<'a>>) -> &mut Self {
self.breadcrumbs = breadcrumbs.to_owned();
self
}

pub fn nav(&mut self, active: &str) -> &mut Self {
let mut nav_links = vec![NavLink::new("Create new cluster", "/clusters/new").icon("add")];

// Adds the spesific cluster to a sublist.
if self.visible_clusters.len() > 0 {
let mut sorted_clusters: Vec<(String, String)> = self
.visible_clusters
.iter()
.map(|(name, id)| (name.to_string(), id.to_string()))
.collect();
sorted_clusters.sort_by_key(|k| k.1.to_owned());

let cluster_links = sorted_clusters
.iter()
.map(|(name, id)| {
NavLink::new(name, &format!("/clusters/{}", id)).icon("developer_board")
})
.collect();

let cluster_nav = Nav {
links: cluster_links,
};

nav_links.push(
NavLink::new("Clusters", "/clusters")
.icon("lan")
.nav(cluster_nav),
)
} else {
nav_links.push(NavLink::new("Clusters", "/clusters").icon("lan"))
}

nav_links.push(NavLink::new("Payments", "/payments").icon("payments"));

// Sets the active left nav item.
let nav_with_active: Vec<NavLink> = nav_links
.into_iter()
.map(|item| {
if item.name.eq(active) {
return item.active();
}
match item.nav {
Some(sub_nav) => {
let sub_links: Vec<NavLink> = sub_nav
.links
.into_iter()
.map(|sub_item| {
if sub_item.name.eq(active) {
sub_item.active()
} else {
sub_item
}
})
.collect();
NavLink {
nav: Some(Nav { links: sub_links }),
..item
}
}
None => item,
}
})
.collect();

self.nav = nav_with_active;
self
}

pub fn content(&mut self, content: &str) -> &mut Self {
self.content = Some(content.to_owned());
self
}

pub fn render<T>(&mut self, template: T) -> String
where
T: sailfish::TemplateOnce,
{
self.content = Some(template.render_once().unwrap());
(*self).clone().into()
}
}

impl<'a> From<WebAppBase<'a>> for String {
fn from(layout: WebAppBase) -> String {
layout.render_once().unwrap()
}
}

#[derive(TemplateOnce)]
#[template(path = "content/article.html")]
pub struct Article {
Expand Down
36 changes: 25 additions & 11 deletions pgml-dashboard/templates/components/breadcrumbs.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,31 @@
</ol>
</nav>

<nav class="d-flex gap-3 align-items-center">
<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">
<span class="material-symbols-outlined">
search
</span>
<span>search</span>
</button>
<a href="/logout" class="btn btn-secondary" data-controller="btn-secondary" data-btn-secondary-target="btnSecondary">Logout</a>
<a href="/support" class="btn btn-tertiary p-0 pe-2">
<img loading="lazy" src="/dashboard/static/images/icons/help.svg" width="30" height="30" alt="help" />
</a>
<nav class="horizontal">
<ul class="navbar-nav flex-row gap-3 mb-2 mb-lg-0">

<li class="nav-item d-flex align-items-center">
<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">
Search
</button>
</li>
<li class="nav-item d-flex align-items-center">
<a class="nav-link p-0" href="/docs/guides/setup/quick_start_with_docker/">Docs</a>
</li>
<li class="nav-item d-flex align-items-center">
<a class="nav-link p-0" href="/blog/mindsdb-vs-postgresml">Blog</a>
</li>
<li class="nav-item d-flex align-items-center">
<a href="/logout" class="btn btn-secondary" data-controller="btn-secondary" data-btn-secondary-target="btnSecondary">Logout</a>
</li>
<li class="nav-item d-flex align-items-center">
<a href="/support" class="btn btn-tertiary p-0 pe-2">
<span class="material-symbols-outlined">
help
</span>
</a>
</li>
</ul>
</nav>
<% include!("search_modal.html"); %>
</nav>
29 changes: 29 additions & 0 deletions pgml-dashboard/templates/layout/web_app_base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<% use crate::templates::components::{Nav, Breadcrumbs}; %>

<!DOCTYPE html>
<html lang="en-US" data-bs-theme="dark">
<%- head %>
<body>
<main>
<div class="container-fluid p-lg-0 min-vh-lg-100">
<div class="row gx-0 min-vh-lg-100">
<div class="sidenav-container col-12 col-lg-3 col-xxl-2 pt-3 pt-lg-0" >
<%- Nav::render( nav ) %>
</div>

<div class="col-12 col-lg-9 col-xxl-10">
<div>
<%- Breadcrumbs::render( breadcrumbs ) %>
</div>

<div>
<%- content.unwrap_or_default() %>
</div>
</div>
</div>
</div>
</main>

<div id="toast-container" class="toast-container position-fixed top-0 end-0 p-3"></div>
</body>
</html>