Loading portal-gui/src/components/data-table/dragable-row.tsx +7 −1 Changes for portal-gui/src/components/data-table/dragable-row.tsx: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -30,7 +30,13 @@ export function DraggableRow<TData>({ row }: DraggableRowProps<TData>) { }} > {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> <TableCell key={cell.id} className={ (cell.column.columnDef.meta as { className?: string } | undefined) ?.className } > {cell.column.id === 'drag' ? ( <button type="button" Loading portal-gui/src/components/federation/confirm-delete-federation-dialog.tsx→portal-gui/src/components/federation/confirm-action-dialog.tsx +28 −17 Changes for portal-gui/src/components/federation/confirm-action-dialog.tsx: 28 added lines, 17 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { useState, type ReactNode } from 'react'; import { useSingleFlight } from '@/hooks/useSingleFlight'; import { AlertDialog, Loading @@ -10,34 +12,40 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import type { Partner } from '@/types/federation'; export function ConfirmDeleteFederationDialog({ partner, export function ConfirmActionDialog({ title, description, confirmLabel, pendingLabel, failureMessage, onOpenChange, onConfirm, }: { partner: Partner; title: string; description: ReactNode; confirmLabel: string; pendingLabel: string; failureMessage: string; onOpenChange: (open: boolean) => void; onConfirm: () => Promise<void>; }) { const [pending, setPending] = useState(false); const [error, setError] = useState<string | null>(null); const once = useSingleFlight(); const handleConfirm = async () => { const handleConfirm = () => once(async () => { setPending(true); setError(null); try { await onConfirm(); onOpenChange(false); } catch (err) { setError( err instanceof Error ? err.message : 'Failed to delete federation.', ); } finally { setError(err instanceof Error ? err.message : failureMessage); setPending(false); } }; }); return ( <AlertDialog Loading @@ -48,14 +56,17 @@ export function ConfirmDeleteFederationDialog({ > <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Delete federation?</AlertDialogTitle> <AlertDialogDescription> Ends our federation with "{partner.name}", including the zones we subscribed to. The partner registration stays. <AlertDialogTitle>{title}</AlertDialogTitle> <AlertDialogDescription className="wrap-anywhere"> {description} </AlertDialogDescription> </AlertDialogHeader> {error && <p className="text-sm text-destructive">{error}</p>} {error && ( <p role="alert" className="text-sm text-destructive"> {error} </p> )} <AlertDialogFooter> <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel> Loading @@ -67,7 +78,7 @@ export function ConfirmDeleteFederationDialog({ disabled={pending} className="text-destructive-foreground bg-destructive hover:bg-destructive/90" > {pending ? 'Deleting…' : 'Delete'} {pending ? pendingLabel : confirmLabel} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> Loading portal-gui/src/components/federation/confirm-unsubscribe-zone-dialog.tsxdeleted 100644 → 0 +0 −74 Changes for portal-gui/src/components/federation/confirm-unsubscribe-zone-dialog.tsx: 0 added lines, 74 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import type { PartnerZone } from '@/types/federation'; export function ConfirmUnsubscribeZoneDialog({ zone, onOpenChange, onConfirm, }: { zone: PartnerZone; onOpenChange: (open: boolean) => void; onConfirm: () => Promise<void>; }) { const [pending, setPending] = useState(false); const [error, setError] = useState<string | null>(null); const handleConfirm = async () => { setPending(true); setError(null); try { await onConfirm(); onOpenChange(false); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to unsubscribe.'); } finally { setPending(false); } }; return ( <AlertDialog open onOpenChange={(open) => { if (!pending) onOpenChange(open); }} > <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Unsubscribe from zone?</AlertDialogTitle> <AlertDialogDescription> The partner releases the resources it reserved for us in " {zone.geographyDetails}". </AlertDialogDescription> </AlertDialogHeader> {error && <p className="text-sm text-destructive">{error}</p>} <AlertDialogFooter> <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel> <AlertDialogAction onClick={(event) => { event.preventDefault(); void handleConfirm(); }} disabled={pending} className="text-destructive-foreground bg-destructive hover:bg-destructive/90" > {pending ? 'Unsubscribing…' : 'Unsubscribe'} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); } portal-gui/src/components/federation/create-federation-dialog.tsx +21 −6 Changes for portal-gui/src/components/federation/create-federation-dialog.tsx: 21 added lines, 6 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { useSingleFlight } from '@/hooks/useSingleFlight'; import { Button } from '@/components/ui/button'; import { Dialog, Loading Loading @@ -35,18 +37,25 @@ export function CreateFederationDialog({ const [partnerStatusLink, setPartnerStatusLink] = useState(''); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState<string[]>([]); const [invalid, setInvalid] = useState<Set<string>>(new Set()); const once = useSingleFlight(); const handleSubmit = async () => { const handleSubmit = () => once(async () => { const result = federationRequestSchema.safeParse({ initialDate, partnerStatusLink: partnerStatusLink.trim(), }); if (!result.success) { setInvalid( new Set(result.error.issues.map((issue) => String(issue.path[0]))), ); setErrors(result.error.issues.map((issue) => issue.message)); return; } setSubmitting(true); setErrors([]); setInvalid(new Set()); try { await onCreate({ initialDate: new Date(result.data.initialDate).toISOString(), Loading @@ -55,10 +64,9 @@ export function CreateFederationDialog({ onOpenChange(false); } catch (err) { setErrors([err instanceof Error ? err.message : 'The request failed.']); } finally { setSubmitting(false); } }; }); return ( <Dialog Loading @@ -68,9 +76,9 @@ export function CreateFederationDialog({ }} > <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogHeader className="min-w-0"> <DialogTitle>Create Federation</DialogTitle> <DialogDescription> <DialogDescription className="wrap-anywhere"> Our Federation Manager asks {partner.name} to federate with us. Our own identity is filled in by the Federation Manager. </DialogDescription> Loading @@ -81,6 +89,8 @@ export function CreateFederationDialog({ <Label htmlFor="federation-initial-date">Initial date *</Label> <Input id="federation-initial-date" aria-required aria-invalid={invalid.has('initialDate') || undefined} type="datetime-local" value={initialDate} onChange={(e) => setInitialDate(e.target.value)} Loading @@ -92,6 +102,8 @@ export function CreateFederationDialog({ </Label> <Input id="federation-status-link" aria-required aria-invalid={invalid.has('partnerStatusLink') || undefined} className="font-mono" value={partnerStatusLink} onChange={(e) => setPartnerStatusLink(e.target.value)} Loading @@ -103,7 +115,10 @@ export function CreateFederationDialog({ </div> {errors.length > 0 && ( <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive"> <div role="alert" className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive" > {errors.join(' ')} </div> )} Loading portal-gui/src/components/federation/federation-state-badge.tsx +6 −1 Changes for portal-gui/src/components/federation/federation-state-badge.tsx: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -5,7 +5,12 @@ import type { FederationState } from '@/types/federation'; export function FederationStateBadge({ state }: { state: FederationState }) { if (state === 'loading') { return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />; return ( <> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> <span className="sr-only">Checking federation</span> </> ); } if (state === 'unknown') { return <span className="text-muted-foreground">—</span>; Loading Loading
portal-gui/src/components/data-table/dragable-row.tsx +7 −1 Changes for portal-gui/src/components/data-table/dragable-row.tsx: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -30,7 +30,13 @@ export function DraggableRow<TData>({ row }: DraggableRowProps<TData>) { }} > {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> <TableCell key={cell.id} className={ (cell.column.columnDef.meta as { className?: string } | undefined) ?.className } > {cell.column.id === 'drag' ? ( <button type="button" Loading
portal-gui/src/components/federation/confirm-delete-federation-dialog.tsx→portal-gui/src/components/federation/confirm-action-dialog.tsx +28 −17 Changes for portal-gui/src/components/federation/confirm-action-dialog.tsx: 28 added lines, 17 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { useState, type ReactNode } from 'react'; import { useSingleFlight } from '@/hooks/useSingleFlight'; import { AlertDialog, Loading @@ -10,34 +12,40 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import type { Partner } from '@/types/federation'; export function ConfirmDeleteFederationDialog({ partner, export function ConfirmActionDialog({ title, description, confirmLabel, pendingLabel, failureMessage, onOpenChange, onConfirm, }: { partner: Partner; title: string; description: ReactNode; confirmLabel: string; pendingLabel: string; failureMessage: string; onOpenChange: (open: boolean) => void; onConfirm: () => Promise<void>; }) { const [pending, setPending] = useState(false); const [error, setError] = useState<string | null>(null); const once = useSingleFlight(); const handleConfirm = async () => { const handleConfirm = () => once(async () => { setPending(true); setError(null); try { await onConfirm(); onOpenChange(false); } catch (err) { setError( err instanceof Error ? err.message : 'Failed to delete federation.', ); } finally { setError(err instanceof Error ? err.message : failureMessage); setPending(false); } }; }); return ( <AlertDialog Loading @@ -48,14 +56,17 @@ export function ConfirmDeleteFederationDialog({ > <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Delete federation?</AlertDialogTitle> <AlertDialogDescription> Ends our federation with "{partner.name}", including the zones we subscribed to. The partner registration stays. <AlertDialogTitle>{title}</AlertDialogTitle> <AlertDialogDescription className="wrap-anywhere"> {description} </AlertDialogDescription> </AlertDialogHeader> {error && <p className="text-sm text-destructive">{error}</p>} {error && ( <p role="alert" className="text-sm text-destructive"> {error} </p> )} <AlertDialogFooter> <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel> Loading @@ -67,7 +78,7 @@ export function ConfirmDeleteFederationDialog({ disabled={pending} className="text-destructive-foreground bg-destructive hover:bg-destructive/90" > {pending ? 'Deleting…' : 'Delete'} {pending ? pendingLabel : confirmLabel} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> Loading
portal-gui/src/components/federation/confirm-unsubscribe-zone-dialog.tsxdeleted 100644 → 0 +0 −74 Changes for portal-gui/src/components/federation/confirm-unsubscribe-zone-dialog.tsx: 0 added lines, 74 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import type { PartnerZone } from '@/types/federation'; export function ConfirmUnsubscribeZoneDialog({ zone, onOpenChange, onConfirm, }: { zone: PartnerZone; onOpenChange: (open: boolean) => void; onConfirm: () => Promise<void>; }) { const [pending, setPending] = useState(false); const [error, setError] = useState<string | null>(null); const handleConfirm = async () => { setPending(true); setError(null); try { await onConfirm(); onOpenChange(false); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to unsubscribe.'); } finally { setPending(false); } }; return ( <AlertDialog open onOpenChange={(open) => { if (!pending) onOpenChange(open); }} > <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Unsubscribe from zone?</AlertDialogTitle> <AlertDialogDescription> The partner releases the resources it reserved for us in " {zone.geographyDetails}". </AlertDialogDescription> </AlertDialogHeader> {error && <p className="text-sm text-destructive">{error}</p>} <AlertDialogFooter> <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel> <AlertDialogAction onClick={(event) => { event.preventDefault(); void handleConfirm(); }} disabled={pending} className="text-destructive-foreground bg-destructive hover:bg-destructive/90" > {pending ? 'Unsubscribing…' : 'Unsubscribe'} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); }
portal-gui/src/components/federation/create-federation-dialog.tsx +21 −6 Changes for portal-gui/src/components/federation/create-federation-dialog.tsx: 21 added lines, 6 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { useSingleFlight } from '@/hooks/useSingleFlight'; import { Button } from '@/components/ui/button'; import { Dialog, Loading Loading @@ -35,18 +37,25 @@ export function CreateFederationDialog({ const [partnerStatusLink, setPartnerStatusLink] = useState(''); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState<string[]>([]); const [invalid, setInvalid] = useState<Set<string>>(new Set()); const once = useSingleFlight(); const handleSubmit = async () => { const handleSubmit = () => once(async () => { const result = federationRequestSchema.safeParse({ initialDate, partnerStatusLink: partnerStatusLink.trim(), }); if (!result.success) { setInvalid( new Set(result.error.issues.map((issue) => String(issue.path[0]))), ); setErrors(result.error.issues.map((issue) => issue.message)); return; } setSubmitting(true); setErrors([]); setInvalid(new Set()); try { await onCreate({ initialDate: new Date(result.data.initialDate).toISOString(), Loading @@ -55,10 +64,9 @@ export function CreateFederationDialog({ onOpenChange(false); } catch (err) { setErrors([err instanceof Error ? err.message : 'The request failed.']); } finally { setSubmitting(false); } }; }); return ( <Dialog Loading @@ -68,9 +76,9 @@ export function CreateFederationDialog({ }} > <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogHeader className="min-w-0"> <DialogTitle>Create Federation</DialogTitle> <DialogDescription> <DialogDescription className="wrap-anywhere"> Our Federation Manager asks {partner.name} to federate with us. Our own identity is filled in by the Federation Manager. </DialogDescription> Loading @@ -81,6 +89,8 @@ export function CreateFederationDialog({ <Label htmlFor="federation-initial-date">Initial date *</Label> <Input id="federation-initial-date" aria-required aria-invalid={invalid.has('initialDate') || undefined} type="datetime-local" value={initialDate} onChange={(e) => setInitialDate(e.target.value)} Loading @@ -92,6 +102,8 @@ export function CreateFederationDialog({ </Label> <Input id="federation-status-link" aria-required aria-invalid={invalid.has('partnerStatusLink') || undefined} className="font-mono" value={partnerStatusLink} onChange={(e) => setPartnerStatusLink(e.target.value)} Loading @@ -103,7 +115,10 @@ export function CreateFederationDialog({ </div> {errors.length > 0 && ( <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive"> <div role="alert" className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive" > {errors.join(' ')} </div> )} Loading
portal-gui/src/components/federation/federation-state-badge.tsx +6 −1 Changes for portal-gui/src/components/federation/federation-state-badge.tsx: 6 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -5,7 +5,12 @@ import type { FederationState } from '@/types/federation'; export function FederationStateBadge({ state }: { state: FederationState }) { if (state === 'loading') { return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />; return ( <> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> <span className="sr-only">Checking federation</span> </> ); } if (state === 'unknown') { return <span className="text-muted-foreground">—</span>; Loading