From e8520133f98c58a254e0581de0efd83dd67fec23 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 1 Apr 2026 16:28:09 -0400 Subject: [PATCH] fix(account-flow): restore grouped quota hover details --- .../account/flow-viz/account-card.tsx | 66 ++++---- .../account/flow-viz/account-card.test.tsx | 141 ++++++++++++++++++ 2 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 ui/tests/unit/components/account/flow-viz/account-card.test.tsx diff --git a/ui/src/components/account/flow-viz/account-card.tsx b/ui/src/components/account/flow-viz/account-card.tsx index b77f6dd7..b4f65156 100644 --- a/ui/src/components/account/flow-viz/account-card.tsx +++ b/ui/src/components/account/flow-viz/account-card.tsx @@ -3,9 +3,16 @@ */ import { AccountSurfaceCard } from '@/components/account/shared/account-surface-card'; +import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { formatQuotaPercent, getProviderMinQuota, getQuotaFailureInfo, cn } from '@/lib/utils'; +import { + cn, + formatQuotaPercent, + getProviderMinQuota, + getProviderResetTime, + getQuotaFailureInfo, +} from '@/lib/utils'; import { GripVertical, Loader2, Pause, Play } from 'lucide-react'; import { useAccountQuota, @@ -207,36 +214,45 @@ export function AccountCard({ hasGroupedVariants && showQuota ? (account.variants ?? []).map((variant, index) => { const quotaQuery = variantQuotaQueries[index]; - const minQuota = getProviderMinQuota(account.provider, quotaQuery?.data); + const quota = quotaQuery?.data; + const minQuota = getProviderMinQuota(account.provider, quota); + const resetTime = getProviderResetTime(account.provider, quota); const quotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null; const quotaValue = quotaLabel !== null ? Number(quotaLabel) : null; - const failureInfo = getQuotaFailureInfo(quotaQuery?.data); + const failureInfo = getQuotaFailureInfo(quota); const label = variant.audienceLabel ?? variant.detailLabel ?? cleanEmail(variant.email); return ( -
-
- {label} - - {quotaQuery?.isLoading - ? t('accountCard.quotaLoading') - : quotaValue !== null - ? `${quotaLabel}%` - : failureInfo?.label || t('accountCard.quotaUnavailable')} - -
- {quotaValue !== null && ( -
-
+ + +
+
+ {label} + + {quotaQuery?.isLoading + ? t('accountCard.quotaLoading') + : quotaValue !== null + ? `${quotaLabel}%` + : failureInfo?.label || t('accountCard.quotaUnavailable')} + +
+ {quotaValue !== null && ( +
+
+
+ )}
- )} -
+
+ + + +
); }) : null; diff --git a/ui/tests/unit/components/account/flow-viz/account-card.test.tsx b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx new file mode 100644 index 00000000..8086b0a9 --- /dev/null +++ b/ui/tests/unit/components/account/flow-viz/account-card.test.tsx @@ -0,0 +1,141 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { render, screen, userEvent } from '@tests/setup/test-utils'; +import { AccountCard } from '@/components/account/flow-viz/account-card'; +import type { AccountData } from '@/components/account/flow-viz/types'; +import type { CodexQuotaResult } from '@/lib/api-client'; +import { useAccountQuota, useAccountQuotas } from '@/hooks/use-cliproxy-stats'; + +vi.mock('@/hooks/use-cliproxy-stats', async () => { + const actual = await vi.importActual( + '@/hooks/use-cliproxy-stats' + ); + + return { + ...actual, + useAccountQuota: vi.fn(), + useAccountQuotas: vi.fn(), + }; +}); + +const mockedUseAccountQuota = vi.mocked(useAccountQuota); +const mockedUseAccountQuotas = vi.mocked(useAccountQuotas); + +function makeCodexQuota(planType: 'plus' | 'team', fiveHour: number, weekly: number) { + return { + success: true, + planType, + lastUpdated: Date.now(), + windows: [ + { + label: 'Primary', + usedPercent: 100 - fiveHour, + remainingPercent: fiveHour, + resetAfterSeconds: 60 * 60, + resetAt: '2026-04-04T08:44:00Z', + }, + { + label: 'Secondary', + usedPercent: 100 - weekly, + remainingPercent: weekly, + resetAfterSeconds: 7 * 24 * 60 * 60, + resetAt: '2026-04-08T10:20:00Z', + }, + ], + coreUsage: { + fiveHour: { + label: 'Primary', + remainingPercent: fiveHour, + resetAfterSeconds: 60 * 60, + resetAt: '2026-04-04T08:44:00Z', + }, + weekly: { + label: 'Secondary', + remainingPercent: weekly, + resetAfterSeconds: 7 * 24 * 60 * 60, + resetAt: '2026-04-08T10:20:00Z', + }, + }, + } satisfies CodexQuotaResult; +} + +const groupedAccount: AccountData = { + id: 'codex:user@example.com', + email: 'user@example.com', + tokenFile: 'codex-user.json', + provider: 'codex', + successCount: 9, + failureCount: 1, + color: '#1e6091', + variants: [ + { + id: 'business@example.com', + email: 'user@example.com', + tokenFile: 'codex-business.json', + isDefault: false, + successCount: 5, + failureCount: 0, + audience: 'business', + audienceLabel: 'Business', + detailLabel: null, + }, + { + id: 'personal@example.com', + email: 'user@example.com', + tokenFile: 'codex-personal.json', + isDefault: true, + successCount: 4, + failureCount: 1, + audience: 'personal', + audienceLabel: 'Personal', + detailLabel: null, + }, + ], +}; + +describe('AccountCard grouped quota tooltip', () => { + beforeEach(() => { + mockedUseAccountQuota.mockReturnValue({ + data: undefined, + isLoading: false, + } as ReturnType); + + mockedUseAccountQuotas.mockReturnValue([ + { + data: makeCodexQuota('team', 95, 81), + isLoading: false, + }, + { + data: makeCodexQuota('plus', 64, 42), + isLoading: false, + }, + ] as ReturnType); + }); + + it('shows provider quota tooltip content for each grouped personal/business row on hover', async () => { + render( + undefined} + onMouseLeave={() => undefined} + onPointerDown={() => undefined} + onPointerMove={() => undefined} + onPointerUp={() => undefined} + /> + ); + + await userEvent.hover(screen.getByText('Business')); + expect((await screen.findAllByText('Plan: team')).length).toBeGreaterThan(0); + expect(screen.getAllByText('5h usage limit').length).toBeGreaterThan(0); + + await userEvent.hover(screen.getByText('Personal')); + expect((await screen.findAllByText('Plan: plus')).length).toBeGreaterThan(0); + expect(screen.getAllByText('Weekly usage limit').length).toBeGreaterThan(0); + }); +});