fix(FN-2715): show initial loading state in Agents view

- Render an accessible loading status when the first agents fetch is in flight
- Keep existing agent cards visible during refresh instead of replacing the grid with a loader
- Add AgentsView CSS for the loading container with responsive sizing
- Add regression tests covering initial loading, post-load cleanup, and refresh behavior
This commit is contained in:
Fusion
2026-04-27 12:04:07 -07:00
committed by gsxdsm
parent 6b2982cda5
commit ac93023f2d
3 changed files with 93 additions and 2 deletions

View File

@@ -139,6 +139,16 @@
padding: 20px;
}
.agents-view-loading {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: calc(var(--space-2xl) * 6);
gap: var(--space-sm);
color: var(--text-muted);
}
.agent-controls {
display: flex;
align-items: center;
@@ -868,6 +878,10 @@
gap: var(--space-sm);
}
.agents-view-loading {
min-height: calc(var(--space-2xl) * 4);
}
.org-chart-node {
min-width: calc(var(--space-2xl) * 5);
}

View File

@@ -707,6 +707,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
return getAgentHealthStatus(agent);
};
const showInitialAgentsLoading = isLoading && agents.length === 0;
return (
<div className="agents-view">
<div className="agents-view-header">
@@ -913,7 +915,12 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
/>
{/* Agent Collection */}
{agentView === "tree" ? (
{showInitialAgentsLoading ? (
<div className="agents-view-loading" role="status" aria-live="polite">
<RefreshCw size={18} className="spin" />
<span>Loading agents...</span>
</div>
) : agentView === "tree" ? (
<div className="agent-tree__view">
{displayAgents.length === 0 ? (
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
@@ -1005,7 +1012,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
return (
<div key={agent.id} className={`agent-card ${stateCardClass}`}>
<div className="agent-card-header">
<div
<div
className="agent-info agent-info--clickable"
onClick={() => setSelectedAgentId(agent.id)}
role="button"

View File

@@ -144,6 +144,76 @@ describe("AgentsView", () => {
});
});
it("shows a loading indicator while the initial agents fetch is pending", async () => {
let resolveAgents: ((value: Agent[]) => void) | undefined;
mockFetchAgents.mockImplementationOnce(
() =>
new Promise<Agent[]>((resolve) => {
resolveAgents = resolve;
}),
);
render(<AgentsView addToast={mockAddToast} />);
const loadingStatus = await screen.findByRole("status");
expect(loadingStatus).toHaveTextContent("Loading agents...");
expect(loadingStatus.getAttribute("aria-live")).toBe("polite");
resolveAgents?.(mockAgents);
await waitFor(() => {
expect(screen.queryByText("Loading agents...")).toBeNull();
});
});
it("hides the loading indicator once agents finish loading", async () => {
let resolveAgents: ((value: Agent[]) => void) | undefined;
mockFetchAgents.mockImplementationOnce(
() =>
new Promise<Agent[]>((resolve) => {
resolveAgents = resolve;
}),
);
render(<AgentsView addToast={mockAddToast} />);
expect(await screen.findByText("Loading agents...")).toBeTruthy();
resolveAgents?.(mockAgents);
await waitFor(() => {
expect(screen.queryByText("Loading agents...")).toBeNull();
expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0);
});
});
it("keeps existing agents visible during refresh loads", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0);
});
let resolveRefresh: ((value: Agent[]) => void) | undefined;
mockFetchAgents.mockImplementationOnce(
() =>
new Promise<Agent[]>((resolve) => {
resolveRefresh = resolve;
}),
);
fireEvent.click(screen.getByTitle("Refresh"));
await waitFor(() => {
expect(screen.queryByText("Loading agents...")).toBeNull();
expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0);
});
resolveRefresh?.(mockAgents);
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenCalledTimes(2);
});
});
it("keeps New Agent directly accessible while controls live in popup", async () => {
render(<AgentsView addToast={mockAddToast} />);