fix(panel): 镜像页构建卡显示真实构建总数并链到构建页,读不到时显示横线
This commit is contained in:
2 files changed
+53
-5
No files matched your search
@@ -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<typeof import("@/lib/api")>();
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<ImageAdmin />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
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")}`));
|
||||
});
|
||||
});
|
||||
@@ -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() {
|
||||
<StatCard icon={Boxes} label={t("filter_all")} value={total} accentClass="text-primary bg-primary/10" />
|
||||
<StatCard icon={CheckCircle2} label={t("enabled")} value={enabled} accentClass="text-emerald-500 bg-emerald-500/10" />
|
||||
<StatCard icon={CircleSlash} label={t("disabled")} value={disabled} accentClass="text-zinc-500 bg-zinc-500/10" />
|
||||
<StatCard icon={Wrench} label={t("builds_title")} value={buildCount} accentClass="text-primary bg-primary/10" />
|
||||
<Link
|
||||
to="/admin/builds"
|
||||
className="block rounded-lg transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<StatCard
|
||||
icon={Wrench}
|
||||
label={t("builds_title")}
|
||||
value={buildsQ.data ? buildsQ.data.total : "—"}
|
||||
accentClass="text-primary bg-primary/10"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Whitelist Table Card */}
|
||||
|
||||
Reference in new issue
Block a user