diff --git a/packages/web/src/components/modals/ConnectedInstances.tsx b/packages/web/src/components/modals/ConnectedInstances.tsx index 527bb851..85f68d7d 100644 --- a/packages/web/src/components/modals/ConnectedInstances.tsx +++ b/packages/web/src/components/modals/ConnectedInstances.tsx @@ -1,8 +1,11 @@ import React, { useState } from 'react'; -import type { InstanceInfoResponse } from '@backspace/shared'; +import ReactDOM from 'react-dom'; +import type { InstanceInfoResponse, FederationRegistryEntry } from '@backspace/shared'; import { useInstanceStore, DifferentPasswordError } from '../../stores/instanceStore'; +import type { ConnectedInstance } from '../../stores/instanceStore'; import { useAuthStore } from '../../stores/authStore'; import { isElectron } from '../../platform/platform'; +import { ConfirmDialog } from '../ui/ConfirmDialog'; // ─── Status indicator ──────────────────────────────────────────────────────── @@ -15,6 +18,53 @@ function StatusDot({ status }: { status: string }) { return
; } +// ─── Registry status helpers ──────────────────────────────────────────────── + +function registryStatusColor(status: string): string { + switch (status) { + case 'connected': return 'bg-status-online/15 text-status-online'; + case 'disconnected': return 'bg-white/5 text-txt-tertiary'; + case 'unreachable': return 'bg-accent-amber/15 text-accent-amber'; + case 'auth_expired': return 'bg-accent-rose/15 text-accent-rose'; + default: return 'bg-white/5 text-txt-tertiary'; + } +} + +function registryStatusDotColor(status: string): string { + switch (status) { + case 'connected': return 'bg-status-online'; + case 'unreachable': return 'bg-accent-amber'; + case 'auth_expired': return 'bg-accent-rose'; + default: return 'bg-txt-tertiary'; + } +} + +function registryStatusLabel(status: string): string { + switch (status) { + case 'connected': return 'Connected'; + case 'disconnected': return 'Disconnected'; + case 'unreachable': return 'Unreachable'; + case 'auth_expired': return 'Auth Expired'; + default: return status; + } +} + +function formatRelativeTime(timestamp: number | null): string { + if (!timestamp) return 'Never'; + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 60) return 'Just now'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function formatAbsoluteDate(timestamp: number): string { + return new Date(timestamp).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + // ─── Add Instance flow ─────────────────────────────────────────────────────── type AddStep = 'url' | 'auth' | 'done'; @@ -263,141 +313,528 @@ function AddInstanceFlow({ onDone }: { onDone: () => void }) { ); } -// ─── Main component ────────────────────────────────────────────────────────── +// ─── Filter / Sort types ──────────────────────────────────────────────────── -function InstanceRow({ inst }: { inst: import('../../stores/instanceStore').ConnectedInstance }) { - const disconnectInstance = useInstanceStore((s) => s.disconnectInstance); - const reconnectInstance = useInstanceStore((s) => s.reconnectInstance); - const reauthenticateInstance = useInstanceStore((s) => s.reauthenticateInstance); - const hasPendingSync = useInstanceStore((s) => s.pendingSyncOrigins.includes(inst.origin)); +type StatusFilter = 'all' | 'connected' | 'disconnected' | 'issues'; +type SortBy = 'name' | 'dateAdded' | 'lastConnected'; - const [showReauth, setShowReauth] = useState(false); - const [reauthPassword, setReauthPassword] = useState(''); - const [reauthLoading, setReauthLoading] = useState(false); - const [reauthError, setReauthError] = useState(''); +// ─── RegistryFilterBar ────────────────────────────────────────────────────── - const isTokenless = !inst.token; +function RegistryFilterBar({ + filter, + setFilter, + sortBy, + setSortBy, + counts, +}: { + filter: StatusFilter; + setFilter: (f: StatusFilter) => void; + sortBy: SortBy; + setSortBy: (s: SortBy) => void; + counts: { all: number; connected: number; disconnected: number; issues: number }; +}) { + const [sortOpen, setSortOpen] = useState(false); - const handleReauth = async () => { - if (!reauthPassword) return; - setReauthError(''); - setReauthLoading(true); - try { - await reauthenticateInstance(inst.origin, reauthPassword); - setShowReauth(false); - setReauthPassword(''); - } catch (err) { - setReauthError((err as Error).message); - } finally { - setReauthLoading(false); - } - }; + const tabs: Array<{ key: StatusFilter; label: string; count: number }> = [ + { key: 'all', label: 'All', count: counts.all }, + { key: 'connected', label: 'Connected', count: counts.connected }, + { key: 'disconnected', label: 'Disconnected', count: counts.disconnected }, + { key: 'issues', label: 'Issues', count: counts.issues }, + ]; + + const sortOptions: Array<{ key: SortBy; label: string }> = [ + { key: 'name', label: 'Name (A-Z)' }, + { key: 'dateAdded', label: 'Date Added' }, + { key: 'lastConnected', label: 'Last Connected' }, + ]; return ( -