From 128bc8d1146c6c9553ac6c5185e21e00ddcc6829 Mon Sep 17 00:00:00 2001 From: iximeow Date: Mon, 17 Jul 2023 23:03:29 -0700 Subject: start factoring out web view rendering --- ci-lib-web/Cargo.toml | 15 +++ ci-lib-web/src/lib.rs | 287 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 ci-lib-web/Cargo.toml create mode 100644 ci-lib-web/src/lib.rs (limited to 'ci-lib-web') diff --git a/ci-lib-web/Cargo.toml b/ci-lib-web/Cargo.toml new file mode 100644 index 0000000..d6c1acc --- /dev/null +++ b/ci-lib-web/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ci-lib-web" +version = "0.0.1" +authors = [ "iximeow " ] +license = "0BSD" +edition = "2021" +description = "shared code supporting web views of state.db" + +[lib] + +[dependencies] +ci-lib-core = { path = "../ci-lib-core" } +serde = { version = "*", features = ["derive"] } +rusqlite = { version = "*", features = ["bundled"] } +chrono = "*" diff --git a/ci-lib-web/src/lib.rs b/ci-lib-web/src/lib.rs new file mode 100644 index 0000000..c6bb3b1 --- /dev/null +++ b/ci-lib-web/src/lib.rs @@ -0,0 +1,287 @@ +use std::sync::Arc; + +use chrono::{Utc, TimeZone}; + +use ci_lib_core::dbctx::DbCtx; +use ci_lib_core::sql::{Job, Run, RunState}; + +/// return a duration rendered as the largest two non-zero units. +/// +/// 60000ms -> 1m +/// 60001ms -> 1m +/// 61000ms -> 1m1s +/// 1030ms -> 1.03s +pub fn duration_as_human_string(duration_ms: u64) -> String { + let duration_sec = duration_ms / 1000; + let duration_min = duration_sec / 60; + let duration_hours = duration_min / 60; + + let duration_ms = duration_ms % 1000; + let duration_sec = duration_sec % 60; + let duration_min = duration_min % 60; + // no need to clamp hours, we're gonna just hope that it's a reasonable number of hours + + if duration_hours != 0 { + let mut res = format!("{}h", duration_hours); + if duration_min != 0 { + res.push_str(&format!("{}m", duration_min)); + } + res + } else if duration_min != 0 { + let mut res = format!("{}m", duration_min); + if duration_min != 0 { + res.push_str(&format!("{}s", duration_sec)); + } + res + } else { + let mut res = format!("{}", duration_sec); + if duration_ms != 0 { + res.push_str(&format!(".{:03}", duration_ms)); + } + res.push('s'); + res + } +} + +/// try producing a url for whatever caused this job to be started, if possible +pub fn commit_url(job: &Job, commit_sha: &str, ctx: &Arc) -> Option { + let remote = ctx.remote_by_id(job.remote_id).expect("query succeeds").expect("existing job references existing remote"); + + match remote.remote_api.as_str() { + "github" => { + Some(format!("{}/commit/{}", remote.remote_url, commit_sha)) + }, + "email" => { + None + }, + _ => { + None + } + } +} + +/// produce a url to the ci.butactuallyin.space job details page +pub fn job_url(job: &Job, commit_sha: &str, ctx: &Arc) -> String { + let remote = ctx.remote_by_id(job.remote_id).expect("query succeeds").expect("existing job references existing remote"); + + if remote.remote_api != "github" { + eprintln!("job url for remote type {} can't be constructed, i think", &remote.remote_api); + } + + format!("{}/{}", &remote.remote_path, commit_sha) +} + +/// render how long a run took, or is taking, in a human-friendly way +pub fn display_run_time(run: &Run) -> String { + if let Some(start_time) = run.start_time { + if let Some(complete_time) = run.complete_time { + if complete_time < start_time { + if run.state == RunState::Started { + // this run has been restarted. the completed time is stale. + // further, this is a currently active run. + let now_ms = ci_lib_core::now_ms(); + let mut duration = duration_as_human_string(now_ms - start_time); + duration.push_str(" (ongoing)"); + duration + } else { + "invalid data".to_string() + } + } else { + let duration_ms = complete_time - start_time; + let duration = duration_as_human_string(duration_ms); + duration + } + } else { + if run.state != RunState::Invalid { + let now_ms = ci_lib_core::now_ms(); + let mut duration = duration_as_human_string(now_ms - start_time); + duration.push_str(" (ongoing)"); + duration + } else { + "n/a".to_string() + } + } + } else { + "not yet run".to_owned() + } +} + +pub fn build_repo_index(ctx: &Arc) -> Result { + let repos = match ctx.get_repos() { + Ok(repos) => repos, + Err(e) => { + eprintln!("failed to get repos: {:?}", e); + return Err("gonna feel that one tomorrow".to_string()); + } + }; + + let mut response = String::new(); + + response.push_str("\n"); + response.push_str("\n"); + response.push_str("

builds and build accessories

\n"); + + match repos.len() { + 0 => { response.push_str(&format!("

no repos configured, so there are no builds

