Skip to content

Commit adc33d6

Browse files
authored
Merge pull request #276 from DevanshuNEU/feat/usage-page
feat: Usage page -- plan info, resource bars, feature list (OPE-103)
2 parents 2b19e67 + 8ab65c4 commit adc33d6

3 files changed

Lines changed: 223 additions & 0 deletions

File tree

frontend/src/components/Dashboard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import { Routes, Route, Navigate } from 'react-router-dom'
22
import { DashboardLayout } from './dashboard/DashboardLayout'
33
import { DashboardHome } from './dashboard/DashboardHome'
44
import { SettingsPage } from '../pages/SettingsPage'
5+
import { UsagePage } from '../pages/UsagePage'
56

67
export function Dashboard() {
78
return (
89
<DashboardLayout>
910
<Routes>
1011
<Route index element={<DashboardHome />} />
12+
<Route path="usage" element={<UsagePage />} />
1113
<Route path="settings" element={<SettingsPage />} />
1214
<Route path="*" element={<Navigate to="/dashboard" replace />} />
1315
</Routes>

frontend/src/components/dashboard/Sidebar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Link, useLocation } from 'react-router-dom'
22
import {
33
FolderGit2,
4+
BarChart3,
45
BookOpen,
56
ChevronLeft,
67
ChevronRight,
@@ -24,6 +25,7 @@ interface NavItem {
2425

2526
const mainNavItems: NavItem[] = [
2627
{ name: 'Repositories', href: '/dashboard', icon: <FolderGit2 className="w-5 h-5" /> },
28+
{ name: 'Usage', href: '/dashboard/usage', icon: <BarChart3 className="w-5 h-5" /> },
2729
]
2830

2931
const bottomNavItems: NavItem[] = [

frontend/src/pages/UsagePage.tsx

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* UsagePage -- plan info, resource usage, and feature availability.
3+
*
4+
* Fetches from GET /users/usage. Shows tier, repo usage,
5+
* limits, and features in a compact two-column layout.
6+
*/
7+
8+
import { useAuth } from '@/contexts/AuthContext'
9+
import { useUserUsage } from '@/hooks/useCachedQuery'
10+
import {
11+
BarChart3, Package, FunctionSquare, Files, Zap, Search,
12+
Server, Sparkles, Lock, ArrowRight, TrendingUp,
13+
} from 'lucide-react'
14+
import { Card, CardContent } from '@/components/ui/card'
15+
import { Button } from '@/components/ui/button'
16+
import { Badge } from '@/components/ui/badge'
17+
import { Skeleton } from '@/components/ui/Skeleton'
18+
import { cn } from '@/lib/utils'
19+
20+
const TIER_COLORS: Record<string, string> = {
21+
free: 'bg-muted text-muted-foreground',
22+
pro: 'bg-primary/10 text-primary border-primary/20',
23+
enterprise: 'bg-amber-500/10 text-amber-500 border-amber-500/20',
24+
}
25+
26+
export function UsagePage() {
27+
const { session } = useAuth()
28+
const { data: usage, isLoading } = useUserUsage(session?.access_token, session?.user?.id)
29+
30+
if (isLoading) return <UsageSkeleton />
31+
if (!usage) {
32+
return (
33+
<div className="flex items-center justify-center min-h-[300px] text-muted-foreground">
34+
Failed to load usage data.
35+
</div>
36+
)
37+
}
38+
39+
const tier = usage.tier || 'free'
40+
const repos = usage.repositories || { current: 0, limit: 3 }
41+
const limits = usage.limits || { max_files_per_repo: 500, max_functions_per_repo: 2000 }
42+
const features = usage.features || { priority_indexing: false, mcp_access: true }
43+
const isFree = tier === 'free'
44+
45+
return (
46+
<div className="space-y-4 max-w-4xl">
47+
{/* Header */}
48+
<div className="flex items-center gap-3">
49+
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center">
50+
<BarChart3 className="w-5 h-5 text-primary" />
51+
</div>
52+
<div>
53+
<div className="flex items-center gap-2">
54+
<h1 className="text-2xl font-bold">Usage</h1>
55+
<Badge variant="outline" className={cn('capitalize', TIER_COLORS[tier])}>
56+
{tier}
57+
</Badge>
58+
</div>
59+
<p className="text-sm text-muted-foreground">Plan details and resource limits</p>
60+
</div>
61+
</div>
62+
63+
{/* Upgrade CTA for free users */}
64+
{isFree && (
65+
<div className="flex items-center justify-between rounded-lg bg-primary/5 border border-primary/10 px-4 py-3">
66+
<div>
67+
<p className="text-sm font-medium">Unlock higher limits and priority indexing</p>
68+
<p className="text-xs text-muted-foreground mt-0.5">
69+
Pro: 20 repos, 20K functions/repo, Cohere reranking
70+
</p>
71+
</div>
72+
<Button
73+
size="sm"
74+
className="ml-4 shrink-0"
75+
onClick={() => window.open('https://opencodeintel.com/#pricing', '_blank')}
76+
>
77+
Upgrade to Pro
78+
<ArrowRight className="w-3.5 h-3.5 ml-1" />
79+
</Button>
80+
</div>
81+
)}
82+
83+
{/* Two-column: Usage + Features */}
84+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
85+
{/* Left: Resource Usage */}
86+
<Card>
87+
<CardContent className="pt-5 space-y-4">
88+
<p className="text-sm font-medium text-muted-foreground">Resource Limits</p>
89+
<UsageRow
90+
icon={<Package className="w-4 h-4" />}
91+
label="Repositories"
92+
value={`${repos.current} / ${repos.limit ?? 'unlimited'}`}
93+
pct={repos.limit ? (repos.current / repos.limit) * 100 : 0}
94+
showBar
95+
/>
96+
<UsageRow
97+
icon={<Files className="w-4 h-4" />}
98+
label="Files / repo"
99+
value={`up to ${limits.max_files_per_repo.toLocaleString()}`}
100+
/>
101+
<UsageRow
102+
icon={<FunctionSquare className="w-4 h-4" />}
103+
label="Functions / repo"
104+
value={`up to ${limits.max_functions_per_repo.toLocaleString()}`}
105+
/>
106+
</CardContent>
107+
</Card>
108+
109+
{/* Right: Features */}
110+
<Card>
111+
<CardContent className="pt-5 space-y-3">
112+
<p className="text-sm font-medium text-muted-foreground">Features</p>
113+
<FeatureRow icon={<Search className="w-4 h-4" />} label="Semantic Code Search" enabled />
114+
<FeatureRow icon={<Sparkles className="w-4 h-4" />} label="Codebase DNA" enabled />
115+
<FeatureRow icon={<Server className="w-4 h-4" />} label="MCP Server Access" enabled={features.mcp_access} />
116+
<FeatureRow icon={<Zap className="w-4 h-4" />} label="Priority Indexing" enabled={features.priority_indexing} />
117+
</CardContent>
118+
</Card>
119+
</div>
120+
121+
{/* Cost tracking placeholder */}
122+
<Card className="border-dashed">
123+
<CardContent className="py-5 flex items-center gap-4">
124+
<div className="w-9 h-9 rounded-full bg-muted flex items-center justify-center shrink-0">
125+
<TrendingUp className="w-4 h-4 text-muted-foreground" />
126+
</div>
127+
<div>
128+
<p className="text-sm font-medium">API Cost Tracking</p>
129+
<p className="text-xs text-muted-foreground">
130+
Token usage, cost breakdown by model, and monthly spend tracking -- coming soon.
131+
</p>
132+
</div>
133+
</CardContent>
134+
</Card>
135+
</div>
136+
)
137+
}
138+
139+
140+
function UsageRow({
141+
icon,
142+
label,
143+
value,
144+
pct,
145+
showBar,
146+
}: {
147+
icon: React.ReactNode
148+
label: string
149+
value: string
150+
pct?: number
151+
showBar?: boolean
152+
}) {
153+
const barColor = (pct ?? 0) > 90 ? 'bg-destructive' : (pct ?? 0) > 70 ? 'bg-amber-500' : 'bg-emerald-500'
154+
155+
return (
156+
<div className="space-y-1.5">
157+
<div className="flex items-center justify-between text-sm">
158+
<div className="flex items-center gap-2">
159+
<span className="text-muted-foreground">{icon}</span>
160+
{label}
161+
</div>
162+
<span className="tabular-nums text-muted-foreground">{value}</span>
163+
</div>
164+
{showBar && pct !== undefined && (
165+
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
166+
<div
167+
className={cn('h-full rounded-full transition-all duration-500', barColor)}
168+
style={{ width: `${pct > 0 ? Math.max(pct, 2) : 0}%` }}
169+
/>
170+
</div>
171+
)}
172+
</div>
173+
)
174+
}
175+
176+
177+
function FeatureRow({
178+
icon,
179+
label,
180+
enabled,
181+
}: {
182+
icon: React.ReactNode
183+
label: string
184+
enabled: boolean
185+
}) {
186+
return (
187+
<div className={cn(
188+
'flex items-center gap-2.5 text-sm py-1',
189+
enabled ? 'text-foreground' : 'text-muted-foreground opacity-50',
190+
)}>
191+
<span className={enabled ? 'text-primary' : ''}>
192+
{enabled ? icon : <Lock className="w-4 h-4" />}
193+
</span>
194+
<span className="flex-1">{label}</span>
195+
{!enabled && (
196+
<Badge variant="outline" className="text-[10px] px-1.5 py-0">Pro</Badge>
197+
)}
198+
</div>
199+
)
200+
}
201+
202+
203+
function UsageSkeleton() {
204+
return (
205+
<div className="space-y-4 max-w-4xl">
206+
<div className="flex items-center gap-3">
207+
<Skeleton className="w-10 h-10 rounded-xl" />
208+
<div className="space-y-2">
209+
<Skeleton className="h-6 w-32" />
210+
<Skeleton className="h-4 w-48" />
211+
</div>
212+
</div>
213+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
214+
<Skeleton className="h-40 rounded-lg" />
215+
<Skeleton className="h-40 rounded-lg" />
216+
</div>
217+
</div>
218+
)
219+
}

0 commit comments

Comments
 (0)