mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
fix(dashboard): scope auth monitor summary by provider
- switch live header and summary cards to provider-scoped totals in detail view - keep the provider grid on global totals when no provider is selected - add a regression test for provider detail metric scoping
This commit is contained in:
@@ -22,6 +22,7 @@ import { useAuthMonitorData } from './hooks';
|
||||
import { LivePulse } from './components/live-pulse';
|
||||
import { ProviderCard } from './components/provider-card';
|
||||
import { SummaryCard } from './components/summary-card';
|
||||
import { getSuccessRate } from './utils';
|
||||
|
||||
const STORAGE_KEY = 'auth-monitor-selected-provider';
|
||||
|
||||
@@ -78,6 +79,13 @@ export function AuthMonitor() {
|
||||
const selectedProviderData = effectiveProvider
|
||||
? providerStats.find((ps) => ps.provider === effectiveProvider)
|
||||
: null;
|
||||
const displayedAccountCount = selectedProviderData?.accountCount ?? accounts.length;
|
||||
const displayedSuccess = selectedProviderData?.successCount ?? totalSuccess;
|
||||
const displayedFailure = selectedProviderData?.failureCount ?? totalFailure;
|
||||
const displayedTotalRequests = selectedProviderData?.totalRequests ?? totalRequests;
|
||||
const displayedSuccessRate = selectedProviderData
|
||||
? getSuccessRate(selectedProviderData.successCount, selectedProviderData.failureCount)
|
||||
: overallSuccessRate;
|
||||
|
||||
const handlePauseToggle = (accountIds: string[], paused: boolean) => {
|
||||
if (
|
||||
@@ -147,8 +155,8 @@ export function AuthMonitor() {
|
||||
<span>Updated {timeSinceUpdate || 'now'}</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
<span>{t('authMonitor.accountsCount', { count: accounts.length })}</span>
|
||||
<span className="font-mono">{totalRequests.toLocaleString()} req</span>
|
||||
<span>{t('authMonitor.accountsCount', { count: displayedAccountCount })}</span>
|
||||
<span className="font-mono">{displayedTotalRequests.toLocaleString()} req</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -157,29 +165,29 @@ export function AuthMonitor() {
|
||||
<SummaryCard
|
||||
icon={<Activity className="w-4 h-4" />}
|
||||
label={t('authMonitor.accounts')}
|
||||
value={accounts.length}
|
||||
value={displayedAccountCount}
|
||||
color="var(--accent)"
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={<CheckCircle2 className="w-4 h-4" />}
|
||||
label={t('authMonitor.success')}
|
||||
value={totalSuccess.toLocaleString()}
|
||||
value={displayedSuccess.toLocaleString()}
|
||||
color={STATUS_COLORS.success}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={<XCircle className="w-4 h-4" />}
|
||||
label={t('authMonitor.failed')}
|
||||
value={totalFailure.toLocaleString()}
|
||||
color={totalFailure > 0 ? STATUS_COLORS.failed : undefined}
|
||||
value={displayedFailure.toLocaleString()}
|
||||
color={displayedFailure > 0 ? STATUS_COLORS.failed : undefined}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={<Activity className="w-4 h-4" />}
|
||||
label={t('authMonitor.successRate')}
|
||||
value={`${overallSuccessRate}%`}
|
||||
value={`${displayedSuccessRate}%`}
|
||||
color={
|
||||
overallSuccessRate === 100
|
||||
displayedSuccessRate === 100
|
||||
? STATUS_COLORS.success
|
||||
: overallSuccessRate >= 95
|
||||
: displayedSuccessRate >= 95
|
||||
? STATUS_COLORS.degraded
|
||||
: STATUS_COLORS.failed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { render, screen, userEvent, within } from '@tests/setup/test-utils';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AuthMonitor } from '@/components/monitoring/auth-monitor';
|
||||
import type { AuthMonitorData } from '@/components/monitoring/auth-monitor/hooks';
|
||||
|
||||
vi.mock('@/components/account-flow-viz', () => ({
|
||||
AccountFlowViz: ({
|
||||
providerData,
|
||||
onBack,
|
||||
}: {
|
||||
providerData: { displayName: string };
|
||||
onBack: () => void;
|
||||
}) => (
|
||||
<div>
|
||||
<button type="button" onClick={onBack}>
|
||||
Back to providers
|
||||
</button>
|
||||
<div data-testid="provider-detail">{providerData.displayName}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock(import('react-i18next'), async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { count?: number }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'authMonitor.accounts': 'Accounts',
|
||||
'authMonitor.success': 'Success',
|
||||
'authMonitor.failed': 'Failed',
|
||||
'authMonitor.successRate': 'Success Rate',
|
||||
};
|
||||
|
||||
if (key === 'authMonitor.accountsCount') {
|
||||
return `${options?.count ?? 0} accounts`;
|
||||
}
|
||||
|
||||
return translations[key] ?? key;
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const { useAuthMonitorDataMock } = vi.hoisted(() => ({
|
||||
useAuthMonitorDataMock: vi.fn<() => AuthMonitorData>(),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/monitoring/auth-monitor/hooks', () => ({
|
||||
useAuthMonitorData: useAuthMonitorDataMock,
|
||||
}));
|
||||
|
||||
function getSummaryRow() {
|
||||
const accountsLabel = screen.getByText('Accounts');
|
||||
const summaryRow = accountsLabel.parentElement?.parentElement?.parentElement;
|
||||
|
||||
if (!summaryRow) {
|
||||
throw new Error('Unable to locate the auth monitor summary row.');
|
||||
}
|
||||
|
||||
return summaryRow;
|
||||
}
|
||||
|
||||
function getSummaryCard(label: string) {
|
||||
const labelNode = within(getSummaryRow()).getByText(label);
|
||||
const card = labelNode.closest('div')?.parentElement;
|
||||
|
||||
if (!card) {
|
||||
throw new Error(`Unable to locate summary card for label "${label}".`);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
const providerStats = [
|
||||
{
|
||||
provider: 'gemini',
|
||||
displayName: 'Google Gemini',
|
||||
totalRequests: 15,
|
||||
successCount: 12,
|
||||
failureCount: 3,
|
||||
accountCount: 2,
|
||||
accounts: [
|
||||
{
|
||||
id: 'gemini-primary',
|
||||
email: 'gemini-primary@example.com',
|
||||
tokenFile: '/tmp/gemini-primary.json',
|
||||
provider: 'gemini',
|
||||
displayName: 'Google Gemini',
|
||||
isDefault: true,
|
||||
successCount: 10,
|
||||
failureCount: 2,
|
||||
color: '#4285F4',
|
||||
},
|
||||
{
|
||||
id: 'gemini-secondary',
|
||||
email: 'gemini-secondary@example.com',
|
||||
tokenFile: '/tmp/gemini-secondary.json',
|
||||
provider: 'gemini',
|
||||
displayName: 'Google Gemini',
|
||||
isDefault: false,
|
||||
successCount: 2,
|
||||
failureCount: 1,
|
||||
color: '#34A853',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: 'codex',
|
||||
displayName: 'OpenAI Codex',
|
||||
totalRequests: 27,
|
||||
successCount: 23,
|
||||
failureCount: 4,
|
||||
accountCount: 2,
|
||||
accounts: [
|
||||
{
|
||||
id: 'codex-primary',
|
||||
email: 'codex-primary@example.com',
|
||||
tokenFile: '/tmp/codex-primary.json',
|
||||
provider: 'codex',
|
||||
displayName: 'OpenAI Codex',
|
||||
isDefault: true,
|
||||
successCount: 15,
|
||||
failureCount: 2,
|
||||
color: '#10a37f',
|
||||
},
|
||||
{
|
||||
id: 'codex-secondary',
|
||||
email: 'codex-secondary@example.com',
|
||||
tokenFile: '/tmp/codex-secondary.json',
|
||||
provider: 'codex',
|
||||
displayName: 'OpenAI Codex',
|
||||
isDefault: false,
|
||||
successCount: 8,
|
||||
failureCount: 2,
|
||||
color: '#0f766e',
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies AuthMonitorData['providerStats'];
|
||||
|
||||
const authMonitorData: AuthMonitorData = {
|
||||
accounts: providerStats.flatMap((provider) => provider.accounts),
|
||||
totalSuccess: 35,
|
||||
totalFailure: 7,
|
||||
totalRequests: 42,
|
||||
providerStats,
|
||||
overallSuccessRate: 83,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
timeSinceUpdate: '2s ago',
|
||||
};
|
||||
|
||||
describe('AuthMonitor', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(window.localStorage.getItem).mockReturnValue(null);
|
||||
useAuthMonitorDataMock.mockReturnValue(authMonitorData);
|
||||
});
|
||||
|
||||
it('scopes header and summary metrics to the selected provider detail view', async () => {
|
||||
render(<AuthMonitor />);
|
||||
|
||||
expect(screen.getByText('4 accounts')).toBeInTheDocument();
|
||||
expect(screen.getByText('42 req')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Accounts')).getByText('4')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Success')).getByText('35')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Failed')).getByText('7')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Success Rate')).getByText('83%')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /Google Gemini/i }));
|
||||
|
||||
expect(screen.getByTestId('provider-detail')).toHaveTextContent('Google Gemini');
|
||||
expect(screen.getByText('2 accounts')).toBeInTheDocument();
|
||||
expect(screen.getByText('15 req')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Accounts')).getByText('2')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Success')).getByText('12')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Failed')).getByText('3')).toBeInTheDocument();
|
||||
expect(within(getSummaryCard('Success Rate')).getByText('80%')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user