Replace link templates with functions

Now that HTML is just a Rust value, we can use normal Rust abstractions
for pretty much everything, which is nice. There's still a few parts I'd
like to clean up, but this is already a lot nicer.
This commit is contained in:
Joscha 2024-05-12 15:16:59 +02:00
parent cf96b72dfb
commit bb3d7b86f0
8 changed files with 134 additions and 175 deletions

View file

@ -1,7 +1,7 @@
mod admin; mod admin;
mod api; mod api;
mod base; mod base;
mod link; mod components;
mod pages; mod pages;
pub mod paths; pub mod paths;
mod server_config_ext; mod server_config_ext;

View file

@ -0,0 +1,75 @@
use maud::{html, Markup};
use time::OffsetDateTime;
use crate::{config::ServerConfig, server::util};
use super::{
paths::{PathCommitByHash, PathRunById, PathWorkerByName},
server_config_ext::ServerConfigExt,
};
pub fn join(sections: &[Markup], with: Markup) -> Markup {
html! {
@for (i, section) in sections.iter().enumerate() {
@if i > 0 { (with) }
(section)
}
}
}
pub fn commit_class_and_title(reachable: i64) -> (&'static str, &'static str) {
if reachable == 0 {
(
"commit-orphaned",
"This commit is orphaned. It can't be reached from any ref.",
)
} else if reachable == -1 {
(
"commit-reachable",
"This commit can only be reached from untracked refs.",
)
} else {
(
"commit-tracked",
"This commit can be reached from a tracked ref.",
)
}
}
pub fn link_commit(config: &ServerConfig, hash: String, message: &str, reachable: i64) -> Markup {
let short = util::truncate(&util::format_commit_short(&hash, message), 80);
let path = config.path(PathCommitByHash { hash });
let (class, title) = commit_class_and_title(reachable);
html! {
a href=(path) .(class) title=(title) { (short) }
}
}
/// Link to a run by its commit's short message.
pub fn link_run_short(config: &ServerConfig, id: String, hash: &str, message: &str) -> Markup {
let short = util::truncate(&util::format_commit_short(hash, message), 80);
let path = config.path(PathRunById { id });
html! {
a href=(path) { "Run of " (short) }
}
}
/// Link to a run by its start time.
pub fn link_run_date(config: &ServerConfig, id: String, start: OffsetDateTime) -> Markup {
let start = util::format_time(start);
let path = config.path(PathRunById { id });
html! {
a href=(path) { "Run from " (start) }
}
}
pub fn link_worker(config: &ServerConfig, name: String) -> Markup {
let path = config.path(PathWorkerByName { name: name.clone() });
html! {
a href=(path) { (name) }
}
}

View file

@ -1,114 +0,0 @@
use maud::{html, Markup};
use time::OffsetDateTime;
use crate::server::util;
use super::{
base::Base,
paths::{PathCommitByHash, PathRunById, PathWorkerByName},
server_config_ext::{AbsPath, ServerConfigExt},
};
pub struct LinkCommit {
link: AbsPath,
short: String,
reachable: i64,
}
impl LinkCommit {
pub fn new(base: &Base, hash: String, message: &str, reachable: i64) -> Self {
Self {
short: util::format_commit_short(&hash, message),
link: base.config.path(PathCommitByHash { hash }),
reachable,
}
}
pub fn class_and_title(&self) -> (&'static str, &'static str) {
if self.reachable == 0 {
(
"commit-orphaned",
"This commit is orphaned. It can't be reached from any ref.",
)
} else if self.reachable == -1 {
(
"commit-reachable",
"This commit can only be reached from untracked refs.",
)
} else {
(
"commit-tracked",
"This commit can be reached from a tracked ref.",
)
}
}
pub fn html(&self) -> Markup {
let (class, title) = self.class_and_title();
let short = util::truncate(&self.short, 80);
html! {
a href=(self.link) .(class) title=(title) { (short) }
}
}
}
pub struct LinkRunShort {
link: AbsPath,
short: String,
}
impl LinkRunShort {
pub fn new(base: &Base, id: String, hash: &str, message: &str) -> Self {
Self {
link: base.config.path(PathRunById { id }),
short: util::format_commit_short(hash, message),
}
}
pub fn html(&self) -> Markup {
html! {
a href=(self.link) { "Run of " (util::truncate(&self.short, 80)) }
}
}
}
pub struct LinkRunDate {
link: AbsPath,
date: String, // TODO base.date(...)?
}
impl LinkRunDate {
pub fn new(base: &Base, id: String, start: OffsetDateTime) -> Self {
Self {
link: base.config.path(PathRunById { id }),
date: util::format_time(start),
}
}
pub fn html(&self) -> Markup {
html! {
a href=(self.link) { "Run from " (self.date) }
}
}
}
pub struct LinkWorker {
link: AbsPath,
name: String,
}
impl LinkWorker {
pub fn new(base: &Base, name: String) -> Self {
Self {
link: base.config.path(PathWorkerByName { name: name.clone() }),
name,
}
}
pub fn html(&self) -> Markup {
html! {
a href=(self.link) { (self.name) }
}
}
}

