OpenVuln / src /components /RepoSubmitForm.tsx
zRzRzRzRzRzRzR
feat: sync latest OpenVuln frontend
d22337b
Raw
History Blame Contribute Delete
9.03 kB
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>
);
}