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:
Jason Woltje
2026-02-03 14:03:44 -06:00
parent ca4f5ec011
commit 5cf02e824b
9 changed files with 1500 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
"use client";
/**
* Federation Connections Page
* Manage connections to remote Mosaic Stack instances
*/
import { useState, useEffect } from "react";
import { ConnectionList } from "@/components/federation/ConnectionList";
import { InitiateConnectionDialog } from "@/components/federation/InitiateConnectionDialog";
import {
mockConnections,
FederationConnectionStatus,
type ConnectionDetails,
} from "@/lib/api/federation";
// TODO: Replace with real API calls when backend is integrated
// import {
// fetchConnections,
// initiateConnection,
// acceptConnection,
// rejectConnection,
// disconnectConnection,
// } from "@/lib/api/federation";
export default function ConnectionsPage(): React.JSX.Element {
const [connections, setConnections] = useState<ConnectionDetails[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
const [dialogLoading, setDialogLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [dialogError, setDialogError] = useState<string | null>(null);
// Load connections on mount
useEffect(() => {
void loadConnections();
}, []);
const loadConnections = async (): Promise<void> => {
setIsLoading(true);
setError(null);
try {
// TODO: Replace with real API call when backend is integrated
// const data = await fetchConnections();
// Using mock data for now
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate network delay
setConnections(mockConnections);
} catch (err) {
setError(
err instanceof Error ? err.message : "Unable to load connections. Please try again."
);
} finally {
setIsLoading(false);
}
};
const handleInitiate = async (_url: string): Promise<void> => {
setDialogLoading(true);
setDialogError(null);
try {
// TODO: Replace with real API call
// const newConnection = await initiateConnection({ remoteUrl: url });
// Simulate API call for now
await new Promise((resolve) => setTimeout(resolve, 1000));
// Close dialog and reload connections
setShowDialog(false);
await loadConnections();
} catch (err) {
setDialogError(
err instanceof Error
? err.message
: "Unable to initiate connection. Please check the URL and try again."
);
} finally {
setDialogLoading(false);
}
};
const handleAccept = async (connectionId: string): Promise<void> => {
setError(null);
try {
// TODO: Replace with real API call
// await acceptConnection(connectionId);
// Simulate API call and optimistic update
await new Promise((resolve) => setTimeout(resolve, 500));
setConnections((prev) =>
prev.map((conn) =>
conn.id === connectionId
? {
...conn,
status: FederationConnectionStatus.ACTIVE,
connectedAt: new Date().toISOString(),
}
: conn
)
);
} catch (err) {
setError(
err instanceof Error ? err.message : "Unable to accept connection. Please try again."
);
}
};
const handleReject = async (connectionId: string): Promise<void> => {
setError(null);
try {
// TODO: Replace with real API call
// await rejectConnection(connectionId, { reason: "User declined" });
// Simulate API call and optimistic update
await new Promise((resolve) => setTimeout(resolve, 500));
setConnections((prev) =>
prev.map((conn) =>
conn.id === connectionId
? {
...conn,
status: FederationConnectionStatus.REJECTED,
}
: conn
)
);
} catch (err) {
setError(
err instanceof Error ? err.message : "Unable to reject connection. Please try again."
);
}
};
const handleDisconnect = async (connectionId: string): Promise<void> => {
setError(null);
try {
// TODO: Replace with real API call
// await disconnectConnection(connectionId);
// Simulate API call and optimistic update
await new Promise((resolve) => setTimeout(resolve, 500));
setConnections((prev) =>
prev.map((conn) =>
conn.id === connectionId
? {
...conn,
status: FederationConnectionStatus.DISCONNECTED,
disconnectedAt: new Date().toISOString(),
}
: conn
)
);
} catch (err) {
setError(err instanceof Error ? err.message : "Unable to disconnect. Please try again.");
}
};
return (
<main className="container mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Federation Connections</h1>
<p className="text-gray-600 mt-2">Manage connections to other Mosaic Stack instances</p>
</div>
<button
onClick={() => {
setShowDialog(true);
}}
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors"
>
Connect to Instance
</button>
</div>
{/* Error Banner */}
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{error}</p>
<button
onClick={() => {
setError(null);
}}
className="text-sm text-red-700 underline mt-2"
>
Dismiss
</button>
</div>
)}
{/* Connection List */}
<ConnectionList
connections={connections}
isLoading={isLoading}
onAccept={handleAccept}
onReject={handleReject}
onDisconnect={handleDisconnect}
/>
{/* Initiate Connection Dialog */}
<InitiateConnectionDialog
open={showDialog}
onInitiate={handleInitiate}
onCancel={() => {
setShowDialog(false);
setDialogError(null);
}}
isLoading={dialogLoading}
{...(dialogError && { error: dialogError })}
/>
</main>
);
}