webs unified
This commit is contained in:
@@ -121,7 +121,7 @@ pub(crate) async fn load_admin_page(
|
||||
};
|
||||
|
||||
Ok(AdminPageState {
|
||||
role: authorization.role,
|
||||
nav: crate::ui::Nav::new(headers, "admin").with_role(authorization.role),
|
||||
profiles,
|
||||
selected_profile,
|
||||
tables,
|
||||
|
||||
@@ -8,7 +8,7 @@ pub(crate) struct AdminSelection {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AdminPageState {
|
||||
pub role: String,
|
||||
pub nav: crate::ui::Nav,
|
||||
pub profiles: Vec<ProfileView>,
|
||||
pub selected_profile: Option<String>,
|
||||
pub tables: Vec<TableView>,
|
||||
@@ -38,6 +38,21 @@ pub(crate) struct ColumnView {
|
||||
pub quantity_ledger: bool,
|
||||
}
|
||||
|
||||
impl ColumnView {
|
||||
/// The badge list rendered under each column name.
|
||||
pub(crate) fn flags(&self) -> Vec<&'static str> {
|
||||
let mut flags = Vec::new();
|
||||
if self.primary_key {
|
||||
flags.push("primary key");
|
||||
}
|
||||
flags.push(if self.nullable { "nullable" } else { "required" });
|
||||
if self.quantity_ledger {
|
||||
flags.push("quantity ledger");
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum LoadError {
|
||||
Unauthenticated,
|
||||
|
||||
@@ -1,124 +1,58 @@
|
||||
use askama::Template;
|
||||
|
||||
use crate::ui::{ErrorPage, Nav, render};
|
||||
|
||||
use super::state::AdminPageState;
|
||||
|
||||
const ADMIN_CSS: &str = include_str!("../../../../static/admin.css");
|
||||
/// GET /admin
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/admin/admin/admin.html")]
|
||||
struct AdminPage<'a> {
|
||||
nav: Nav,
|
||||
page: &'a AdminPageState,
|
||||
}
|
||||
|
||||
/// GET /admin/workspace — the HTMX swap for the three-pane browser.
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/admin/admin/workspace.html")]
|
||||
struct AdminWorkspace<'a> {
|
||||
page: &'a AdminPageState,
|
||||
}
|
||||
|
||||
pub(crate) fn render_page(page: &AdminPageState) -> String {
|
||||
format!(
|
||||
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Admin panel</title><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js\"></script><style>{ADMIN_CSS}</style></head><body><header class=\"topbar\"><div><strong>Komp Accounting</strong><span class=\"role\">{}</span></div><nav><a class=\"active\" href=\"/admin\">Admin</a><a href=\"/\">Analytics</a><form hx-post=\"/logout\" hx-swap=\"none\"><button class=\"link-button\" type=\"submit\">Log out</button></form></nav></header><main><section class=\"heading\"><div><p class=\"eyebrow\">Workspace</p><h1>Admin panel</h1><p>Browse profiles, tables, and their physical columns.</p></div><div class=\"actions\"><a href=\"/admin/tables/new\">Add table</a><a href=\"/admin/logic/new\">Add logic</a><a href=\"/admin/validation/new\">Add validation</a><a href=\"/admin/validation/sets/new\">Add rule</a><a href=\"/admin/import\">Import</a><a href=\"/admin/export\">Export</a></div></section><div id=\"admin-workspace\">{}</div></main></body></html>",
|
||||
crate::escape_html(&page.role),
|
||||
render_workspace(page),
|
||||
)
|
||||
render(&AdminPage {
|
||||
nav: page.nav.clone(),
|
||||
page,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_workspace(page: &AdminPageState) -> String {
|
||||
format!(
|
||||
"<div class=\"workspace\" aria-live=\"polite\">{}{}{}</div>",
|
||||
render_profiles(page),
|
||||
render_tables(page),
|
||||
render_columns(page),
|
||||
)
|
||||
render(&AdminWorkspace { page })
|
||||
}
|
||||
|
||||
pub(crate) fn render_error(message: &str) -> String {
|
||||
format!(
|
||||
"<div class=\"error-page\"><h1>Admin panel unavailable</h1><p>{}</p><a href=\"/admin\">Try again</a></div>",
|
||||
crate::escape_html(message),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_profiles(page: &AdminPageState) -> String {
|
||||
let items = if page.profiles.is_empty() {
|
||||
"<p class=\"empty\">No profiles available.</p>".to_string()
|
||||
} else {
|
||||
page.profiles
|
||||
.iter()
|
||||
.map(|profile| {
|
||||
let selected = page.selected_profile.as_deref() == Some(profile.name.as_str());
|
||||
format!(
|
||||
"<form hx-get=\"/admin/workspace\" hx-target=\"#admin-workspace\" hx-swap=\"innerHTML\"><button class=\"browser-item {}\" type=\"submit\" name=\"profile\" value=\"{}\"><span>{}</span><small>{} tables</small></button></form>",
|
||||
if selected { "selected" } else { "" },
|
||||
crate::escape_html(&profile.name),
|
||||
crate::escape_html(&profile.name),
|
||||
profile.table_count,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
format!("<section class=\"pane\"><div class=\"pane-title\"><h2>Profiles</h2><span>{}</span></div><div class=\"pane-list\">{items}</div></section>", page.profiles.len())
|
||||
}
|
||||
|
||||
fn render_tables(page: &AdminPageState) -> String {
|
||||
let items = if page.selected_profile.is_none() {
|
||||
"<p class=\"empty\">Select a profile to see its tables.</p>".to_string()
|
||||
} else if page.tables.is_empty() {
|
||||
"<p class=\"empty\">This profile has no tables.</p>".to_string()
|
||||
} else {
|
||||
page.tables
|
||||
.iter()
|
||||
.map(|table| {
|
||||
let selected = page.selected_table.as_deref() == Some(table.name.as_str());
|
||||
let dependencies = if table.depends_on.is_empty() {
|
||||
"No dependencies".to_string()
|
||||
} else {
|
||||
format!("Depends on {}", table.depends_on.join(", "))
|
||||
};
|
||||
format!(
|
||||
"<form hx-get=\"/admin/workspace\" hx-target=\"#admin-workspace\" hx-swap=\"innerHTML\"><input type=\"hidden\" name=\"profile\" value=\"{}\"><button class=\"browser-item {}\" type=\"submit\" name=\"table\" value=\"{}\"><span>{}</span><small>{} · display: {}</small></button></form>",
|
||||
crate::escape_html(page.selected_profile.as_deref().unwrap_or_default()),
|
||||
if selected { "selected" } else { "" },
|
||||
crate::escape_html(&table.name),
|
||||
crate::escape_html(&table.name),
|
||||
crate::escape_html(&dependencies),
|
||||
crate::escape_html(&table.row_display_columns.join(", ")),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
format!("<section class=\"pane\"><div class=\"pane-title\"><h2>Tables</h2><span>{}</span></div><div class=\"pane-list\">{items}</div></section>", page.tables.len())
|
||||
}
|
||||
|
||||
fn render_columns(page: &AdminPageState) -> String {
|
||||
let items = if page.selected_table.is_none() {
|
||||
"<p class=\"empty\">Select a table to inspect its columns.</p>".to_string()
|
||||
} else if page.columns.is_empty() {
|
||||
"<p class=\"empty\">This table has no visible columns.</p>".to_string()
|
||||
} else {
|
||||
page.columns
|
||||
.iter()
|
||||
.map(|column| {
|
||||
let mut flags = Vec::new();
|
||||
if column.primary_key {
|
||||
flags.push("primary key");
|
||||
}
|
||||
if column.nullable {
|
||||
flags.push("nullable");
|
||||
} else {
|
||||
flags.push("required");
|
||||
}
|
||||
if column.quantity_ledger {
|
||||
flags.push("quantity ledger");
|
||||
}
|
||||
format!(
|
||||
"<div class=\"column\"><span>{}</span><code>{}</code><small>{}</small></div>",
|
||||
crate::escape_html(&column.name),
|
||||
crate::escape_html(&column.data_type),
|
||||
flags.join(" · "),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
format!("<section class=\"pane\"><div class=\"pane-title\"><h2>Columns</h2><span>{}</span></div><div class=\"pane-list\">{items}</div></section>", page.columns.len())
|
||||
render(&ErrorPage {
|
||||
nav: Nav::default(),
|
||||
heading: "Admin panel unavailable",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pages::admin::admin::state::AdminPageState;
|
||||
|
||||
#[test]
|
||||
fn dashboard_links_every_admin_action() {
|
||||
let page = AdminPageState {
|
||||
// The admin page is only reachable with a session, so the navbar
|
||||
// shows the role badge and the log-out button.
|
||||
let nav = Nav {
|
||||
authenticated: true,
|
||||
role: "admin".to_string(),
|
||||
active: "admin",
|
||||
};
|
||||
let page = AdminPageState {
|
||||
nav,
|
||||
profiles: Vec::new(),
|
||||
selected_profile: None,
|
||||
tables: Vec::new(),
|
||||
|
||||
Reference in New Issue
Block a user