swarm-ui, swarm-controller: the review try-out's repo and PR fields offer the repos and PRs to pick
Two read-only routes feed them: GET /api/pulls/repos lists the repos with an open pull request, and GET /api/pulls?repo=owner/name lists a repo's open pull requests, highest number first. The page offers the lists through the browser's own suggestion list and keeps both fields free text, since a run also works on a merged or closed PR.
This commit is contained in:
parent
60ea7fdc04
commit
9a4059e130
10 changed files with 399 additions and 7 deletions
|
|
@ -6,6 +6,10 @@
|
|||
// `202 {id}`, and polls `GET .../try-out/{id}` until the run is `done` or
|
||||
// `failed`. A run takes a minute or more, so the page says it is running.
|
||||
//
|
||||
// The repo and PR fields offer the repos that have an open PR and the open PRs
|
||||
// of the repo (`GET /api/pulls/repos`, `GET /api/pulls?repo=…`), and stay free
|
||||
// text: the run also works on a merged or closed PR.
|
||||
//
|
||||
// **load the repo's rules** fills the rows from the rules on the repo's default
|
||||
// branch (`GET /api/review-classifier/rules`), to run as they are or to edit.
|
||||
//
|
||||
|
|
@ -31,6 +35,7 @@ import {
|
|||
repoProblem,
|
||||
toBody,
|
||||
type ClassificationDraft,
|
||||
type OpenPull,
|
||||
type RulesAnswer,
|
||||
type TryOutResult as Result,
|
||||
} from "./TryOutTypes.js";
|
||||
|
|
@ -38,6 +43,11 @@ import "./ReviewTryOutPage.css";
|
|||
|
||||
const TRY_OUT = "/api/review-classifier/try-out";
|
||||
const RULES = "/api/review-classifier/rules";
|
||||
const PULL_REPOS = "/api/pulls/repos";
|
||||
const PULLS = "/api/pulls";
|
||||
// How long the repo field rests before its PRs are fetched, so typing a repo
|
||||
// does not ask for the PRs of each prefix of it.
|
||||
const PICK_DELAY_MS = 300;
|
||||
const POLL_MS = 2000;
|
||||
// More classifications than this are not offered a row. It is the try-out
|
||||
// endpoint's own cap, which answers a longer list with a 400 that the page
|
||||
|
|
@ -77,8 +87,68 @@ export function ReviewTryOutPage() {
|
|||
);
|
||||
const [confirmLoad, setConfirmLoad] = useState(false);
|
||||
|
||||
// What the two fields offer to complete to. The fields stay free text, so a
|
||||
// repo or a PR the lists lack, a merged one say, can still be typed.
|
||||
const [repoChoices, setRepoChoices] = useState<string[]>([]);
|
||||
const [repoChoicesError, setRepoChoicesError] =
|
||||
useState<ProblemDetails | null>(null);
|
||||
const [pulls, setPulls] = useState<OpenPull[]>([]);
|
||||
const [pullsError, setPullsError] = useState<ProblemDetails | null>(null);
|
||||
|
||||
const running = starting || runId !== null;
|
||||
|
||||
// The repos with an open PR, read once when the page opens.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(PULL_REPOS);
|
||||
if (cancelled) return;
|
||||
if (!res.ok) {
|
||||
setRepoChoicesError(await readApiError(res));
|
||||
return;
|
||||
}
|
||||
setRepoChoices((await res.json()) as string[]);
|
||||
} catch (e: unknown) {
|
||||
if (!cancelled) setRepoChoicesError({ detail: String(e) });
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// The open PRs of the repo once it is typed or picked and has stopped
|
||||
// changing. A repo that is not valid yet has no list.
|
||||
useEffect(() => {
|
||||
setPulls([]);
|
||||
setPullsError(null);
|
||||
const target = repo.trim();
|
||||
if (repoProblem(target) !== null) return;
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${PULLS}?repo=${encodeURIComponent(target)}`,
|
||||
);
|
||||
if (cancelled) return;
|
||||
if (!res.ok) {
|
||||
setPullsError(await readApiError(res));
|
||||
return;
|
||||
}
|
||||
setPulls((await res.json()) as OpenPull[]);
|
||||
} catch (e: unknown) {
|
||||
if (!cancelled) setPullsError({ detail: String(e) });
|
||||
}
|
||||
})();
|
||||
}, PICK_DELAY_MS);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [repo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!running) return;
|
||||
setElapsed(0);
|
||||
|
|
@ -217,6 +287,7 @@ export function ReviewTryOutPage() {
|
|||
value={repo}
|
||||
placeholder="owner/name"
|
||||
onInput={setRepo}
|
||||
suggestions={repoChoices.map((value) => ({ value }))}
|
||||
/>
|
||||
<TextField
|
||||
id="tryout-pr"
|
||||
|
|
@ -224,6 +295,10 @@ export function ReviewTryOutPage() {
|
|||
value={pr}
|
||||
pattern="[0-9]+"
|
||||
onInput={setPr}
|
||||
suggestions={pulls.map((p) => ({
|
||||
value: String(p.number),
|
||||
label: p.title,
|
||||
}))}
|
||||
/>
|
||||
<Button
|
||||
onClick={askLoadRules}
|
||||
|
|
@ -232,6 +307,18 @@ export function ReviewTryOutPage() {
|
|||
{loadingRules ? "loading…" : "load the repo's rules"}
|
||||
</Button>
|
||||
</div>
|
||||
{repoChoicesError ? (
|
||||
<ApiErrorPanel
|
||||
context="the repos with an open PR could not be listed"
|
||||
problem={repoChoicesError}
|
||||
/>
|
||||
) : null}
|
||||
{pullsError ? (
|
||||
<ApiErrorPanel
|
||||
context="the repo's open PRs could not be listed"
|
||||
problem={pullsError}
|
||||
/>
|
||||
) : null}
|
||||
{rulesError ? (
|
||||
<ApiErrorPanel
|
||||
context="the repo's rules could not be loaded"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ export interface RulesAnswer {
|
|||
classifications: ClassificationBody[];
|
||||
}
|
||||
|
||||
/** An open PR, as `GET /api/pulls?repo=…` lists it. */
|
||||
export interface OpenPull {
|
||||
number: number;
|
||||
title: string;
|
||||
head_sha?: string;
|
||||
}
|
||||
|
||||
export type Verdict = "match" | "no-match" | "not-applicable";
|
||||
|
||||
export interface TryOutRow {
|
||||
|
|
|
|||
Loading…
Reference in a new issue