Files
maestro/ui/src/components/settings/GatewayKeyUsagePanel.tsx
T
oss-sync d061ad08d8
CI / build-and-test (push) Has been cancelled
sync: update from private repo (e62f5c7)
2026-06-11 01:52:48 +00:00

153 lines
6.1 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 { useTranslation } from 'react-i18next';
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 { t } = useTranslation('settings');
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-surface 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">{t('gateway.usagePanel.title')}</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">{t('gateway.keys.fetchError', { msg: 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">
{t('gateway.usagePanel.thisMonth', { period: 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">
{t('gateway.usagePanel.past12')}
</div>
{data.history.length === 0 ? (
<div className="text-sm text-slate-400 italic">{t('gateway.usagePanel.noHistory')}</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>
);
}