feat(FN-1655): merge fusion/fn-1655

This commit is contained in:
gsxdsm
2026-04-14 09:59:00 -07:00
parent 703b6d487f
commit 76dec1e9f1
5 changed files with 56 additions and 14 deletions

View File

@@ -141,6 +141,7 @@ export function AddNodeModal({ isOpen, onClose, onSubmit, addToast }: AddNodeMod
placeholder="Build Machine"
disabled={isSubmitting}
aria-invalid={Boolean(errors.name)}
autoFocus
/>
{errors.name && <span className="add-node-modal__error">{errors.name}</span>}
</label>

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Plus, Server, Wifi, WifiOff, Globe, RefreshCw } from "lucide-react";
import { Plus, Server, Wifi, WifiOff, Globe, RefreshCw, X } from "lucide-react";
import { useNodes } from "../hooks/useNodes";
import { useProjects } from "../hooks/useProjects";
import type { NodeInfo, NodeUpdateInput } from "../api";
@@ -11,9 +11,10 @@ import type { ToastType } from "../hooks/useToast";
interface NodesViewProps {
addToast: (message: string, type?: ToastType) => void;
onClose: () => void;
}
export function NodesView({ addToast }: NodesViewProps) {
export function NodesView({ addToast, onClose }: NodesViewProps) {
const { nodes, loading, error, refresh, register, update, unregister, healthCheck } = useNodes();
const { projects } = useProjects();
const [addModalOpen, setAddModalOpen] = useState(false);
@@ -84,6 +85,13 @@ export function NodesView({ addToast }: NodesViewProps) {
</div>
<div className="nodes-view-actions">
<button
className="btn-icon nodes-view-close"
onClick={onClose}
aria-label="Close nodes view"
>
<X size={16} />
</button>
<button className="btn btn-sm" onClick={() => void handleRefresh()} disabled={loading}>
<RefreshCw size={14} className={loading ? "spin" : ""} />
Refresh

View File

@@ -87,7 +87,7 @@ describe("NodesView", () => {
],
}));
render(<NodesView addToast={vi.fn()} />);
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
// Check node cards are rendered - use the node card class to find elements
const nodeCards = document.querySelectorAll(".node-card");
@@ -106,7 +106,7 @@ describe("NodesView", () => {
it("renders empty state when there are no nodes", () => {
mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: [] }));
render(<NodesView addToast={vi.fn()} />);
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
expect(screen.getByText("No nodes are registered yet.")).toBeDefined();
expect(screen.getByText("Add First Node")).toBeDefined();
@@ -119,7 +119,7 @@ describe("NodesView", () => {
it("opens Add Node modal when Add Node button is clicked", () => {
mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: [] }));
render(<NodesView addToast={vi.fn()} />);
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
fireEvent.click(screen.getByText("Add Node"));
expect(screen.getByRole("dialog", { name: "Add Node" })).toBeDefined();
@@ -140,7 +140,7 @@ describe("NodesView", () => {
nodes: [makeNode({ id: "node-1", name: "Detail Node" })],
}));
render(<NodesView addToast={vi.fn()} />);
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
// Click on the node card (not the topology node)
const nodeCard = document.querySelector(".node-card");
@@ -167,7 +167,7 @@ describe("NodesView", () => {
nodes: [makeNode({ id: "node-1", name: "Local Node", type: "local" })],
}));
render(<NodesView addToast={vi.fn()} />);
render(<NodesView addToast={vi.fn()} onClose={vi.fn()} />);
// Click on the node card to open detail modal
const nodeCard = document.querySelector(".node-card");
@@ -177,4 +177,17 @@ describe("NodesView", () => {
// Modal should show "Projects (2)" - including the unassigned project
expect(screen.getByText("Projects (2)")).toBeDefined();
});
it("renders close button and calls onClose when clicked", () => {
mockUseNodes.mockReturnValue(makeUseNodesResult({ nodes: [] }));
const onClose = vi.fn();
render(<NodesView addToast={vi.fn()} onClose={onClose} />);
const closeButton = screen.getByRole("button", { name: "Close nodes view" });
expect(closeButton).toBeInTheDocument();
fireEvent.click(closeButton);
expect(onClose).toHaveBeenCalledTimes(1);
});
});