diff --git a/ui/src/components/account-flow-viz.tsx b/ui/src/components/account-flow-viz.tsx index 8ed2a6b0..3951dd40 100644 --- a/ui/src/components/account-flow-viz.tsx +++ b/ui/src/components/account-flow-viz.tsx @@ -4,11 +4,12 @@ * Inspired by modern dark theme design with glass panels and glow effects */ -import { useRef, useEffect, useState, useCallback } from 'react'; +import { useRef, useEffect, useState, useCallback, useMemo } from 'react'; import { cn } from '@/lib/utils'; import { ProviderIcon } from '@/components/provider-icon'; import { PROVIDER_COLORS } from '@/lib/provider-config'; -import { ChevronRight, X, CheckCircle2, XCircle, Clock } from 'lucide-react'; +import { STATUS_COLORS } from '@/lib/utils'; +import { ChevronRight, X, CheckCircle2, XCircle, Clock, Activity } from 'lucide-react'; interface AccountData { id: string; @@ -32,6 +33,157 @@ interface AccountFlowVizProps { onBack?: () => void; } +interface ConnectionEvent { + id: string; + timestamp: Date; + accountEmail: string; + status: 'success' | 'failed' | 'pending'; + latencyMs?: number; +} + +/** Generate mock connection events based on account data */ +function generateConnectionEvents(accounts: AccountData[]): ConnectionEvent[] { + const events: ConnectionEvent[] = []; + const now = new Date(); + + accounts.forEach((account) => { + // Generate events based on success/failure counts + const successEvents = Math.min(account.successCount, 3); + const failEvents = Math.min(account.failureCount, 2); + + for (let i = 0; i < successEvents; i++) { + events.push({ + id: `${account.id}-s-${i}`, + timestamp: new Date(now.getTime() - Math.random() * 3600000), // Last hour + accountEmail: account.email, + status: 'success', + latencyMs: Math.floor(Math.random() * 500) + 50, + }); + } + + for (let i = 0; i < failEvents; i++) { + events.push({ + id: `${account.id}-f-${i}`, + timestamp: new Date(now.getTime() - Math.random() * 7200000), // Last 2 hours + accountEmail: account.email, + status: 'failed', + }); + } + }); + + // Sort by timestamp descending (most recent first) + return events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); +} + +/** Format timestamp for timeline display */ +function formatTimelineTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + + if (diffMins < 1) return 'now'; + if (diffMins < 60) return `${diffMins}m`; + const diffHours = Math.floor(diffMins / 60); + if (diffHours < 24) return `${diffHours}h`; + return `${Math.floor(diffHours / 24)}d`; +} + +/** Connection Timeline Component - right sidebar panel */ +function ConnectionTimeline({ events }: { events: ConnectionEvent[] }) { + if (events.length === 0) { + return ( +
+
No recent connections
+
+ ); + } + + return ( +
+ {/* Header */} +
+ + + Connection Timeline + +
+ + {/* Timeline container */} +
+
+ {/* Vertical line */} +
+ + {/* Events */} +
+ {events.slice(0, 8).map((event) => { + const statusColor = + event.status === 'success' + ? STATUS_COLORS.success + : event.status === 'failed' + ? STATUS_COLORS.failed + : STATUS_COLORS.degraded; + + return ( +
+ {/* Timeline dot */} +
+ + {/* Event content */} +
+
+ + {cleanEmail(event.accountEmail)} + + + {formatTimelineTime(event.timestamp)} + +
+
+ + {event.status} + + {event.latencyMs && ( + + {event.latencyMs}ms + + )} +
+
+
+ ); + })} +
+ + {/* Show more indicator */} + {events.length > 8 && ( +
+ + +{events.length - 8} more events + +
+ )} +
+
+
+ ); +} + /** Strip common email domains for cleaner display */ function cleanEmail(email: string): string { return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, ''); @@ -64,6 +216,9 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { const maxRequests = Math.max(...accounts.map((a) => a.successCount + a.failureCount), 1); const totalRequests = accounts.reduce((acc, a) => acc + a.successCount + a.failureCount, 0); + // Generate connection events for timeline + const connectionEvents = useMemo(() => generateConnectionEvents(accounts), [accounts]); + // Calculate SVG paths for bezier curves const calculatePaths = useCallback(() => { if (!containerRef.current || !svgRef.current) return; @@ -116,233 +271,246 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) { const providerColor = PROVIDER_COLORS[providerData.provider.toLowerCase()] || '#6b7280'; return ( -
+
{/* Back button */} {onBack && ( )} - {/* Main visualization area */} -
- {/* SVG Canvas (Background) */} - - - - - - - - {paths.map((d, i) => { - const account = accounts[i]; - const total = account.successCount + account.failureCount; - const strokeWidth = Math.max(2, (total / maxRequests) * 10); - const isHovered = hoveredAccount === i; - const isDimmed = hoveredAccount !== null && hoveredAccount !== i; + {/* Main visualization area - 2 column layout: Flow viz (left) | Timeline (right) */} +
+ {/* Left Section: Account → Provider Flow - expands to fill available space */} +
+ {/* SVG Canvas (Background) */} + + + + + + + + {paths.map((d, i) => { + const account = accounts[i]; + const total = account.successCount + account.failureCount; + const strokeWidth = Math.max(2, (total / maxRequests) * 10); + const isHovered = hoveredAccount === i; + const isDimmed = hoveredAccount !== null && hoveredAccount !== i; - return ( - - ); - })} - + return ( + + ); + })} + - {/* Left Column: Source Accounts */} -
- {accounts.map((account, i) => { - const total = account.successCount + account.failureCount; - const isHovered = hoveredAccount === i; + {/* Source Accounts */} +
+ {accounts.map((account, i) => { + const total = account.successCount + account.failureCount; + const isHovered = hoveredAccount === i; - return ( -
setSelectedAccount(account)} - onMouseEnter={() => setHoveredAccount(i)} - onMouseLeave={() => setHoveredAccount(null)} - className={cn( - 'group/card relative rounded-lg p-3 pr-6 cursor-pointer transition-all duration-300', - 'bg-muted/30 dark:bg-zinc-900/60 backdrop-blur-sm', - 'border border-border/50 dark:border-white/[0.08]', - 'border-l-2 hover:translate-x-1', - isHovered && 'bg-muted/50 dark:bg-zinc-800/60' - )} - style={{ borderLeftColor: account.color }} - > -
- - {cleanEmail(account.email)} - - setSelectedAccount(account)} + onMouseEnter={() => setHoveredAccount(i)} + onMouseLeave={() => setHoveredAccount(null)} + className={cn( + 'group/card relative rounded-lg p-3 pr-6 cursor-pointer transition-all duration-300', + 'bg-muted/30 dark:bg-zinc-900/60 backdrop-blur-sm', + 'border border-border/50 dark:border-white/[0.08]', + 'border-l-2 hover:translate-x-1', + isHovered && 'bg-muted/50 dark:bg-zinc-800/60' + )} + style={{ borderLeftColor: account.color }} + > +
+ + {cleanEmail(account.email)} + + +
+
+ + {total.toLocaleString()} reqs + +
+ {account.failureCount > 0 && ( +
+ )} +
+
+
+ {/* Connector Dot */} +
-
- - {total.toLocaleString()} reqs - -
- {account.failureCount > 0 && ( -
- )} -
-
-
- {/* Connector Dot */} -
-
- ); - })} -
+ ); + })} +
- {/* Right Column: Destination Provider */} -
-
- {/* Connector Point */} + {/* Destination Provider */} +
+ > + {/* Connector Point */} +
-
- -
-

