From 6bdd7889a61496421400c5efc0c90dadb04d41e0 Mon Sep 17 00:00:00 2001 From: Subin George Date: Fri, 22 May 2026 22:47:11 +0530 Subject: [PATCH 1/2] Add OOT HUD frontend components: summary, per-backend dashboard, PR section Add the frontend pages and components for the OOT HUD: - OOT Summary page (pages/oot/index.tsx) showing all registered backends with latest status, PR count, and success rates - Per-backend dashboard (pages/oot/[org]/[repo].tsx) with detailed job history per downstream repo - OotPrSection component (components/oot/OotPrSection.tsx) showing OOT CI results inline on pytorch/pytorch PR pages All components consume the ClickHouse queries and utility library added in Part 1. This is Part 2 of the OOT HUD pipeline split from #8069. --- torchci/components/oot/OotPrSection.tsx | 145 ++++++++++++ torchci/pages/oot/[org]/[repo].tsx | 290 ++++++++++++++++++++++++ torchci/pages/oot/index.tsx | 183 +++++++++++++++ 3 files changed, 618 insertions(+) create mode 100644 torchci/components/oot/OotPrSection.tsx create mode 100644 torchci/pages/oot/[org]/[repo].tsx create mode 100644 torchci/pages/oot/index.tsx diff --git a/torchci/components/oot/OotPrSection.tsx b/torchci/components/oot/OotPrSection.tsx new file mode 100644 index 0000000000..0829394c7c --- /dev/null +++ b/torchci/components/oot/OotPrSection.tsx @@ -0,0 +1,145 @@ +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Chip, + Link, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { durationDisplay } from "components/common/TimeUtils"; +import { fetcher } from "lib/GeneralUtils"; +import { conclusionColor, conclusionLabel } from "lib/oot/ootUtils"; +import useSWR from "swr"; + +interface OotPrResult { + downstream_repo: string; + workflow_name: string; + job_name: string; + check_run_id: string; + run_id: string; + run_attempt: number; + status: string; + conclusion: string; + duration_seconds: number; + workflow_run_url: string; + artifact_url: string; + started_at: string; + queue_time: number | null; + execution_time: number | null; +} + +export default function OotPrSection({ prNumber }: { prNumber: number }) { + const url = `/api/clickhouse/oot_pr_results?parameters=${encodeURIComponent( + JSON.stringify({ pr: String(prNumber) }) + )}`; + const { data, error } = useSWR(url, fetcher, { + refreshInterval: 60_000, + }); + + if (error || !data || data.length === 0) return null; + + const successCount = data.filter( + (r) => r.status === "completed" && r.conclusion === "success" + ).length; + const totalCompleted = data.filter((r) => r.status === "completed").length; + const inProgress = data.filter((r) => r.status === "in_progress").length; + + const summaryText = [ + totalCompleted > 0 ? `${successCount}/${totalCompleted} passed` : null, + inProgress > 0 ? `${inProgress} running` : null, + ] + .filter(Boolean) + .join(", "); + + return ( + + }> + + + Out-of-Tree Backends + + + ({summaryText}) + + + + + + + + + + Backend + + + Job + + + Status + + + Duration + + + Links + + + + + {data.map((row, i) => ( + + {row.downstream_repo} + {row.job_name} + + + + + {row.duration_seconds + ? durationDisplay(Math.round(row.duration_seconds)) + : "–"} + + + + {row.workflow_run_url && ( + + Run + + )} + {row.artifact_url && ( + + Artifacts + + )} + + + + ))} + +
+
+
+
+ ); +} diff --git a/torchci/pages/oot/[org]/[repo].tsx b/torchci/pages/oot/[org]/[repo].tsx new file mode 100644 index 0000000000..5578f3548d --- /dev/null +++ b/torchci/pages/oot/[org]/[repo].tsx @@ -0,0 +1,290 @@ +import { + Box, + Chip, + FormControl, + InputLabel, + Link, + MenuItem, + Paper, + Select, + SelectChangeEvent, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tooltip, + Typography, +} from "@mui/material"; +import { durationDisplay } from "components/common/TimeUtils"; +import { fetcher } from "lib/GeneralUtils"; +import { conclusionColor, conclusionLabel } from "lib/oot/ootUtils"; +import Head from "next/head"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; +import { useMemo, useState } from "react"; +import useSWR from "swr"; + +interface OotJobRow { + pr_number: number; + pytorch_head_sha: string; + workflow_name: string; + job_name: string; + check_run_id: string; + run_id: string; + run_attempt: number; + status: string; + conclusion: string; + started_at: string; + completed_at: string; + duration_seconds: number; + total_tests: number; + passed_tests: number; + failed_tests: number; + skipped_tests: number; + workflow_run_url: string; + artifact_url: string; + queue_time: number | null; + execution_time: number | null; +} + +function JobChip({ job }: { job: OotJobRow }) { + const color = conclusionColor(job.status, job.conclusion); + const label = conclusionLabel(job.status, job.conclusion); + const tooltipContent = [ + `Job: ${job.job_name}`, + job.run_attempt > 1 ? `Attempt: ${job.run_attempt}` : null, + `Duration: ${ + job.duration_seconds + ? durationDisplay(Math.round(job.duration_seconds)) + : "–" + }`, + job.total_tests + ? `Tests: ${job.passed_tests}/${job.total_tests} passed` + : null, + job.queue_time != null ? `Queue: ${job.queue_time.toFixed(1)}s` : null, + ] + .filter(Boolean) + .join("\n"); + + return ( + {tooltipContent}} + > + + + ); +} + +interface MatrixRow { + prNumber: number; + sha: string; + jobs: Map; +} + +function buildMatrix(data: OotJobRow[]): { + jobNames: string[]; + rows: MatrixRow[]; +} { + const jobNamesSet = new Set(); + const prMap = new Map(); + + for (const job of data) { + jobNamesSet.add(job.job_name); + let row = prMap.get(job.pr_number); + if (!row) { + row = { + prNumber: job.pr_number, + sha: job.pytorch_head_sha, + jobs: new Map(), + }; + prMap.set(job.pr_number, row); + } + // Keep the latest attempt per job_name (highest run_attempt wins) + const existing = row.jobs.get(job.job_name); + if (!existing || job.run_attempt > existing.run_attempt) { + row.jobs.set(job.job_name, job); + } + } + + const jobNames = Array.from(jobNamesSet).sort(); + const rows = Array.from(prMap.values()).sort( + (a, b) => b.prNumber - a.prNumber + ); + return { jobNames, rows }; +} + +function HealthSummary({ data }: { data: OotJobRow[] }) { + const completed = data.filter((j) => j.status === "completed"); + const total = completed.length; + const success = completed.filter((j) => j.conclusion === "success").length; + const rate = total > 0 ? success / total : 0; + + return ( + + = 0.95 ? "success" : rate >= 0.8 ? "warning" : "error"} + /> + + {success}/{total} jobs passed + + + ); +} + +function OotMatrix({ + repoFullName, + days, +}: { + repoFullName: string; + days: number; +}) { + const url = `/api/clickhouse/oot_backend_dashboard?parameters=${encodeURIComponent( + JSON.stringify({ repo: repoFullName, days: String(days) }) + )}`; + const { data, error } = useSWR(url, fetcher, { + refreshInterval: 60_000, + }); + + const matrix = useMemo(() => (data ? buildMatrix(data) : null), [data]); + + if (error) { + return ( + + Failed to load dashboard: {error.message} + + ); + } + if (!data || !matrix) { + return ; + } + if (data.length === 0) { + return ( + + No results for {repoFullName} in the last {days} days. + + ); + } + + return ( + <> + + + + + + + PR + + + SHA + + {matrix.jobNames.map((name) => ( + + {name} + + ))} + + + + {matrix.rows.map((row) => ( + + + + + #{row.prNumber} + + + + + + {row.sha.slice(0, 7)} + + + {matrix.jobNames.map((name) => { + const job = row.jobs.get(name); + return ( + + {job ? : "–"} + + ); + })} + + ))} + +
+
+ + ); +} + +export default function OotBackendPage() { + const router = useRouter(); + const { org, repo } = router.query; + const [days, setDays] = useState(7); + + if (!org || !repo) return null; + + const repoFullName = `${org}/${repo}`; + + return ( + <> + + {repoFullName} — OOT CI | PyTorch HUD + + + + + {repoFullName} + + + ← Back to OOT Summary + + + + + Time Range + + + + + + Rows = PyTorch PRs, columns = downstream CI jobs. Click a chip to open + the workflow run. + + + + + + ); +} diff --git a/torchci/pages/oot/index.tsx b/torchci/pages/oot/index.tsx new file mode 100644 index 0000000000..bf9e83b4aa --- /dev/null +++ b/torchci/pages/oot/index.tsx @@ -0,0 +1,183 @@ +import { + Box, + Chip, + FormControl, + InputLabel, + Link, + MenuItem, + Paper, + Select, + SelectChangeEvent, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { durationDisplay } from "components/common/TimeUtils"; +import { fetcher } from "lib/GeneralUtils"; +import Head from "next/head"; +import NextLink from "next/link"; +import { useState } from "react"; +import useSWR from "swr"; + +interface OotSummaryRow { + repo: string; + downstream_repo_level: string; + successes: number; + failures: number; + total: number; + pass_rate: number; + avg_duration_s: number; + last_run: string; +} + +function PassRateChip({ rate }: { rate: number }) { + const pct = (rate * 100).toFixed(1) + "%"; + if (rate >= 0.95) return ; + if (rate >= 0.8) return ; + return ; +} + +function OotSummaryTable({ days }: { days: number }) { + const url = `/api/clickhouse/oot_summary?parameters=${encodeURIComponent( + JSON.stringify({ days: String(days) }) + )}`; + const { data, error } = useSWR(url, fetcher, { + refreshInterval: 60_000, + }); + + if (error) { + return ( + + Failed to load OOT summary: {error.message} + + ); + } + if (!data) { + return ; + } + if (data.length === 0) { + return ( + + No OOT CI results in the last {days} days. + + ); + } + + return ( + + + + + + Backend Repository + + + Level + + + Pass Rate + + + Success + + + Failures + + + Total + + + Avg Duration + + + Last Run + + + + + {data.map((row) => { + const parts = row.repo?.split("/") ?? []; + if (parts.length !== 2) return null; + const [org, repo] = parts; + return ( + + + + {row.repo} + + + + + + + + + {row.successes} + {row.failures} + {row.total} + + {durationDisplay(Math.round(row.avg_duration_s))} + + + {new Date(row.last_run).toLocaleString()} + + + ); + })} + +
+
+ ); +} + +export default function OotSummaryPage() { + const [days, setDays] = useState(7); + + return ( + <> + + Out-of-Tree CI Summary | PyTorch HUD + + + + Out-of-Tree CI Summary + + Time Range + + + + + + Cross-repo CI health overview. Repos sorted by pass rate (worst + first). Click a row to see the per-backend dashboard. + + + + + + ); +} From 6c37a8d153b0acf5139769b6f6e161e1a7ade4c6 Mon Sep 17 00:00:00 2001 From: Subin George Date: Sun, 24 May 2026 11:46:29 +0530 Subject: [PATCH 2/2] Add missing upstreamRepo to MatrixRow and OotJobRow interfaces The ClickHouse query returns upstream_repo but the TypeScript interfaces were missing it, causing a tsc build failure. --- torchci/pages/oot/[org]/[repo].tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/torchci/pages/oot/[org]/[repo].tsx b/torchci/pages/oot/[org]/[repo].tsx index 5578f3548d..caffcdd181 100644 --- a/torchci/pages/oot/[org]/[repo].tsx +++ b/torchci/pages/oot/[org]/[repo].tsx @@ -29,6 +29,7 @@ import { useMemo, useState } from "react"; import useSWR from "swr"; interface OotJobRow { + upstream_repo: string; pr_number: number; pytorch_head_sha: string; workflow_name: string; @@ -92,6 +93,7 @@ function JobChip({ job }: { job: OotJobRow }) { interface MatrixRow { prNumber: number; sha: string; + upstreamRepo: string; jobs: Map; } @@ -109,6 +111,7 @@ function buildMatrix(data: OotJobRow[]): { row = { prNumber: job.pr_number, sha: job.pytorch_head_sha, + upstreamRepo: job.upstream_repo ?? "pytorch/pytorch", jobs: new Map(), }; prMap.set(job.pr_number, row);