fix(#338): Handle non-OK responses in ActiveProjectsWidget
- Add error state tracking for both projects and agents API calls - Show error UI (amber alert icon + message) when fetch fails - Clear data on error to avoid showing stale information - Added tests for error handling: API failures, network errors Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,17 +38,23 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps):
|
|||||||
const [agentSessions, setAgentSessions] = useState<AgentSession[]>([]);
|
const [agentSessions, setAgentSessions] = useState<AgentSession[]>([]);
|
||||||
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
||||||
const [isLoadingAgents, setIsLoadingAgents] = useState(true);
|
const [isLoadingAgents, setIsLoadingAgents] = useState(true);
|
||||||
|
const [projectsError, setProjectsError] = useState<string | null>(null);
|
||||||
|
const [agentsError, setAgentsError] = useState<string | null>(null);
|
||||||
const [expandedSession, setExpandedSession] = useState<string | null>(null);
|
const [expandedSession, setExpandedSession] = useState<string | null>(null);
|
||||||
|
|
||||||
// Fetch active projects
|
// Fetch active projects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProjects = async (): Promise<void> => {
|
const fetchProjects = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
setProjectsError(null);
|
||||||
// Use API client to ensure CSRF token is included
|
// Use API client to ensure CSRF token is included
|
||||||
const data = await apiPost<ActiveProject[]>("/api/widgets/data/active-projects");
|
const data = await apiPost<ActiveProject[]>("/api/widgets/data/active-projects");
|
||||||
setProjects(data);
|
setProjects(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch active projects:", error);
|
console.error("Failed to fetch active projects:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
setProjectsError(errorMessage);
|
||||||
|
setProjects([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingProjects(false);
|
setIsLoadingProjects(false);
|
||||||
}
|
}
|
||||||
@@ -67,11 +73,15 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps):
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchAgentSessions = async (): Promise<void> => {
|
const fetchAgentSessions = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
setAgentsError(null);
|
||||||
// Use API client to ensure CSRF token is included
|
// Use API client to ensure CSRF token is included
|
||||||
const data = await apiPost<AgentSession[]>("/api/widgets/data/agent-chains");
|
const data = await apiPost<AgentSession[]>("/api/widgets/data/agent-chains");
|
||||||
setAgentSessions(data);
|
setAgentSessions(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch agent sessions:", error);
|
console.error("Failed to fetch agent sessions:", error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
setAgentsError(errorMessage);
|
||||||
|
setAgentSessions([]);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingAgents(false);
|
setIsLoadingAgents(false);
|
||||||
}
|
}
|
||||||
@@ -137,6 +147,13 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps):
|
|||||||
<div className="overflow-auto max-h-48 space-y-2">
|
<div className="overflow-auto max-h-48 space-y-2">
|
||||||
{isLoadingProjects ? (
|
{isLoadingProjects ? (
|
||||||
<div className="text-center text-gray-500 text-xs py-4">Loading projects...</div>
|
<div className="text-center text-gray-500 text-xs py-4">Loading projects...</div>
|
||||||
|
) : projectsError ? (
|
||||||
|
<div className="text-center text-xs py-4">
|
||||||
|
<div className="flex items-center justify-center gap-1 text-amber-600">
|
||||||
|
<AlertCircle className="w-3 h-3" />
|
||||||
|
<span>Unable to load projects</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : projects.length === 0 ? (
|
) : projects.length === 0 ? (
|
||||||
<div className="text-center text-gray-500 text-xs py-4">No active projects</div>
|
<div className="text-center text-gray-500 text-xs py-4">No active projects</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -185,6 +202,13 @@ export function ActiveProjectsWidget({ id: _id, config: _config }: WidgetProps):
|
|||||||
<div className="overflow-auto max-h-48 space-y-2">
|
<div className="overflow-auto max-h-48 space-y-2">
|
||||||
{isLoadingAgents ? (
|
{isLoadingAgents ? (
|
||||||
<div className="text-center text-gray-500 text-xs py-4">Loading agents...</div>
|
<div className="text-center text-gray-500 text-xs py-4">Loading agents...</div>
|
||||||
|
) : agentsError ? (
|
||||||
|
<div className="text-center text-xs py-4">
|
||||||
|
<div className="flex items-center justify-center gap-1 text-amber-600">
|
||||||
|
<AlertCircle className="w-3 h-3" />
|
||||||
|
<span>Unable to load agents</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : agentSessions.length === 0 ? (
|
) : agentSessions.length === 0 ? (
|
||||||
<div className="text-center text-gray-500 text-xs py-4">No running agents</div>
|
<div className="text-center text-gray-500 text-xs py-4">No running agents</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -317,4 +317,110 @@ describe("ActiveProjectsWidget", (): void => {
|
|||||||
expect(screen.getByText("(2)")).toBeInTheDocument(); // Project count badge
|
expect(screen.getByText("(2)")).toBeInTheDocument(); // Project count badge
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should display error state when projects API fails", async (): Promise<void> => {
|
||||||
|
vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => {
|
||||||
|
const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : "";
|
||||||
|
|
||||||
|
// Return CSRF token
|
||||||
|
if (urlString.includes("csrf")) {
|
||||||
|
return Promise.resolve(mockCsrfResponse());
|
||||||
|
}
|
||||||
|
if (urlString.includes("active-projects")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: "Internal Server Error",
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({ code: "SERVER_ERROR", message: "Failed to fetch projects" }),
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
// Agent chains succeeds
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve([]),
|
||||||
|
} as Response);
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ActiveProjectsWidget id="active-projects-1" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display error state when agent chains API fails", async (): Promise<void> => {
|
||||||
|
vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => {
|
||||||
|
const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : "";
|
||||||
|
|
||||||
|
// Return CSRF token
|
||||||
|
if (urlString.includes("csrf")) {
|
||||||
|
return Promise.resolve(mockCsrfResponse());
|
||||||
|
}
|
||||||
|
if (urlString.includes("agent-chains")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: "Internal Server Error",
|
||||||
|
json: () => Promise.resolve({ code: "SERVER_ERROR", message: "Failed to fetch agents" }),
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
// Active projects succeeds
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve([]),
|
||||||
|
} as Response);
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ActiveProjectsWidget id="active-projects-1" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display error state when both APIs fail", async (): Promise<void> => {
|
||||||
|
vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => {
|
||||||
|
const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : "";
|
||||||
|
|
||||||
|
// Return CSRF token
|
||||||
|
if (urlString.includes("csrf")) {
|
||||||
|
return Promise.resolve(mockCsrfResponse());
|
||||||
|
}
|
||||||
|
// Both endpoints fail
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: "Internal Server Error",
|
||||||
|
json: () => Promise.resolve({ code: "SERVER_ERROR", message: "Server error" }),
|
||||||
|
} as Response);
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ActiveProjectsWidget id="active-projects-1" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle network errors gracefully", async (): Promise<void> => {
|
||||||
|
vi.mocked(global.fetch).mockImplementation((url: RequestInfo | URL) => {
|
||||||
|
const urlString = typeof url === "string" ? url : url instanceof URL ? url.toString() : "";
|
||||||
|
|
||||||
|
// Return CSRF token
|
||||||
|
if (urlString.includes("csrf")) {
|
||||||
|
return Promise.resolve(mockCsrfResponse());
|
||||||
|
}
|
||||||
|
// Network error
|
||||||
|
return Promise.reject(new Error("Network error"));
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ActiveProjectsWidget id="active-projects-1" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/unable to load projects/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/unable to load agents/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user