swarm-controller: split issue-report handlers into their own module
This commit is contained in:
parent
f1b729e6b5
commit
957291e3a7
2 changed files with 124 additions and 104 deletions
120
swarm-controller/src/issue_report.rs
Normal file
120
swarm-controller/src/issue_report.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
//! The swarm-ui issue-report page's data source — repo listing plus the
|
||||
//! two shapes of the report itself (all-repos default, single-repo
|
||||
//! filtered). See [`crate::forge::Client::issue_report`] and
|
||||
//! [`crate::forge::Client::issue_report_all`] for how a row's `blocked` /
|
||||
//! `depended_on_by_count` fields are actually computed; this module is
|
||||
//! just the HTTP surface over that logic, split out into its own file per
|
||||
//! mara's review call on the PR that introduced it (same shape
|
||||
//! [`crate::webhook`] already uses: business logic in
|
||||
//! `crate::forge::Client`, the axum handlers next to their own routes).
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
|
||||
use super::{AppState, StatusUnavailable};
|
||||
use crate::forge;
|
||||
|
||||
/// Repos with at least one open issue, as `owner/name` full names — the
|
||||
/// data source for swarm-ui's repo-filter dropdown (mara: "repo filter
|
||||
/// should be dropdown (list only repos with open issues)"). Not scoped to
|
||||
/// [`forge::CONFIG_ORG`] the way `main.rs`'s `get_config_prs` is: mara's
|
||||
/// own framing for this issue-report page was "repos cannot be fixed for
|
||||
/// a hyperhive feature", so this deliberately covers the whole forge
|
||||
/// instance, not one org.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/repos",
|
||||
responses(
|
||||
(status = 200, description = "repos with at least one open issue, as owner/name", body = Vec<String>),
|
||||
(status = 503, description = "no forge is configured on this host", body = String),
|
||||
),
|
||||
tag = "repos"
|
||||
)]
|
||||
pub async fn get_repos(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<String>>, StatusUnavailable> {
|
||||
let Some(client) = state.forge.as_ref() else {
|
||||
return Err(StatusUnavailable(
|
||||
"no forge is configured on this host".to_owned(),
|
||||
));
|
||||
};
|
||||
match client.list_repos_with_open_issues().await {
|
||||
Ok(repos) => Ok(Json(repos)),
|
||||
Err(e) => {
|
||||
let detail = format!("{e:#}");
|
||||
tracing::warn!(error = %detail, "listing repos failed");
|
||||
Err(StatusUnavailable(detail))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every open issue across every repo with at least one — the default,
|
||||
/// no-repo-filter view (mara: "by default, i want it to not filter by
|
||||
/// repo"). See [`get_issue_report`] for the single-repo counterpart (used
|
||||
/// once the dropdown's repo filter is set) and
|
||||
/// [`forge::Client::issue_report_all`] for how the fan-out works.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/issue-report",
|
||||
responses(
|
||||
(status = 200, description = "every open issue across every repo with one, blocked/dependent-count pre-resolved", body = Vec<forge::IssueReportRow>),
|
||||
(status = 503, description = "no forge is configured on this host, or the report could not be built", body = String),
|
||||
),
|
||||
tag = "repos"
|
||||
)]
|
||||
pub async fn get_issue_report_all(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<forge::IssueReportRow>>, StatusUnavailable> {
|
||||
let Some(client) = state.forge.as_ref() else {
|
||||
return Err(StatusUnavailable(
|
||||
"no forge is configured on this host".to_owned(),
|
||||
));
|
||||
};
|
||||
match client.issue_report_all().await {
|
||||
Ok(rows) => Ok(Json(rows)),
|
||||
Err(e) => {
|
||||
let detail = format!("{e:#}");
|
||||
tracing::warn!(error = %detail, "building the all-repos issue report failed");
|
||||
Err(StatusUnavailable(detail))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every open issue in `{org}/{repo}` specifically — the filtered view once
|
||||
/// swarm-ui's repo dropdown (populated by [`get_repos`]) has a selection.
|
||||
/// See [`forge::Client::issue_report`] for how `blocked` /
|
||||
/// `depended_on_by_count` are computed and why this has to be a
|
||||
/// server-side pass rather than something swarm-ui resolves itself per row.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/repos/{org}/{repo}/issue-report",
|
||||
params(
|
||||
("org" = String, Path, description = "repo owner (user or org)"),
|
||||
("repo" = String, Path, description = "repo name"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "every open issue in this repo, blocked/dependent-count pre-resolved", body = Vec<forge::IssueReportRow>),
|
||||
(status = 503, description = "no forge is configured on this host, or the report could not be built", body = String),
|
||||
),
|
||||
tag = "repos"
|
||||
)]
|
||||
pub async fn get_issue_report(
|
||||
State(state): State<AppState>,
|
||||
Path((org, repo)): Path<(String, String)>,
|
||||
) -> Result<Json<Vec<forge::IssueReportRow>>, StatusUnavailable> {
|
||||
let Some(client) = state.forge.as_ref() else {
|
||||
return Err(StatusUnavailable(
|
||||
"no forge is configured on this host".to_owned(),
|
||||
));
|
||||
};
|
||||
match client.issue_report(&org, &repo).await {
|
||||
Ok(rows) => Ok(Json(rows)),
|
||||
Err(e) => {
|
||||
let detail = format!("{e:#}");
|
||||
tracing::warn!(error = %detail, %org, %repo, "building issue report failed");
|
||||
Err(StatusUnavailable(detail))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue