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:
245
apps/web/src/lib/api/federation.ts
Normal file
245
apps/web/src/lib/api/federation.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Federation API Client
|
||||
* Handles federation connection management API requests
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost } from "./client";
|
||||
|
||||
/**
|
||||
* Federation connection status
|
||||
*/
|
||||
export enum FederationConnectionStatus {
|
||||
PENDING = "PENDING",
|
||||
ACTIVE = "ACTIVE",
|
||||
DISCONNECTED = "DISCONNECTED",
|
||||
REJECTED = "REJECTED",
|
||||
}
|
||||
|
||||
/**
|
||||
* Federation capabilities
|
||||
*/
|
||||
export interface FederationCapabilities {
|
||||
supportsQuery: boolean;
|
||||
supportsCommand: boolean;
|
||||
supportsEvent: boolean;
|
||||
supportsAgentSpawn: boolean;
|
||||
protocolVersion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection details
|
||||
*/
|
||||
export interface ConnectionDetails {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
remoteInstanceId: string;
|
||||
remoteUrl: string;
|
||||
remotePublicKey: string;
|
||||
remoteCapabilities: FederationCapabilities;
|
||||
status: FederationConnectionStatus;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
connectedAt: string | null;
|
||||
disconnectedAt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public instance identity
|
||||
*/
|
||||
export interface PublicInstanceIdentity {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
name: string;
|
||||
url: string;
|
||||
publicKey: string;
|
||||
capabilities: FederationCapabilities;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate connection request
|
||||
*/
|
||||
export interface InitiateConnectionRequest {
|
||||
remoteUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept connection request
|
||||
*/
|
||||
export interface AcceptConnectionRequest {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject connection request
|
||||
*/
|
||||
export interface RejectConnectionRequest {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect connection request
|
||||
*/
|
||||
export interface DisconnectConnectionRequest {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all connections
|
||||
*/
|
||||
export async function fetchConnections(
|
||||
status?: FederationConnectionStatus
|
||||
): Promise<ConnectionDetails[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (status) {
|
||||
params.append("status", status);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const endpoint = queryString
|
||||
? `/api/v1/federation/connections?${queryString}`
|
||||
: "/api/v1/federation/connections";
|
||||
|
||||
return apiGet<ConnectionDetails[]>(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single connection
|
||||
*/
|
||||
export async function fetchConnection(connectionId: string): Promise<ConnectionDetails> {
|
||||
return apiGet<ConnectionDetails>(`/api/v1/federation/connections/${connectionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a new connection
|
||||
*/
|
||||
export async function initiateConnection(
|
||||
request: InitiateConnectionRequest
|
||||
): Promise<ConnectionDetails> {
|
||||
return apiPost<ConnectionDetails>("/api/v1/federation/connections/initiate", request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a pending connection
|
||||
*/
|
||||
export async function acceptConnection(
|
||||
connectionId: string,
|
||||
request?: AcceptConnectionRequest
|
||||
): Promise<ConnectionDetails> {
|
||||
return apiPost<ConnectionDetails>(
|
||||
`/api/v1/federation/connections/${connectionId}/accept`,
|
||||
request ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a pending connection
|
||||
*/
|
||||
export async function rejectConnection(
|
||||
connectionId: string,
|
||||
request: RejectConnectionRequest
|
||||
): Promise<ConnectionDetails> {
|
||||
return apiPost<ConnectionDetails>(
|
||||
`/api/v1/federation/connections/${connectionId}/reject`,
|
||||
request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect an active connection
|
||||
*/
|
||||
export async function disconnectConnection(
|
||||
connectionId: string,
|
||||
request?: DisconnectConnectionRequest
|
||||
): Promise<ConnectionDetails> {
|
||||
return apiPost<ConnectionDetails>(
|
||||
`/api/v1/federation/connections/${connectionId}/disconnect`,
|
||||
request ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this instance's public identity
|
||||
*/
|
||||
export async function fetchInstanceIdentity(): Promise<PublicInstanceIdentity> {
|
||||
return apiGet<PublicInstanceIdentity>("/api/v1/federation/instance");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock connections for development
|
||||
*/
|
||||
export const mockConnections: 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,
|
||||
},
|
||||
{
|
||||
id: "conn-2",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-partner-001",
|
||||
remoteUrl: "https://mosaic.partner.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: false,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: false,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.PENDING,
|
||||
metadata: {
|
||||
name: "Partner Instance",
|
||||
description: "Shared project collaboration",
|
||||
},
|
||||
createdAt: new Date("2026-02-02").toISOString(),
|
||||
updatedAt: new Date("2026-02-02").toISOString(),
|
||||
connectedAt: null,
|
||||
disconnectedAt: null,
|
||||
},
|
||||
{
|
||||
id: "conn-3",
|
||||
workspaceId: "workspace-1",
|
||||
remoteInstanceId: "instance-old-001",
|
||||
remoteUrl: "https://mosaic.old.example.com",
|
||||
remotePublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
|
||||
remoteCapabilities: {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: false,
|
||||
supportsAgentSpawn: false,
|
||||
protocolVersion: "1.0",
|
||||
},
|
||||
status: FederationConnectionStatus.DISCONNECTED,
|
||||
metadata: {
|
||||
name: "Previous Instance",
|
||||
description: "No longer in use",
|
||||
},
|
||||
createdAt: new Date("2026-01-15").toISOString(),
|
||||
updatedAt: new Date("2026-01-30").toISOString(),
|
||||
connectedAt: new Date("2026-01-15").toISOString(),
|
||||
disconnectedAt: new Date("2026-01-30").toISOString(),
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user