costhawk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +32 -0
- package/STRATEGIC_PLAN_2025-12-01.md +934 -0
- package/costcanary/.claude/settings.local.json +9 -0
- package/costcanary/.env.production.template +38 -0
- package/costcanary/.eslintrc.json +22 -0
- package/costcanary/.nvmrc +1 -0
- package/costcanary/.prettierignore +11 -0
- package/costcanary/.prettierrc.json +12 -0
- package/costcanary/ADMIN_SETUP.md +68 -0
- package/costcanary/CLAUDE.md +228 -0
- package/costcanary/CLERK_SETUP.md +69 -0
- package/costcanary/DATABASE_SETUP.md +136 -0
- package/costcanary/DEMO_CHECKLIST.md +62 -0
- package/costcanary/DEPLOYMENT.md +31 -0
- package/costcanary/PRODUCTION_RECOVERY.md +109 -0
- package/costcanary/README.md +247 -0
- package/costcanary/STRIPE_SECURITY_AUDIT.md +123 -0
- package/costcanary/TESTING_ADMIN.md +92 -0
- package/costcanary/app/(auth)/sign-in/[[...sign-in]]/page.tsx +25 -0
- package/costcanary/app/(auth)/sign-up/[[...sign-up]]/page.tsx +25 -0
- package/costcanary/app/(dashboard)/dashboard/admin/page.tsx +260 -0
- package/costcanary/app/(dashboard)/dashboard/alerts/page.tsx +64 -0
- package/costcanary/app/(dashboard)/dashboard/api-keys/page.tsx +231 -0
- package/costcanary/app/(dashboard)/dashboard/billing/page.tsx +349 -0
- package/costcanary/app/(dashboard)/dashboard/layout.tsx +188 -0
- package/costcanary/app/(dashboard)/dashboard/page.tsx +13 -0
- package/costcanary/app/(dashboard)/dashboard/playground/page.tsx +605 -0
- package/costcanary/app/(dashboard)/dashboard/settings/page.tsx +86 -0
- package/costcanary/app/(dashboard)/dashboard/usage/page.tsx +354 -0
- package/costcanary/app/(dashboard)/dashboard/wrapped-keys/page.tsx +677 -0
- package/costcanary/app/(marketing)/page.tsx +90 -0
- package/costcanary/app/(marketing)/pricing/page.tsx +272 -0
- package/costcanary/app/admin/pricing-status/page.tsx +338 -0
- package/costcanary/app/api/admin/check-pricing/route.ts +127 -0
- package/costcanary/app/api/admin/debug/route.ts +44 -0
- package/costcanary/app/api/admin/fix-pricing/route.ts +216 -0
- package/costcanary/app/api/admin/pricing-jobs/[jobId]/route.ts +48 -0
- package/costcanary/app/api/admin/pricing-jobs/route.ts +45 -0
- package/costcanary/app/api/admin/trigger-pricing/route.ts +209 -0
- package/costcanary/app/api/admin/whoami/route.ts +44 -0
- package/costcanary/app/api/auth/clerk/[...nextjs]/route.ts +93 -0
- package/costcanary/app/api/debug/wrapped-key/route.ts +51 -0
- package/costcanary/app/api/debug-status/route.ts +9 -0
- package/costcanary/app/api/debug-version/route.ts +12 -0
- package/costcanary/app/api/health/route.ts +14 -0
- package/costcanary/app/api/health-simple/route.ts +18 -0
- package/costcanary/app/api/keys/route.ts +162 -0
- package/costcanary/app/api/keys/wrapped/[id]/revoke/route.ts +86 -0
- package/costcanary/app/api/keys/wrapped/[id]/rotate/route.ts +81 -0
- package/costcanary/app/api/keys/wrapped/route.ts +241 -0
- package/costcanary/app/api/optimizer/preview/route.ts +147 -0
- package/costcanary/app/api/optimizer/route.ts +118 -0
- package/costcanary/app/api/pricing/models/route.ts +102 -0
- package/costcanary/app/api/proxy/[...path]/route.ts +391 -0
- package/costcanary/app/api/proxy/anthropic/route.ts +539 -0
- package/costcanary/app/api/proxy/google/route.ts +395 -0
- package/costcanary/app/api/proxy/openai/route.ts +529 -0
- package/costcanary/app/api/simple-test/route.ts +7 -0
- package/costcanary/app/api/stripe/checkout/route.ts +201 -0
- package/costcanary/app/api/stripe/webhook/route.ts +392 -0
- package/costcanary/app/api/test-connection/route.ts +209 -0
- package/costcanary/app/api/test-proxy/route.ts +7 -0
- package/costcanary/app/api/test-simple/route.ts +20 -0
- package/costcanary/app/api/usage/current/route.ts +112 -0
- package/costcanary/app/api/usage/stats/route.ts +129 -0
- package/costcanary/app/api/usage/stream/route.ts +113 -0
- package/costcanary/app/api/usage/summary/route.ts +67 -0
- package/costcanary/app/api/usage/trend/route.ts +119 -0
- package/costcanary/app/api/ws/route.ts +23 -0
- package/costcanary/app/globals.css +280 -0
- package/costcanary/app/layout.tsx +87 -0
- package/costcanary/components/Header.tsx +85 -0
- package/costcanary/components/dashboard/AddApiKeyModal.tsx +264 -0
- package/costcanary/components/dashboard/dashboard-content.tsx +329 -0
- package/costcanary/components/landing/DashboardPreview.tsx +222 -0
- package/costcanary/components/landing/Features.tsx +238 -0
- package/costcanary/components/landing/Footer.tsx +83 -0
- package/costcanary/components/landing/Hero.tsx +193 -0
- package/costcanary/components/landing/Pricing.tsx +250 -0
- package/costcanary/components/landing/Testimonials.tsx +248 -0
- package/costcanary/components/theme-provider.tsx +8 -0
- package/costcanary/components/ui/alert.tsx +59 -0
- package/costcanary/components/ui/badge.tsx +36 -0
- package/costcanary/components/ui/button.tsx +56 -0
- package/costcanary/components/ui/card.tsx +79 -0
- package/costcanary/components/ui/dialog.tsx +122 -0
- package/costcanary/components/ui/input.tsx +22 -0
- package/costcanary/components/ui/label.tsx +26 -0
- package/costcanary/components/ui/progress.tsx +28 -0
- package/costcanary/components/ui/select.tsx +160 -0
- package/costcanary/components/ui/separator.tsx +31 -0
- package/costcanary/components/ui/switch.tsx +29 -0
- package/costcanary/components/ui/tabs.tsx +55 -0
- package/costcanary/components/ui/toast.tsx +127 -0
- package/costcanary/components/ui/toaster.tsx +35 -0
- package/costcanary/components/ui/use-toast.ts +189 -0
- package/costcanary/components.json +17 -0
- package/costcanary/debug-wrapped-keys.md +117 -0
- package/costcanary/fix-console.sh +30 -0
- package/costcanary/lib/admin-auth.ts +226 -0
- package/costcanary/lib/admin-security.ts +124 -0
- package/costcanary/lib/audit-events.ts +62 -0
- package/costcanary/lib/audit.ts +158 -0
- package/costcanary/lib/chart-colors.ts +152 -0
- package/costcanary/lib/cost-calculator.ts +212 -0
- package/costcanary/lib/db-utils.ts +325 -0
- package/costcanary/lib/db.ts +14 -0
- package/costcanary/lib/encryption.ts +120 -0
- package/costcanary/lib/kms.ts +358 -0
- package/costcanary/lib/model-alias.ts +180 -0
- package/costcanary/lib/pricing.ts +292 -0
- package/costcanary/lib/prisma.ts +52 -0
- package/costcanary/lib/railway-db.ts +157 -0
- package/costcanary/lib/sse-parser.ts +283 -0
- package/costcanary/lib/stripe-client.ts +81 -0
- package/costcanary/lib/stripe-server.ts +52 -0
- package/costcanary/lib/tokens.ts +396 -0
- package/costcanary/lib/usage-limits.ts +164 -0
- package/costcanary/lib/utils.ts +6 -0
- package/costcanary/lib/websocket.ts +153 -0
- package/costcanary/lib/wrapped-keys.ts +531 -0
- package/costcanary/market-research.md +443 -0
- package/costcanary/middleware.ts +48 -0
- package/costcanary/next.config.js +43 -0
- package/costcanary/nia-sources.md +151 -0
- package/costcanary/package-lock.json +12162 -0
- package/costcanary/package.json +92 -0
- package/costcanary/package.json.backup +89 -0
- package/costcanary/postcss.config.js +6 -0
- package/costcanary/pricing-worker/.env.example +8 -0
- package/costcanary/pricing-worker/README.md +81 -0
- package/costcanary/pricing-worker/package-lock.json +1109 -0
- package/costcanary/pricing-worker/package.json +26 -0
- package/costcanary/pricing-worker/railway.json +13 -0
- package/costcanary/pricing-worker/schema.prisma +326 -0
- package/costcanary/pricing-worker/src/index.ts +115 -0
- package/costcanary/pricing-worker/src/services/pricing-updater.ts +79 -0
- package/costcanary/pricing-worker/src/services/tavily-client.ts +474 -0
- package/costcanary/pricing-worker/test-tavily.ts +47 -0
- package/costcanary/pricing-worker/tsconfig.json +24 -0
- package/costcanary/prisma/migrations/001_add_stripe_fields.sql +26 -0
- package/costcanary/prisma/schema.prisma +326 -0
- package/costcanary/prisma/seed-pricing.ts +133 -0
- package/costcanary/public/costhawk-logo.png +0 -0
- package/costcanary/railway.json +30 -0
- package/costcanary/railway.toml +16 -0
- package/costcanary/research-nia.md +298 -0
- package/costcanary/research.md +411 -0
- package/costcanary/scripts/build-production.js +65 -0
- package/costcanary/scripts/check-current-pricing.ts +51 -0
- package/costcanary/scripts/check-pricing-data.ts +174 -0
- package/costcanary/scripts/create-stripe-prices.js +49 -0
- package/costcanary/scripts/fix-pricing-data.ts +135 -0
- package/costcanary/scripts/fix-pricing-db.ts +148 -0
- package/costcanary/scripts/postinstall.js +58 -0
- package/costcanary/scripts/railway-deploy.sh +52 -0
- package/costcanary/scripts/run-migration.js +61 -0
- package/costcanary/scripts/start-production.js +175 -0
- package/costcanary/scripts/test-wrapped-key.ts +85 -0
- package/costcanary/scripts/validate-deployment.js +176 -0
- package/costcanary/scripts/validate-production.js +119 -0
- package/costcanary/server.js.backup +38 -0
- package/costcanary/tailwind.config.ts +216 -0
- package/costcanary/test-pricing-status.sh +27 -0
- package/costcanary/tsconfig.json +42 -0
- package/docs/sessions/session-2025-12-01.md +570 -0
- package/executive-summary.md +302 -0
- package/index.js +1 -0
- package/nia-sources.md +163 -0
- package/package.json +16 -0
- package/research.md +750 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { motion } from "framer-motion"
|
|
5
|
+
import { Download, TrendingUp, DollarSign, Activity, BarChart3 } from "lucide-react"
|
|
6
|
+
import {
|
|
7
|
+
BarChart, Bar, PieChart, Pie, Cell, Legend,
|
|
8
|
+
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer
|
|
9
|
+
} from "recharts"
|
|
10
|
+
import { chartColors, providerColors, chartConfig } from "@/lib/chart-colors"
|
|
11
|
+
|
|
12
|
+
interface UsageData {
|
|
13
|
+
period: string
|
|
14
|
+
provider: string | null
|
|
15
|
+
usage: {
|
|
16
|
+
totalCost: number
|
|
17
|
+
totalRequests: number
|
|
18
|
+
avgLatency: number
|
|
19
|
+
byProvider: Array<{
|
|
20
|
+
provider: string
|
|
21
|
+
cost: number
|
|
22
|
+
requests: number
|
|
23
|
+
}>
|
|
24
|
+
}
|
|
25
|
+
budgets: Array<{
|
|
26
|
+
name: string
|
|
27
|
+
spent: number
|
|
28
|
+
limit: number
|
|
29
|
+
percentage: number
|
|
30
|
+
remaining: number
|
|
31
|
+
status: string
|
|
32
|
+
}>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use consistent provider colors from chart-colors.ts
|
|
36
|
+
|
|
37
|
+
export default function UsagePage() {
|
|
38
|
+
const [usageData, setUsageData] = useState<UsageData | null>(null)
|
|
39
|
+
const [loading, setLoading] = useState(true)
|
|
40
|
+
const [period, setPeriod] = useState<string>("DAILY")
|
|
41
|
+
const [selectedProvider, setSelectedProvider] = useState<string | null>(null)
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
fetchUsageData()
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [period, selectedProvider])
|
|
47
|
+
|
|
48
|
+
const fetchUsageData = async () => {
|
|
49
|
+
setLoading(true)
|
|
50
|
+
try {
|
|
51
|
+
const params = new URLSearchParams({
|
|
52
|
+
period,
|
|
53
|
+
...(selectedProvider && { provider: selectedProvider }),
|
|
54
|
+
})
|
|
55
|
+
const response = await fetch(`/api/usage/current?${params}`)
|
|
56
|
+
const data = await response.json()
|
|
57
|
+
setUsageData(data)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Failed to fetch usage data:", error)
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleExportCSV = async () => {
|
|
66
|
+
// TODO: Implement CSV export
|
|
67
|
+
alert("CSV export coming soon!")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const formatCurrency = (value: number) => {
|
|
71
|
+
return new Intl.NumberFormat("en-US", {
|
|
72
|
+
style: "currency",
|
|
73
|
+
currency: "USD",
|
|
74
|
+
minimumFractionDigits: 2,
|
|
75
|
+
maximumFractionDigits: 4,
|
|
76
|
+
}).format(value)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Prepare chart data - ensure values are numbers (Prisma returns Decimals)
|
|
80
|
+
const pieData = usageData?.usage?.byProvider?.map((p: any) => ({
|
|
81
|
+
name: p.provider,
|
|
82
|
+
value: Number(p.cost) || 0,
|
|
83
|
+
})) || []
|
|
84
|
+
|
|
85
|
+
const barData = usageData?.usage?.byProvider?.map((p: any) => ({
|
|
86
|
+
provider: p.provider,
|
|
87
|
+
requests: Number(p.requests) || 0,
|
|
88
|
+
cost: Number(p.cost) || 0,
|
|
89
|
+
})) || []
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="space-y-6">
|
|
93
|
+
{/* Header */}
|
|
94
|
+
<div className="flex items-center justify-between">
|
|
95
|
+
<div>
|
|
96
|
+
<h1 className="text-3xl font-bold text-white">Usage Analytics</h1>
|
|
97
|
+
<p className="mt-2 text-charcoal-400">
|
|
98
|
+
Track your API usage and costs across all providers
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
<motion.button
|
|
102
|
+
whileHover={{ scale: 1.05 }}
|
|
103
|
+
whileTap={{ scale: 0.95 }}
|
|
104
|
+
onClick={handleExportCSV}
|
|
105
|
+
className="flex items-center space-x-2 rounded-lg border border-charcoal-700 bg-charcoal-800 px-4 py-2 text-white transition-colors hover:bg-charcoal-700"
|
|
106
|
+
>
|
|
107
|
+
<Download className="h-5 w-5" />
|
|
108
|
+
<span>Export CSV</span>
|
|
109
|
+
</motion.button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Filters */}
|
|
113
|
+
<div className="flex space-x-4">
|
|
114
|
+
<select
|
|
115
|
+
value={period}
|
|
116
|
+
onChange={(e) => setPeriod(e.target.value)}
|
|
117
|
+
className="rounded-lg border border-charcoal-700 bg-charcoal-800 px-4 py-2 text-white focus:border-coral-500 focus:outline-none"
|
|
118
|
+
>
|
|
119
|
+
<option value="DAILY">Today</option>
|
|
120
|
+
<option value="WEEKLY">This Week</option>
|
|
121
|
+
<option value="MONTHLY">This Month</option>
|
|
122
|
+
<option value="QUARTERLY">This Quarter</option>
|
|
123
|
+
<option value="YEARLY">This Year</option>
|
|
124
|
+
</select>
|
|
125
|
+
|
|
126
|
+
<select
|
|
127
|
+
value={selectedProvider || ""}
|
|
128
|
+
onChange={(e) => setSelectedProvider(e.target.value || null)}
|
|
129
|
+
className="rounded-lg border border-charcoal-700 bg-charcoal-800 px-4 py-2 text-white focus:border-coral-500 focus:outline-none"
|
|
130
|
+
>
|
|
131
|
+
<option value="">All Providers</option>
|
|
132
|
+
<option value="OPENAI">OpenAI</option>
|
|
133
|
+
<option value="ANTHROPIC">Anthropic</option>
|
|
134
|
+
<option value="GOOGLE">Google</option>
|
|
135
|
+
<option value="GROK">xAI Grok</option>
|
|
136
|
+
</select>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{loading ? (
|
|
140
|
+
<div className="space-y-4">
|
|
141
|
+
{[1, 2, 3].map((i) => (
|
|
142
|
+
<div key={i} className="h-32 animate-pulse rounded-lg bg-charcoal-900" />
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
<>
|
|
147
|
+
{/* Stats Cards */}
|
|
148
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
149
|
+
<motion.div
|
|
150
|
+
initial={{ opacity: 0, y: 20 }}
|
|
151
|
+
animate={{ opacity: 1, y: 0 }}
|
|
152
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
153
|
+
>
|
|
154
|
+
<div className="flex items-center justify-between">
|
|
155
|
+
<div>
|
|
156
|
+
<p className="text-sm text-charcoal-400">Total Cost</p>
|
|
157
|
+
<p className="mt-2 text-2xl font-bold text-white">
|
|
158
|
+
{formatCurrency(usageData?.usage.totalCost || 0)}
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
<DollarSign className="h-8 w-8 text-coral-500" />
|
|
162
|
+
</div>
|
|
163
|
+
</motion.div>
|
|
164
|
+
|
|
165
|
+
<motion.div
|
|
166
|
+
initial={{ opacity: 0, y: 20 }}
|
|
167
|
+
animate={{ opacity: 1, y: 0 }}
|
|
168
|
+
transition={{ delay: 0.1 }}
|
|
169
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
170
|
+
>
|
|
171
|
+
<div className="flex items-center justify-between">
|
|
172
|
+
<div>
|
|
173
|
+
<p className="text-sm text-charcoal-400">Total Requests</p>
|
|
174
|
+
<p className="mt-2 text-2xl font-bold text-white">
|
|
175
|
+
{usageData?.usage.totalRequests.toLocaleString() || 0}
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
<BarChart3 className="h-8 w-8 text-coral-500" />
|
|
179
|
+
</div>
|
|
180
|
+
</motion.div>
|
|
181
|
+
|
|
182
|
+
<motion.div
|
|
183
|
+
initial={{ opacity: 0, y: 20 }}
|
|
184
|
+
animate={{ opacity: 1, y: 0 }}
|
|
185
|
+
transition={{ delay: 0.2 }}
|
|
186
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
187
|
+
>
|
|
188
|
+
<div className="flex items-center justify-between">
|
|
189
|
+
<div>
|
|
190
|
+
<p className="text-sm text-charcoal-400">Avg Latency</p>
|
|
191
|
+
<p className="mt-2 text-2xl font-bold text-white">
|
|
192
|
+
{usageData?.usage.avgLatency || 0}ms
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
<Activity className="h-8 w-8 text-coral-500" />
|
|
196
|
+
</div>
|
|
197
|
+
</motion.div>
|
|
198
|
+
|
|
199
|
+
<motion.div
|
|
200
|
+
initial={{ opacity: 0, y: 20 }}
|
|
201
|
+
animate={{ opacity: 1, y: 0 }}
|
|
202
|
+
transition={{ delay: 0.3 }}
|
|
203
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
204
|
+
>
|
|
205
|
+
<div className="flex items-center justify-between">
|
|
206
|
+
<div>
|
|
207
|
+
<p className="text-sm text-charcoal-400">Active Providers</p>
|
|
208
|
+
<p className="mt-2 text-2xl font-bold text-white">
|
|
209
|
+
{usageData?.usage.byProvider.length || 0}
|
|
210
|
+
</p>
|
|
211
|
+
</div>
|
|
212
|
+
<TrendingUp className="h-8 w-8 text-coral-500" />
|
|
213
|
+
</div>
|
|
214
|
+
</motion.div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Charts */}
|
|
218
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
219
|
+
{/* Cost by Provider Pie Chart */}
|
|
220
|
+
<motion.div
|
|
221
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
222
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
223
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
224
|
+
>
|
|
225
|
+
<h3 className="mb-4 text-lg font-medium text-white">Cost by Provider</h3>
|
|
226
|
+
{pieData.length > 0 ? (
|
|
227
|
+
<ResponsiveContainer width="100%" height={300}>
|
|
228
|
+
<PieChart>
|
|
229
|
+
<Pie
|
|
230
|
+
data={pieData}
|
|
231
|
+
cx="50%"
|
|
232
|
+
cy="45%"
|
|
233
|
+
innerRadius={50}
|
|
234
|
+
outerRadius={90}
|
|
235
|
+
dataKey="value"
|
|
236
|
+
nameKey="name"
|
|
237
|
+
fill={chartColors.coral}
|
|
238
|
+
>
|
|
239
|
+
{pieData.map((entry, index) => (
|
|
240
|
+
<Cell
|
|
241
|
+
key={`cell-${index}`}
|
|
242
|
+
fill={providerColors[entry.name] || chartColors.coral}
|
|
243
|
+
/>
|
|
244
|
+
))}
|
|
245
|
+
</Pie>
|
|
246
|
+
<Tooltip
|
|
247
|
+
content={({ active, payload }) => {
|
|
248
|
+
if (active && payload && payload.length) {
|
|
249
|
+
const data = payload[0].payload
|
|
250
|
+
return (
|
|
251
|
+
<div style={{
|
|
252
|
+
backgroundColor: '#2D2F36',
|
|
253
|
+
border: '1px solid #3D3F47',
|
|
254
|
+
borderRadius: '8px',
|
|
255
|
+
padding: '12px',
|
|
256
|
+
}}>
|
|
257
|
+
<p style={{ color: '#F8FAFC', fontWeight: 600, marginBottom: '4px' }}>
|
|
258
|
+
{data.name}
|
|
259
|
+
</p>
|
|
260
|
+
<p style={{ color: '#CBD5E1' }}>
|
|
261
|
+
Cost: {formatCurrency(data.value)}
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
return null
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
<Legend
|
|
270
|
+
verticalAlign="bottom"
|
|
271
|
+
height={36}
|
|
272
|
+
formatter={(value) => <span style={{ color: '#F8FAFC' }}>{value}</span>}
|
|
273
|
+
/>
|
|
274
|
+
</PieChart>
|
|
275
|
+
</ResponsiveContainer>
|
|
276
|
+
) : (
|
|
277
|
+
<div className="flex h-[300px] items-center justify-center text-charcoal-500">
|
|
278
|
+
No data available
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</motion.div>
|
|
282
|
+
|
|
283
|
+
{/* Requests by Provider Bar Chart */}
|
|
284
|
+
<motion.div
|
|
285
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
286
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
287
|
+
transition={{ delay: 0.1 }}
|
|
288
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
289
|
+
>
|
|
290
|
+
<h3 className="mb-4 text-lg font-medium text-white">Requests by Provider</h3>
|
|
291
|
+
{barData.length > 0 ? (
|
|
292
|
+
<ResponsiveContainer width="100%" height={300}>
|
|
293
|
+
<BarChart data={barData}>
|
|
294
|
+
<CartesianGrid {...chartConfig.grid} />
|
|
295
|
+
<XAxis dataKey="provider" {...chartConfig.xAxis} />
|
|
296
|
+
<YAxis {...chartConfig.yAxis} />
|
|
297
|
+
<Tooltip
|
|
298
|
+
contentStyle={chartConfig.tooltip.contentStyle}
|
|
299
|
+
labelStyle={chartConfig.tooltip.labelStyle}
|
|
300
|
+
/>
|
|
301
|
+
<Bar dataKey="requests" fill={chartColors.coral} radius={[6, 6, 0, 0]} />
|
|
302
|
+
</BarChart>
|
|
303
|
+
</ResponsiveContainer>
|
|
304
|
+
) : (
|
|
305
|
+
<div className="flex h-[300px] items-center justify-center text-charcoal-500">
|
|
306
|
+
No data available
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</motion.div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{/* Budget Status */}
|
|
313
|
+
{usageData?.budgets && usageData.budgets.length > 0 && (
|
|
314
|
+
<motion.div
|
|
315
|
+
initial={{ opacity: 0, y: 20 }}
|
|
316
|
+
animate={{ opacity: 1, y: 0 }}
|
|
317
|
+
className="rounded-lg border border-charcoal-800 bg-charcoal-900/50 p-6 backdrop-blur-sm"
|
|
318
|
+
>
|
|
319
|
+
<h3 className="mb-4 text-lg font-medium text-white">Budget Status</h3>
|
|
320
|
+
<div className="space-y-4">
|
|
321
|
+
{usageData.budgets.map((budget, index) => (
|
|
322
|
+
<div key={index}>
|
|
323
|
+
<div className="flex items-center justify-between text-sm">
|
|
324
|
+
<span className="text-charcoal-400">{budget.name}</span>
|
|
325
|
+
<span className={`font-medium ${
|
|
326
|
+
budget.status === "exceeded" ? "text-red-500" :
|
|
327
|
+
budget.status === "warning" ? "text-yellow-500" :
|
|
328
|
+
"text-green-500"
|
|
329
|
+
}`}>
|
|
330
|
+
{formatCurrency(budget.spent)} / {formatCurrency(budget.limit)}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="mt-2 h-2 overflow-hidden rounded-full bg-charcoal-800">
|
|
334
|
+
<motion.div
|
|
335
|
+
initial={{ width: 0 }}
|
|
336
|
+
animate={{ width: `${Math.min(budget.percentage, 100)}%` }}
|
|
337
|
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
|
338
|
+
className={`h-full ${
|
|
339
|
+
budget.status === "exceeded" ? "bg-red-500" :
|
|
340
|
+
budget.status === "warning" ? "bg-yellow-500" :
|
|
341
|
+
"bg-green-500"
|
|
342
|
+
}`}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
</motion.div>
|
|
349
|
+
)}
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|