Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/components/gateways/ExposeComponentsForm.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(<ExposeComponentsForm {...defaultProps} />);

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(<ExposeComponentsForm {...defaultProps} />);

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(<ExposeComponentsForm {...defaultProps} />);

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();
});
});
});
96 changes: 90 additions & 6 deletions src/components/gateways/ExposeComponentsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<ToolsResponse>(`/tools?limit=1000&gateway_id=${gatewayId}`);
const {
data: resourcesData,
error: resourcesError,
isLoading: resourcesLoading,
refetch: refetchResources,
} = useQuery<ResourcesResponse>(`/resources?limit=1000&gateway_id=${gatewayId}`);
const {
data: promptsData,
error: promptsError,
isLoading: promptsLoading,
refetch: refetchPrompts,
} = useQuery<PromptsResponse>(`/prompts?limit=1000&gateway_id=${gatewayId}`);
Expand Down Expand Up @@ -352,8 +356,14 @@ export function ExposeComponentsForm({
aria-hidden="true"
/>
</div>
<span className="text-base font-normal text-neutral-600 dark:text-neutral-400">
{toolCount} {toolCount === 1 ? "tool" : "tools"}
<span
className={`text-base font-normal ${
toolsError ? STATUS_TONE_CLASS.error : "text-neutral-600 dark:text-neutral-400"
}`}
>
{toolsError
? "Failed to load tools"
: `${toolCount} ${toolCount === 1 ? "tool" : "tools"}`}
</span>
</div>
{expandedSection === "tools" ? (
Expand All @@ -369,6 +379,24 @@ export function ExposeComponentsForm({
)}
</Button>

{toolsError && (
<div className="px-6 pb-4">
<InlineNotification
type="error"
message={
toolsError.message
? `Failed to load tools: ${toolsError.message}`
: "Failed to load tools"
}
action={{
label: "Retry",
onClick: () =>
refetchTools().catch((err) => console.error("Failed to refetch tools:", err)),
}}
/>
</div>
)}

{expandedSection === "tools" && tools.length > 0 && (
<div id="tools-region" role="region" aria-label="Tools">
<MCPObjectsTable
Expand Down Expand Up @@ -400,8 +428,16 @@ export function ExposeComponentsForm({
aria-hidden="true"
/>
</div>
<span className="text-base font-normal text-neutral-600 dark:text-neutral-400">
{resourceCount} {resourceCount === 1 ? "resource" : "resources"}
<span
className={`text-base font-normal ${
resourcesError
? STATUS_TONE_CLASS.error
: "text-neutral-600 dark:text-neutral-400"
}`}
>
{resourcesError
? "Failed to load resources"
: `${resourceCount} ${resourceCount === 1 ? "resource" : "resources"}`}
</span>
</div>
{expandedSection === "resources" ? (
Expand All @@ -416,6 +452,26 @@ export function ExposeComponentsForm({
/>
)}
</Button>

{resourcesError && (
<div className="px-6 pb-4">
<InlineNotification
type="error"
message={
resourcesError.message
? `Failed to load resources: ${resourcesError.message}`
: "Failed to load resources"
}
action={{
label: "Retry",
onClick: () =>
refetchResources().catch((err) =>
console.error("Failed to refetch resources:", err),
),
}}
/>
</div>
)}
{expandedSection === "resources" && resources.length > 0 && (
<div id="resources-region" role="region" aria-label="Resources">
<MCPObjectsTable
Expand Down Expand Up @@ -447,8 +503,16 @@ export function ExposeComponentsForm({
aria-hidden="true"
/>
</div>
<span className="text-base font-normal text-neutral-600 dark:text-neutral-400">
{promptCount} prompt {promptCount === 1 ? "template" : "templates"}
<span
className={`text-base font-normal ${
promptsError
? STATUS_TONE_CLASS.error
: "text-neutral-600 dark:text-neutral-400"
}`}
>
{promptsError
? "Failed to load prompt templates"
: `${promptCount} prompt ${promptCount === 1 ? "template" : "templates"}`}
</span>
</div>
{expandedSection === "prompts" ? (
Expand All @@ -463,6 +527,26 @@ export function ExposeComponentsForm({
/>
)}
</Button>

{promptsError && (
<div className="px-6 pb-4">
<InlineNotification
type="error"
message={
promptsError.message
? `Failed to load prompt templates: ${promptsError.message}`
: "Failed to load prompt templates"
}
action={{
label: "Retry",
onClick: () =>
refetchPrompts().catch((err) =>
console.error("Failed to refetch prompts:", err),
),
}}
/>
</div>
)}
{expandedSection === "prompts" && prompts.length > 0 && (
<div id="prompts-region" role="region" aria-label="Prompt templates">
<MCPObjectsTable
Expand Down