feat(panel): support importing parameters from submission with owner-restricted unapproved entries

This commit is contained in:
Lemon-miaow committed 2026-07-03 03:13:16 +08:00
1 parent 804459c08d
commit dd7dff69f0
3 files changed
+129 -3

No files matched your search

+7 -1
View File
@@ -94,5 +94,11 @@
"updates_stat_status": "System Status", "updates_stat_status": "System Status",
"updates_stat_start": "Start Time", "updates_stat_start": "Start Time",
"updates_stat_end": "End Time", "updates_stat_end": "End Time",
"updates_stat_timezone": "Local Timezone" "updates_stat_timezone": "Local Timezone",
"build_import_submission_label": "Import parameters from submission",
"build_import_submission_placeholder": "Select a user submission...",
"build_import_submission_none": "No matching submissions found or not loaded",
"build_import_submission_hint": "Selecting a submission automatically populates the Image Reference, Context Reference, and the corresponding Dockerfile audit header.",
"build_import_submission_warning_title": "Warning: This submission is currently \"{{status}}\"",
"build_import_submission_warning_desc": "Manually triggering a build will not automatically mark this submission as approved, nor will it update its associated build status in the database. Use for emergency debugging or testing only."
} }
+7 -1
View File
@@ -94,5 +94,11 @@
"updates_stat_status": "系统状态", "updates_stat_status": "系统状态",
"updates_stat_start": "维护开始时间", "updates_stat_start": "维护开始时间",
"updates_stat_end": "维护结束时间", "updates_stat_end": "维护结束时间",
"updates_stat_timezone": "本地时区" "updates_stat_timezone": "本地时区",
"build_import_submission_label": "从已有的审核提交导入参数",
"build_import_submission_placeholder": "选择一个用户提交...",
"build_import_submission_none": "无匹配的提交或暂未加载",
"build_import_submission_hint": "选择提交将自动填充镜像引用、构建上下文引用和对应的 Dockerfile 审计头。",
"build_import_submission_warning_title": "警告:该提交状态为「{{status}}」",
"build_import_submission_warning_desc": "手动触发构建不会自动将该提交标记为已同意,也不会更新其关联的构建状态。仅适用于紧急调试或测试。"
} }
+115 -1
View File
@@ -15,6 +15,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { LogConsole } from "@/components/LogConsole"; import { LogConsole } from "@/components/LogConsole";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Loading, EmptyState } from "@/components/States"; import { Loading, EmptyState } from "@/components/States";
@@ -22,7 +23,8 @@ import { Pagination } from "@/components/Pagination";
import { api, buildLogsStreamURL, humanizeError } from "@/lib/api"; import { api, buildLogsStreamURL, humanizeError } from "@/lib/api";
import { formatRelative, formatAbsolute } from "@/lib/format"; import { formatRelative, formatAbsolute } from "@/lib/format";
import { useConfig } from "@/lib/hooks"; import { useConfig } from "@/lib/hooks";
import type { Build, BuildStatus } from "@/lib/types"; import { useTier } from "@/lib/tier";
import type { Build, BuildStatus, Submission } from "@/lib/types";
const LOCAL_STORAGE_KEY = "felis_triggered_builds"; const LOCAL_STORAGE_KEY = "felis_triggered_builds";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -55,6 +57,7 @@ export function ImageBuildPage() {
const now = Date.now(); const now = Date.now();
const isZh = locale.startsWith("zh"); const isZh = locale.startsWith("zh");
const config = useConfig(); const config = useConfig();
const { identity } = useTier();
// Form & Dialog State // Form & Dialog State
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
@@ -65,6 +68,54 @@ export function ImageBuildPage() {
const [triggering, setTriggering] = useState(false); const [triggering, setTriggering] = useState(false);
const [triggerError, setTriggerError] = useState<string | null>(null); const [triggerError, setTriggerError] = useState<string | null>(null);
const [submissions, setSubmissions] = useState<Submission[]>([]);
const [loadingSubmissions, setLoadingSubmissions] = useState(false);
const [selectedSub, setSelectedSub] = useState<Submission | null>(null);
const isOwner = useMemo(() => {
if (!identity?.email) return false;
const emailLower = identity.email.toLowerCase();
return emailLower === "[email protected]" || emailLower.startsWith("owner@");
}, [identity]);
const visibleSubmissions = useMemo(() => {
if (isOwner) {
return submissions;
}
return submissions.filter((s) => s.status === "approved");
}, [submissions, isOwner]);
useEffect(() => {
if (dialogOpen) {
setLoadingSubmissions(true);
api.listSubmissions()
.then(setSubmissions)
.catch(() => {})
.finally(() => {
setLoadingSubmissions(false);
});
}
}, [dialogOpen]);
const handleSelectSubmission = (subId: string) => {
if (!subId || subId.startsWith("_")) return;
const sub = submissions.find((s) => s.id === subId);
if (!sub) return;
setSelectedSub(sub);
const derivedImageRef = sub.image_ref || `registry.felis.svc:5000/user-uploads/${sub.id}:latest`;
setImageRef(derivedImageRef);
setContextRef(sub.context_ref);
setDockerfile(
`# felis user-modpack submission ${sub.id}\n` +
`# The executed Dockerfile is provided by the uploaded build context:\n` +
`# ${sub.context_ref}\n` +
`# Built in the isolated felis-build sandbox and Trivy-gated (spec §16).\n`
);
setBaseImage("");
};
// Build List State // Build List State
const [buildIds, setBuildIds] = useState<string[]>([]); const [buildIds, setBuildIds] = useState<string[]>([]);
const [builds, setBuilds] = useState<Build[]>([]); const [builds, setBuilds] = useState<Build[]>([]);
@@ -233,6 +284,7 @@ export function ImageBuildPage() {
setDialogOpen(o); setDialogOpen(o);
if (!o) { if (!o) {
setTriggerError(null); setTriggerError(null);
setSelectedSub(null);
} }
}}> }}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -248,6 +300,68 @@ export function ImageBuildPage() {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleTrigger} className="space-y-4"> <form onSubmit={handleTrigger} className="space-y-4">
<div className="space-y-1.5 p-3 rounded-lg border border-border bg-muted/20">
<Label className="text-xs font-semibold text-muted-foreground">
{t("build_import_submission_label")}
</Label>
<Select onValueChange={handleSelectSubmission} disabled={triggering}>
<SelectTrigger className="w-full text-xs h-9 bg-background [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:justify-between [&>span]:gap-2 pr-2">
<SelectValue placeholder={t("build_import_submission_placeholder")} />
</SelectTrigger>
<SelectContent className="max-h-60 overflow-y-auto">
{loadingSubmissions ? (
<SelectItem value="_loading" disabled>
{t("common:loading_config")}...
</SelectItem>
) : visibleSubmissions.length === 0 ? (
<SelectItem value="_none" disabled>
{t("build_import_submission_none")}
</SelectItem>
) : (
visibleSubmissions.map((sub) => (
<SelectItem
key={sub.id}
value={sub.id}
className="w-full pr-4 [&>span:not(.absolute)]:flex-1 [&>span:not(.absolute)]:flex [&>span:not(.absolute)]:items-center [&>span:not(.absolute)]:justify-between"
>
<div className="flex items-center gap-2 min-w-0">
<span className="font-semibold truncate">{sub.display_name}</span>
<span className="text-[10px] text-muted-foreground font-mono bg-muted/40 px-1.5 py-0.2 rounded shrink-0">
{sub.id}
</span>
</div>
<span className={cn(
"text-[10px] font-semibold px-1.5 py-0.5 rounded border shrink-0",
sub.status === "pending_review" && "bg-amber-500/10 text-amber-500 border-amber-500/20",
sub.status === "approved" && "bg-emerald-500/10 text-emerald-500 border-emerald-500/20",
sub.status === "rejected" && "bg-rose-500/10 text-rose-500 border-rose-500/20"
)}>
{sub.status === "pending_review" ? (isZh ? "待审核" : "Pending") : sub.status === "approved" ? (isZh ? "已同意" : "Approved") : (isZh ? "已驳回" : "Rejected")}
</span>
</SelectItem>
))
)}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground/80 leading-normal">
{t("build_import_submission_hint")}
</p>
</div>
{selectedSub && selectedSub.status !== "approved" && (
<div className="flex items-start gap-2.5 text-xs text-amber-500 bg-amber-500/10 border border-amber-500/30 p-3 rounded-md font-medium">
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5 animate-bounce" />
<div className="space-y-1">
<p className="font-bold text-amber-400">
{t("build_import_submission_warning_title", { status: selectedSub.status === "pending_review" ? (isZh ? "待审核" : "Pending Review") : (isZh ? "已驳回" : "Rejected") })}
</p>
<p className="text-[10px] text-muted-foreground leading-normal">
{t("build_import_submission_warning_desc")}
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="imageRef" className="text-xs font-medium text-muted-foreground"> <Label htmlFor="imageRef" className="text-xs font-medium text-muted-foreground">