diff --git a/panel/src/i18n/resources/en-US/admin.json b/panel/src/i18n/resources/en-US/admin.json index adaf6f1..f0cafb9 100644 --- a/panel/src/i18n/resources/en-US/admin.json +++ b/panel/src/i18n/resources/en-US/admin.json @@ -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." } \ No newline at end of file diff --git a/panel/src/i18n/resources/zh-CN/admin.json b/panel/src/i18n/resources/zh-CN/admin.json index 551b79e..0d495b6 100644 --- a/panel/src/i18n/resources/zh-CN/admin.json +++ b/panel/src/i18n/resources/zh-CN/admin.json @@ -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": "手动触发构建不会自动将该提交标记为已同意,也不会更新其关联的构建状态。仅适用于紧急调试或测试。" } \ No newline at end of file diff --git a/panel/src/pages/admin/ImageBuildPage.tsx b/panel/src/pages/admin/ImageBuildPage.tsx index 159a634..45ac33b 100644 --- a/panel/src/pages/admin/ImageBuildPage.tsx +++ b/panel/src/pages/admin/ImageBuildPage.tsx @@ -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(null); + const [submissions, setSubmissions] = useState([]); + const [loadingSubmissions, setLoadingSubmissions] = useState(false); + const [selectedSub, setSelectedSub] = useState(null); + + const isOwner = useMemo(() => { + if (!identity?.email) return false; + const emailLower = identity.email.toLowerCase(); + return emailLower === "owner@mock.felis.local" || 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([]); const [builds, setBuilds] = useState([]); @@ -233,6 +284,7 @@ export function ImageBuildPage() { setDialogOpen(o); if (!o) { setTriggerError(null); + setSelectedSub(null); } }}> @@ -248,6 +300,68 @@ export function ImageBuildPage() {
+
+ + +

+ {t("build_import_submission_hint")} +

+
+ + {selectedSub && selectedSub.status !== "approved" && ( +
+ +
+

+ {t("build_import_submission_warning_title", { status: selectedSub.status === "pending_review" ? (isZh ? "待审核" : "Pending Review") : (isZh ? "已驳回" : "Rejected") })} +

+

+ {t("build_import_submission_warning_desc")} +

+
+
+ )} +