|
| 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 | + |
1 | 46 | 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 | + ); |
3 | 191 | } |
0 commit comments