Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/__tests__/performance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ describe('performanceApi', () => {
expect(mockGet).toHaveBeenCalledWith('/performance/summary');
});

it('registrar calls GET /performance/registrar', async () => {
await performanceApi.registrar();
expect(mockGet).toHaveBeenCalledWith('/performance/registrar');
});

it('database calls GET /system/database', async () => {
await performanceApi.database();
expect(mockGet).toHaveBeenCalledWith('/system/database');
Expand Down
4 changes: 4 additions & 0 deletions src/api/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const performanceApi = {
return apiClient.get<PerformanceSummary>('/performance/summary');
},

registrar() {
return apiClient.get('/performance/registrar');
},

database() {
return apiClient.get('/system/database');
},
Expand Down
43 changes: 43 additions & 0 deletions src/pages/Performance/Performance.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { KpiCard } from '@/components/common/KpiCard';
import { performanceApi } from '@/api/performance';
import { agentsApi } from '@/api/agents';
import type { IntegrationService } from '@/types';

const CIRCUIT_BREAKER_LABELS: Record<string, string> = {
closed: 'Closed',
Expand Down Expand Up @@ -29,6 +32,31 @@ export function Performance() {
select: (res) => res.data,
});

const { data: services } = useQuery({
queryKey: ['integrations', 'status'],
queryFn: () => performanceApi.integrations(),
select: (res) => res.data,
});

const registrar = useMemo(() => {
const list = (Array.isArray(services) ? services : []) as IntegrationService[];
return list.find((s) => s.name.toLowerCase().includes('registrar')) ?? null;
}, [services]);

const { data: agentData } = useQuery({
queryKey: ['agents', 'count'],
queryFn: () => agentsApi.list({ per_page: 1 }),
select: (res) => res.data,
});

const registrarUp = registrar
? registrar.status.toLowerCase() === 'up'
: perf?.registrar_reachable ?? null;

const registrarLatency = registrar?.latency_ms ?? perf?.registrar_latency_ms ?? null;

const registeredAgentCount = perf?.registered_agent_count ?? agentData?.total_items ?? null;

return (
<div>
<div className="page-header">
Expand Down Expand Up @@ -60,6 +88,21 @@ export function Performance() {
/>
</div>

<h2 className="section__title" style={{ marginTop: '24px' }}>Registrar Metrics</h2>
<div className="kpi-grid">
<KpiCard
title="Registrar Status"
value={registrarUp != null ? (registrarUp ? 'Reachable' : 'Unreachable') : '--'}
subtitle={registrarLatency != null ? `${registrarLatency} ms` : undefined}
variant={registrarUp != null ? (registrarUp ? 'success' : 'danger') : 'default'}
/>
<KpiCard
title="Registered Agents"
value={registeredAgentCount ?? '--'}
variant="info"
/>
</div>

<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
<div className="section">
<h2 className="section__title">Verifier Cluster Metrics</h2>
Expand Down
56 changes: 55 additions & 1 deletion src/pages/Performance/__tests__/Performance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,23 @@ vi.mock('@/api/performance', () => ({
estimated_attestation_rate: 120,
capacity_utilization_pct: 55.3,
database_status: 'ok',
registered_agent_count: 12,
},
}),
integrations: vi.fn().mockResolvedValue({
data: [
{ name: 'keylime-registrar', endpoint: 'http://localhost:3001', status: 'UP', latency_ms: 18 },
{ name: 'keylime-verifier', endpoint: 'http://localhost:3000', status: 'UP', latency_ms: 42 },
],
}),
},
}));

vi.mock('@/api/agents', () => ({
agentsApi: {
list: vi.fn().mockResolvedValue({
data: { items: [], page: 1, page_size: 1, total_items: 12, total_pages: 12 },
}),
},
}));

Expand All @@ -40,7 +55,8 @@ describe('Performance', () => {

it('renders KPI cards with performance data', async () => {
renderWithProviders(<Performance />);
expect(await screen.findByText('Reachable')).toBeInTheDocument();
const reachables = await screen.findAllByText('Reachable');
expect(reachables).toHaveLength(2);
expect(await screen.findByText('42 ms')).toBeInTheDocument();
expect(await screen.findByText('Closed')).toBeInTheDocument();
expect(await screen.findByText('120/s')).toBeInTheDocument();
Expand All @@ -54,6 +70,13 @@ describe('Performance', () => {
expect(screen.getByText('Circuit Breaker Status')).toBeInTheDocument();
});

it('renders registrar KPI cards from integrations data', async () => {
renderWithProviders(<Performance />);
expect(await screen.findByText('Registrar Metrics')).toBeInTheDocument();
expect(await screen.findByText('18 ms')).toBeInTheDocument();
expect(await screen.findByText('12')).toBeInTheDocument();
});

it('shows danger variant for unreachable verifier', async () => {
const { performanceApi } = await import('@/api/performance');
vi.mocked(performanceApi.summary).mockResolvedValueOnce({
Expand All @@ -67,11 +90,39 @@ describe('Performance', () => {
database_status: 'ok',
},
} as never);
vi.mocked(performanceApi.integrations).mockResolvedValueOnce({
data: [
{ name: 'keylime-registrar', endpoint: 'http://localhost:3001', status: 'UP', latency_ms: 10 },
],
} as never);
renderWithProviders(<Performance />);
expect(await screen.findByText('Unreachable')).toBeInTheDocument();
expect(await screen.findByText('Open')).toBeInTheDocument();
});

it('shows danger variant for unreachable registrar', async () => {
const { performanceApi } = await import('@/api/performance');
vi.mocked(performanceApi.summary).mockResolvedValueOnce({
data: {
verifier_reachable: true,
verifier_latency_ms: 20,
circuit_breaker_state: 'closed',
agent_count: 5,
estimated_attestation_rate: 100,
capacity_utilization_pct: 40.0,
database_status: 'ok',
},
} as never);
vi.mocked(performanceApi.integrations).mockResolvedValueOnce({
data: [
{ name: 'keylime-registrar', endpoint: 'http://localhost:3001', status: 'DOWN', latency_ms: 0 },
],
} as never);
renderWithProviders(<Performance />);
const unreachables = await screen.findAllByText('Unreachable');
expect(unreachables).toHaveLength(1);
});

it('shows warning variant for high capacity', async () => {
const { performanceApi } = await import('@/api/performance');
vi.mocked(performanceApi.summary).mockResolvedValueOnce({
Expand All @@ -92,7 +143,10 @@ describe('Performance', () => {

it('renders dashes when no data', async () => {
const { performanceApi } = await import('@/api/performance');
const { agentsApi } = await import('@/api/agents');
vi.mocked(performanceApi.summary).mockResolvedValueOnce({ data: null } as never);
vi.mocked(performanceApi.integrations).mockResolvedValueOnce({ data: [] } as never);
vi.mocked(agentsApi.list).mockResolvedValueOnce({ data: null } as never);
renderWithProviders(<Performance />);
const dashes = screen.getAllByText('--');
expect(dashes.length).toBeGreaterThanOrEqual(4);
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export interface PerformanceSummary {
estimated_attestation_rate: number | null;
capacity_utilization_pct: number | null;
database_status: string;
registrar_reachable?: boolean;
registrar_latency_ms?: number | null;
registered_agent_count?: number;
}

export interface TimeRange {
Expand Down