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
@ -505,6 +505,10 @@ pub struct PatchJobSummary {
|
|||||||
pub status: JobStatus,
|
pub status: JobStatus,
|
||||||
pub immediate: bool,
|
pub immediate: bool,
|
||||||
pub host_count: i64,
|
pub host_count: i64,
|
||||||
|
/// Display names of hosts targeted by this job (falls back to fqdn).
|
||||||
|
#[serde(default)]
|
||||||
|
#[sqlx(skip)]
|
||||||
|
pub host_names: Vec<String>,
|
||||||
pub succeeded_count: i64,
|
pub succeeded_count: i64,
|
||||||
pub failed_count: i64,
|
pub failed_count: i64,
|
||||||
pub notes: String,
|
pub notes: String,
|
||||||
|
|||||||
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::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
@ -52,6 +53,13 @@ struct JobListResponse {
|
|||||||
offset: i64,
|
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.
|
/// Per-host row included in `GET /api/v1/jobs/{id}` response.
|
||||||
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
|
||||||
struct JobHostRow {
|
struct JobHostRow {
|
||||||
@ -229,7 +237,7 @@ async fn list_jobs(
|
|||||||
let limit = q.limit.unwrap_or(50).min(200);
|
let limit = q.limit.unwrap_or(50).min(200);
|
||||||
let offset = q.offset.unwrap_or(0);
|
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.
|
// Admins see every job.
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
@ -298,6 +306,40 @@ async fn list_jobs(
|
|||||||
err(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", "Database error")
|
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.
|
// Total count for pagination metadata.
|
||||||
let total: i64 = if auth.role.is_admin() {
|
let total: i64 = if auth.role.is_admin() {
|
||||||
sqlx::query_scalar("SELECT COUNT(*) FROM patch_jobs")
|
sqlx::query_scalar("SELECT COUNT(*) FROM patch_jobs")
|
||||||
|
|||||||
@ -70,12 +70,16 @@ Security: JWT Bearer Token (except Public Endpoints)
|
|||||||
## 7. Jobs & Patch Deployment
|
## 7. Jobs & Patch Deployment
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
|--------|----------|-------------|
|
||||||
| GET | `/jobs` | List patch jobs |
|
| GET | `/jobs` | List patch jobs (includes `host_names` per job) |
|
||||||
| POST | `/jobs` | Create new patch job |
|
| POST | `/jobs` | Create new patch job |
|
||||||
| GET | `/jobs/{id}` | Get job status/details |
|
| GET | `/jobs/{id}` | Get job status/details |
|
||||||
| POST | `/jobs/{id}/cancel` | Cancel running job |
|
| POST | `/jobs/{id}/cancel` | Cancel running job |
|
||||||
| POST | `/jobs/{id}/rollback` | Rollback completed job |
|
| POST | `/jobs/{id}/rollback` | Rollback completed job |
|
||||||
|
|
||||||
|
### GET /jobs Response Fields
|
||||||
|
Each job summary object includes:
|
||||||
|
- `host_names`: Array of display names for hosts targeted by this job. Falls back to `fqdn` when `display_name` is empty. Single-host jobs show one name; multi-host jobs show all names sorted alphabetically.
|
||||||
|
|
||||||
## 8. Maintenance Windows
|
## 8. Maintenance Windows
|
||||||
*Scoped to host.*
|
*Scoped to host.*
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|
|||||||
@ -194,7 +194,13 @@ function JobRow({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<StatusChip status={job.status} />
|
<StatusChip status={job.status} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{job.host_count}</TableCell>
|
<TableCell>
|
||||||
|
{job.host_names.length === 1
|
||||||
|
? job.host_names[0]
|
||||||
|
: job.host_names.length > 1
|
||||||
|
? <Tooltip title={job.host_names.join(', ')}><span>{job.host_names[0]} +{job.host_names.length - 1}</span></Tooltip>
|
||||||
|
: '—'}
|
||||||
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography color="success.main" fontWeight={600}>
|
<Typography color="success.main" fontWeight={600}>
|
||||||
{job.succeeded_count}
|
{job.succeeded_count}
|
||||||
@ -512,7 +518,7 @@ export default function JobsPage() {
|
|||||||
<TableCell>Created</TableCell>
|
<TableCell>Created</TableCell>
|
||||||
<TableCell>Kind</TableCell>
|
<TableCell>Kind</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell align="right">Hosts</TableCell>
|
<TableCell>Hosts</TableCell>
|
||||||
<TableCell align="right">Succeeded</TableCell>
|
<TableCell align="right">Succeeded</TableCell>
|
||||||
<TableCell align="right">Failed</TableCell>
|
<TableCell align="right">Failed</TableCell>
|
||||||
<TableCell>Schedule</TableCell>
|
<TableCell>Schedule</TableCell>
|
||||||
|
|||||||
@ -144,6 +144,7 @@ export interface PatchJobSummary {
|
|||||||
status: JobStatus
|
status: JobStatus
|
||||||
immediate: boolean
|
immediate: boolean
|
||||||
host_count: number
|
host_count: number
|
||||||
|
host_names: string[]
|
||||||
succeeded_count: number
|
succeeded_count: number
|
||||||
failed_count: number
|
failed_count: number
|
||||||
notes: string
|
notes: string
|
||||||
|
|||||||
Reference in New Issue
Block a user