feat(FN-1232): add remote node mesh dashboard integration

- Add core proxy API infrastructure (api-node.ts) with remote node communication
- Create NodeContext for centralized node state management
- Implement useNodeProxy hook for node operations (metrics, events, health)
- Implement useRemoteNodeData hook with 10-second polling for live metrics
- Implement useRemoteNodeEvents hook for real-time SSE event subscription
- Add NodeStatusIndicator component with animated status indicators
- Add comprehensive tests for all new hooks and components
- Update dashboard styles for node status visualization
This commit is contained in:
gsxdsm
2026-04-09 13:01:19 -07:00
parent 7ab96cc876
commit fc23e522a7
13 changed files with 1771 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
/**
* NodeStatusIndicator - displays the connection status of the currently viewed node.
* Shows a green/red/yellow dot and optional text based on node state.
*/
import type { NodeConfig } from "@fusion/core";
export interface NodeStatusIndicatorProps {
/** The node to display, or null for local node */
node: NodeConfig | null;
/** Whether to show additional details (node name, type, status text) */
showDetails?: boolean;
}
/**
* Get display configuration for a node status
*/
function getStatusDisplay(status: NodeConfig["status"]): {
label: string;
dotClass: string;
} {
switch (status) {
case "online":
return { label: "Online", dotClass: "node-status-indicator__dot--online" };
case "offline":
return { label: "Offline", dotClass: "node-status-indicator__dot--offline" };
case "connecting":
return { label: "Connecting", dotClass: "node-status-indicator__dot--connecting" };
case "error":
return { label: "Error", dotClass: "node-status-indicator__dot--error" };
default:
return { label: "Unknown", dotClass: "node-status-indicator__dot--offline" };
}
}
/**
* Status indicator component for nodes.
* Shows connection status with a colored dot and optional details.
*/
export function NodeStatusIndicator({ node, showDetails = false }: NodeStatusIndicatorProps) {
// Local or null node - show "Local" badge
if (!node || node.type === "local") {
return (
<div className="node-status-indicator node-status-indicator--local">
<span className="node-status-indicator__label">Local</span>
</div>
);
}
// Remote node - show status with dot and optional details
const { label: statusLabel, dotClass } = getStatusDisplay(node.status);
const isConnecting = node.status === "connecting";
return (
<div className="node-status-indicator node-status-indicator--remote">
<span className={`node-status-indicator__dot ${dotClass}`}>
{isConnecting && <span className="node-status-indicator__spinner" />}
</span>
<span className="node-status-indicator__name">{node.name}</span>
{showDetails && (
<span className="node-status-indicator__details">
{node.type} · {statusLabel}
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,155 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { NodeStatusIndicator } from "../NodeStatusIndicator";
import type { NodeConfig } from "@fusion/core";
describe("NodeStatusIndicator", () => {
describe("when node is null", () => {
it("renders Local text badge", () => {
render(<NodeStatusIndicator node={null} />);
expect(screen.getByText("Local")).toBeInTheDocument();
expect(screen.getByText("Local")).toHaveClass("node-status-indicator__label");
});
it("does not show details when showDetails is true but node is null", () => {
render(<NodeStatusIndicator node={null} showDetails />);
expect(screen.getByText("Local")).toBeInTheDocument();
expect(screen.queryByText(/·/)).not.toBeInTheDocument();
});
});
describe("when node type is local", () => {
const localNode: NodeConfig = {
id: "node_local",
name: "Local Node",
type: "local",
status: "online",
maxConcurrent: 4,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
it("renders Local text badge", () => {
render(<NodeStatusIndicator node={localNode} />);
expect(screen.getByText("Local")).toBeInTheDocument();
});
it("does not show status dot for local nodes", () => {
const { container } = render(<NodeStatusIndicator node={localNode} />);
const dots = container.querySelectorAll(".node-status-indicator__dot");
expect(dots).toHaveLength(0);
});
it("renders with --local modifier class", () => {
render(<NodeStatusIndicator node={localNode} />);
const container = screen.getByText("Local").closest(".node-status-indicator");
expect(container).toHaveClass("node-status-indicator--local");
});
});
describe("when node type is remote", () => {
const remoteNode: NodeConfig = {
id: "node_abc123",
name: "Remote Server",
type: "remote",
url: "http://remote:4040",
status: "online",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
it("renders node name", () => {
render(<NodeStatusIndicator node={remoteNode} />);
expect(screen.getByText("Remote Server")).toBeInTheDocument();
});
it("shows green dot for online status", () => {
const { container } = render(<NodeStatusIndicator node={remoteNode} />);
const dot = container.querySelector(".node-status-indicator__dot--online");
expect(dot).toBeInTheDocument();
});
it("renders with --remote modifier class", () => {
render(<NodeStatusIndicator node={remoteNode} />);
const container = screen.getByText("Remote Server").closest(".node-status-indicator");
expect(container).toHaveClass("node-status-indicator--remote");
});
it("shows red dot for offline status", () => {
const offlineNode = { ...remoteNode, status: "offline" as const };
const { container } = render(<NodeStatusIndicator node={offlineNode} />);
const dot = container.querySelector(".node-status-indicator__dot--offline");
expect(dot).toBeInTheDocument();
});
it("shows red dot for error status", () => {
const errorNode = { ...remoteNode, status: "error" as const };
const { container } = render(<NodeStatusIndicator node={errorNode} />);
const dot = container.querySelector(".node-status-indicator__dot--error");
expect(dot).toBeInTheDocument();
});
it("shows yellow spinner for connecting status", () => {
const connectingNode = { ...remoteNode, status: "connecting" as const };
const { container } = render(<NodeStatusIndicator node={connectingNode} />);
const dot = container.querySelector(".node-status-indicator__dot--connecting");
expect(dot).toBeInTheDocument();
expect(dot?.querySelector(".node-status-indicator__spinner")).toBeInTheDocument();
});
});
describe("showDetails option", () => {
const remoteNode: NodeConfig = {
id: "node_abc123",
name: "Remote Server",
type: "remote",
url: "http://remote:4040",
status: "online",
maxConcurrent: 2,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
it("does not show details by default", () => {
render(<NodeStatusIndicator node={remoteNode} />);
expect(screen.queryByText(/·/)).not.toBeInTheDocument();
});
it("shows details when showDetails is true", () => {
render(<NodeStatusIndicator node={remoteNode} showDetails />);
const details = screen.getByText(/remote · Online/);
expect(details).toBeInTheDocument();
expect(details).toHaveClass("node-status-indicator__details");
});
it("shows correct details for offline status", () => {
const offlineNode = { ...remoteNode, status: "offline" as const };
render(<NodeStatusIndicator node={offlineNode} showDetails />);
const details = screen.getByText(/remote · Offline/);
expect(details).toBeInTheDocument();
});
it("shows correct details for connecting status", () => {
const connectingNode = { ...remoteNode, status: "connecting" as const };
render(<NodeStatusIndicator node={connectingNode} showDetails />);
const details = screen.getByText(/remote · Connecting/);
expect(details).toBeInTheDocument();
});
});
});