feat(#91): implement Connection Manager UI for federation
Implemented comprehensive UI for managing federation connections: Features: - View existing federation connections grouped by status - Initiate new connections to remote instances - Accept/reject pending connection requests - Disconnect active connections - Display connection status, metadata, and capabilities - PDA-friendly design throughout (no demanding language) Components: - ConnectionCard: Display individual connections with actions - ConnectionList: Grouped list view with status sections - InitiateConnectionDialog: Modal for connecting to new instances - Connections page: Main management interface Implementation: - Full test coverage (42 tests, 100% passing) - TypeScript strict mode compliance - ESLint passing with no warnings - Mock data for development (ready for backend integration) - Proper error handling and loading states - PDA-friendly language (calm, supportive, stress-free) Status indicators: - 🟢 Active (soft green) - 🔵 Pending (soft blue) - ⏸️ Disconnected (soft yellow) - ⚪ Rejected (light gray) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
201
apps/web/src/components/federation/ConnectionCard.test.tsx
Normal file
201
apps/web/src/components/federation/ConnectionCard.test.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* ConnectionCard Component Tests
|
||||
* Following TDD - write tests first!
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { ConnectionCard } from "./ConnectionCard";
|
||||
import { FederationConnectionStatus, type ConnectionDetails } from "@/lib/api/federation";
|
||||
|
||||
describe("ConnectionCard", (): void => {
|
||||
const mockActiveConnection: ConnectionDetails = {
|
||||
id: "conn-1",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-work-001",
|
||||
remoteUrl: "https://mosaic.work.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: true,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.ACTIVE,
|
||||
metadata: {
|
||||
name: "Work Instance",
|
||||
description: "Corporate Mosaic instance",
|
||||
},
|
||||
createdAt: new Date("2026-02-01").toISOString(),
|
||||
updatedAt: new Date("2026-02-01").toISOString(),
|
||||
connectedAt: new Date("2026-02-01").toISOString(),
|
||||
disconnectedAt: null,
|
||||
};
|
||||
|
||||
const mockPendingConnection: ConnectionDetails = {
|
||||
...mockActiveConnection,
|
||||
id: "conn-2",
|
||||
status: FederationConnectionStatus.PENDING,
|
||||
metadata: {
|
||||
name: "Partner Instance",
|
||||
description: "Awaiting acceptance",
|
||||
},
|
||||
connectedAt: null,
|
||||
};
|
||||
|
||||
const mockOnAccept = vi.fn();
|
||||
const mockOnReject = vi.fn();
|
||||
const mockOnDisconnect = vi.fn();
|
||||
|
||||
it("should render connection name from metadata", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} />);
|
||||
expect(screen.getByText("Work Instance")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render connection URL", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} />);
|
||||
expect(screen.getByText("https://mosaic.work.example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render connection description from metadata", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} />);
|
||||
expect(screen.getByText("Corporate Mosaic instance")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Active status with green indicator for active connections", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} />);
|
||||
expect(screen.getByText("Active")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Pending status with blue indicator for pending connections", (): void => {
|
||||
render(<ConnectionCard connection={mockPendingConnection} />);
|
||||
expect(screen.getByText("Pending")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Disconnected status for disconnected connections", (): void => {
|
||||
const disconnectedConnection = {
|
||||
...mockActiveConnection,
|
||||
status: FederationConnectionStatus.DISCONNECTED,
|
||||
disconnectedAt: new Date("2026-02-02").toISOString(),
|
||||
};
|
||||
render(<ConnectionCard connection={disconnectedConnection} />);
|
||||
expect(screen.getByText("Disconnected")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Rejected status for rejected connections", (): void => {
|
||||
const rejectedConnection = {
|
||||
...mockActiveConnection,
|
||||
status: FederationConnectionStatus.REJECTED,
|
||||
};
|
||||
render(<ConnectionCard connection={rejectedConnection} />);
|
||||
expect(screen.getByText("Rejected")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Disconnect button for active connections", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} onDisconnect={mockOnDisconnect} />);
|
||||
expect(screen.getByRole("button", { name: /disconnect/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Accept and Reject buttons for pending connections", (): void => {
|
||||
render(
|
||||
<ConnectionCard
|
||||
connection={mockPendingConnection}
|
||||
onAccept={mockOnAccept}
|
||||
onReject={mockOnReject}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /accept/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /reject/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show action buttons for disconnected connections", (): void => {
|
||||
const disconnectedConnection = {
|
||||
...mockActiveConnection,
|
||||
status: FederationConnectionStatus.DISCONNECTED,
|
||||
};
|
||||
render(<ConnectionCard connection={disconnectedConnection} />);
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onAccept when accept button clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConnectionCard
|
||||
connection={mockPendingConnection}
|
||||
onAccept={mockOnAccept}
|
||||
onReject={mockOnReject}
|
||||
/>
|
||||
);
|
||||
|
||||
const acceptButton = screen.getByRole("button", { name: /accept/i });
|
||||
await user.click(acceptButton);
|
||||
|
||||
expect(mockOnAccept).toHaveBeenCalledWith(mockPendingConnection.id);
|
||||
});
|
||||
|
||||
it("should call onReject when reject button clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConnectionCard
|
||||
connection={mockPendingConnection}
|
||||
onAccept={mockOnAccept}
|
||||
onReject={mockOnReject}
|
||||
/>
|
||||
);
|
||||
|
||||
const rejectButton = screen.getByRole("button", { name: /reject/i });
|
||||
await user.click(rejectButton);
|
||||
|
||||
expect(mockOnReject).toHaveBeenCalledWith(mockPendingConnection.id);
|
||||
});
|
||||
|
||||
it("should call onDisconnect when disconnect button clicked", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
render(<ConnectionCard connection={mockActiveConnection} onDisconnect={mockOnDisconnect} />);
|
||||
|
||||
const disconnectButton = screen.getByRole("button", { name: /disconnect/i });
|
||||
await user.click(disconnectButton);
|
||||
|
||||
expect(mockOnDisconnect).toHaveBeenCalledWith(mockActiveConnection.id);
|
||||
});
|
||||
|
||||
it("should display capabilities when showDetails is true", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} showDetails={true} />);
|
||||
expect(screen.getByText(/Query/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Command/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Events/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Agent Spawn/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display capabilities by default", (): void => {
|
||||
render(<ConnectionCard connection={mockActiveConnection} />);
|
||||
// Capabilities should not be visible without showDetails=true
|
||||
const card = screen.getByText("Work Instance").closest("div");
|
||||
expect(card?.textContent).not.toMatch(/Query.*Command.*Events/);
|
||||
});
|
||||
|
||||
it("should use fallback name if metadata name is missing", (): void => {
|
||||
const connectionWithoutName = {
|
||||
...mockActiveConnection,
|
||||
metadata: {},
|
||||
};
|
||||
render(<ConnectionCard connection={connectionWithoutName} />);
|
||||
expect(screen.getByText("Remote Instance")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with compact layout when compact prop is true", (): void => {
|
||||
const { container } = render(
|
||||
<ConnectionCard connection={mockActiveConnection} compact={true} />
|
||||
);
|
||||
// Verify compact class is applied
|
||||
expect(container.querySelector(".p-3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with full layout by default", (): void => {
|
||||
const { container } = render(<ConnectionCard connection={mockActiveConnection} />);
|
||||
// Verify full padding is applied
|
||||
expect(container.querySelector(".p-4")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user