From 9acffed2fb2a97fc15f722411de52b2a7c538838 Mon Sep 17 00:00:00 2001 From: arferreira Date: Sun, 8 Mar 2026 14:44:37 -0400 Subject: [PATCH] Simplify dashboard to unified PR list with pagination --- src/dashboard.rs | 264 ++++++++++++++++++++++++----------------------- 1 file changed, 134 insertions(+), 130 deletions(-) diff --git a/src/dashboard.rs b/src/dashboard.rs index a682758..48edda3 100644 --- a/src/dashboard.rs +++ b/src/dashboard.rs @@ -5,166 +5,170 @@ use rapina::http::Response; use rapina::http::header::CONTENT_TYPE; use rapina::prelude::*; use rapina::response::BoxBody; -use rapina::sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; -use std::collections::HashMap; +use rapina::sea_orm::{ + ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, +}; -use crate::entity::batch::Column as BatchColumn; -use crate::entity::merge_event::Column as EventColumn; +use crate::entity::PullRequest; use crate::entity::pull_request::Column as PrColumn; -use crate::entity::{Batch, MergeEvent, PullRequest}; use crate::types::PrStatus; +const PAGE_SIZE: u64 = 50; + +#[derive(serde::Deserialize)] +struct DashboardQuery { + page: Option, +} + #[public] #[get("/")] -pub async fn dashboard(db: Db) -> Result> { - let queued = PullRequest::find() - .filter(PrColumn::Status.eq(PrStatus::Queued.as_ref())) - .order_by_desc(PrColumn::Priority) - .order_by_asc(PrColumn::QueuedAt) - .all(db.conn()) +pub async fn dashboard(db: Db, query: Query) -> Result> { + let page = query.page.unwrap_or(1).max(1); + let now = chrono::Utc::now(); + + // Stats + let total = PullRequest::find() + .count(db.conn()) .await .map_err(DbError)?; - let testing = PullRequest::find() - .filter(PrColumn::Status.eq(PrStatus::Testing.as_ref())) + let in_queue = PullRequest::find() + .filter( + PrColumn::Status + .eq(PrStatus::Queued.as_ref()) + .or(PrColumn::Status.eq(PrStatus::Testing.as_ref())) + .or(PrColumn::Status.eq(PrStatus::Batched.as_ref())), + ) + .count(db.conn()) + .await + .map_err(DbError)?; + + let failed = PullRequest::find() + .filter(PrColumn::Status.eq(PrStatus::Failed.as_ref())) + .count(db.conn()) + .await + .map_err(DbError)?; + + let merged = PullRequest::find() + .filter(PrColumn::Status.eq(PrStatus::Merged.as_ref())) + .count(db.conn()) + .await + .map_err(DbError)?; + + // All PRs: active first (queued/testing/batched), then rest by id desc + // SeaORM doesn't support CASE ordering easily, so we do two queries + let active_statuses = vec![ + PrStatus::Queued.to_string(), + PrStatus::Testing.to_string(), + PrStatus::Batched.to_string(), + ]; + + let active_prs = PullRequest::find() + .filter(PrColumn::Status.is_in(active_statuses)) + .order_by_desc(PrColumn::Priority) .order_by_asc(PrColumn::QueuedAt) .all(db.conn()) .await .map_err(DbError)?; - let batches = Batch::find() - .order_by_desc(BatchColumn::Id) - .limit(10) + let inactive_statuses = vec![ + PrStatus::Merged.to_string(), + PrStatus::Failed.to_string(), + PrStatus::Cancelled.to_string(), + ]; + + let offset = (page - 1) * PAGE_SIZE; + let inactive_prs = PullRequest::find() + .filter(PrColumn::Status.is_in(inactive_statuses)) + .order_by_desc(PrColumn::Id) + .offset(offset) + .limit(PAGE_SIZE) .all(db.conn()) .await .map_err(DbError)?; - let events = MergeEvent::find() - .order_by_desc(EventColumn::Id) - .limit(20) - .all(db.conn()) + let inactive_total = PullRequest::find() + .filter( + PrColumn::Status + .eq(PrStatus::Merged.as_ref()) + .or(PrColumn::Status.eq(PrStatus::Failed.as_ref())) + .or(PrColumn::Status.eq(PrStatus::Cancelled.as_ref())), + ) + .count(db.conn()) .await .map_err(DbError)?; - let mut html = String::with_capacity(16384); + let total_pages = (inactive_total as f64 / PAGE_SIZE as f64).ceil() as u64; + + let mut html = String::with_capacity(32768); html.push_str(HEADER); - // Queue section - html.push_str("

Queue

"); - if queued.is_empty() { - html.push_str("

No PRs in queue.

