diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index c8e748ad..3ab06e1e 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -9,9 +9,11 @@ import { useCustomer } from "autumn-js/react" import { Check, ChevronDown, - Clock, FolderOpen, + History, Loader, + Loader2, + Play, Trash2, Zap, } from "lucide-react" @@ -30,6 +32,11 @@ import { DropdownMenuTrigger, } from "@ui/components/dropdown-menu" import { RemoveConnectionDialog } from "@/components/remove-connection-dialog" +import { SyncStatusBadge } from "@/components/settings/sync-status-badge" +import { SyncHistorySheet } from "@/components/settings/sync-history-sheet" +import { useTriggerSync } from "@/hooks/use-trigger-sync" +import { formatRelativeTime } from "@/components/settings/sync-utils" +import type { ImportProvider } from "@/components/settings/sync-utils" type GDriveSyncScope = "scoped" | "full" @@ -71,17 +78,20 @@ const CONNECTORS: Record< }, } as const -function formatRelativeTime(date: string | null | undefined): string { - if (!date) return "Never" - const d = new Date(date) - const diffMs = Date.now() - d.getTime() - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) - const diffDays = Math.floor(diffHours / 24) - if (diffHours < 1) return "Just now" - if (diffHours < 24) return `${diffHours}h ago` - if (diffDays === 1) return "Yesterday" - if (diffDays < 7) return `${diffDays} days ago` - return d.toLocaleDateString() +/** Extract typed metadata from a connection, with runtime validation. */ +function getConnectionMeta(connection: Connection) { + const m = connection.metadata as Record | undefined + return { + syncInProgress: m?.syncInProgress === true, + lastSyncedAt: + typeof m?.lastSyncedAt === "number" ? m.lastSyncedAt : undefined, + documentCount: typeof m?.documentCount === "number" ? m.documentCount : 0, + } +} + +/** Check if a connection's auth token has expired. */ +function isConnectionExpired(connection: Connection): boolean { + return !!connection.expiresAt && new Date(connection.expiresAt) <= new Date() } function ConnectionRow({ @@ -89,18 +99,24 @@ function ConnectionRow({ onDelete, isDeleting, projects, + onTriggerSync, + isSyncing, + onViewHistory, }: { connection: Connection onDelete: () => void isDeleting: boolean projects: Project[] + onTriggerSync: () => void + isSyncing: boolean + onViewHistory: () => void }) { const config = CONNECTORS[connection.provider as ConnectorProvider] if (!config) return null const Icon = config.icon - const isConnected = - !connection.expiresAt || new Date(connection.expiresAt) > new Date() + const meta = getConnectionMeta(connection) + const expired = isConnectionExpired(connection) const getProjectName = (tag: string): string => { if (tag === DEFAULT_PROJECT_ID) return "Default" @@ -110,12 +126,8 @@ function ConnectionRow({ ) } - const documentCount = (connection.metadata?.documentCount as number) ?? 0 - const containerTags = ( - connection as Connection & { containerTags?: string[] } - ).containerTags - const projectName = containerTags?.[0] - ? getProjectName(containerTags[0]) + const projectName = connection.containerTags?.[0] + ? getProjectName(connection.containerTags[0]) : null return ( @@ -138,23 +150,11 @@ function ConnectionRow({ > {config.title} -
-
- - {isConnected ? "Connected" : "Disconnected"} - -
+
- +
+ + + +
@@ -186,17 +231,11 @@ function ConnectionRow({
)} -
- - - {formatRelativeTime(connection.createdAt)} - -
+ + Last synced: {formatRelativeTime(meta.lastSyncedAt)} +
- {documentCount} + {meta.documentCount} ({ open: false, connection: null }) + const triggerSync = useTriggerSync() + const [syncHistorySheet, setSyncHistorySheet] = useState<{ + open: boolean + connection: Connection | null + }>({ open: false, connection: null }) const projects = (queryClient.getQueryData(["projects"]) || []) as Project[] @@ -282,7 +326,13 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { return response.data as Connection[] }, staleTime: 30 * 1000, - refetchInterval: 60 * 1000, + refetchInterval: (query) => { + const conns = query.state.data as Connection[] | undefined + if (conns?.some((c) => getConnectionMeta(c).syncInProgress)) { + return 5000 + } + return 60 * 1000 + }, refetchIntervalInBackground: true, }) @@ -644,6 +694,21 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { projects={projects} onDelete={() => setRemoveDialog({ open: true, connection })} isDeleting={deleteConnectionMutation.isPending} + onTriggerSync={() => + triggerSync.mutate({ + connectionId: connection.id, + provider: connection.provider as ImportProvider, + containerTags: connection.containerTags, + }) + } + isSyncing={ + (triggerSync.isPending && + triggerSync.variables?.connectionId === connection.id) || + getConnectionMeta(connection).syncInProgress + } + onViewHistory={() => + setSyncHistorySheet({ open: true, connection }) + } /> ))}
@@ -734,6 +799,13 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { }} isDeleting={deleteConnectionMutation.isPending} /> + { + if (!open) setSyncHistorySheet({ open: false, connection: null }) + }} + connection={syncHistorySheet.connection} + /> ) }