From 975ef93cc059f5c663680b0ef9d62edc98bd1398 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:10:07 +0200 Subject: [PATCH] feat: pending approval requests section in Federation panel --- .../FederationPanel.tsx | 152 +++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx index 81f4747a..7c0fc858 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx @@ -5,7 +5,7 @@ import { Toggle } from '../../ui/Toggle'; import { ConfirmDialog } from '../../ui/ConfirmDialog'; import { api } from '../../../api/client'; import type { InstanceAdminSettings } from '@backspace/shared'; -import type { FederationPeer } from '../../../api/client'; +import type { FederationPeer, ApprovalRequest } from '../../../api/client'; // ─── Global Settings ───────────────────────────────────────────────────────── @@ -578,11 +578,157 @@ function PeerRow({ peer, view, expanded, onToggleExpand, onAction, defaultAutoRo ); } +// ─── Pending Approvals ────────────────────────────────────────────────────── + +function PendingApprovals({ onCountChange }: { onCountChange?: (count: number) => void }) { + const addToast = useUIStore((s) => s.addToast); + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(false); + const [actionLoading, setActionLoading] = useState(null); + const [confirmAction, setConfirmAction] = useState<{ + type: 'approve' | 'deny'; + request: ApprovalRequest; + } | null>(null); + const [errors, setErrors] = useState>({}); + + const fetchRequests = useCallback(async () => { + setLoading(true); + try { + const result = await api.federation.approvalRequests(); + setRequests(result.requests); + onCountChange?.(result.requests.length); + } catch { + // Silently fail — empty list shown + } finally { + setLoading(false); + } + }, [onCountChange]); + + useEffect(() => { + fetchRequests(); + }, [fetchRequests]); + + const handleConfirm = async () => { + if (!confirmAction) return; + const { type, request: req } = confirmAction; + setActionLoading(req.id); + setErrors((prev) => { const next = { ...prev }; delete next[req.id]; return next; }); + + try { + if (type === 'approve') { + await api.federation.approveRequest(req.id); + setRequests((prev) => prev.filter((r) => r.id !== req.id)); + onCountChange?.(requests.length - 1); + addToast(`Peering established with ${req.instanceName || req.origin}`, 'success', 3000); + } else { + await api.federation.denyRequest(req.id); + setRequests((prev) => prev.filter((r) => r.id !== req.id)); + onCountChange?.(requests.length - 1); + addToast(`Denied peering request from ${req.instanceName || req.origin}`, 'success', 3000); + } + } catch (err) { + const msg = err instanceof Error ? err.message : 'Action failed'; + setErrors((prev) => ({ ...prev, [req.id]: msg })); + } finally { + setActionLoading(null); + setConfirmAction(null); + } + }; + + if (requests.length === 0 && !loading) return null; + + return ( +
+
+
Pending Approval Requests
+ + {requests.length} + +
+
+ {loading && requests.length === 0 && ( +
Loading...
+ )} + {requests.map((req) => { + const name = req.instanceName || new URL(req.origin).host; + return ( +
+
+
+
{name}
+
{req.origin}
+
+ Requested {formatRelativeTime(req.requestedAt)} +
+
+
+ + +
+
+ {errors[req.id] && ( +
+ {errors[req.id]} + +
+ )} +
+ ); + })} +
+ + {confirmAction && ( + { if (!actionLoading) setConfirmAction(null); }} + onConfirm={handleConfirm} + title={confirmAction.type === 'approve' ? 'Approve Peering Request' : 'Deny Peering Request'} + description={ + confirmAction.type === 'approve' + ? `This will initiate a peering handshake with ${confirmAction.request.instanceName || confirmAction.request.origin}. The remote instance must be reachable.` + : `This will deny the request and block future auto-peering requests from ${confirmAction.request.instanceName || confirmAction.request.origin}. You can unblock them later from the rejected peers list.` + } + confirmLabel={confirmAction.type === 'approve' ? 'Approve' : 'Deny'} + variant={confirmAction.type === 'approve' ? 'warning' : 'danger'} + loading={!!actionLoading} + /> + )} +
+ ); +} + // ─── Main Panel ────────────────────────────────────────────────────────────── -export function FederationPanel() { +export function FederationPanel({ onApprovalCountChange }: { onApprovalCountChange?: (count: number) => void }) { const addToast = useUIStore((s) => s.addToast); + const [, setApprovalCount] = useState(0); + + const handleApprovalCountChange = useCallback((count: number) => { + setApprovalCount(count); + onApprovalCountChange?.(count); + }, [onApprovalCountChange]); + // Peer list state const [peers, setPeers] = useState([]); const [peersLoading, setPeersLoading] = useState(false); @@ -729,6 +875,8 @@ export function FederationPanel() { + + {/* Peered Instances */}