Files
CASAN/packages/casan-control-panel/frontend/src/pages/FinOps.tsx
T
2026-07-08 19:07:35 +09:00

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>
</>
);
}