From a8b686d643f38bf11efb83f1753442783ebed2c9 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 30 Apr 2026 10:05:19 -0400 Subject: [PATCH] feat(ui): show split Claude quota bars --- .../account/shared/account-quota-panel.tsx | 96 ++++++--- .../shared/account-quota-panel.test.tsx | 185 +++++++++++++++++- 2 files changed, 250 insertions(+), 31 deletions(-) diff --git a/ui/src/components/account/shared/account-quota-panel.tsx b/ui/src/components/account/shared/account-quota-panel.tsx index 09c23ffb..f35c7bf9 100644 --- a/ui/src/components/account/shared/account-quota-panel.tsx +++ b/ui/src/components/account/shared/account-quota-panel.tsx @@ -26,6 +26,13 @@ import { useTranslation } from 'react-i18next'; type AccountSurfaceMode = 'compact' | 'detailed'; +interface QuotaRow { + id: 'five-hour' | 'weekly'; + label: string; + compactLabel: string; + value: number; +} + interface AccountQuotaPanelProps { provider: string; quota?: UnifiedQuotaResult; @@ -101,18 +108,27 @@ export function AccountQuotaPanel({ isCodexProvider && quota && isCodexQuotaResult(quota) ? getCodexQuotaBreakdown(quota.windows) : null; - const compactQuotaRows = isCodexProvider + const compactQuotaRows: QuotaRow[] = isCodexProvider ? [ - { label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null }, { - label: mode === 'compact' ? 'Wk' : 'Weekly', + id: 'five-hour', + label: t('quotaTooltip.fiveHourLimit'), + compactLabel: '5h', + value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null, + }, + { + id: 'weekly', + label: t('quotaTooltip.weeklyLimit'), + compactLabel: 'Week', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null, }, - ] + ].filter((row): row is QuotaRow => row.value !== null) : isClaudeProvider && quota && isClaudeQuotaResult(quota) ? [ { - label: '5h', + id: 'five-hour', + label: t('quotaTooltip.fiveHourLimit'), + compactLabel: '5h', value: quota.coreUsage?.fiveHour?.remainingPercent ?? quota.windows.find((window) => window.rateLimitType === 'five_hour') @@ -120,7 +136,9 @@ export function AccountQuotaPanel({ null, }, { - label: mode === 'compact' ? 'Wk' : 'Weekly', + id: 'weekly', + label: t('quotaTooltip.weeklyLimit'), + compactLabel: 'Week', value: quota.coreUsage?.weekly?.remainingPercent ?? quota.windows.find((window) => @@ -134,11 +152,9 @@ export function AccountQuotaPanel({ )?.remainingPercent ?? null, }, - ] + ].filter((row): row is QuotaRow => row.value !== null) : []; - const quotaRows = compactQuotaRows.filter( - (row): row is { label: string; value: number } => row.value !== null - ); + const quotaRows = compactQuotaRows; if (quotaLoading) { return ( @@ -176,28 +192,42 @@ export function AccountQuotaPanel({ {minQuotaLabel}% - {quotaRows.length > 0 && ( -
+ {quotaRows.length > 0 ? ( +
{quotaRows.map((row) => ( - - {row.label} {row.value}% - +
+ + {row.compactLabel} + + + + {formatQuotaPercent(row.value)}% + +
))}
- )} -
-
50 ? 'bg-emerald-500' : minQuotaValue > 20 ? 'bg-amber-500' : 'bg-red-500' - )} - style={{ width: `${minQuotaValue}%` }} + } /> -
+ )}
) : (
@@ -228,18 +258,23 @@ export function AccountQuotaPanel({ )}
{quotaRows.length > 0 ? ( -
+
{quotaRows.map((row) => ( -
- - {row.label} - +
+
+ + {row.label} + + + {formatQuotaPercent(row.value)}% + +
- {row.value}%
))}
@@ -247,6 +282,7 @@ export function AccountQuotaPanel({
diff --git a/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx b/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx index 4263945f..93741255 100644 --- a/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx +++ b/ui/tests/unit/components/account/shared/account-quota-panel.test.tsx @@ -1,7 +1,7 @@ import { render, screen, userEvent } from '@tests/setup/test-utils'; import { describe, expect, it } from 'vitest'; import { AccountQuotaPanel } from '@/components/account/shared/account-quota-panel'; -import type { GeminiCliQuotaResult } from '@/lib/api-client'; +import type { ClaudeQuotaResult, CodexQuotaResult, GeminiCliQuotaResult } from '@/lib/api-client'; function createGeminiFailureQuota(): GeminiCliQuotaResult { return { @@ -19,6 +19,189 @@ function createGeminiFailureQuota(): GeminiCliQuotaResult { }; } +function createClaudeQuota(): ClaudeQuotaResult { + return { + success: true, + lastUpdated: Date.now(), + windows: [ + { + rateLimitType: 'five_hour', + label: '5-hour usage', + status: 'allowed', + utilization: 0.57, + usedPercent: 57, + remainingPercent: 43, + resetAt: '2026-05-01T01:00:00.000Z', + }, + { + rateLimitType: 'seven_day_sonnet', + label: 'Weekly usage', + status: 'allowed', + utilization: 0.6, + usedPercent: 60, + remainingPercent: 40, + resetAt: '2026-05-07T01:00:00.000Z', + }, + ], + coreUsage: { + fiveHour: { + rateLimitType: 'five_hour', + label: '5-hour usage', + remainingPercent: 43, + resetAt: '2026-05-01T01:00:00.000Z', + status: 'allowed', + }, + weekly: { + rateLimitType: 'seven_day_sonnet', + label: 'Weekly usage', + remainingPercent: 40, + resetAt: '2026-05-07T01:00:00.000Z', + status: 'allowed', + }, + }, + }; +} + +function createCodexQuota(): CodexQuotaResult { + return { + success: true, + planType: 'pro', + lastUpdated: Date.now(), + windows: [ + { + label: 'Primary', + category: 'usage', + cadence: '5h', + usedPercent: 38, + remainingPercent: 62, + resetAfterSeconds: 60 * 60, + resetAt: '2026-05-01T01:00:00.000Z', + }, + { + label: 'Secondary', + category: 'usage', + cadence: 'weekly', + usedPercent: 61, + remainingPercent: 39, + resetAfterSeconds: 7 * 24 * 60 * 60, + resetAt: '2026-05-07T01:00:00.000Z', + }, + ], + coreUsage: { + fiveHour: { + label: 'Primary', + remainingPercent: 62, + resetAfterSeconds: 60 * 60, + resetAt: '2026-05-01T01:00:00.000Z', + }, + weekly: { + label: 'Secondary', + remainingPercent: 39, + resetAfterSeconds: 7 * 24 * 60 * 60, + resetAt: '2026-05-07T01:00:00.000Z', + }, + }, + }; +} + +function createGeminiSuccessQuota(): GeminiCliQuotaResult { + return { + success: true, + buckets: [ + { + id: 'flash::input', + label: 'Gemini Flash', + tokenType: 'input', + remainingFraction: 0.6, + remainingPercent: 60, + resetTime: '2026-05-01T01:00:00.000Z', + modelIds: ['gemini-2.5-flash'], + }, + ], + projectId: 'test-project', + lastUpdated: Date.now(), + }; +} + +describe('AccountQuotaPanel quota bars', () => { + it('shows separate compact Claude 5h and weekly quota bars', () => { + render( + + ); + + expect(screen.getByText('5h')).toBeInTheDocument(); + expect(screen.getByText('Week')).toBeInTheDocument(); + expect(screen.getByText('43%')).toBeInTheDocument(); + expect(screen.getAllByText('40%').length).toBeGreaterThan(0); + expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute( + 'aria-valuenow', + '43' + ); + expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute( + 'aria-valuenow', + '40' + ); + }); + + it('shows separate compact Codex 5h and weekly quota bars', () => { + render( + + ); + + expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute( + 'aria-valuenow', + '62' + ); + expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute( + 'aria-valuenow', + '39' + ); + }); + + it('keeps the compact single-bar fallback for providers without split quota rows', () => { + render( + + ); + + expect(screen.getByRole('progressbar', { name: 'Quota' })).toBeInTheDocument(); + expect(screen.queryByRole('progressbar', { name: '5h quota' })).not.toBeInTheDocument(); + expect(screen.queryByRole('progressbar', { name: 'Week quota' })).not.toBeInTheDocument(); + }); + + it('shows explicit detailed Claude quota labels', () => { + render( + + ); + + expect(screen.getByText('5h usage limit')).toBeInTheDocument(); + expect(screen.getByText('Weekly usage limit')).toBeInTheDocument(); + expect(screen.getByRole('progressbar', { name: '5h usage limit quota' })).toBeInTheDocument(); + expect( + screen.getByRole('progressbar', { name: 'Weekly usage limit quota' }) + ).toBeInTheDocument(); + }); +}); + describe('AccountQuotaPanel failure tooltip', () => { it('renders the shared failure tooltip content with viewport-safe shell classes', async () => { render(