View file

@ -13,7 +13,7 @@ use crate::{
util, util,
web::{ web::{
base::{Base, Tab}, base::{Base, Tab},
link::{LinkCommit, LinkRunDate}, components,
paths::{PathAdminQueueAdd, PathCommitByHash}, paths::{PathAdminQueueAdd, PathCommitByHash},
server_config_ext::ServerConfigExt, server_config_ext::ServerConfigExt,
}, },
@ -59,7 +59,7 @@ pub async fn get_commit_by_hash(
path.hash, path.hash,
) )
.fetch(&db) .fetch(&db)
.map_ok(|r| LinkCommit::new(&base, r.hash, &r.message, r.reachable)) .map_ok(|r| components::link_commit(config, r.hash, &r.message, r.reachable))
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await?; .await?;
@ -73,7 +73,7 @@ pub async fn get_commit_by_hash(
path.hash, path.hash,
) )
.fetch(&db) .fetch(&db)
.map_ok(|r| LinkCommit::new(&base, r.hash, &r.message, r.reachable)) .map_ok(|r| components::link_commit(config, r.hash, &r.message, r.reachable))
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await?; .await?;
@ -87,18 +87,11 @@ pub async fn get_commit_by_hash(
path.hash path.hash
) )
.fetch(&db) .fetch(&db)
.map_ok(|r| LinkRunDate::new(&base, r.id, r.start)) .map_ok(|r| components::link_run_date(config, r.id, r.start))
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
.await?; .await?;
// TODO Somewhat inefficient to construct full LinkCommit for this let (class, title) = components::commit_class_and_title(commit.reachable);
let (class, title) = LinkCommit::new(
&base,
commit.hash.clone(),
&commit.message,
commit.reachable,
)
.class_and_title();
Ok(base Ok(base
.html( .html(
@ -123,12 +116,12 @@ pub async fn get_commit_by_hash(
@for commit in parents { @for commit in parents {
dt { "Parent:" } dt { "Parent:" }
dd { (commit.html()) } dd { (commit) }
} }
@for commit in children { @for commit in children {
dt { "Child:" } dt { "Child:" }
dd { (commit.html()) } dd { (commit) }
} }
} }
pre .(class) title=(title) { pre .(class) title=(title) {
@ -142,7 +135,7 @@ pub async fn get_commit_by_hash(
} @else { } @else {
ul { ul {
@for run in runs { @for run in runs {
li { (run.html()) } li { (run) }
} }
} }
} }

View file

@ -1,13 +1,13 @@
use axum::{extract::State, response::IntoResponse}; use axum::{extract::State, response::IntoResponse};
use futures::TryStreamExt; use futures::TryStreamExt;
use maud::html; use maud::{html, Markup};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
config::ServerConfig, config::ServerConfig,
server::web::{ server::web::{
base::{Base, Tab}, base::{Base, Tab},
link::LinkCommit, components,
paths::{PathAdminRefsTrack, PathAdminRefsUntrack, PathAdminRefsUpdate, PathIndex}, paths::{PathAdminRefsTrack, PathAdminRefsUntrack, PathAdminRefsUpdate, PathIndex},
server_config_ext::ServerConfigExt, server_config_ext::ServerConfigExt,
}, },
@ -16,7 +16,7 @@ use crate::{
struct Ref { struct Ref {
name: String, name: String,
commit: LinkCommit, commit: Markup,
tracked: bool, tracked: bool,
} }
@ -38,7 +38,7 @@ pub async fn get_index(
.fetch(&db) .fetch(&db)
.map_ok(|r| Ref { .map_ok(|r| Ref {
name: r.name.clone(), name: r.name.clone(),
commit: LinkCommit::new(&base, r.hash, &r.message, r.reachable), commit: components::link_commit(config, r.hash, &r.message, r.reachable),
tracked: r.tracked != 0, tracked: r.tracked != 0,
}) })
.try_collect::<Vec<_>>() .try_collect::<Vec<_>>()
@ -69,7 +69,7 @@ pub async fn get_index(
button .linkish name="ref" value=(r#ref.name) { "untrack" } button .linkish name="ref" value=(r#ref.name) { "untrack" }
"]" "]"
} }
dd { (r#ref.commit.html()) } dd { (r#ref.commit) }
} }
} }
} }
@ -84,7 +84,7 @@ pub async fn get_index(
button .linkish name="ref" value=(r#ref.name) { "track" } button .linkish name="ref" value=(r#ref.name) { "track" }
"]" "]"
} }
dd { (r#ref.commit.html()) } dd { (r#ref.commit) }
} }
} }
} }

