fix(account-flow): restore grouped quota hover details

This commit is contained in:
Tam Nhu Tran
2026-04-01 16:29:27 -04:00
parent 08045500ed
commit e8520133f9
2 changed files with 182 additions and 25 deletions
@@ -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 (
<div key={variant.id} className="space-y-0.5">
<div className="flex items-center justify-between gap-2 text-[8px]">
<span className="text-muted-foreground/80 truncate">{label}</span>
<span className="font-mono text-foreground/80 shrink-0">
{quotaQuery?.isLoading
? t('accountCard.quotaLoading')
: quotaValue !== null
? `${quotaLabel}%`
: failureInfo?.label || t('accountCard.quotaUnavailable')}
</span>
</div>
{quotaValue !== null && (
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all',
getCompactQuotaColor(quotaValue)
)}
style={{ width: `${quotaValue}%` }}
/>
<Tooltip key={variant.id}>
<TooltipTrigger asChild>
<div className="space-y-0.5 cursor-help">
<div className="flex items-center justify-between gap-2 text-[8px]">
<span className="text-muted-foreground/80 truncate">{label}</span>
<span className="font-mono text-foreground/80 shrink-0">
{quotaQuery?.isLoading
? t('accountCard.quotaLoading')
: quotaValue !== null
? `${quotaLabel}%`
: failureInfo?.label || t('accountCard.quotaUnavailable')}
</span>
</div>
{quotaValue !== null && (
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all',
getCompactQuotaColor(quotaValue)
)}
style={{ width: `${quotaValue}%` }}
/>
</div>
)}
</div>
)}
</div>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<QuotaTooltipContent quota={quota} resetTime={resetTime} />
</TooltipContent>
</Tooltip>
);
})
: null;
@@ -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<typeof import('@/hooks/use-cliproxy-stats')>(
'@/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<typeof useAccountQuota>);
mockedUseAccountQuotas.mockReturnValue([
{
data: makeCodexQuota('team', 95, 81),
isLoading: false,
},
{
data: makeCodexQuota('plus', 64, 42),
isLoading: false,
},
] as ReturnType<typeof useAccountQuotas>);
});
it('shows provider quota tooltip content for each grouped personal/business row on hover', async () => {
render(
<AccountCard
account={groupedAccount}
zone="left"
originalIndex={0}
isHovered={false}
isDragging={false}
offset={{ x: 0, y: 0 }}
showDetails={false}
privacyMode={false}
onMouseEnter={() => 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);
});
});