Unverified Commit dd7dff69 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

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

parent 804459c0
Loading
Loading
Loading
Loading
+7 −1
Changes for panel/src/i18n/resources/en-US/admin.json: 7 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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
+7 −1
Changes for panel/src/i18n/resources/zh-CN/admin.json: 7 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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
+115 −1
Changes for panel/src/pages/admin/ImageBuildPage.tsx: 115 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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">