View file

@ -18,7 +18,7 @@ use crate::{
util, util,
web::{ web::{
base::{Base, Tab}, base::{Base, Tab},
link::{LinkCommit, LinkRunShort, LinkWorker}, components,
paths::{ paths::{
PathAdminQueueAddBatch, PathAdminQueueDecrease, PathAdminQueueDelete, PathAdminQueueAddBatch, PathAdminQueueDecrease, PathAdminQueueDelete,
PathAdminQueueIncrease, PathQueue, PathQueueDelete, PathQueueInner, PathAdminQueueIncrease, PathQueue, PathQueueDelete, PathQueueInner,
@ -35,11 +35,11 @@ use crate::{
enum Status { enum Status {
Idle, Idle,
Busy, Busy,
Working(LinkRunShort), Working(Markup),
} }
struct Worker { struct Worker {
link: LinkWorker, link: Markup,
status: Status, status: Status,
} }
@ -48,10 +48,10 @@ struct Task {
link_increase: AbsPath, link_increase: AbsPath,
link_decrease: AbsPath, link_decrease: AbsPath,
hash: String, hash: String,
commit: LinkCommit, commit: Markup,
since: String, since: String,
priority: i64, priority: i64,
workers: Vec<LinkWorker>, workers: Vec<Markup>,
odd: bool, odd: bool,
} }
@ -68,9 +68,9 @@ fn sorted_workers(workers: &Mutex<Workers>) -> Vec<(String, WorkerInfo)> {
} }
async fn get_workers( async fn get_workers(
config: &ServerConfig,
db: &SqlitePool, db: &SqlitePool,
workers: &[(String, WorkerInfo)], workers: &[(String, WorkerInfo)],
base: &Base,
) -> somehow::Result<Vec<Worker>> { ) -> somehow::Result<Vec<Worker>> {
let mut result = vec![]; let mut result = vec![];
for (name, info) in workers { for (name, info) in workers {
@ -82,12 +82,17 @@ async fn get_workers(
sqlx::query_scalar!("SELECT message FROM commits WHERE hash = ?", run.hash) sqlx::query_scalar!("SELECT message FROM commits WHERE hash = ?", run.hash)
.fetch_one(db) .fetch_one(db)
.await?; .await?;
Status::Working(LinkRunShort::new(base, run.id.clone(), &run.hash, &message)) Status::Working(components::link_run_short(
config,
run.id.clone(),
&run.hash,
&message,
))
} }
}; };
result.push(Worker { result.push(Worker {
link: LinkWorker::new(base, name.clone()), link: components::link_worker(config, name.clone()),
status, status,
}) })
} }
@ -95,18 +100,18 @@ async fn get_workers(
} }
async fn get_queue_data( async fn get_queue_data(
config: &ServerConfig,
db: &SqlitePool, db: &SqlitePool,
workers: &[(String, WorkerInfo)], workers: &[(String, WorkerInfo)],
base: &Base,
) -> somehow::Result<Vec<Task>> { ) -> somehow::Result<Vec<Task>> {
// Group workers by commit hash // Group workers by commit hash
let mut workers_by_commit: HashMap<String, Vec<LinkWorker>> = HashMap::new(); let mut workers_by_commit: HashMap<String, Vec<Markup>> = HashMap::new();
for (name, info) in workers { for (name, info) in workers {
if let WorkerStatus::Working(run) = &info.status { if let WorkerStatus::Working(run) = &info.status {
workers_by_commit workers_by_commit
.entry(run.hash.clone()) .entry(run.hash.clone())
.or_default() .or_default()
.push(LinkWorker::new(base, name.clone())); .push(components::link_worker(config, name.clone()));
} }
} }
@ -126,13 +131,13 @@ async fn get_queue_data(
.fetch(db) .fetch(db)
.map_ok(|r| Task { .map_ok(|r| Task {
workers: workers_by_commit.remove(&r.hash).unwrap_or_default(), workers: workers_by_commit.remove(&r.hash).unwrap_or_default(),
link_delete: base.config.path(PathQueueDelete { link_delete: config.path(PathQueueDelete {
hash: r.hash.clone(), hash: r.hash.clone(),
}), }),
link_increase: base.config.path(PathAdminQueueIncrease {}), link_increase: config.path(PathAdminQueueIncrease {}),
link_decrease: base.config.path(PathAdminQueueDecrease {}), link_decrease: config.path(PathAdminQueueDecrease {}),
hash: r.hash.clone(), hash: r.hash.clone(),
commit: LinkCommit::new(base, r.hash, &r.message, r.reachable), commit: components::link_commit(config, r.hash, &r.message, r.reachable),
since: util::format_delta_from_now(r.date), since: util::format_delta_from_now(r.date),
priority: r.priority, priority: r.priority,
odd: false, odd: false,
@ -168,11 +173,11 @@ fn page_inner(workers: Vec<Worker>, tasks: Vec<Task>) -> Markup {
} }
tbody { tbody {
@for worker in workers { tr { @for worker in workers { tr {
td { (worker.link.html()) } td { (worker.link) }
td { @match worker.status { td { @match worker.status {
Status::Idle => "idle", Status::Idle => "idle",
Status::Busy => "busy", Status::Busy => "busy",
Status::Working(link) => (link.html()), Status::Working(link) => (link),
} } } }
} } } }
} }
@ -191,7 +196,7 @@ fn page_inner(workers: Vec<Worker>, tasks: Vec<Task>) -> Markup {
} }
tbody { tbody {
@for task in tasks { tr .odd[task.odd] { @for task in tasks { tr .odd[task.odd] {
td { (task.commit.html()) } td { (task.commit) }
td { td {
(task.since) " [" (task.since) " ["
a href=(task.link_delete) title="Delete from queue" { "del" } a href=(task.link_delete) title="Delete from queue" { "del" }
@ -207,10 +212,7 @@ fn page_inner(workers: Vec<Worker>, tasks: Vec<Task>) -> Markup {
@if task.workers.is_empty() { @if task.workers.is_empty() {
"-" "-"
} }
@for (i, worker) in task.workers.iter().enumerate() { (components::join(&task.workers, html! { ", " }))
@if i > 0 { ", " }
(worker.html())
}
} }
} }
} } } }
@ -226,10 +228,9 @@ pub async fn get_queue_inner(
State(db): State<SqlitePool>, State(db): State<SqlitePool>,
State(workers): State<Arc<Mutex<Workers>>>, State(workers): State<Arc<Mutex<Workers>>>,
) -> somehow::Result<impl IntoResponse> { ) -> somehow::Result<impl IntoResponse> {
let base = Base::new(config, Tab::Queue);
let sorted_workers = sorted_workers(&workers); let sorted_workers = sorted_workers(&workers);
let workers = get_workers(&db, &sorted_workers, &base).await?; let workers = get_workers(config, &db, &sorted_workers).await?;
let tasks = get_queue_data(&db, &sorted_workers, &base).await?; let tasks = get_queue_data(config, &db, &sorted_workers).await?;
Ok(page_inner(workers, tasks)) Ok(page_inner(workers, tasks))
} }
@ -241,8 +242,8 @@ pub async fn get_queue(
) -> somehow::Result<impl IntoResponse> { ) -> somehow::Result<impl IntoResponse> {
let base = Base::new(config, Tab::Queue); let base = Base::new(config, Tab::Queue);
let sorted_workers = sorted_workers(&workers); let sorted_workers = sorted_workers(&workers);
let workers = get_workers(&db, &sorted_workers, &base).await?; let workers = get_workers(config, &db, &sorted_workers).await?;
let tasks = get_queue_data(&db, &sorted_workers, &base).await?; let tasks = get_queue_data(config, &db, &sorted_workers).await?;
Ok(base.html( Ok(base.html(
&format!("queue ({})", tasks.len()), &format!("queue ({})", tasks.len()),
@ -289,7 +290,7 @@ pub async fn get_queue_delete(
return Ok(StatusCode::NOT_FOUND.into_response()); return Ok(StatusCode::NOT_FOUND.into_response());
}; };
let commit = LinkCommit::new(&base, r.hash.clone(), &r.message, r.reachable); let commit = components::link_commit(config, r.hash.clone(), &r.message, r.reachable);
Ok(base Ok(base
.html( .html(
@ -298,7 +299,7 @@ pub async fn get_queue_delete(
html! { html! {
h2 { "Delete commit from queue" } h2 { "Delete commit from queue" }
p { "You are about to delete this commit from the queue:" } p { "You are about to delete this commit from the queue:" }
p { (commit.html()) } p { (commit) }
p { "All runs of this commit currently in progress will be aborted!" } p { "All runs of this commit currently in progress will be aborted!" }
form method="post" action=(config.path(PathAdminQueueDelete {})) { form method="post" action=(config.path(PathAdminQueueDelete {})) {
input name="hash" type="hidden" value=(r.hash); input name="hash" type="hidden" value=(r.hash);

View file

@ -13,7 +13,7 @@ use crate::{
util, util,
web::{ web::{
base::{Base, Tab}, base::{Base, Tab},
link::LinkCommit, components,
paths::PathRunById, paths::PathRunById,
}, },
}, },
@ -98,7 +98,7 @@ async fn from_finished_run(
let base = Base::new(config, Tab::None); let base = Base::new(config, Tab::None);
let commit = LinkCommit::new(&base, run.hash, &run.message, run.reachable); let commit = components::link_commit(config, run.hash, &run.message, run.reachable);
Ok(Some( Ok(Some(
base.html( base.html(
@ -110,7 +110,7 @@ async fn from_finished_run(
span .title { "run " (run.id) } span .title { "run " (run.id) }
dl { dl {
dt { "Commit:" } dt { "Commit:" }
dd { (commit.html())} dd { (commit)}
dt { "Benchmark:" } dt { "Benchmark:" }
dd { (run.bench_method) } dd { (run.bench_method) }

View file

@ -5,7 +5,7 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use maud::html; use maud::{html, Markup};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
@ -14,7 +14,7 @@ use crate::{
util, util,
web::{ web::{
base::{Base, Tab}, base::{Base, Tab},
link::LinkRunShort, components,
paths::PathWorkerByName, paths::PathWorkerByName,
}, },
workers::Workers, workers::Workers,
@ -26,10 +26,14 @@ use crate::{
enum Status { enum Status {
Idle, Idle,
Busy, Busy,
Working { link: LinkRunShort, since: String }, Working { link: Markup, since: String },
} }
async fn status(status: &WorkerStatus, db: &SqlitePool, base: &Base) -> somehow::Result<Status> { async fn status(
config: &ServerConfig,
status: &WorkerStatus,
db: &SqlitePool,
) -> somehow::Result<Status> {
Ok(match status { Ok(match status {
WorkerStatus::Idle => Status::Idle, WorkerStatus::Idle => Status::Idle,
WorkerStatus::Busy => Status::Busy, WorkerStatus::Busy => Status::Busy,
@ -39,7 +43,7 @@ async fn status(status: &WorkerStatus, db: &SqlitePool, base: &Base) -> somehow:
.fetch_one(db) .fetch_one(db)
.await?; .await?;
Status::Working { Status::Working {
link: LinkRunShort::new(base, run.id.clone(), &run.hash, &message), link: components::link_run_short(config, run.id.clone(), &run.hash, &message),
since: util::format_time(run.start.0), since: util::format_time(run.start.0),
} }
} }
@ -59,7 +63,7 @@ pub async fn get_worker_by_name(
let base = Base::new(config, Tab::None); let base = Base::new(config, Tab::None);
let status = status(&info.status, &db, &base).await?; let status = status(config, &info.status, &db).await?;
Ok(base Ok(base
.html( .html(
@ -84,7 +88,7 @@ pub async fn get_worker_by_name(
} }
Status::Working { link, since } => { Status::Working { link, since } => {
dt { "Working on:" } dt { "Working on:" }
dd { (link.html()) } dd { (link) }
dt { "Working since:" } dt { "Working since:" }
dd { (since) } dd { (since) }