From 87226e05c40be83299187b35ba1ca4ca2113fbe3 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 24 Jan 2026 12:23:12 -0500 Subject: [PATCH] feat(ui): add expandable provider cards with account controls - add ExpandableAccountList for inline account management - add expand/collapse to ProviderCard with smooth transitions - wire pause/resume/solo mutations to auth-monitor index - add paused field to AccountRow type for UI display --- .../components/expandable-account-list.tsx | 83 ++++++++++ .../auth-monitor/components/provider-card.tsx | 144 ++++++++++++------ .../monitoring/auth-monitor/hooks.ts | 1 + .../monitoring/auth-monitor/index.tsx | 26 ++++ .../monitoring/auth-monitor/types.ts | 2 + 5 files changed, 213 insertions(+), 43 deletions(-) create mode 100644 ui/src/components/monitoring/auth-monitor/components/expandable-account-list.tsx diff --git a/ui/src/components/monitoring/auth-monitor/components/expandable-account-list.tsx b/ui/src/components/monitoring/auth-monitor/components/expandable-account-list.tsx new file mode 100644 index 00000000..8f8627bf --- /dev/null +++ b/ui/src/components/monitoring/auth-monitor/components/expandable-account-list.tsx @@ -0,0 +1,83 @@ +/** + * Expandable Account List - Account rows with toggle and solo controls + * Used in ProviderCard when expanded to show individual account controls + */ + +import { Radio } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Switch } from '@/components/ui/switch'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import { cleanEmail } from '../utils'; +import type { AccountRow } from '../types'; + +interface ExpandableAccountListProps { + accounts: AccountRow[]; + privacyMode: boolean; + onPauseToggle: (accountId: string, paused: boolean) => void; + onSoloMode: (accountId: string) => void; + isPausingAccount?: boolean; + isSoloingAccount?: boolean; +} + +export function ExpandableAccountList({ + accounts, + privacyMode, + onPauseToggle, + onSoloMode, + isPausingAccount, + isSoloingAccount, +}: ExpandableAccountListProps) { + return ( +
+ {accounts.map((acc) => ( +
+
+
+ + {privacyMode ? '••••••' : cleanEmail(acc.email)} + +
+
+ + + + + + + Solo mode - activate this, pause others + + + + { + onPauseToggle(acc.id, !checked); + }} + disabled={isPausingAccount} + className="scale-75" + onClick={(e) => e.stopPropagation()} + /> +
+
+ ))} +
+ ); +} diff --git a/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx index 56faedbc..56818c73 100644 --- a/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx +++ b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx @@ -1,9 +1,11 @@ /** - * ProviderCard - Provider status card with account color dots and stats + * ProviderCard - Provider status card with expandable account controls + * Click to expand and show individual account toggle/solo buttons */ import type React from 'react'; -import { ChevronRight, AlertTriangle } from 'lucide-react'; +import { useState } from 'react'; +import { ChevronRight, ChevronDown, AlertTriangle } from 'lucide-react'; import { cn, STATUS_COLORS } from '@/lib/utils'; import { PROVIDER_COLORS } from '@/lib/provider-config'; import { ProviderIcon } from '@/components/shared/provider-icon'; @@ -11,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import type { ProviderStats } from '../types'; import { getSuccessRate, cleanEmail } from '../utils'; import { InlineStatsBadge } from './inline-stats-badge'; +import { ExpandableAccountList } from './expandable-account-list'; interface ProviderCardProps { stats: ProviderStats; @@ -19,6 +22,10 @@ interface ProviderCardProps { onSelect: () => void; onMouseEnter: () => void; onMouseLeave: () => void; + onPauseToggle?: (accountId: string, paused: boolean) => void; + onSoloMode?: (accountId: string) => void; + isPausingAccount?: boolean; + isSoloingAccount?: boolean; } export function ProviderCard({ @@ -28,15 +35,32 @@ export function ProviderCard({ onSelect, onMouseEnter, onMouseLeave, + onPauseToggle, + onSoloMode, + isPausingAccount, + isSoloingAccount, }: ProviderCardProps) { + const [isExpanded, setIsExpanded] = useState(false); const successRate = getSuccessRate(stats.successCount, stats.failureCount); const providerColor = PROVIDER_COLORS[stats.provider.toLowerCase()] || '#6b7280'; + // Only expandable if account control callbacks are provided + const isExpandable = !!(onPauseToggle && onSoloMode); + + const handleClick = () => { + if (isExpandable) { + setIsExpanded(!isExpanded); + } else { + onSelect(); + } + }; + return (
- + {isExpandable ? ( + isExpanded ? ( + + ) : ( + + ) + ) : ( + + )}
@@ -104,40 +141,61 @@ export function ProviderCard({
{/* Account color dots with warning for agy accounts missing projectId */} -
- {stats.accounts.slice(0, 5).map((acc) => { - const isMissingProjectId = stats.provider === 'agy' && !acc.projectId; - return ( -
-
- {isMissingProjectId && ( - - - - - - - Missing Project ID - re-add account to fix - - - - )} -
- ); - })} - {stats.accounts.length > 5 && ( - - +{stats.accounts.length - 5} - - )} -
+ {!isExpanded && ( +
+ {stats.accounts.slice(0, 5).map((acc) => { + const isMissingProjectId = stats.provider === 'agy' && !acc.projectId; + return ( +
+
+ {isMissingProjectId && ( + + + + + + + Missing Project ID - re-add account to fix + + + + )} +
+ ); + })} + {stats.accounts.length > 5 && ( + + +{stats.accounts.length - 5} + + )} +
+ )} + + {/* Expandable account list */} + {isExpandable && ( +
+ +
+ )} ); } diff --git a/ui/src/components/monitoring/auth-monitor/hooks.ts b/ui/src/components/monitoring/auth-monitor/hooks.ts index dd19b8bc..4af76b84 100644 --- a/ui/src/components/monitoring/auth-monitor/hooks.ts +++ b/ui/src/components/monitoring/auth-monitor/hooks.ts @@ -100,6 +100,7 @@ export function useAuthMonitorData(): AuthMonitorData { lastUsedAt: realStats?.lastUsedAt ?? account.lastUsedAt, color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length], projectId: account.projectId, + paused: account.paused, }; accountsList.push(row); providerData.accounts.push(row); diff --git a/ui/src/components/monitoring/auth-monitor/index.tsx b/ui/src/components/monitoring/auth-monitor/index.tsx index d43e1d48..6cd55d83 100644 --- a/ui/src/components/monitoring/auth-monitor/index.tsx +++ b/ui/src/components/monitoring/auth-monitor/index.tsx @@ -9,6 +9,7 @@ import { STATUS_COLORS } from '@/lib/utils'; import { Skeleton } from '@/components/ui/skeleton'; import { AccountFlowViz } from '@/components/account-flow-viz'; import { usePrivacy } from '@/contexts/privacy-context'; +import { usePauseAccount, useResumeAccount, useSoloAccount } from '@/hooks/use-cliproxy'; import { Activity, CheckCircle2, XCircle, Radio } from 'lucide-react'; import { useAuthMonitorData } from './hooks'; @@ -33,11 +34,30 @@ export function AuthMonitor() { const [selectedProvider, setSelectedProvider] = useState(null); const [hoveredProvider, setHoveredProvider] = useState(null); + // Account control mutations + const pauseMutation = usePauseAccount(); + const resumeMutation = useResumeAccount(); + const soloMutation = useSoloAccount(); + // Get selected provider data for detail view const selectedProviderData = selectedProvider ? providerStats.find((ps) => ps.provider === selectedProvider) : null; + const handlePauseToggle = (provider: string, accountId: string, paused: boolean) => { + if (pauseMutation.isPending || resumeMutation.isPending) return; + if (paused) { + pauseMutation.mutate({ provider, accountId }); + } else { + resumeMutation.mutate({ provider, accountId }); + } + }; + + const handleSoloMode = (provider: string, accountId: string) => { + if (soloMutation.isPending) return; + soloMutation.mutate({ provider, accountId }); + }; + if (isLoading) { return (
@@ -137,6 +157,12 @@ export function AuthMonitor() { onSelect={() => setSelectedProvider(ps.provider)} onMouseEnter={() => setHoveredProvider(ps.provider)} onMouseLeave={() => setHoveredProvider(null)} + onPauseToggle={(accountId, paused) => + handlePauseToggle(ps.provider, accountId, paused) + } + onSoloMode={(accountId) => handleSoloMode(ps.provider, accountId)} + isPausingAccount={pauseMutation.isPending || resumeMutation.isPending} + isSoloingAccount={soloMutation.isPending} /> ))}
diff --git a/ui/src/components/monitoring/auth-monitor/types.ts b/ui/src/components/monitoring/auth-monitor/types.ts index 408b1e71..1490c134 100644 --- a/ui/src/components/monitoring/auth-monitor/types.ts +++ b/ui/src/components/monitoring/auth-monitor/types.ts @@ -14,6 +14,8 @@ export interface AccountRow { color: string; /** GCP Project ID (Antigravity only) - read-only */ projectId?: string; + /** Whether account is paused (skipped in quota rotation) */ + paused?: boolean; } export interface ProviderStats {