feat(jobs): add host_names to job list API and UI (#24)
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 51s
CI Pipeline / Rust Unit Tests (push) Successful in 1m7s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 51s
CI Pipeline / Rust Unit Tests (push) Successful in 1m7s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
* feat(jobs): add host_names to job list API and UI Closes #23 * fix(jobs): add mut for host_names merge loop
This commit is contained in:
committed by
GitHub
parent
b9fb3427e0
commit
fda70ecf9e
44
crates/pm-web/src/routes/jobs.rs
Executable file → Normal file
44
crates/pm-web/src/routes/jobs.rs
Executable file → Normal file
@ -20,6 +20,7 @@ use pm_core::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::AppState;
|
||||
@ -52,6 +53,13 @@ struct JobListResponse {
|
||||
offset: i64,
|
||||
}
|
||||
|
||||
/// Helper struct for the host_names aggregation query.
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct JobHostNames {
|
||||
id: Uuid,
|
||||
host_names: Vec<String>,
|
||||
}
|
||||
|
||||
/// Per-host row included in `GET /api/v1/jobs/{id}` response.
|
||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||
struct JobHostRow {
|
||||
@ -229,7 +237,7 @@ async fn list_jobs(
|
||||
let limit = q.limit.unwrap_or(50).min(200);
|
||||
let offset = q.offset.unwrap_or(0);
|
||||
|
||||
let jobs: Vec<PatchJobSummary> = if auth.role.is_admin() {
|
||||
let mut jobs: Vec<PatchJobSummary> = if auth.role.is_admin() {
|
||||
// Admins see every job.
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
@ -298,6 +306,40 @@ async fn list_jobs(
|
||||
err(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", "Database error")
|
||||
})?;
|
||||
|
||||
// Fetch host names for all jobs in this page.
|
||||
let job_ids: Vec<Uuid> = jobs.iter().map(|j| j.id).collect();
|
||||
let host_names_rows: Vec<JobHostNames> = if job_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT pjh.job_id AS id,
|
||||
array_agg(COALESCE(NULLIF(h.display_name, ''), h.fqdn)
|
||||
ORDER BY h.fqdn) AS host_names
|
||||
FROM patch_job_hosts pjh
|
||||
JOIN hosts h ON h.id = pjh.host_id
|
||||
WHERE pjh.job_id = ANY($1)
|
||||
GROUP BY pjh.job_id
|
||||
"#,
|
||||
)
|
||||
.bind(&job_ids)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!(error = %e, "list_jobs: host_names query failed, using empty defaults");
|
||||
Vec::new()
|
||||
})
|
||||
};
|
||||
|
||||
// Merge host_names into summaries.
|
||||
let mut host_names_map: HashMap<Uuid, Vec<String>> = host_names_rows
|
||||
.into_iter()
|
||||
.map(|r| (r.id, r.host_names))
|
||||
.collect();
|
||||
for job in &mut jobs {
|
||||
job.host_names = host_names_map.remove(&job.id).unwrap_or_default();
|
||||
}
|
||||
|
||||
// Total count for pagination metadata.
|
||||
let total: i64 = if auth.role.is_admin() {
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM patch_jobs")
|
||||
|
||||
Reference in New Issue
Block a user