feat(panel): support importing parameters from submission with owner-restricted unapproved entries
This commit is contained in:
3 files changed
+129
-3
No files matched your search
@@ -94,5 +94,11 @@
|
||||
"updates_stat_status": "System Status",
|
||||
"updates_stat_start": "Start 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."
|
||||
}
|
||||
@@ -94,5 +94,11 @@
|
||||
"updates_stat_status": "系统状态",
|
||||
"updates_stat_start": "维护开始时间",
|
||||
"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": "手动触发构建不会自动将该提交标记为已同意,也不会更新其关联的构建状态。仅适用于紧急调试或测试。"
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { LogConsole } from "@/components/LogConsole";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Loading, EmptyState } from "@/components/States";
|
||||
@@ -22,7 +23,8 @@ import { Pagination } from "@/components/Pagination";
|
||||
import { api, buildLogsStreamURL, humanizeError } from "@/lib/api";
|
||||
import { formatRelative, formatAbsolute } from "@/lib/format";
|
||||
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 PAGE_SIZE = 10;
|
||||
@@ -55,6 +57,7 @@ export function ImageBuildPage() {
|
||||
const now = Date.now();
|
||||
const isZh = locale.startsWith("zh");
|
||||
const config = useConfig();
|
||||
const { identity } = useTier();
|
||||
|
||||
// Form & Dialog State
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -65,6 +68,54 @@ export function ImageBuildPage() {
|
||||
const [triggering, setTriggering] = useState(false);
|
||||
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
|
||||
const [buildIds, setBuildIds] = useState<string[]>([]);
|
||||
const [builds, setBuilds] = useState<Build[]>([]);
|
||||
@@ -233,6 +284,7 @@ export function ImageBuildPage() {
|
||||
setDialogOpen(o);
|
||||
if (!o) {
|
||||
setTriggerError(null);
|
||||
setSelectedSub(null);
|
||||
}
|
||||
}}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -248,6 +300,68 @@ export function ImageBuildPage() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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="space-y-1.5">
|
||||
<Label htmlFor="imageRef" className="text-xs font-medium text-muted-foreground">
|
||||
|
||||
Reference in new issue
Block a user