\n")); }, + 1 => { response.push_str("

1 repo configured

\n"); }, + other => { response.push_str(&format!("

{} repos configured

\n", other)); }, + } + + response.push_str(""); + response.push_str("\n"); + let headings = ["repo", "last build", "job", "build commit", "duration", "status", "result"]; + for heading in headings { + response.push_str(&format!("", heading)); + } + response.push_str("\n"); + + let mut row_num = 0; + + for repo in repos { + let mut most_recent_run: Option<(Job, Run)> = None; + + for remote in ctx.remotes_by_repo(repo.id).expect("remotes by repo works") { + let last_job = ctx.last_job_from_remote(remote.id).expect("job by remote works"); + if let Some(last_job) = last_job { + if let Some(last_run) = ctx.last_run_for_job(last_job.id).expect("can query") { + if most_recent_run.as_ref().map(|run| run.1.create_time < last_run.create_time).unwrap_or(true) { + most_recent_run = Some((last_job, last_run)); + } + } + } + } + + let repo_html = format!("{}", &repo.name, &repo.name); + + let row_html: String = match most_recent_run { + Some((job, run)) => { + let job_commit = ctx.commit_sha(job.commit_id).expect("job has a commit"); + let commit_html = match commit_url(&job, &job_commit, &ctx) { + Some(url) => format!("{}", url, &job_commit), + None => job_commit.clone() + }; + + let job_html = format!("{}", job_url(&job, &job_commit, &ctx), job.id); + + let last_build_time = Utc.timestamp_millis_opt(run.create_time as i64).unwrap().to_rfc2822(); + let duration = display_run_time(&run); + + let status = format!("{:?}", run.state).to_lowercase(); + + let result = match run.build_result { + Some(0) => "pass", + Some(_) => "fail", + None => match run.state { + RunState::Pending => { "unstarted" }, + RunState::Started => { "in progress" }, + _ => { "unreported" } + } + }; + + let entries = [repo_html.as_str(), last_build_time.as_str(), job_html.as_str(), commit_html.as_str(), &duration, &status, &result]; + let entries = entries.iter().chain(std::iter::repeat(&"")).take(headings.len()); + + let mut row_html = String::new(); + for entry in entries { + row_html.push_str(&format!("", entry)); + } + row_html + } + None => { + let entries = [repo_html.as_str()]; + let entries = entries.iter().chain(std::iter::repeat(&"")).take(headings.len()); + + let mut row_html = String::new(); + for entry in entries { + row_html.push_str(&format!("", entry)); + } + row_html + } + }; + + let row_index = row_num % 2; + response.push_str(&format!("", ["even-row", "odd-row"][row_index])); + response.push_str(&row_html); + response.push_str(""); + response.push('\n'); + + row_num += 1; + } + response.push_str("
{}
{}{}
"); + + response.push_str("

active tasks

\n"); + + let runs = ctx.get_active_runs().expect("can query"); + if runs.len() == 0 { + response.push_str("

(none)

\n"); + } else { + response.push_str(""); + response.push_str("\n"); + let headings = ["repo", "last build", "job", "build commit", "duration", "status", "result"]; + for heading in headings { + response.push_str(&format!("", heading)); + } + response.push_str("\n"); + + let mut row_num = 0; + + for run in runs.iter() { + let row_index = row_num % 2; + + let job = ctx.job_by_id(run.job_id).expect("query succeeds").expect("job id is valid"); + let remote = ctx.remote_by_id(job.remote_id).expect("query succeeds").expect("remote id is valid"); + let repo = ctx.repo_by_id(remote.repo_id).expect("query succeeds").expect("repo id is valid"); + + let repo_html = format!("{}", &repo.name, &repo.name); + + let job_commit = ctx.commit_sha(job.commit_id).expect("job has a commit"); + let commit_html = match commit_url(&job, &job_commit, &ctx) { + Some(url) => format!("{}", url, &job_commit), + None => job_commit.clone() + }; + + let job_html = format!("{}", job_url(&job, &job_commit, &ctx), job.id); + + let last_build_time = Utc.timestamp_millis_opt(run.create_time as i64).unwrap().to_rfc2822(); + let duration = display_run_time(&run); + + let status = format!("{:?}", run.state).to_lowercase(); + + let result = match run.build_result { + Some(0) => "pass", + Some(_) => "fail", + None => match run.state { + RunState::Pending => { "unstarted" }, + RunState::Started => { "in progress" }, + _ => { "unreported" } + } + }; + + let entries = [repo_html.as_str(), last_build_time.as_str(), job_html.as_str(), commit_html.as_str(), &duration, &status, &result]; + let entries = entries.iter().chain(std::iter::repeat(&"")).take(headings.len()); + + let mut row_html = String::new(); + for entry in entries { + row_html.push_str(&format!("", entry)); + } + + + response.push_str(&format!("", ["even-row", "odd-row"][row_index])); + response.push_str(&row_html); + response.push_str(""); + response.push('\n'); + + row_num += 1; + } + + response.push_str("
{}
{}
\n"); + } + + response.push_str(""); + Ok(response) +} -- cgit v1.1