From ec861c10e0121921a87ad2434d7e01354d9a6212 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 15:44:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(panel):=20=E9=95=9C=E5=83=8F=E9=A1=B5?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=8D=A1=E6=98=BE=E7=A4=BA=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E6=80=BB=E6=95=B0=E5=B9=B6=E9=93=BE=E5=88=B0?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E9=A1=B5=EF=BC=8C=E8=AF=BB=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=E6=97=B6=E6=98=BE=E7=A4=BA=E6=A8=AA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- panel/src/pages/admin/ImageAdmin.test.tsx | 37 ++++++++++++++++++++++- panel/src/pages/admin/ImageAdmin.tsx | 21 ++++++++++--- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/panel/src/pages/admin/ImageAdmin.test.tsx b/panel/src/pages/admin/ImageAdmin.test.tsx index dd11c29..c13fa3f 100644 --- a/panel/src/pages/admin/ImageAdmin.test.tsx +++ b/panel/src/pages/admin/ImageAdmin.test.tsx @@ -6,7 +6,7 @@ import { MemoryRouter } from "react-router-dom"; import i18next from "i18next"; import { ImageAdmin } from "./ImageAdmin"; -const calls = vi.hoisted(() => ({ listImages: vi.fn(), removeImage: vi.fn() })); +const calls = vi.hoisted(() => ({ listImages: vi.fn(), removeImage: vi.fn(), listBuilds: vi.fn() })); vi.mock("@/lib/api", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, api: { ...actual.api, ...calls } }; @@ -18,6 +18,7 @@ const REF = "registry.felis.svc:5000/paper:1.21"; beforeEach(() => { for (const fn of Object.values(calls)) fn.mockReset(); calls.listImages.mockResolvedValue([{ image_ref: REF, source: "external", enabled: true }]); + calls.listBuilds.mockResolvedValue({ builds: [], total: 0 }); // A native dialog here would block (or be blocked in an embedded browser). vi.spyOn(window, "confirm").mockImplementation(() => { throw new Error("window.confirm used"); @@ -94,3 +95,37 @@ describe("ImageAdmin source", () => { expect(screen.getByText("registry.felis.svc:5000/paper:demo")).toBeTruthy(); }); }); + +describe("ImageAdmin build pipeline card", () => { + const renderPage = () => + render( + + + , + ); + const card = () => screen.getByRole("link", { name: new RegExp(t("admin:builds_title")) }); + + it("counts builds, whatever the number of images, and leads to the pipeline", async () => { + calls.listImages.mockResolvedValue([ + { image_ref: REF, source: "external", enabled: true }, + { image_ref: `${REF}-b`, source: "external", enabled: false }, + ]); + calls.listBuilds.mockResolvedValue({ + builds: [{ id: "b-7", image_ref: REF, status: "succeeded", requested_by: "admin", created_at: "2026-09-01T00:00:00Z" }], + total: 7, + }); + renderPage(); + + await vi.waitFor(() => expect(card().textContent).toBe(`7${t("admin:builds_title")}`)); + expect(card().getAttribute("href")).toBe("/admin/builds"); + expect(calls.listBuilds.mock.calls).toEqual([[{ limit: 1 }]]); + }); + + it("shows a dash when the build history can't be read", async () => { + calls.listBuilds.mockRejectedValue({ status: 503, code: "build_unavailable", message: "builder off" }); + renderPage(); + await screen.findByText(REF); + + await vi.waitFor(() => expect(card().textContent).toBe(`—${t("admin:builds_title")}`)); + }); +}); diff --git a/panel/src/pages/admin/ImageAdmin.tsx b/panel/src/pages/admin/ImageAdmin.tsx index 488991c..c99494e 100644 --- a/panel/src/pages/admin/ImageAdmin.tsx +++ b/panel/src/pages/admin/ImageAdmin.tsx @@ -1,4 +1,5 @@ import { useState, useMemo } from "react"; +import { Link } from "react-router-dom"; import { Boxes, CheckCircle2, CircleSlash, Plus, Trash2, Wrench } from "lucide-react"; import { SearchInput } from "@/components/SearchInput"; import { StatCard } from "@/components/StatCard"; @@ -64,10 +65,12 @@ export function ImageAdmin() { const total = images.length; const enabled = images.filter((img) => img.enabled).length; const disabled = total - enabled; - const buildCount = total; - return { total, enabled, disabled, buildCount }; + return { total, enabled, disabled }; }, [images]); - const { total, enabled, disabled, buildCount } = stats; + const { total, enabled, disabled } = stats; + // The pipeline card counts builds, which live in their own history: one row is + // enough, the answer carries the total. A read that failed shows a dash. + const buildsQ = useAsync(() => api.listBuilds({ limit: 1 }), []); // Filtered & Paginated Images const filteredImages = useMemo(() => { @@ -185,7 +188,17 @@ export function ImageAdmin() { - + + + {/* Whitelist Table Card */}