phase 16: React dashboard with full platform control and analytics
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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 = (throughput ?? []).map((row: unknown) => {
|
||||
const r = row as Record<string, unknown>;
|
||||
return {
|
||||
time: r.bucket_start ? new Date(r.bucket_start as string).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '',
|
||||
completed: Number(r.completed ?? 0),
|
||||
failed: Number(r.failed ?? 0),
|
||||
items: Number(r.items_fetched ?? 0),
|
||||
};
|
||||
});
|
||||
|
||||
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={s.total_runs} />
|
||||
<StatCard label="Completed" value={s.completed} color="text-green-400" />
|
||||
<StatCard label="Failed" value={s.failed} color="text-red-400" />
|
||||
<StatCard label="Items Fetched" value={s.total_items_fetched} />
|
||||
<StatCard label="New Items" value={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">{row.source_type as string}</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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color = 'text-gray-100' }: { label: string; value: unknown; color?: string }) {
|
||||
return (
|
||||
<Card className="text-center">
|
||||
<div className={`text-xl font-bold ${color}`}>{value != null ? String(value) : '—'}</div>
|
||||
<div className="text-xs text-gray-500">{label}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user