add metrics page
This commit is contained in:
220
dashboard/src/pages/MetricsPage.tsx
Normal file
220
dashboard/src/pages/MetricsPage.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line,
|
||||
} from 'recharts';
|
||||
import { useMetrics } from '../api/reviews';
|
||||
import { LoadingSpinner } from '../components/common/LoadingSpinner';
|
||||
|
||||
const VERDICT_COLORS = {
|
||||
approve: '#22c55e',
|
||||
request_changes: '#ef4444',
|
||||
comment: '#3b82f6',
|
||||
};
|
||||
|
||||
const SEVERITY_COLORS = {
|
||||
critical: '#dc2626',
|
||||
high: '#f97316',
|
||||
medium: '#eab308',
|
||||
low: '#3b82f6',
|
||||
info: '#9ca3af',
|
||||
};
|
||||
|
||||
const AGENT_COLORS = {
|
||||
security: '#ef4444',
|
||||
style: '#3b82f6',
|
||||
complexity: '#a855f7',
|
||||
};
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
subtext?: string;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, subtext }: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-500">{label}</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1">{value}</p>
|
||||
{subtext && <p className="text-xs text-gray-500 mt-1">{subtext}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MetricsPage() {
|
||||
const { data: metrics, isLoading, error } = useMetrics();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !metrics) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
|
||||
<p className="text-red-800">Failed to load metrics</p>
|
||||
<p className="text-sm text-red-600 mt-1">{error?.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const verdictData = Object.entries(metrics.verdict_counts).map(
|
||||
([name, value]) => ({
|
||||
name: name === 'request_changes' ? 'Changes' : name,
|
||||
value,
|
||||
}),
|
||||
);
|
||||
|
||||
const severityData = Object.entries(metrics.severity_counts).map(
|
||||
([name, value]) => ({
|
||||
name: name.charAt(0).toUpperCase() + name.slice(1),
|
||||
value,
|
||||
fill: SEVERITY_COLORS[name as keyof typeof SEVERITY_COLORS],
|
||||
}),
|
||||
);
|
||||
|
||||
const costByAgentData = Object.entries(metrics.cost_by_agent).map(
|
||||
([name, value]) => ({
|
||||
name: name.charAt(0).toUpperCase() + name.slice(1),
|
||||
cost: value,
|
||||
fill: AGENT_COLORS[name as keyof typeof AGENT_COLORS],
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Metrics</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Review statistics and trends
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stat Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<StatCard label="Total Reviews" value={metrics.total_reviews} />
|
||||
<StatCard
|
||||
label="Completed"
|
||||
value={metrics.completed_reviews}
|
||||
subtext={`${Math.round((metrics.completed_reviews / metrics.total_reviews) * 100)}% completion rate`}
|
||||
/>
|
||||
<StatCard
|
||||
label="Average Cost"
|
||||
value={`$${metrics.average_cost_usd.toFixed(4)}`}
|
||||
subtext="Per review"
|
||||
/>
|
||||
<StatCard
|
||||
label="Total Cost"
|
||||
value={`$${(metrics.average_cost_usd * metrics.completed_reviews).toFixed(2)}`}
|
||||
subtext="All reviews"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Verdict Distribution */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Verdict Distribution
|
||||
</h2>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={verdictData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
label={({ name, percent }) =>
|
||||
`${name} (${((percent ?? 0) * 100).toFixed(0)}%)`
|
||||
}
|
||||
>
|
||||
{verdictData.map((_, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={
|
||||
Object.values(VERDICT_COLORS)[
|
||||
index % Object.values(VERDICT_COLORS).length
|
||||
]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Severity Distribution */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Findings by Severity
|
||||
</h2>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={severityData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="value" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Reviews Over Time */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Reviews Over Time
|
||||
</h2>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<LineChart data={metrics.reviews_by_day}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Cost by Agent */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Cost by Agent
|
||||
</h2>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<BarChart data={costByAgentData} layout="vertical">
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" tickFormatter={(v) => `$${v.toFixed(2)}`} />
|
||||
<YAxis type="category" dataKey="name" width={80} />
|
||||
<Tooltip formatter={(value) => `$${Number(value).toFixed(4)}`} />
|
||||
<Bar dataKey="cost" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user