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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user