Files
maestro/ui/src/components/settings/GatewayKeyUsagePanel.tsx
T
cladeandswallow 7049a874f3 feat: initial public release (MAESTRO v0.1.0)
Open-source release of MAESTRO, an agent orchestration platform that runs
LLM-driven tasks through sandboxed tools, with a web UI. Apache-2.0.
See README.md and docs/ (getting-started, configuration, architecture).
2026-06-03 04:01:14 +00:00

151 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useQuery } from '@tanstack/react-query';
import { getGatewayKeyUsage } from '../../api';
interface Props {
keyId: string;
onClose: () => void;
}
function fmtTokens(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toLocaleString();
}
/**
* Per-key usage detail. Shows current-month stats (with a progress bar
* vs budget) and a simple bar chart of the last 6-12 months of token
* usage. No external chart library — pure CSS bars keep the UI bundle
* lean.
*/
export function GatewayKeyUsagePanel({ keyId, onClose }: Props) {
const { data, isLoading, error } = useQuery({
queryKey: ['gateway-key-usage', keyId],
queryFn: () => getGatewayKeyUsage(keyId),
staleTime: 5_000,
});
const maxHistTokens = data
? Math.max(1, ...data.history.map(h => h.tokensIn + h.tokensOut))
: 1;
const pctUsed =
data && data.tokensBudget !== null && data.tokensBudget > 0
? Math.min(100, (data.tokensTotal / data.tokensBudget) * 100)
: null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 p-6">
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-semibold text-slate-800">Key 使用状況</h3>
<p className="text-xs text-slate-500 font-mono">{keyId}</p>
</div>
<button
type="button"
onClick={onClose}
className="text-slate-400 hover:text-slate-700 text-xl leading-none"
aria-label="Close"
>
×
</button>
</div>
{isLoading && <div className="text-sm text-slate-500">Loading</div>}
{error && (
<div className="text-sm text-red-600">取得エラー: {String((error as Error).message ?? error)}</div>
)}
{data && (
<>
{/* Current period summary */}
<div className="border border-hairline rounded p-3 mb-4">
<div className="flex justify-between items-baseline mb-2">
<span className="text-xs font-medium text-slate-600 uppercase tracking-wide">
今月 ({data.currentPeriod})
</span>
<span className="text-xs text-slate-500">
Requests: {data.requestsThisMonth.toLocaleString()}
</span>
</div>
<div className="grid grid-cols-3 gap-3 text-sm">
<div>
<div className="text-xs text-slate-500">Input tokens</div>
<div className="font-mono">{fmtTokens(data.tokensIn)}</div>
</div>
<div>
<div className="text-xs text-slate-500">Output tokens</div>
<div className="font-mono">{fmtTokens(data.tokensOut)}</div>
</div>
<div>
<div className="text-xs text-slate-500">Total / Budget</div>
<div className="font-mono">
{fmtTokens(data.tokensTotal)}{' '}
<span className="text-slate-400">
/ {data.tokensBudget !== null ? fmtTokens(data.tokensBudget) : '∞'}
</span>
</div>
</div>
</div>
{pctUsed !== null && (
<div className="mt-3">
<div className="h-2 rounded bg-slate-100 overflow-hidden">
<div
className={`h-full ${pctUsed >= 100 ? 'bg-red-500' : pctUsed >= 80 ? 'bg-amber-500' : 'bg-accent'}`}
style={{ width: `${pctUsed}%` }}
/>
</div>
<div className="text-xs text-slate-500 mt-1 text-right">
{pctUsed.toFixed(1)}% used
{data.remaining !== null && ` · ${fmtTokens(data.remaining)} remaining`}
</div>
</div>
)}
{data.rateLimitRpm !== null && (
<div className="text-xs text-slate-500 mt-2">
Rate limit: {data.rateLimitRpm} rpm
</div>
)}
</div>
{/* History bars */}
<div className="border border-hairline rounded p-3">
<div className="text-xs font-medium text-slate-600 uppercase tracking-wide mb-2">
過去 12 か月
</div>
{data.history.length === 0 ? (
<div className="text-sm text-slate-400 italic">履歴なし</div>
) : (
<div className="space-y-1.5">
{data.history.map((h) => {
const total = h.tokensIn + h.tokensOut;
const widthPct = (total / maxHistTokens) * 100;
return (
<div key={h.period} className="flex items-center gap-2 text-xs">
<span className="font-mono w-16 text-slate-500">{h.period}</span>
<div className="flex-1 h-3 bg-slate-100 rounded overflow-hidden">
<div
className="h-full bg-accent/70"
style={{ width: `${Math.max(2, widthPct)}%` }}
/>
</div>
<span className="font-mono w-20 text-right text-slate-600">
{fmtTokens(total)}
</span>
<span className="font-mono w-12 text-right text-slate-400">
{h.requests.toLocaleString()} rq
</span>
</div>
);
})}
</div>
)}
</div>
</>
)}
</div>
</div>
);
}