import { useEffect, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Ban, ChevronDown, CircleCheck, LoaderCircle } from "lucide-react"; import { api, type ScanJobSummary } from "../../shared/api/client"; import { ConfirmDialog } from "../../components/ConfirmDialog"; import { formatRelativeTime, shortSha } from "../../shared/lib/format"; export interface ViewJob { id: string; label: string; } /** * 项目头部版本条(owner 专属,fish No.1253):版本切换从 tab 内容区上移到标题区。 * 紧凑单行:当前版本下拉(切换查看历史版本)+ 进行中状态(可取消)+ Rescan。 */ export function VersionBar({ projectId, viewJob, onViewJob, }: { projectId: string; viewJob: ViewJob | null; onViewJob: (v: ViewJob | null) => void; }) { const qc = useQueryClient(); const [open, setOpen] = useState(false); const [cancelTarget, setCancelTarget] = useState(null); const rootRef = useRef(null); const scansQ = useQuery({ queryKey: ["project-scans", projectId], queryFn: () => api.projectScans(projectId), refetchInterval: (q) => (q.state.data?.scans ?? []).some((s) => ["queued", "dispatching", "scanning"].includes(s.state)) ? 5000 : false, retry: false, }); useEffect(() => { if (!open) return; const onDoc = (e: MouseEvent) => { if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); }; }, [open]); const invalidateAll = () => { void qc.invalidateQueries({ queryKey: ["project-scans", projectId] }); void qc.invalidateQueries({ queryKey: ["public", "project"] }); void qc.invalidateQueries({ queryKey: ["owner-findings", projectId] }); }; const cancelM = useMutation({ mutationFn: (jobId: string) => api.cancelScanJob(projectId, jobId), onSettled: () => { setCancelTarget(null); invalidateAll(); }, }); const scans = scansQ.data?.scans ?? []; const inflight = scans.find((s) => ["queued", "dispatching", "scanning"].includes(s.state)); const completed = scans.filter((s) => s.state === "completed"); const current = completed[0] ?? null; // 列表按创建时间倒序 const viewing = viewJob ?? (current ? { id: current.id, label: current.commit_sha?.slice(0, 7) ?? "current" } : null); return (
{/* 版本切换下拉 */}
{open && (
    {completed.map((s) => { const isCurrent = current?.id === s.id; const isSelected = viewing?.id === s.id; return (
  • ); })} {completed.length === 0 && (
  • No completed scans yet
  • )}
)}
{/* 进行中状态 + Cancel */} {inflight && ( {inflight.state} {inflight.state === "scanning" && ( {inflight.findings_so_far} so far )} {(inflight.state === "queued" || inflight.state === "scanning") && ( )} )} cancelTarget && cancelM.mutate(cancelTarget.id)} onCancel={() => setCancelTarget(null)} />
); }