feat(FN-1655): merge fusion/fn-1655
This commit is contained in:
@@ -286,11 +286,8 @@ function AppInner() {
|
||||
if (nodesOpen) {
|
||||
return (
|
||||
<div className="nodes-management-overlay">
|
||||
<div className="nodes-management-overlay__header">
|
||||
<button className="btn btn-sm" onClick={() => setNodesOpen(false)}>Close Nodes</button>
|
||||
</div>
|
||||
<PageErrorBoundary>
|
||||
<NodesView addToast={addToast} />
|
||||
<NodesView addToast={addToast} onClose={() => setNodesOpen(false)} />
|
||||
</PageErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26116,6 +26116,10 @@ html .column.drag-over * {
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.nodes-view-close {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nodes-view-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
@@ -26341,24 +26345,43 @@ html .column.drag-over * {
|
||||
.add-node-modal__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.add-node-modal__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.add-node-modal__field > span {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.add-node-modal__field input,
|
||||
.add-node-modal__field select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-node-modal__field input:focus,
|
||||
.add-node-modal__field select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
|
||||
.add-node-modal__error {
|
||||
color: var(--color-error);
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.node-detail-modal {
|
||||
|
||||
Reference in New Issue
Block a user