Skip to content

Commit 75643a7

Browse files
committed
feat: add usage page with charts and date range toggle
1 parent e80144f commit 75643a7

2 files changed

Lines changed: 218 additions & 1 deletion

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export default function UsageMeter({
2+
label,
3+
current,
4+
limit,
5+
}: {
6+
label: string;
7+
current: number;
8+
limit: number;
9+
}) {
10+
const percentage = limit > 0 ? Math.min((current / limit) * 100, 100) : 0;
11+
const isNearLimit = percentage >= 80;
12+
13+
return (
14+
<div>
15+
<div className="mb-1 flex items-center justify-between text-sm">
16+
<span className="text-on-surface-variant">{label}</span>
17+
<span className="text-outline">
18+
{current.toLocaleString()} / {limit.toLocaleString()}
19+
</span>
20+
</div>
21+
<div className="h-2 bg-surface-bright">
22+
<div
23+
className={`h-full transition-all ${isNearLimit ? 'bg-error' : 'bg-primary'}`}
24+
style={{ width: `${percentage}%` }}
25+
/>
26+
</div>
27+
</div>
28+
);
29+
}

src/pages/Usage.tsx

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,191 @@
1+
import { useState } from 'react';
2+
import {
3+
LineChart,
4+
Line,
5+
BarChart,
6+
Bar,
7+
XAxis,
8+
YAxis,
9+
CartesianGrid,
10+
Tooltip,
11+
ResponsiveContainer,
12+
} from 'recharts';
13+
import { useTeam } from '../hooks/useTeam';
14+
import {
15+
useDailyUsage,
16+
useUsageByKey,
17+
useUsageByEndpoint,
18+
useUsageSummary,
19+
} from '../hooks/useUsage';
20+
import { useQuery } from '@tanstack/react-query';
21+
import { getCurrentBilling } from '../api/billing';
22+
import UsageMeter from '../components/billing/UsageMeter';
23+
24+
type Period = 'daily' | 'weekly' | 'monthly';
25+
26+
function getDateRange(period: Period) {
27+
const to = new Date();
28+
const from = new Date();
29+
if (period === 'daily') from.setDate(from.getDate() - 7);
30+
else if (period === 'weekly') from.setDate(from.getDate() - 30);
31+
else from.setDate(from.getDate() - 90);
32+
return {
33+
from: from.toISOString().split('T')[0]!,
34+
to: to.toISOString().split('T')[0]!,
35+
};
36+
}
37+
38+
const tooltipStyle = {
39+
backgroundColor: '#141414',
40+
border: '1px solid #444444',
41+
borderRadius: 0,
42+
color: '#c4c7c5',
43+
fontSize: 12,
44+
};
45+
146
export default function Usage() {
2-
return <div className="text-on-surface-variant">Usage</div>;
47+
const { currentTeam } = useTeam();
48+
const teamId = currentTeam?.id;
49+
const [period, setPeriod] = useState<Period>('daily');
50+
const { from, to } = getDateRange(period);
51+
52+
const { data: daily = [] } = useDailyUsage(teamId, from, to);
53+
const { data: byEndpoint = [] } = useUsageByEndpoint(teamId);
54+
const { data: byKey = [] } = useUsageByKey(teamId);
55+
const { data: summary } = useUsageSummary(teamId);
56+
const { data: billing } = useQuery({
57+
queryKey: ['billing', teamId],
58+
queryFn: () => getCurrentBilling(teamId!),
59+
enabled: !!teamId,
60+
});
61+
62+
return (
63+
<div className="space-y-6">
64+
{billing && summary && (
65+
<div className="border border-outline-variant bg-surface-container p-5">
66+
<h3 className="mb-4 font-heading text-sm font-semibold text-on-surface">Plan Usage</h3>
67+
<div className="space-y-3">
68+
<UsageMeter
69+
label="Requests"
70+
current={billing.usage.requests}
71+
limit={billing.plan.limits.requestsPerMonth}
72+
/>
73+
<UsageMeter
74+
label="Tokens"
75+
current={billing.usage.tokens}
76+
limit={billing.plan.limits.tokensPerMonth}
77+
/>
78+
</div>
79+
</div>
80+
)}
81+
82+
<div className="border border-outline-variant bg-surface-container p-5">
83+
<div className="mb-4 flex items-center justify-between">
84+
<h3 className="font-heading text-sm font-semibold text-on-surface">Requests Over Time</h3>
85+
<div className="flex gap-1">
86+
{(['daily', 'weekly', 'monthly'] as Period[]).map((p) => (
87+
<button
88+
key={p}
89+
onClick={() => setPeriod(p)}
90+
className={`px-3 py-1 text-xs ${
91+
period === p
92+
? 'bg-surface-bright text-on-surface'
93+
: 'text-outline hover:text-on-surface-variant'
94+
}`}
95+
>
96+
{p}
97+
</button>
98+
))}
99+
</div>
100+
</div>
101+
{daily.length > 0 ? (
102+
<ResponsiveContainer width="100%" height={280}>
103+
<LineChart data={daily}>
104+
<CartesianGrid strokeDasharray="3 3" stroke="#444444" />
105+
<XAxis dataKey="date" stroke="#767575" tick={{ fontSize: 12 }} />
106+
<YAxis stroke="#767575" tick={{ fontSize: 12 }} />
107+
<Tooltip contentStyle={tooltipStyle} />
108+
<Line
109+
type="monotone"
110+
dataKey="requests"
111+
stroke="#c6c6c7"
112+
strokeWidth={2}
113+
dot={false}
114+
/>
115+
</LineChart>
116+
</ResponsiveContainer>
117+
) : (
118+
<p className="text-sm text-outline">No data available</p>
119+
)}
120+
</div>
121+
122+
<div className="border border-outline-variant bg-surface-container p-5">
123+
<h3 className="mb-4 font-heading text-sm font-semibold text-on-surface">
124+
Requests by Endpoint
125+
</h3>
126+
{byEndpoint.length > 0 ? (
127+
<ResponsiveContainer width="100%" height={280}>
128+
<BarChart data={byEndpoint}>
129+
<CartesianGrid strokeDasharray="3 3" stroke="#444444" />
130+
<XAxis dataKey="endpoint" stroke="#767575" tick={{ fontSize: 10 }} />
131+
<YAxis stroke="#767575" tick={{ fontSize: 12 }} />
132+
<Tooltip contentStyle={tooltipStyle} />
133+
<Bar dataKey="requests" fill="#c6c6c7" />
134+
</BarChart>
135+
</ResponsiveContainer>
136+
) : (
137+
<p className="text-sm text-outline">No data available</p>
138+
)}
139+
</div>
140+
141+
<div className="border border-outline-variant bg-surface-container p-5">
142+
<h3 className="mb-4 font-heading text-sm font-semibold text-on-surface">Token Usage</h3>
143+
{daily.length > 0 ? (
144+
<ResponsiveContainer width="100%" height={240}>
145+
<LineChart data={daily}>
146+
<CartesianGrid strokeDasharray="3 3" stroke="#444444" />
147+
<XAxis dataKey="date" stroke="#767575" tick={{ fontSize: 12 }} />
148+
<YAxis stroke="#767575" tick={{ fontSize: 12 }} />
149+
<Tooltip contentStyle={tooltipStyle} />
150+
<Line type="monotone" dataKey="tokens" stroke="#22c55e" strokeWidth={2} dot={false} />
151+
</LineChart>
152+
</ResponsiveContainer>
153+
) : (
154+
<p className="text-sm text-outline">No data available</p>
155+
)}
156+
</div>
157+
158+
<div className="border border-outline-variant bg-surface-container">
159+
<h3 className="p-5 pb-3 font-heading text-sm font-semibold text-on-surface">
160+
Usage by API Key
161+
</h3>
162+
{byKey.length > 0 ? (
163+
<table className="w-full text-left text-sm">
164+
<thead>
165+
<tr className="border-b border-outline-variant text-xs text-outline">
166+
<th className="px-5 py-3 font-medium">Key</th>
167+
<th className="px-5 py-3 font-medium">Prefix</th>
168+
<th className="px-5 py-3 font-medium">Requests</th>
169+
<th className="px-5 py-3 font-medium">Tokens</th>
170+
</tr>
171+
</thead>
172+
<tbody>
173+
{byKey.map((k) => (
174+
<tr key={k.keyId} className="border-b border-outline-variant">
175+
<td className="px-5 py-3 text-on-surface-variant">{k.keyName}</td>
176+
<td className="px-5 py-3 font-mono text-xs text-outline">{k.keyPrefix}...</td>
177+
<td className="px-5 py-3 text-on-surface-variant">
178+
{k.requests.toLocaleString()}
179+
</td>
180+
<td className="px-5 py-3 text-on-surface-variant">{k.tokens.toLocaleString()}</td>
181+
</tr>
182+
))}
183+
</tbody>
184+
</table>
185+
) : (
186+
<p className="px-5 pb-5 text-sm text-outline">No data available</p>
187+
)}
188+
</div>
189+
</div>
190+
);
3191
}

0 commit comments

Comments
 (0)