"); - } else { - html.push_str( - "", - ); - let now = chrono::Utc::now(); - for pr in &queued { - let time_in_queue = pr - .queued_at - .map(|t| relative_time(now, t)) - .unwrap_or_default(); - let short_sha = &pr.head_sha[..7.min(pr.head_sha.len())]; - let approved = pr.approved_by.as_deref().unwrap_or("\u{2014}"); - html.push_str(&format!( - "", - pr.repo_owner, pr.repo_name, pr.pr_number, pr.pr_number, pr.title, pr.author, approved, pr.repo_owner, pr.repo_name, pr.head_sha, short_sha, time_in_queue, - )); - } - html.push_str("
PRTitleAuthorShipped byHEADStatusTime in queue
#{}{}{}{}{}queued{}
"); - } - html.push_str("
"); + // Summary stats + html.push_str(&format!( + "
{} total · {} in queue · {} merged · {} failed
", + total, in_queue, merged, failed, + )); - // In Progress section - html.push_str("

In Progress

"); - if testing.is_empty() { - html.push_str("

No PRs currently testing.

"); + // PR table + html.push_str("
"); + if active_prs.is_empty() && inactive_prs.is_empty() { + html.push_str("

No PRs yet.

"); } else { html.push_str( - "", + "
PRTitleAuthorShipped byHEADStatus
", ); - for pr in &testing { + + for pr in active_prs.iter().chain(inactive_prs.iter()) { let short_sha = &pr.head_sha[..7.min(pr.head_sha.len())]; let approved = pr.approved_by.as_deref().unwrap_or("\u{2014}"); - html.push_str(&format!( - "", - pr.repo_owner, pr.repo_name, pr.pr_number, pr.pr_number, pr.title, pr.author, approved, pr.repo_owner, pr.repo_name, pr.head_sha, short_sha, - )); - } - html.push_str("
#StatusTitleAuthorShipped byHEADTime
#{}{}{}{}{}testing
"); - } - html.push_str("
"); + let status = PrStatus::from(pr.status.as_str()); + let time_col = match status { + PrStatus::Queued | PrStatus::Testing | PrStatus::Batched => pr + .queued_at + .map(|t| relative_time(now, t)) + .unwrap_or_default(), + PrStatus::Merged => pr + .merged_at + .map(|t| relative_time(now, t)) + .unwrap_or_default(), + _ => pr + .queued_at + .map(|t| relative_time(now, t)) + .unwrap_or_default(), + }; - // Batches section - html.push_str("

Recent Batches

"); - if batches.is_empty() { - html.push_str("

No batches yet.

"); - } else { - html.push_str( - "", - ); - for batch in &batches { html.push_str(&format!( - "", - batch.id, - batch.status, - batch.status, - batch.completed_at.map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()).unwrap_or_else(|| "\u{2014}".to_string()), + "", + pr.repo_owner, pr.repo_name, pr.pr_number, pr.pr_number, + pr.status, pr.status, + pr.title, pr.author, approved, + pr.repo_owner, pr.repo_name, pr.head_sha, short_sha, + time_col, )); } html.push_str("
IDStatusCompleted At
{}{}{}
{}{}{}{}{}{}{}
"); - } - html.push_str("
"); - - // Recent Activity section — look up PR info for links - html.push_str("

Recent Activity

"); - if events.is_empty() { - html.push_str("

No events yet.

"); - } else { - let pr_ids: Vec = events.iter().map(|e| e.pull_request_id).collect(); - let prs = PullRequest::find() - .filter(PrColumn::Id.is_in(pr_ids)) - .all(db.conn()) - .await - .map_err(DbError)?; - let pr_map: HashMap = prs.into_iter().map(|p| (p.id, p)).collect(); - - html.push_str( - "", - ); - for event in &events { - let pr_cell = if let Some(pr) = pr_map.get(&event.pull_request_id) { - format!( - "#{}", - pr.repo_owner, pr.repo_name, pr.pr_number, pr.pr_number - ) - } else { - format!("#{}", event.pull_request_id) - }; - - let details = match event.details.as_deref() { - Some(sha) if sha.len() >= 7 && sha.chars().all(|c| c.is_ascii_hexdigit()) => { - if let Some(pr) = pr_map.get(&event.pull_request_id) { - format!( - "{}", - pr.repo_owner, - pr.repo_name, - sha, - &sha[..7] - ) - } else { - sha[..7].to_string() - } - } - Some(d) => d.to_string(), - None => "\u{2014}".to_string(), - }; + // Pagination + if total_pages > 1 { + html.push_str("
"); + if page > 1 { + html.push_str(&format!( + "← Newer", + page - 1 + )); + } html.push_str(&format!( - "
", - event.event_type, event.event_type, pr_cell, details, + "Page {} of {}", + page, total_pages )); + if page < total_pages { + html.push_str(&format!( + "Older →", + page + 1 + )); + } + html.push_str(""); } - html.push_str("
EventPRDetails
{}{}{}
"); } html.push_str("
"); @@ -211,14 +215,15 @@ const HEADER: &str = r#" @@ -243,7 +247,7 @@ a:hover { text-decoration: underline; } const FOOTER: &str = r#"