mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +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:
1 parent
e69a58c74f
commit
5746e43a36
2 files changed
+200
-9
No files matched your search
@@ -22,6 +22,7 @@ import { useAuthMonitorData } from './hooks';
|
|||||||
import { LivePulse } from './components/live-pulse';
|
import { LivePulse } from './components/live-pulse';
|
||||||
import { ProviderCard } from './components/provider-card';
|
import { ProviderCard } from './components/provider-card';
|
||||||
import { SummaryCard } from './components/summary-card';
|
import { SummaryCard } from './components/summary-card';
|
||||||
|
import { getSuccessRate } from './utils';
|
||||||
|
|
||||||
const STORAGE_KEY = 'auth-monitor-selected-provider';
|
const STORAGE_KEY = 'auth-monitor-selected-provider';
|
||||||
|
|
||||||
@@ -78,6 +79,13 @@ export function AuthMonitor() {
|
|||||||
const selectedProviderData = effectiveProvider
|
const selectedProviderData = effectiveProvider
|
||||||
? providerStats.find((ps) => ps.provider === effectiveProvider)
|
? providerStats.find((ps) => ps.provider === effectiveProvider)
|
||||||
: null;
|
: 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) => {
|
const handlePauseToggle = (accountIds: string[], paused: boolean) => {
|
||||||
if (
|
if (
|
||||||
@@ -147,8 +155,8 @@ export function AuthMonitor() {
|
|||||||
<span>Updated {timeSinceUpdate || 'now'}</span>
|
<span>Updated {timeSinceUpdate || 'now'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-muted-foreground/50">|</span>
|
<span className="text-muted-foreground/50">|</span>
|
||||||
<span>{t('authMonitor.accountsCount', { count: accounts.length })}</span>
|
<span>{t('authMonitor.accountsCount', { count: displayedAccountCount })}</span>
|
||||||
<span className="font-mono">{totalRequests.toLocaleString()} req</span>
|
<span className="font-mono">{displayedTotalRequests.toLocaleString()} req</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,29 +165,29 @@ export function AuthMonitor() {
|
|||||||
<SummaryCard
|
<SummaryCard
|
||||||
icon={<Activity className="w-4 h-4" />}
|
icon={<Activity className="w-4 h-4" />}
|
||||||
label={t('authMonitor.accounts')}
|
label={t('authMonitor.accounts')}
|
||||||
value={accounts.length}
|
value={displayedAccountCount}
|
||||||
color="var(--accent)"
|
color="var(--accent)"
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
icon={<CheckCircle2 className="w-4 h-4" />}
|
icon={<CheckCircle2 className="w-4 h-4" />}
|
||||||
label={t('authMonitor.success')}
|
label={t('authMonitor.success')}
|
||||||
value={totalSuccess.toLocaleString()}
|
value={displayedSuccess.toLocaleString()}
|
||||||
color={STATUS_COLORS.success}
|
color={STATUS_COLORS.success}
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
icon={<XCircle className="w-4 h-4" />}
|
icon={<XCircle className="w-4 h-4" />}
|
||||||
label={t('authMonitor.failed')}
|
label={t('authMonitor.failed')}
|
||||||
value={totalFailure.toLocaleString()}
|
value={displayedFailure.toLocaleString()}
|
||||||
color={totalFailure > 0 ? STATUS_COLORS.failed : undefined}
|
color={displayedFailure > 0 ? STATUS_COLORS.failed : undefined}
|
||||||
/>
|
/>
|
||||||
<SummaryCard
|
<SummaryCard
|
||||||
icon={<Activity className="w-4 h-4" />}
|
icon={<Activity className="w-4 h-4" />}
|
||||||
label={t('authMonitor.successRate')}
|
label={t('authMonitor.successRate')}
|
||||||
value={`${overallSuccessRate}%`}
|
value={`${displayedSuccessRate}%`}
|
||||||
color={
|
color={
|
||||||
overallSuccessRate === 100
|
displayedSuccessRate === 100
|
||||||
? STATUS_COLORS.success
|
? STATUS_COLORS.success
|
||||||
: overallSuccessRate >= 95
|
: displayedSuccessRate >= 95
|
||||||
? STATUS_COLORS.degraded
|
? STATUS_COLORS.degraded
|
||||||
: STATUS_COLORS.failed
|
: 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