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:
129
apps/web/src/components/federation/InitiateConnectionDialog.tsx
Normal file
129
apps/web/src/components/federation/InitiateConnectionDialog.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* InitiateConnectionDialog Component
|
||||
* Dialog for initiating a new federation connection
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface InitiateConnectionDialogProps {
|
||||
open: boolean;
|
||||
onInitiate: (url: string) => void;
|
||||
onCancel: () => void;
|
||||
isLoading?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a string is a valid URL
|
||||
*/
|
||||
function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function InitiateConnectionDialog({
|
||||
open,
|
||||
onInitiate,
|
||||
onCancel,
|
||||
isLoading = false,
|
||||
error,
|
||||
}: InitiateConnectionDialogProps): React.JSX.Element | null {
|
||||
const [url, setUrl] = useState("");
|
||||
const [validationError, setValidationError] = useState("");
|
||||
|
||||
// Clear input when dialog closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setUrl("");
|
||||
setValidationError("");
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConnect = (): void => {
|
||||
// Validate URL
|
||||
if (!url.trim()) {
|
||||
setValidationError("Please enter a URL");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidUrl(url)) {
|
||||
setValidationError("Please enter a valid URL (must start with http:// or https://)");
|
||||
return;
|
||||
}
|
||||
|
||||
setValidationError("");
|
||||
onInitiate(url);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === "Enter" && url.trim() && !isLoading) {
|
||||
handleConnect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
||||
{/* Header */}
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Connect to Remote Instance</h2>
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
Enter the URL of the Mosaic Stack instance you'd like to connect to
|
||||
</p>
|
||||
|
||||
{/* URL Input */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="instance-url" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Instance URL
|
||||
</label>
|
||||
<input
|
||||
id="instance-url"
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
setValidationError("");
|
||||
}}
|
||||
onKeyDown={handleKeyPress}
|
||||
disabled={isLoading}
|
||||
placeholder="https://mosaic.example.com"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
/>
|
||||
{validationError && <p className="text-sm text-red-600 mt-1">{validationError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={!url.trim() || isLoading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? "Connecting..." : "Connect"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user