b5c0c6d7c9
The throughput API returns one row per source_type per time bucket, but the chart was mapping each row as a separate bar. With 5 source types × 24 hours, the bars were tiny and overlapping. Now aggregates completed/failed/items across source types per time bucket so the chart shows meaningful totals.
119 lines
5.2 KiB
TypeScript
119 lines
5.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useIngestionThroughput, useIngestionSummary } from '../api/hooks';
|
|
import { LoadingSpinner, DateRangeSelector, Card } from '../components/ui';
|
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
|
|
|
export function OpsIngestionPage() {
|
|
const [hours, setHours] = useState(24);
|
|
const [bucket, setBucket] = useState('1h');
|
|
const { data: throughput, isLoading: tpLoading } = useIngestionThroughput(hours, bucket);
|
|
const { data: summary } = useIngestionSummary(hours);
|
|
|
|
if (tpLoading) return <LoadingSpinner />;
|
|
|
|
const chartData = (() => {
|
|
const buckets = new Map<string, { time: string; completed: number; failed: number; items: number }>();
|
|
for (const row of throughput ?? []) {
|
|
const r = row as Record<string, unknown>;
|
|
const time = r.bucket_start
|
|
? new Date(r.bucket_start as string).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
: '';
|
|
if (!time) continue;
|
|
const existing = buckets.get(time) ?? { time, completed: 0, failed: 0, items: 0 };
|
|
existing.completed += Number(r.completed ?? 0);
|
|
existing.failed += Number(r.failed ?? 0);
|
|
existing.items += Number(r.items_fetched ?? 0);
|
|
buckets.set(time, existing);
|
|
}
|
|
return Array.from(buckets.values());
|
|
})();
|
|
|
|
const s = (summary ?? {}) as Record<string, unknown>;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-xl font-semibold text-gray-100">Ingestion Monitor</h1>
|
|
<div className="flex items-center gap-3">
|
|
<DateRangeSelector value={hours} onChange={setHours} />
|
|
<div className="inline-flex rounded-md border border-surface-700" role="group">
|
|
{['15m', '1h', '6h', '1d'].map((b) => (
|
|
<button
|
|
key={b}
|
|
onClick={() => setBucket(b)}
|
|
className={`px-2 py-1 text-xs font-medium first:rounded-l-md last:rounded-r-md ${bucket === b ? 'bg-brand-600 text-white' : 'bg-surface-900 text-gray-400 hover:bg-surface-800'}`}
|
|
>
|
|
{b}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary stats */}
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
|
<StatCard label="Total Runs" value={String(s.total_runs ?? '—')} />
|
|
<StatCard label="Completed" value={String(s.completed ?? '—')} color="text-green-400" />
|
|
<StatCard label="Failed" value={String(s.failed ?? '—')} color="text-red-400" />
|
|
<StatCard label="Items Fetched" value={String(s.total_items_fetched ?? '—')} />
|
|
<StatCard label="New Items" value={String(s.total_items_new ?? '—')} />
|
|
</div>
|
|
|
|
{/* Throughput chart */}
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">Throughput</h2>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={chartData}>
|
|
<XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 11 }} />
|
|
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} />
|
|
<Tooltip contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: 8 }} labelStyle={{ color: '#9ca3af' }} />
|
|
<Legend />
|
|
<Bar dataKey="completed" fill="#22c55e" name="Completed" />
|
|
<Bar dataKey="failed" fill="#ef4444" name="Failed" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Card>
|
|
|
|
{/* By source type */}
|
|
{s.by_source_type ? (
|
|
<Card>
|
|
<h2 className="mb-3 text-sm font-medium text-gray-400">By Source Type</h2>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-surface-700 text-left text-gray-500">
|
|
<th className="px-3 py-2">Type</th>
|
|
<th className="px-3 py-2">Runs</th>
|
|
<th className="px-3 py-2">Completed</th>
|
|
<th className="px-3 py-2">Failed</th>
|
|
<th className="px-3 py-2">Items</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(s.by_source_type as Array<Record<string, unknown>>).map((row, i) => (
|
|
<tr key={i} className="border-b border-surface-700/50">
|
|
<td className="px-3 py-2 text-gray-300">{String(row.source_type)}</td>
|
|
<td className="px-3 py-2 text-gray-300">{String(row.runs)}</td>
|
|
<td className="px-3 py-2 text-green-400">{String(row.completed)}</td>
|
|
<td className="px-3 py-2 text-red-400">{String(row.failed)}</td>
|
|
<td className="px-3 py-2 text-gray-300">{String(row.items_fetched)}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatCard({ label, value, color = 'text-gray-100' }: { label: string; value: string; color?: string }) {
|
|
return (
|
|
<Card className="text-center">
|
|
<div className={`text-xl font-bold ${color}`}>{value}</div>
|
|
<div className="text-xs text-gray-500">{label}</div>
|
|
</Card>
|
|
);
|
|
}
|