- {providerData.displayName} -

-

Provider

+
+ +
+

+ {providerData.displayName} +

+

+ Provider +

+
-
-
-
- Total Requests - {totalRequests.toLocaleString()} -
-
- Accounts - {accounts.length} -
-
-
+
+
+ Total Requests + + {totalRequests.toLocaleString()} + +
+
+ Accounts + {accounts.length} +
+
+
+
+ + {/* Right Section: Connection Timeline - Fixed compact width */} +
+ +
- {/* Detail Panel (Bottom slide-up) */} + {/* Detail Panel - slides in from bottom, pushes content */}
-
- +
+
+ - {selectedAccount && ( -
- {/* Account Info */} -
-
-
- - {cleanEmail(selectedAccount.email)} - + {selectedAccount && ( +
+ {/* Account Info */} +
+
+
+ + {cleanEmail(selectedAccount.email)} + +
+
+ Source Account +
-
- Source Account + + {/* Stats */} +
+
+ + SUCCESSFUL +
+
+ {selectedAccount.successCount.toLocaleString()} +
+
+ +
+
+ + FAILED +
+
+ {selectedAccount.failureCount.toLocaleString()} +
+
+ +
+
+ + LAST SYNC +
+
+ {getTimeAgo(selectedAccount.lastUsedAt)} +
- - {/* Stats */} -
-
- - SUCCESSFUL -
-
- {selectedAccount.successCount.toLocaleString()} -
-
- -
-
- - FAILED -
-
- {selectedAccount.failureCount.toLocaleString()} -
-
- -
-
- - LAST SYNC -
-
- {getTimeAgo(selectedAccount.lastUsedAt)} -
-
-
- )} + )} +