diff --git a/pgml-dashboard/.editorconfig b/pgml-dashboard/.editorconfig
index 8b67e0f71..88b69042e 100644
--- a/pgml-dashboard/.editorconfig
+++ b/pgml-dashboard/.editorconfig
@@ -13,4 +13,4 @@ indent_size = 4
[*.html]
ident_style = space
-indent_size = 4
+indent_size = 2
diff --git a/pgml-dashboard/build.rs b/pgml-dashboard/build.rs
index d76aa7923..0c9604dee 100644
--- a/pgml-dashboard/build.rs
+++ b/pgml-dashboard/build.rs
@@ -3,7 +3,6 @@ use std::process::Command;
fn main() {
println!("cargo:rerun-if-changed=migrations");
- println!("cargo:rerun-if-changed=src");
let output = Command::new("git")
.args(&["rev-parse", "HEAD"])
diff --git a/pgml-dashboard/src/components/mod.rs b/pgml-dashboard/src/components/mod.rs
index fd6a206da..40205f3c2 100644
--- a/pgml-dashboard/src/components/mod.rs
+++ b/pgml-dashboard/src/components/mod.rs
@@ -4,7 +4,6 @@
mod component;
pub(crate) use component::{component, Component};
-
// src/components/breadcrumbs
pub mod breadcrumbs;
pub use breadcrumbs::Breadcrumbs;
@@ -49,6 +48,9 @@ pub use navbar::Navbar;
pub mod navbar_web_app;
pub use navbar_web_app::NavbarWebApp;
+// src/components/navigation
+pub mod navigation;
+
// src/components/postgres_logo
pub mod postgres_logo;
pub use postgres_logo::PostgresLogo;
@@ -65,6 +67,9 @@ pub use static_nav::StaticNav;
pub mod static_nav_link;
pub use static_nav_link::StaticNavLink;
+// src/components/tables
+pub mod tables;
+
// src/components/test_component
pub mod test_component;
pub use test_component::TestComponent;
diff --git a/pgml-dashboard/src/components/navigation/mod.rs b/pgml-dashboard/src/components/navigation/mod.rs
new file mode 100644
index 000000000..e615f8406
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/mod.rs
@@ -0,0 +1,5 @@
+// This file is automatically generated.
+// You shouldn't modify it manually.
+
+// src/components/navigation/tabs
+pub mod tabs;
diff --git a/pgml-dashboard/src/components/navigation/tabs/mod.rs b/pgml-dashboard/src/components/navigation/tabs/mod.rs
new file mode 100644
index 000000000..122d9b659
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/tabs/mod.rs
@@ -0,0 +1,10 @@
+// This file is automatically generated.
+// You shouldn't modify it manually.
+
+// src/components/navigation/tabs/tab
+pub mod tab;
+pub use tab::Tab;
+
+// src/components/navigation/tabs/tabs
+pub mod tabs;
+pub use tabs::Tabs;
diff --git a/pgml-dashboard/src/components/navigation/tabs/tab/mod.rs b/pgml-dashboard/src/components/navigation/tabs/tab/mod.rs
new file mode 100644
index 000000000..b1e33cc00
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/tabs/tab/mod.rs
@@ -0,0 +1,70 @@
+#![allow(unused_variables)]
+use crate::components::component;
+use crate::components::component::Component;
+use sailfish::TemplateOnce;
+
+#[derive(TemplateOnce, Default, Clone)]
+#[template(path = "navigation/tabs/tab/template.html")]
+pub struct Tab {
+ content: Component,
+ active: bool,
+ name: String,
+}
+
+impl Tab {
+ pub fn new(name: impl ToString, content: Component) -> Tab {
+ Tab {
+ content,
+ active: false,
+ name: name.to_string(),
+ }
+ }
+
+ pub fn button_classes(&self) -> String {
+ if self.active {
+ "nav-link active btn btn-tertiary rounded-0".to_string()
+ } else {
+ "nav-link btn btn-tertiary rounded-0".to_string()
+ }
+ }
+
+ pub fn content_classes(&self) -> String {
+ if self.active {
+ "tab-pane my-4 show active".to_string()
+ } else {
+ "tab-pane my-4".to_string()
+ }
+ }
+
+ pub fn id(&self) -> String {
+ format!("tab-{}", self.name.to_lowercase().replace(" ", "-"))
+ }
+
+ pub fn selected(&self) -> String {
+ if self.active {
+ "selected".to_string()
+ } else {
+ "".to_string()
+ }
+ }
+
+ pub fn name(&self) -> String {
+ self.name.clone()
+ }
+
+ pub fn active(mut self) -> Self {
+ self.active = true;
+ self
+ }
+
+ pub fn inactive(mut self) -> Self {
+ self.active = false;
+ self
+ }
+
+ pub fn is_active(&self) -> bool {
+ self.active
+ }
+}
+
+component!(Tab);
diff --git a/pgml-dashboard/src/components/navigation/tabs/tab/tab.scss b/pgml-dashboard/src/components/navigation/tabs/tab/tab.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/pgml-dashboard/src/components/navigation/tabs/tab/template.html b/pgml-dashboard/src/components/navigation/tabs/tab/template.html
new file mode 100644
index 000000000..3033744fa
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/tabs/tab/template.html
@@ -0,0 +1,3 @@
+
+ <%+ content %>
+
diff --git a/pgml-dashboard/src/components/navigation/tabs/tabs/mod.rs b/pgml-dashboard/src/components/navigation/tabs/tabs/mod.rs
new file mode 100644
index 000000000..21b176837
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/tabs/tabs/mod.rs
@@ -0,0 +1,44 @@
+use crate::components::component;
+use crate::components::navigation::tabs::Tab;
+use sailfish::TemplateOnce;
+
+#[derive(TemplateOnce, Default)]
+#[template(path = "navigation/tabs/tabs/template.html")]
+pub struct Tabs {
+ tabs: Vec,
+}
+
+impl Tabs {
+ pub fn new(tabs: &[Tab]) -> Tabs {
+ // Set the first tab to active if none are.
+ let mut tabs = tabs.to_vec();
+ if tabs.iter().all(|t| !t.is_active()) {
+ tabs = tabs
+ .into_iter()
+ .enumerate()
+ .map(|(i, tab)| if i == 0 { tab.active() } else { tab })
+ .collect();
+ }
+
+ Tabs { tabs }
+ }
+
+ pub fn active_tab(mut self, name: impl ToString) -> Self {
+ let tabs = self
+ .tabs
+ .into_iter()
+ .map(|tab| {
+ if tab.name() == name.to_string() {
+ tab.active()
+ } else {
+ tab.inactive()
+ }
+ })
+ .collect();
+
+ self.tabs = tabs;
+ self
+ }
+}
+
+component!(Tabs);
diff --git a/pgml-dashboard/src/components/navigation/tabs/tabs/tabs.scss b/pgml-dashboard/src/components/navigation/tabs/tabs/tabs.scss
new file mode 100644
index 000000000..2da2868c6
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/tabs/tabs/tabs.scss
@@ -0,0 +1,21 @@
+.nav-tabs {
+ // These tabs are used in docs as well, where they are
+ // generated using Bootstrap. It wasn't obvious to me
+ // how to replace those with this component yet, so I'm just
+ // enforcing the font-family here so they look the same. Docs use Roboto by default.
+ font-family: 'silka', 'Roboto', 'sans-serif';
+
+ --bs-nav-tabs-border-width: 4px;
+
+ .nav-link {
+ border: none;
+
+ &.active, &:focus, &:active {
+ border-bottom: 4px solid #{$slate-tint-700};
+ color: #{$slate-tint-700};
+ text-shadow: none;
+ }
+
+ color: #{$slate-tint-100};
+ }
+}
diff --git a/pgml-dashboard/src/components/navigation/tabs/tabs/template.html b/pgml-dashboard/src/components/navigation/tabs/tabs/template.html
new file mode 100644
index 000000000..c43ca94ec
--- /dev/null
+++ b/pgml-dashboard/src/components/navigation/tabs/tabs/template.html
@@ -0,0 +1,30 @@
+
+ <% for tab in &tabs { %>
+ -
+
+
+ <% } %>
+
+
+ <% for (i, tab) in tabs.into_iter().enumerate() { %>
+
+ <%+ tab %>
+
+ <% } %>
+
diff --git a/pgml-dashboard/src/components/tables/large/mod.rs b/pgml-dashboard/src/components/tables/large/mod.rs
new file mode 100644
index 000000000..17fdf1b6f
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/mod.rs
@@ -0,0 +1,10 @@
+// This file is automatically generated.
+// You shouldn't modify it manually.
+
+// src/components/tables/large/row
+pub mod row;
+pub use row::Row;
+
+// src/components/tables/large/table
+pub mod table;
+pub use table::Table;
diff --git a/pgml-dashboard/src/components/tables/large/row/mod.rs b/pgml-dashboard/src/components/tables/large/row/mod.rs
new file mode 100644
index 000000000..eac8a2e65
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/row/mod.rs
@@ -0,0 +1,19 @@
+use crate::components::component;
+use crate::components::component::Component;
+use sailfish::TemplateOnce;
+
+#[derive(TemplateOnce, Default, Clone)]
+#[template(path = "tables/large/row/template.html")]
+pub struct Row {
+ columns: Vec,
+}
+
+impl Row {
+ pub fn new(columns: &[Component]) -> Row {
+ Row {
+ columns: columns.to_vec(),
+ }
+ }
+}
+
+component!(Row);
diff --git a/pgml-dashboard/src/components/tables/large/row/row.scss b/pgml-dashboard/src/components/tables/large/row/row.scss
new file mode 100644
index 000000000..5a65b7722
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/row/row.scss
@@ -0,0 +1,37 @@
+table.table.table-lg {
+ tr {
+ &:first-of-type {
+ td {
+ padding-top: 16px;
+ }
+ }
+
+ &:hover {
+ div.table-cell-content {
+ background: #{$gray-800};
+ }
+ }
+
+ td {
+ vertical-align: middle;
+ padding: 8px 0;
+
+ div.table-cell-content {
+ background: #{$gray-600};
+ padding: 20px 0;
+ }
+
+ &:first-of-type {
+ div.table-cell-content {
+ padding-left: 67px;
+ }
+ }
+
+ &:last-of-type {
+ div.table-cell-content {
+ padding-right: 67px;
+ }
+ }
+ }
+ }
+}
diff --git a/pgml-dashboard/src/components/tables/large/row/template.html b/pgml-dashboard/src/components/tables/large/row/template.html
new file mode 100644
index 000000000..32b9c8714
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/row/template.html
@@ -0,0 +1,9 @@
+
+ <% for column in columns { %>
+
+
+ <%+ column %>
+
+ |
+ <% } %>
+
diff --git a/pgml-dashboard/src/components/tables/large/table/mod.rs b/pgml-dashboard/src/components/tables/large/table/mod.rs
new file mode 100644
index 000000000..42da7d9fd
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/table/mod.rs
@@ -0,0 +1,21 @@
+use crate::components::component;
+use crate::components::tables::large::Row;
+use sailfish::TemplateOnce;
+
+#[derive(TemplateOnce, Default)]
+#[template(path = "tables/large/table/template.html")]
+pub struct Table {
+ rows: Vec,
+ headers: Vec,
+}
+
+impl Table {
+ pub fn new(headers: &[impl ToString], rows: &[Row]) -> Table {
+ Table {
+ headers: headers.iter().map(|h| h.to_string()).collect(),
+ rows: rows.to_vec(),
+ }
+ }
+}
+
+component!(Table);
diff --git a/pgml-dashboard/src/components/tables/large/table/table.scss b/pgml-dashboard/src/components/tables/large/table/table.scss
new file mode 100644
index 000000000..3befa6e55
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/table/table.scss
@@ -0,0 +1,19 @@
+table.table.table-lg {
+ * {
+ border-width: 0;
+ }
+
+ thead {
+ th {
+ color: #{$slate-shade-100};
+ background: #{$gray-800};
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ padding: 16px 0;
+
+ &:first-of-type {
+ padding-left: 67px;
+ }
+ }
+ }
+}
diff --git a/pgml-dashboard/src/components/tables/large/table/template.html b/pgml-dashboard/src/components/tables/large/table/template.html
new file mode 100644
index 000000000..1aa2a831b
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/large/table/template.html
@@ -0,0 +1,14 @@
+
+
+
+ <% for header in headers { %>
+ <%= header %> |
+ <% } %>
+
+
+
+ <% for row in rows { %>
+ <%+ row %>
+ <% } %>
+
+
diff --git a/pgml-dashboard/src/components/tables/mod.rs b/pgml-dashboard/src/components/tables/mod.rs
new file mode 100644
index 000000000..48a76b04c
--- /dev/null
+++ b/pgml-dashboard/src/components/tables/mod.rs
@@ -0,0 +1,5 @@
+// This file is automatically generated.
+// You shouldn't modify it manually.
+
+// src/components/tables/large
+pub mod large;
diff --git a/pgml-dashboard/src/utils/markdown.rs b/pgml-dashboard/src/utils/markdown.rs
index 0957e5e39..b13636c6a 100644
--- a/pgml-dashboard/src/utils/markdown.rs
+++ b/pgml-dashboard/src/utils/markdown.rs
@@ -669,7 +669,7 @@ impl<'a> Tab<'a> {
"