From 2b07ab672af10cb1bd3908e9a62a8fa61759cf95 Mon Sep 17 00:00:00 2001 From: Li Fengmin <2080291162@qq.com> Date: Sat, 5 Sep 2026 18:42:51 +0800 Subject: [PATCH] fix: surface failed component list loads in expose components form A failed /tools, /resources, or /prompts request rendered identically to a successful empty one: the section showed a zero count with no error. Read the useQuery error per section, show the count line as "Failed to load ..." with an error notification and a retry action, and keep real counts (including legitimate zeros) for sections whose request succeeded. Signed-off-by: Li Fengmin <2080291162@qq.com> --- .../gateways/ExposeComponentsForm.test.tsx | 49 +++++++++- .../gateways/ExposeComponentsForm.tsx | 96 +++++++++++++++++-- 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/components/gateways/ExposeComponentsForm.test.tsx b/src/components/gateways/ExposeComponentsForm.test.tsx index 5027c7ba..a0174afc 100644 --- a/src/components/gateways/ExposeComponentsForm.test.tsx +++ b/src/components/gateways/ExposeComponentsForm.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, beforeAll, afterAll, afterEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; @@ -593,4 +593,51 @@ describe("ExposeComponentsForm", () => { }); }); }); + + describe("Failed list loads", () => { + it("should distinguish a failed load from a successful empty list", async () => { + server.use(http.get("/api/tools", () => new HttpResponse(null, { status: 500 }))); + + renderWithProviders(); + + const alert = await screen.findByRole("alert"); + expect(alert).toHaveTextContent("Failed to load tools"); + expect(screen.queryByText("0 tools")).not.toBeInTheDocument(); + // Sections whose own request succeeded keep showing their real counts. + expect(screen.getByText("2 resources")).toBeInTheDocument(); + expect(screen.getByText("3 prompt templates")).toBeInTheDocument(); + }); + + it("should restore the count and clear the error after a successful retry", async () => { + server.use(http.get("/api/tools", () => new HttpResponse(null, { status: 500 }))); + + const user = userEvent.setup(); + renderWithProviders(); + + const alert = await screen.findByRole("alert"); + server.resetHandlers(); + await user.click(within(alert).getByRole("button", { name: "Retry" })); + + await waitFor(() => { + expect(screen.getByText("3 tools")).toBeInTheDocument(); + }); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + }); + + it("should show a failed state for each section that failed to load", async () => { + server.use( + http.get("/api/tools", () => new HttpResponse(null, { status: 500 })), + http.get("/api/prompts", () => new HttpResponse(null, { status: 500 })), + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("Failed to load tools")).toBeInTheDocument(); + }); + expect(screen.getByText("Failed to load prompt templates")).toBeInTheDocument(); + // The section whose request succeeded keeps showing its real count. + expect(screen.getByText("2 resources")).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/gateways/ExposeComponentsForm.tsx b/src/components/gateways/ExposeComponentsForm.tsx index b6a1937a..751790e0 100644 --- a/src/components/gateways/ExposeComponentsForm.tsx +++ b/src/components/gateways/ExposeComponentsForm.tsx @@ -24,6 +24,7 @@ import { useQuery } from "@/hooks/useQuery"; import { Loading } from "@/components/ui/loading"; import { createVirtualServer } from "@/api/virtualServers"; import { InlineNotification } from "@/components/ui/inline-notification"; +import { STATUS_TONE_CLASS } from "@/lib/status"; import { useRouter } from "@/router"; import type { CreateServerDetails } from "@/components/gateways/types"; import type { Visibility } from "@/types/server"; @@ -159,16 +160,19 @@ export function ExposeComponentsForm({ // Fetch tools, resources, and prompts for this gateway const { data: toolsData, + error: toolsError, isLoading: toolsLoading, refetch: refetchTools, } = useQuery(`/tools?limit=1000&gateway_id=${gatewayId}`); const { data: resourcesData, + error: resourcesError, isLoading: resourcesLoading, refetch: refetchResources, } = useQuery(`/resources?limit=1000&gateway_id=${gatewayId}`); const { data: promptsData, + error: promptsError, isLoading: promptsLoading, refetch: refetchPrompts, } = useQuery(`/prompts?limit=1000&gateway_id=${gatewayId}`); @@ -352,8 +356,14 @@ export function ExposeComponentsForm({ aria-hidden="true" /> - - {toolCount} {toolCount === 1 ? "tool" : "tools"} + + {toolsError + ? "Failed to load tools" + : `${toolCount} ${toolCount === 1 ? "tool" : "tools"}`} {expandedSection === "tools" ? ( @@ -369,6 +379,24 @@ export function ExposeComponentsForm({ )} + {toolsError && ( +
+ + refetchTools().catch((err) => console.error("Failed to refetch tools:", err)), + }} + /> +
+ )} + {expandedSection === "tools" && tools.length > 0 && (
- - {resourceCount} {resourceCount === 1 ? "resource" : "resources"} + + {resourcesError + ? "Failed to load resources" + : `${resourceCount} ${resourceCount === 1 ? "resource" : "resources"}`} {expandedSection === "resources" ? ( @@ -416,6 +452,26 @@ export function ExposeComponentsForm({ /> )} + + {resourcesError && ( +
+ + refetchResources().catch((err) => + console.error("Failed to refetch resources:", err), + ), + }} + /> +
+ )} {expandedSection === "resources" && resources.length > 0 && (
- - {promptCount} prompt {promptCount === 1 ? "template" : "templates"} + + {promptsError + ? "Failed to load prompt templates" + : `${promptCount} prompt ${promptCount === 1 ? "template" : "templates"}`} {expandedSection === "prompts" ? ( @@ -463,6 +527,26 @@ export function ExposeComponentsForm({ /> )} + + {promptsError && ( +
+ + refetchPrompts().catch((err) => + console.error("Failed to refetch prompts:", err), + ), + }} + /> +
+ )} {expandedSection === "prompts" && prompts.length > 0 && (