import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, ChevronDown, Download, FileCode2, LoaderCircle, Lock, ShieldCheck, } from "lucide-react"; import { api, apiUrl, type OwnerFindingSummary, } from "../../shared/api/client"; import { Button } from "../../components/Button"; import { ConfirmDialog } from "../../components/ConfirmDialog"; import { ReportBody, type StructuredReport } from "../../components/ReportBody"; import { SeverityChip } from "../../components/SeverityChip"; import type { ViewJob } from "./VersionBar"; /** * Owner 专属「Manage findings」:全部 findings(含未披露)+ 勾选披露 + 全量报告下载。 * 仅在后端鉴权通过(父组件 owner 查询成功)时渲染。 */ export function OwnerFindings({ projectId, currentFindings, viewJob, onViewJob, }: { projectId: string; /** 父级 access probe 已拉的当前版本 findings(兼作 current 的 initialData)。 */ currentFindings: OwnerFindingSummary[]; /** 版本查看状态(页面级,头部 VersionBar 控制)。 */ viewJob: ViewJob | null; onViewJob: (v: ViewJob | null) => void; }) { const qc = useQueryClient(); const setViewJob = onViewJob; const [selected, setSelected] = useState>(new Set()); const [openKey, setOpenKey] = useState(null); const [confirmOpen, setConfirmOpen] = useState(false); const [flash, setFlash] = useState(null); // 版本化查询:viewJob=null → 当前版本 const findingsQ = useQuery({ queryKey: ["owner-findings", projectId, viewJob?.id ?? "current"], queryFn: () => api.ownerFindings(projectId, viewJob?.id), initialData: viewJob === null ? { project_id: projectId, findings: currentFindings } : undefined, retry: false, }); const findings = findingsQ.data?.findings ?? currentFindings; const viewingHistorical = viewJob !== null; const disclosable = useMemo( () => findings.filter((f) => f.disclosure_state !== "disclosed"), [findings], ); const allChecked = disclosable.length > 0 && disclosable.every((f) => selected.has(f.id)); const toggle = (id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const toggleAll = () => { setSelected(allChecked ? new Set() : new Set(disclosable.map((f) => f.id))); }; const discloseM = useMutation({ mutationFn: (ids: string[]) => api.ownerDisclose(projectId, ids), onSuccess: (res) => { setConfirmOpen(false); setSelected(new Set()); setFlash( `${res.disclosed_count} finding${res.disclosed_count === 1 ? "" : "s"} disclosed — now publicly visible with full report content.`, ); void qc.invalidateQueries({ queryKey: ["owner-findings", projectId] }); void qc.invalidateQueries({ queryKey: ["public", "project"] }); void qc.invalidateQueries({ queryKey: ["public", "overview"] }); }, }); return (
{/* 工具行 */}

All findings ({findings.length})

Full report (.md)
{viewingHistorical && (
Viewing version {viewJob.label} — disclosure is only available on the current version.
)} {/* 披露操作条(历史版本只读,披露仅当前版本) */} {!viewingHistorical && disclosable.length > 0 && (
{selected.size > 0 ? `${selected.size} selected` : ""}
)} {flash && (

{flash}

)} {discloseM.isError && (

Disclosure failed. Please try again — if it persists, contact an operator.

)} {/* findings 卡片 */} {findings.length === 0 ? (
No findings on the current scan.
) : (
{findings.map((f) => { const disclosed = f.disclosure_state === "disclosed"; const open = openKey === f.finding_key; return (
setOpenKey(open ? null : f.finding_key)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpenKey(open ? null : f.finding_key); } }} aria-expanded={open} className="flex w-full cursor-pointer items-start gap-3 px-5 py-3 text-left transition-colors hover:bg-surface-sunken/50 focus-ring" > e.stopPropagation()} onChange={() => toggle(f.id)} aria-label={disclosed ? `${f.finding_key} already disclosed` : `Select ${f.finding_key}`} className="mt-1 h-4 w-4 shrink-0 rounded border-line accent-[#EDEFF4] disabled:opacity-40" />
{f.cwe && ( {f.cwe} )} {f.cvss_score != null && ( CVSS {f.cvss_score.toFixed(1)} )}

{f.title}

{f.finding_key}

{disclosed ? ( Disclosed ) : ( Owner only )}
{open && }
); })}
)} discloseM.mutate([...selected])} onCancel={() => setConfirmOpen(false)} />
); } /** 展开后按需取单条全文(report + artifacts)。 */ function OwnerFindingDetailBody({ projectId, findingKey, }: { projectId: string; findingKey: string; }) { const detailQ = useQuery({ queryKey: ["owner-finding", projectId, findingKey], queryFn: () => api.ownerFinding(projectId, findingKey), staleTime: 60_000, retry: false, }); if (detailQ.isPending) { return (
Loading full report…
); } if (detailQ.isError || !detailQ.data) { return (
Failed to load the full report.
); } const f = detailQ.data.finding; const arts = (f.artifacts ?? []).filter((a) => a.has_content || a.size_bytes > 0); return (
{arts.length > 0 && (

Artifacts ({arts.length})

    {arts.slice(0, 20).map((a) => (
  • {a.file_name} {a.kind} · {formatSize(a.size_bytes)}
  • ))} {arts.length > 20 && (
  • …and {arts.length - 20} more
  • )}
)}
); } function formatSize(n: number): string { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / 1024 / 1024).toFixed(1)} MB`; }