86 lines
3.7 KiB
TypeScript
86 lines
3.7 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { api, SettingsActor } from '../lib/api';
|
|
import { Card, StatTile, StatusBadge } from '../components/ui/Card';
|
|
|
|
const viewer: SettingsActor = {
|
|
actor: 'finops-viewer',
|
|
role: 'viewer',
|
|
project: 'default',
|
|
tenant: 'default',
|
|
};
|
|
|
|
function money(v: number | null | undefined): string {
|
|
return typeof v === 'number' && Number.isFinite(v) ? `$${v.toFixed(4)}` : 'n/a';
|
|
}
|
|
|
|
export function FinOps() {
|
|
const cost = useQuery({ queryKey: ['cost'], queryFn: api.cost });
|
|
const settings = useQuery({ queryKey: ['settings', 'finops-viewer'], queryFn: () => api.settings(viewer), retry: false });
|
|
|
|
if (cost.isLoading || !cost.data) return <div className="text-gray-500">Loading…</div>;
|
|
|
|
const capRaw = settings.data?.settings['cost.absolute_cap_usd']?.value;
|
|
const cap = typeof capRaw === 'number' ? capRaw : null;
|
|
const actual = cost.data.provider_cost;
|
|
const budgetStatus = cap === null ? 'not configured' : actual <= cap ? 'ok' : 'breach';
|
|
const kpis = Array.isArray(cost.data.business_kpi?.kpis) ? cost.data.business_kpi.kpis : [];
|
|
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatTile label="Provider cost" value={money(actual)} sub="from provider usage artifact" />
|
|
<StatTile label="Provider tokens" value={cost.data.provider_tokens} />
|
|
<StatTile label="Budget cap" value={money(cap)} sub="cost.absolute_cap_usd setting" />
|
|
<StatTile label="Budget status" value={<StatusBadge value={budgetStatus} />} />
|
|
</div>
|
|
|
|
<Card title="Provider usage">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="text-left text-gray-500 border-b border-gray-200">
|
|
<th className="py-2">time</th><th>provider</th><th>model</th><th>step</th><th>tokens</th><th>cost</th><th>status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{cost.data.by_provider.map((r: any, i: number) => (
|
|
<tr key={`${r.timestamp}-${i}`} className="border-b border-gray-100">
|
|
<td className="py-2 text-gray-500">{r.timestamp?.replace('T', ' ').replace('Z', '')}</td>
|
|
<td>{r.provider}</td>
|
|
<td>{r.model}</td>
|
|
<td>{r.step}</td>
|
|
<td>{r.total_tokens}</td>
|
|
<td>{money(r.cost_usd)}</td>
|
|
<td><StatusBadge value={r.status ?? 'unknown'} /></td>
|
|
</tr>
|
|
))}
|
|
{cost.data.by_provider.length === 0 && (
|
|
<tr><td className="py-4 text-gray-500" colSpan={7}>No provider usage records.</td></tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card title="SLO / KPI board">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{kpis.map((k: any) => (
|
|
<div key={k.id} className="rounded border border-gray-200 p-4">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="font-medium text-gray-800">{String(k.id).replaceAll('_', ' ')}</div>
|
|
<StatusBadge value={k.target_met ? 'ok' : 'breach'} />
|
|
</div>
|
|
<div className="mt-2 grid grid-cols-3 gap-2 text-xs text-gray-600">
|
|
<div><span className="block text-gray-400">baseline</span>{k.baseline}</div>
|
|
<div><span className="block text-gray-400">current</span>{k.current}</div>
|
|
<div><span className="block text-gray-400">target</span>{k.target}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{kpis.length === 0 && <div className="text-gray-500">No KPI artifact found.</div>}
|
|
</div>
|
|
</Card>
|
|
</>
|
|
);
|
|
}
|