diff options
-rw-r--r-- | Cargo.lock | 11 | ||||
-rw-r--r-- | ci-lib-core/src/dbctx.rs | 44 | ||||
-rw-r--r-- | ci-lib-web/Cargo.toml | 15 | ||||
-rw-r--r-- | ci-lib-web/src/lib.rs | 287 | ||||
-rw-r--r-- | ci-web-server/Cargo.toml | 1 | ||||
-rw-r--r-- | ci-web-server/src/main.rs | 295 |
6 files changed, 348 insertions, 305 deletions
@@ -367,6 +367,16 @@ dependencies = [ ] [[package]] +name = "ci-lib-web" +version = "0.0.1" +dependencies = [ + "chrono", + "ci-lib-core", + "rusqlite", + "serde", +] + +[[package]] name = "ci-runner" version = "0.0.1" dependencies = [ @@ -394,6 +404,7 @@ dependencies = [ "chrono", "ci-lib-core", "ci-lib-native", + "ci-lib-web", "hex", "hmac", "http", diff --git a/ci-lib-core/src/dbctx.rs b/ci-lib-core/src/dbctx.rs index 7493030..eec5e72 100644 --- a/ci-lib-core/src/dbctx.rs +++ b/ci-lib-core/src/dbctx.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; // use futures_util::StreamExt; -use rusqlite::{Connection, OptionalExtension}; +use rusqlite::{params, Connection, OptionalExtension}; use std::time::{SystemTime, UNIX_EPOCH}; // use tokio::io::{AsyncReadExt, AsyncWriteExt}; use std::path::Path; @@ -40,16 +40,16 @@ impl DbCtx { pub fn create_tables(&self) -> Result<(), ()> { let conn = self.conn.lock().unwrap(); - conn.execute(sql::CREATE_ARTIFACTS_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_JOBS_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_METRICS_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_COMMITS_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_REPOS_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_REPO_NAME_INDEX, ()).unwrap(); - conn.execute(sql::CREATE_REMOTES_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_REMOTES_INDEX, ()).unwrap(); - conn.execute(sql::CREATE_RUNS_TABLE, ()).unwrap(); - conn.execute(sql::CREATE_HOSTS_TABLE, ()).unwrap(); + conn.execute(sql::CREATE_ARTIFACTS_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_JOBS_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_METRICS_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_COMMITS_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_REPOS_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_REPO_NAME_INDEX, params![]).unwrap(); + conn.execute(sql::CREATE_REMOTES_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_REMOTES_INDEX, params![]).unwrap(); + conn.execute(sql::CREATE_RUNS_TABLE, params![]).unwrap(); + conn.execute(sql::CREATE_HOSTS_TABLE, params![]).unwrap(); Ok(()) } @@ -59,7 +59,7 @@ impl DbCtx { conn .execute( "insert into metrics (run_id, name, value) values (?1, ?2, ?3) on conflict (run_id, name) do update set value=excluded.value", - (run_id, name, value) + params![run_id, name, value] ) .expect("can upsert"); Ok(()) @@ -96,7 +96,7 @@ impl DbCtx { conn .execute( "update artifacts set completed_time=?1 where id=?2", - (crate::now_ms(), artifact_id) + params![crate::now_ms(), artifact_id] ) .map(|_| ()) .map_err(|e| { @@ -245,7 +245,7 @@ impl DbCtx { conn .execute( "insert into remotes (repo_id, remote_path, remote_api, remote_url, remote_git_url, notifier_config_path) values (?1, ?2, ?3, ?4, ?5, ?6);", - (repo_id, remote_path, remote_api, remote_url, remote_git_url, config_path) + params![repo_id, remote_path, remote_api, remote_url, remote_git_url, config_path] ) .expect("can insert"); @@ -267,7 +267,7 @@ impl DbCtx { let rows_modified = conn.execute( "insert into jobs (remote_id, commit_id, created_time, source, run_preferences) values (?1, ?2, ?3, ?4, ?5);", - (remote_id, commit_id, created_time, pusher, repo_default_run_pref) + params![remote_id, commit_id, created_time, pusher, repo_default_run_pref] ).unwrap(); assert_eq!(1, rows_modified); @@ -287,7 +287,7 @@ impl DbCtx { let rows_modified = conn.execute( "insert into runs (job_id, state, created_time, host_preference) values (?1, ?2, ?3, ?4);", - (job_id, crate::sql::RunState::Pending as u64, created_time, host_preference) + params![job_id, crate::sql::RunState::Pending as u64, created_time, host_preference] ).unwrap(); assert_eq!(1, rows_modified); @@ -511,7 +511,7 @@ impl DbCtx { hostname=?1 and cpu_vendor_id=?2 and cpu_model_name=?3 and cpu_family=?4 and \ cpu_model=?5 and cpu_max_freq_khz=?6 and cpu_cores=?7 and mem_total=?8 and \ arch=?9);", - ( + params![ &host_info.hostname, &host_info.cpu_info.vendor_id, &host_info.cpu_info.model_name, @@ -521,7 +521,7 @@ impl DbCtx { &host_info.cpu_info.cores, &host_info.memory_info.total, &host_info.env_info.arch, - ), + ], |row| { row.get(0) } ) .map_err(|e| e.to_string()) @@ -544,7 +544,7 @@ impl DbCtx { ?5, ?6, ?7, ?8, \ ?9, ?10, ?11, ?12 \ );", - ( + params![ &host_info.hostname, &host_info.cpu_info.vendor_id, &host_info.cpu_info.model_name, @@ -557,7 +557,7 @@ impl DbCtx { &host_info.env_info.arch, &host_info.env_info.family, &host_info.env_info.os, - ) + ] ) .expect("can insert"); @@ -567,7 +567,7 @@ impl DbCtx { hostname=?1 and cpu_vendor_id=?2 and cpu_model_name=?3 and cpu_family=?4 and \ cpu_model=?5 and cpu_microcode=?6 and cpu_max_freq_khz=?7 and \ cpu_cores=?8 and mem_total=?9 and arch=?10 and family=?11 and os=?12;", - ( + params![ &host_info.hostname, &host_info.cpu_info.vendor_id, &host_info.cpu_info.model_name, @@ -580,7 +580,7 @@ impl DbCtx { &host_info.env_info.arch, &host_info.env_info.family, &host_info.env_info.os, - ), + ], |row| { row.get(0) } ) .map_err(|e| e.to_string()) 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 <me@iximeow.net>" ] +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<DbCtx>) -> Option<String> { + 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<DbCtx>) -> 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<DbCtx>) -> Result<String, String> { + 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("<html>\n"); + response.push_str("<style>\n"); + response.push_str(".build-table { font-family: monospace; border: 1px solid black; border-collapse: collapse; }\n"); + response.push_str(".row-item { padding-left: 4px; padding-right: 4px; border-right: 1px solid black; }\n"); + response.push_str(".odd-row { background: #eee; }\n"); + response.push_str(".even-row { background: #ddd; }\n"); + response.push_str("</style>\n"); + response.push_str("<h1>builds and build accessories</h1>\n"); + + match repos.len() { + 0 => { response.push_str(&format!("<p>no repos configured, so there are no builds</p>\n")); }, + 1 => { response.push_str("<p>1 repo configured</p>\n"); }, + other => { response.push_str(&format!("<p>{} repos configured</p>\n", other)); }, + } + + response.push_str("<table class='build-table'>"); + response.push_str("<tr>\n"); + let headings = ["repo", "last build", "job", "build commit", "duration", "status", "result"]; + for heading in headings { + response.push_str(&format!("<th class='row-item'>{}</th>", heading)); + } + response.push_str("</tr>\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!("<a href=\"/{}\">{}</a>", &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!("<a href=\"{}\">{}</a>", url, &job_commit), + None => job_commit.clone() + }; + + let job_html = format!("<a href=\"{}\">{}</a>", 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) => "<span style='color:green;'>pass</span>", + Some(_) => "<span style='color:red;'>fail</span>", + None => match run.state { + RunState::Pending => { "unstarted" }, + RunState::Started => { "<span style='color:darkgoldenrod;'>in progress</span>" }, + _ => { "<span style='color:red;'>unreported</span>" } + } + }; + + 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!("<td class='row-item'>{}</td>", 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!("<td class='row-item'>{}</td>", entry)); + } + row_html + } + }; + + let row_index = row_num % 2; + response.push_str(&format!("<tr class=\"{}\">", ["even-row", "odd-row"][row_index])); + response.push_str(&row_html); + response.push_str("</tr>"); + response.push('\n'); + + row_num += 1; + } + response.push_str("</table>"); + + response.push_str("<h4>active tasks</h4>\n"); + + let runs = ctx.get_active_runs().expect("can query"); + if runs.len() == 0 { + response.push_str("<p>(none)</p>\n"); + } else { + response.push_str("<table class='build-table'>"); + response.push_str("<tr>\n"); + let headings = ["repo", "last build", "job", "build commit", "duration", "status", "result"]; + for heading in headings { + response.push_str(&format!("<th class='row-item'>{}</th>", heading)); + } + response.push_str("</tr>\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!("<a href=\"/{}\">{}</a>", &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!("<a href=\"{}\">{}</a>", url, &job_commit), + None => job_commit.clone() + }; + + let job_html = format!("<a href=\"{}\">{}</a>", 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) => "<span style='color:green;'>pass</span>", + Some(_) => "<span style='color:red;'>fail</span>", + None => match run.state { + RunState::Pending => { "unstarted" }, + RunState::Started => { "<span style='color:darkgoldenrod;'>in progress</span>" }, + _ => { "<span style='color:red;'>unreported</span>" } + } + }; + + 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!("<td class='row-item'>{}</td>", entry)); + } + + + response.push_str(&format!("<tr class=\"{}\">", ["even-row", "odd-row"][row_index])); + response.push_str(&row_html); + response.push_str("</tr>"); + response.push('\n'); + + row_num += 1; + } + + response.push_str("</table>\n"); + } + + response.push_str("</html>"); + Ok(response) +} diff --git a/ci-web-server/Cargo.toml b/ci-web-server/Cargo.toml index 2771e3a..bb0b57c 100644 --- a/ci-web-server/Cargo.toml +++ b/ci-web-server/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" [dependencies] ci-lib-core = { path = "../ci-lib-core" } ci-lib-native = { path = "../ci-lib-native" } +ci-lib-web = { path = "../ci-lib-web" } tokio = { features = ["full"] } tokio-stream = "*" diff --git a/ci-web-server/src/main.rs b/ci-web-server/src/main.rs index e2be54f..31e7a8c 100644 --- a/ci-web-server/src/main.rs +++ b/ci-web-server/src/main.rs @@ -79,107 +79,6 @@ enum GithubEvent { Other {} } -/// return a duration rendered as the largest two non-zero units. -/// -/// 60000ms -> 1m -/// 60001ms -> 1m -/// 61000ms -> 1m1s -/// 1030ms -> 1.03s -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 -fn commit_url(job: &Job, commit_sha: &str, ctx: &Arc<DbCtx>) -> Option<String> { - 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 -fn job_url(job: &Job, commit_sha: &str, ctx: &Arc<DbCtx>) -> 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 -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() - } -} - fn parse_push_event(body: serde_json::Value) -> Result<GithubEvent, GithubHookError> { let body = body.as_object() .ok_or(GithubHookError::BodyNotObject)?; @@ -304,184 +203,14 @@ async fn handle_github_event(ctx: Arc<DbCtx>, owner: String, repo: String, event async fn handle_ci_index(State(ctx): State<WebserverState>) -> impl IntoResponse { eprintln!("root index"); - let repos = match ctx.dbctx.get_repos() { - Ok(repos) => repos, - Err(e) => { - eprintln!("failed to get repos: {:?}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, Html("gonna feel that one tomorrow".to_string())); - } - }; - - let mut response = String::new(); - - response.push_str("<html>\n"); - response.push_str("<style>\n"); - response.push_str(".build-table { font-family: monospace; border: 1px solid black; border-collapse: collapse; }\n"); - response.push_str(".row-item { padding-left: 4px; padding-right: 4px; border-right: 1px solid black; }\n"); - response.push_str(".odd-row { background: #eee; }\n"); - response.push_str(".even-row { background: #ddd; }\n"); - response.push_str("</style>\n"); - response.push_str("<h1>builds and build accessories</h1>\n"); - - match repos.len() { - 0 => { response.push_str(&format!("<p>no repos configured, so there are no builds</p>\n")); }, - 1 => { response.push_str("<p>1 repo configured</p>\n"); }, - other => { response.push_str(&format!("<p>{} repos configured</p>\n", other)); }, - } - - response.push_str("<table class='build-table'>"); - response.push_str("<tr>\n"); - let headings = ["repo", "last build", "job", "build commit", "duration", "status", "result"]; - for heading in headings { - response.push_str(&format!("<th class='row-item'>{}</th>", heading)); - } - response.push_str("</tr>\n"); - - let mut row_num = 0; - - for repo in repos { - let mut most_recent_run: Option<(Job, Run)> = None; - - for remote in ctx.dbctx.remotes_by_repo(repo.id).expect("remotes by repo works") { - let last_job = ctx.dbctx.last_job_from_remote(remote.id).expect("job by remote works"); - if let Some(last_job) = last_job { - if let Some(last_run) = ctx.dbctx.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!("<a href=\"/{}\">{}</a>", &repo.name, &repo.name); - - let row_html: String = match most_recent_run { - Some((job, run)) => { - let job_commit = ctx.dbctx.commit_sha(job.commit_id).expect("job has a commit"); - let commit_html = match commit_url(&job, &job_commit, &ctx.dbctx) { - Some(url) => format!("<a href=\"{}\">{}</a>", url, &job_commit), - None => job_commit.clone() - }; - - let job_html = format!("<a href=\"{}\">{}</a>", job_url(&job, &job_commit, &ctx.dbctx), 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) => "<span style='color:green;'>pass</span>", - Some(_) => "<span style='color:red;'>fail</span>", - None => match run.state { - RunState::Pending => { "unstarted" }, - RunState::Started => { "<span style='color:darkgoldenrod;'>in progress</span>" }, - _ => { "<span style='color:red;'>unreported</span>" } - } - }; - - 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!("<td class='row-item'>{}</td>", 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!("<td class='row-item'>{}</td>", entry)); - } - row_html - } - }; - - let row_index = row_num % 2; - response.push_str(&format!("<tr class=\"{}\">", ["even-row", "odd-row"][row_index])); - response.push_str(&row_html); - response.push_str("</tr>"); - response.push('\n'); - - row_num += 1; - } - response.push_str("</table>"); - - response.push_str("<h4>active tasks</h4>\n"); - - let runs = ctx.dbctx.get_active_runs().expect("can query"); - if runs.len() == 0 { - response.push_str("<p>(none)</p>\n"); - } else { - response.push_str("<table class='build-table'>"); - response.push_str("<tr>\n"); - let headings = ["repo", "last build", "job", "build commit", "duration", "status", "result"]; - for heading in headings { - response.push_str(&format!("<th class='row-item'>{}</th>", heading)); + match ci_lib_web::build_repo_index(&ctx.dbctx) { + Ok(html) => { + (StatusCode::OK, Html(html)) } - response.push_str("</tr>\n"); - - let mut row_num = 0; - - for run in runs.iter() { - let row_index = row_num % 2; - - let job = ctx.dbctx.job_by_id(run.job_id).expect("query succeeds").expect("job id is valid"); - let remote = ctx.dbctx.remote_by_id(job.remote_id).expect("query succeeds").expect("remote id is valid"); - let repo = ctx.dbctx.repo_by_id(remote.repo_id).expect("query succeeds").expect("repo id is valid"); - - let repo_html = format!("<a href=\"/{}\">{}</a>", &repo.name, &repo.name); - - let job_commit = ctx.dbctx.commit_sha(job.commit_id).expect("job has a commit"); - let commit_html = match commit_url(&job, &job_commit, &ctx.dbctx) { - Some(url) => format!("<a href=\"{}\">{}</a>", url, &job_commit), - None => job_commit.clone() - }; - - let job_html = format!("<a href=\"{}\">{}</a>", job_url(&job, &job_commit, &ctx.dbctx), 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) => "<span style='color:green;'>pass</span>", - Some(_) => "<span style='color:red;'>fail</span>", - None => match run.state { - RunState::Pending => { "unstarted" }, - RunState::Started => { "<span style='color:darkgoldenrod;'>in progress</span>" }, - _ => { "<span style='color:red;'>unreported</span>" } - } - }; - - 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!("<td class='row-item'>{}</td>", entry)); - } - - - response.push_str(&format!("<tr class=\"{}\">", ["even-row", "odd-row"][row_index])); - response.push_str(&row_html); - response.push_str("</tr>"); - response.push('\n'); - - row_num += 1; + Err(e) => { + (StatusCode::INTERNAL_SERVER_ERROR, Html(e)) } - - response.push_str("</table>\n"); } - - response.push_str("</html>"); - - (StatusCode::OK, Html(response)) } async fn handle_commit_status(Path(path): Path<(String, String, String)>, State(ctx): State<WebserverState>) -> impl IntoResponse { @@ -560,7 +289,7 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( short_sha, path.0, path.1, status_desc, - display_run_time(&run) + ci_lib_web::display_run_time(&run) ); head.push_str(&format!("<meta name=\"description\" content=\"{}\"\n>", build_og_description)); head.push_str(&format!("<meta property=\"og:description\" content=\"{}\"\n>", build_og_description)); @@ -589,7 +318,7 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( for artifact in old_artifacts.iter() { let created_time_str = Utc.timestamp_millis_opt(artifact.created_time as i64).unwrap().to_rfc2822(); artifacts_fragment.push_str(&format!("<div><pre style='display:inline;'>{}</pre> step: <pre style='display:inline;'>{}</pre></div>\n", created_time_str, &artifact.name)); - let duration_str = duration_as_human_string(artifact.completed_time.unwrap_or_else(ci_lib_core::now_ms) - artifact.created_time); + let duration_str = ci_lib_web::duration_as_human_string(artifact.completed_time.unwrap_or_else(ci_lib_core::now_ms) - artifact.created_time); let size_str = (std::fs::metadata(&format!("./artifacts/{}/{}", artifact.run_id, artifact.id)).expect("metadata exists").len() / 1024).to_string(); artifacts_fragment.push_str(&format!("<pre> {}kb in {} </pre>\n", size_str, duration_str)); } @@ -602,7 +331,7 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( artifacts_fragment.push_str(&std::fs::read_to_string(format!("./artifacts/{}/{}", artifact.run_id, artifact.id)).unwrap()); artifacts_fragment.push_str("</pre>\n"); } else { - let duration_str = duration_as_human_string(artifact.completed_time.unwrap_or_else(ci_lib_core::now_ms) - artifact.created_time); + let duration_str = ci_lib_web::duration_as_human_string(artifact.completed_time.unwrap_or_else(ci_lib_core::now_ms) - artifact.created_time); let size_str = std::fs::metadata(&format!("./artifacts/{}/{}", artifact.run_id, artifact.id)).map(|md| { (md.len() / 1024).to_string() }).unwrap_or_else(|e| format!("[{}]", e)); @@ -619,7 +348,7 @@ async fn handle_commit_status(Path(path): Path<(String, String, String)>, State( html.push_str(" <pre>\n"); html.push_str(&format!("repo: {}\n", repo_html)); html.push_str(&format!("commit: {}, run: {}\n", remote_commit_elem, run.id)); - html.push_str(&format!("status: {} in {}\n", status_elem, display_run_time(&run))); + html.push_str(&format!("status: {} in {}\n", status_elem, ci_lib_web::display_run_time(&run))); if let Some(desc) = run.final_text.as_ref() { html.push_str(&format!(" description: {}\n ", desc)); } @@ -827,15 +556,15 @@ async fn handle_repo_summary(Path(path): Path<String>, State(ctx): State<Webserv for job in last_builds.iter().take(10) { let run = ctx.dbctx.last_run_for_job(job.id).expect("query succeeds").expect("TODO: run exists if job exists (small race if querying while creating job ...."); let job_commit = ctx.dbctx.commit_sha(job.commit_id).expect("job has a commit"); - let commit_html = match commit_url(&job, &job_commit, &ctx.dbctx) { + let commit_html = match ci_lib_web::commit_url(&job, &job_commit, &ctx.dbctx) { Some(url) => format!("<a href=\"{}\">{}</a>", url, &job_commit), None => job_commit.clone() }; - let job_html = format!("<a href=\"{}\">{}</a>", job_url(&job, &job_commit, &ctx.dbctx), job.id); + let job_html = format!("<a href=\"{}\">{}</a>", ci_lib_web::job_url(&job, &job_commit, &ctx.dbctx), 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 duration = ci_lib_web::display_run_time(&run); let status = format!("{:?}", run.state).to_lowercase(); |