import { useCallback, useEffect, useState } from "react"; import { API } from "../config"; type PRScan = { id: number; pr_number: number; repo_name: string; pr_title: string; pr_url: string; source_branch: string; target_branch: string; author: string; state: "open" | "merged" | "closed"; scan_status: "pending" | "completed"; issues_count: number; security_issues: number; created_at: string; }; type PRDetail = PRScan & { scan_result: Record; scan_details_with_code: unknown[]; ai_review: unknown; report_path: string; }; type ChangedFile = { filename: string; status: string; additions: number; deletions: number; }; type FileContent = { content: string; issues: Array<{ line: number; severity: string; message: string; rule?: string; }>; }; type View = "dashboard" | "pr-list" | "settings"; type QualityGateProps = { view: View; }; const BASE = API.quality; async function apiGet(path: string): Promise { const res = await fetch(`${BASE}${path}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json(); } async function apiPost(path: string): Promise { const res = await fetch(`${BASE}${path}`, { method: "POST" }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json(); } export default function QualityGate({ view }: QualityGateProps) { const [prs, setPrs] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [filter, setFilter] = useState<"all" | "open" | "merged" | "closed">("all"); const [selectedPR, setSelectedPR] = useState(null); const [changedFiles, setChangedFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(""); const [fileContent, setFileContent] = useState(null); const [modalOpen, setModalOpen] = useState(false); const fetchPRs = useCallback(async () => { setLoading(true); setError(""); try { const query = filter === "all" ? "" : `?state=${filter}`; const data = await apiGet<{ prs: PRScan[] } | PRScan[]>(`/prs${query}`); setPrs(Array.isArray(data) ? data : data.prs || []); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, [filter]); useEffect(() => { fetchPRs(); }, [fetchPRs]); const openPRDetail = useCallback(async (prId: number) => { setLoading(true); setError(""); try { const [detail, files] = await Promise.all([ apiGet(`/prs/${prId}`), apiGet<{ files: ChangedFile[] } | ChangedFile[]>(`/prs/${prId}/files`), ]); setSelectedPR(detail); const fileList = Array.isArray(files) ? files : files.files || []; setChangedFiles(fileList); if (fileList.length > 0) { setSelectedFile(fileList[0].filename); } setModalOpen(true); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }, []); useEffect(() => { if (!modalOpen || !selectedPR || !selectedFile) return; setFileContent(null); apiGet(`/prs/${selectedPR.id}/file?path=${encodeURIComponent(selectedFile)}`) .then(setFileContent) .catch(() => setFileContent({ content: "// Failed to load file", issues: [] })); }, [modalOpen, selectedPR, selectedFile]); const handleMerge = async () => { if (!selectedPR) return; setLoading(true); try { await apiPost(`/prs/${selectedPR.id}/merge`); setModalOpen(false); fetchPRs(); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }; const handleClose = async () => { if (!selectedPR) return; setLoading(true); try { await apiPost(`/prs/${selectedPR.id}/close`); setModalOpen(false); fetchPRs(); } catch (e) { setError((e as Error).message); } finally { setLoading(false); } }; const stats = { pending: prs.filter((p) => p.state === "open").length, passed: prs.filter((p) => p.state === "merged").length, rejected: prs.filter((p) => p.state === "closed").length, totalIssues: prs.reduce((s, p) => s + p.issues_count, 0), }; return (
{error && (
{error}
)} {view === "dashboard" && ( <>

Overview

{[ { label: "Open PRs", value: stats.pending, color: "text-yellow-600", bg: "bg-yellow-50" }, { label: "Merged", value: stats.passed, color: "text-green-600", bg: "bg-green-50" }, { label: "Rejected", value: stats.rejected, color: "text-red-600", bg: "bg-red-50" }, { label: "Total Issues", value: stats.totalIssues, color: "text-magenta", bg: "bg-magenta-50" }, ].map((s) => (
{s.value}
{s.label}
))}

Recent Pull Requests

openPRDetail(pr.id)} /> )} {view === "pr-list" && ( <>

Pull Request List

{(["all", "open", "merged", "closed"] as const).map((f) => ( ))}
openPRDetail(pr.id)} /> )} {view === "settings" && (

Settings

Webhook Configuration

Add this URL to your Gitea repository webhook settings:

POST {window.location.origin}/quality-api/webhook/gitea

Quick Notes

  • Supports Gitea Push and Pull Request events
  • Runs Pylint, Flake8, ESLint, and Bandit automatically
  • Optional AI review using DeepSeek-V3
  • Scan summary can be pushed to Feishu channels
)} {modalOpen && selectedPR && (

#{selectedPR.pr_number} {selectedPR.pr_title}

{selectedPR.author} · {selectedPR.source_branch} → {selectedPR.target_branch} · {selectedPR.repo_name}

Changed Files

{changedFiles.map((f) => ( ))}
{fileContent ? (
                    {fileContent.content.split("\n").map((line, i) => {
                      const lineNum = i + 1;
                      const issues = fileContent.issues.filter((iss) => iss.line === lineNum);
                      return (
                        
0 ? "bg-red-50/80" : ""}`}> {lineNum} {line} {issues.length > 0 && ( ● {issues[0].message} )}
); })}
) : (
{selectedFile ? "Loading file..." : "Select a file to inspect"}
)}

Issue Panel

{fileContent?.issues.length === 0 && (

No issues

)} {fileContent?.issues.map((iss, i) => (
{iss.severity === "error" ? "❌" : iss.severity === "warning" ? "⚠️" : "ℹ️"} L{iss.line}

{iss.message}

{iss.rule &&

{iss.rule}

}
))}
)}
); } function PRTable({ prs, loading, onView, }: { prs: PRScan[]; loading: boolean; onView: (pr: PRScan) => void; }) { if (loading && prs.length === 0) { return
Loading...
; } if (prs.length === 0) { return
No PR records found
; } return (
{prs.map((pr) => ( ))}
PR# Title Repository Author Branch State Issues Created Action
#{pr.pr_number} {pr.pr_title} {pr.repo_name} {pr.author} {pr.source_branch} → {pr.target_branch} {pr.state === "open" ? "Open" : pr.state === "merged" ? "Merged" : "Closed"} 0 ? "text-red-600" : "text-green-600"}`}> {pr.issues_count} {new Date(pr.created_at).toLocaleString("en-US")}
); }