Spaces:
Running
Running
| import { useState } from "react"; | |
| import { useNavigate } from "react-router-dom"; | |
| import { ArrowUp, CircleAlert, Github, LoaderCircle } from "lucide-react"; | |
| import { ApiError, api, navigateToLogin } from "../shared/api/client"; | |
| import { useMe } from "../features/auth/useAuth"; | |
| import { Button } from "./Button"; | |
| function mapError(err: unknown): string { | |
| if (!(err instanceof ApiError)) { | |
| return "Something went wrong. Please try again."; | |
| } | |
| const reason = String(err.context?.reason ?? ""); | |
| const message = String(err.context?.message ?? ""); | |
| if (err.status === 401) { | |
| return "Please sign in with GitHub to submit a repository."; | |
| } | |
| if (err.status === 403) { | |
| return ( | |
| message || | |
| "Only accounts with admin or maintain permission on this repository can submit it." | |
| ); | |
| } | |
| if (reason === "ref_not_found" || reason === "invalid_ref") { | |
| return "That branch/tag/commit was not found in this repository."; | |
| } | |
| if (reason === "invalid_github_url") { | |
| return "That doesn't look like a GitHub repository URL. Expected: https://github.com/owner/repo"; | |
| } | |
| if (reason === "private_repo") { | |
| return "This repository is private or does not exist. OpenVuln scans public projects only."; | |
| } | |
| if (reason === "cooldown") { | |
| const days = err.context?.retry_after_days; | |
| return `This project was scanned recently. You can resubmit after ${days ?? "a few"} day(s).`; | |
| } | |
| if (reason === "duplicate" || err.status === 409) { | |
| return message || "This project is already on OpenVuln."; | |
| } | |
| if (err.status === 404) { | |
| return "This repository is private or does not exist. OpenVuln scans public projects only."; | |
| } | |
| if (err.status >= 500) { | |
| return "OpenVuln is temporarily unavailable. Please try again in a moment."; | |
| } | |
| return message || err.message || "Submission failed."; | |
| } | |
| export function RepoSubmitForm({ | |
| size = "default", | |
| appearance = "default", | |
| className = "", | |
| align = "center", | |
| }: { | |
| size?: "default" | "hero"; | |
| appearance?: "default" | "dark"; | |
| className?: string; | |
| align?: "center" | "left"; | |
| }) { | |
| const nav = useNavigate(); | |
| const meQ = useMe(); | |
| const [url, setUrl] = useState(""); | |
| const [ref, setRef] = useState(""); | |
| const [showRef, setShowRef] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [pending, setPending] = useState(false); | |
| const hero = size === "hero"; | |
| const dark = appearance === "dark"; | |
| const onSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setError(null); | |
| const git_url = url.trim(); | |
| if (!git_url) { | |
| setError("Please paste a GitHub repository URL."); | |
| return; | |
| } | |
| // 未登录 → 整页跳 GitHub OAuth,登录后回到部署首页 | |
| if (meQ.data && !meQ.data.authenticated) { | |
| navigateToLogin(); | |
| window.dispatchEvent(new Event("ov-oauth-popup-opened")); | |
| return; | |
| } | |
| setPending(true); | |
| try { | |
| const res = await api.submitProject({ git_url, ...(ref.trim() ? { ref: ref.trim() } : {}) }); | |
| nav(`/p/${res.project.owner_login}/${res.project.name}`, { | |
| state: { justSubmitted: true }, | |
| }); | |
| } catch (err) { | |
| setError(mapError(err)); | |
| } finally { | |
| setPending(false); | |
| } | |
| }; | |
| if (dark) { | |
| // hero composer(暗色控制台);appearance prop 名保留为调用方兼容 | |
| const centered = align === "center"; | |
| return ( | |
| <form | |
| onSubmit={(e) => void onSubmit(e)} | |
| className={className} | |
| aria-label="Submit a GitHub repository" | |
| aria-busy={pending} | |
| > | |
| <div | |
| className={`openvuln-composer group flex min-h-[58px] items-center gap-3 rounded-xl border bg-surface-raised p-2 pl-4 shadow-[0_20px_48px_-20px_rgba(0,0,0,0.65)] transition focus-within:bg-surface-sunken/60 ${ | |
| error ? "border-danger/60" : "border-line focus-within:border-line-strong" | |
| }`} | |
| > | |
| <Github | |
| size={18} | |
| className="shrink-0 text-ink-tertiary transition group-focus-within:text-ink-secondary" | |
| /> | |
| <input | |
| type="url" | |
| value={url} | |
| onChange={(e) => { | |
| setUrl(e.target.value); | |
| setError(null); | |
| }} | |
| placeholder="Paste a public GitHub repository URL" | |
| spellCheck={false} | |
| className="min-w-0 flex-1 bg-transparent py-3 font-mono text-[13px] text-ink outline-none placeholder:text-ink-tertiary sm:text-[14px]" | |
| aria-label="GitHub repository URL" | |
| aria-invalid={!!error} | |
| /> | |
| <button | |
| type="submit" | |
| aria-label={pending ? "Submitting repository" : "Analyze repository"} | |
| className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink text-surface shadow-sm transition hover:bg-white active:scale-95 focus-ring disabled:cursor-not-allowed disabled:bg-surface-sunken disabled:text-ink-tertiary disabled:opacity-100" | |
| disabled={!url.trim() || pending} | |
| > | |
| {pending ? ( | |
| <LoaderCircle size={19} className="animate-spin motion-reduce:animate-none" /> | |
| ) : ( | |
| <ArrowUp size={19} strokeWidth={2.2} /> | |
| )} | |
| </button> | |
| </div> | |
| <div className={`mt-2.5 flex ${centered ? "justify-center" : "justify-start"}`}> | |
| {showRef ? ( | |
| <input | |
| type="text" | |
| value={ref} | |
| onChange={(e) => setRef(e.target.value)} | |
| placeholder="Branch, tag, or commit SHA (optional)" | |
| spellCheck={false} | |
| className="h-8 w-72 rounded-lg border border-line bg-surface-raised px-3 font-mono text-xs text-ink outline-none placeholder:text-ink-tertiary focus:border-line-strong" | |
| aria-label="Version to scan (branch, tag, or commit SHA)" | |
| /> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={() => setShowRef(true)} | |
| className="font-mono text-[11px] text-ink-tertiary underline decoration-line underline-offset-2 transition hover:text-ink-secondary" | |
| > | |
| Scan a specific version | |
| </button> | |
| )} | |
| </div> | |
| <div className={`mt-2.5 flex min-h-5 items-start gap-1.5 text-xs text-ink-tertiary ${centered ? "justify-center text-center" : "justify-start text-left"}`}> | |
| {error ? ( | |
| <span className="inline-flex items-start gap-1.5 text-danger" role="alert"> | |
| <CircleAlert size={14} className="mt-px shrink-0" /> | |
| <span>{error}</span> | |
| </span> | |
| ) : pending ? ( | |
| <span className="text-ink-secondary" role="status">Submitting repository…</span> | |
| ) : ( | |
| <span className="inline-flex items-center gap-1.5"> | |
| <CircleAlert size={13} strokeWidth={1.8} className="shrink-0" aria-hidden /> | |
| <span>Public repositories · Repository maintainers only</span> | |
| </span> | |
| )} | |
| </div> | |
| </form> | |
| ); | |
| } | |
| return ( | |
| <form onSubmit={(e) => void onSubmit(e)} className={className}> | |
| <div className="flex flex-col gap-2 sm:flex-row"> | |
| <input | |
| type="text" | |
| value={url} | |
| onChange={(e) => setUrl(e.target.value)} | |
| placeholder="https://github.com/owner/repo" | |
| spellCheck={false} | |
| className={`h-12 flex-1 rounded-lg border bg-surface-raised px-3.5 font-mono text-sm text-ink placeholder:text-ink-tertiary focus-ring ${ | |
| error ? "border-danger" : "border-line" | |
| }`} | |
| aria-invalid={!!error} | |
| /> | |
| <Button type="submit" size="lg" disabled={pending} className="rounded-lg shrink-0"> | |
| {pending ? "Submitting…" : "Submit"} | |
| </Button> | |
| </div> | |
| <div className="mt-2"> | |
| {showRef ? ( | |
| <input | |
| type="text" | |
| value={ref} | |
| onChange={(e) => setRef(e.target.value)} | |
| placeholder="Branch, tag, or commit SHA (optional — default: default branch HEAD)" | |
| spellCheck={false} | |
| className="h-9 w-full rounded-md border border-line bg-surface-raised px-3 font-mono text-xs text-ink placeholder:text-ink-tertiary focus-ring" | |
| aria-label="Version to scan (branch, tag, or commit SHA)" | |
| /> | |
| ) : ( | |
| <button | |
| type="button" | |
| onClick={() => setShowRef(true)} | |
| className="text-[12px] text-ink-tertiary underline decoration-line underline-offset-2 transition-colors hover:text-ink-secondary" | |
| > | |
| Scan a specific version | |
| </button> | |
| )} | |
| </div> | |
| {error && ( | |
| <p | |
| className={`mt-2 flex items-start gap-1.5 text-sm text-danger ${hero ? "justify-center text-left" : ""}`} | |
| role="alert" | |
| > | |
| <CircleAlert size={16} className="mt-0.5 shrink-0" /> | |
| <span>{error}</span> | |
| </p> | |
| )} | |
| </form> | |
| ); | |
| } | |