Skip to content

Commit 67e8a2f

Browse files
brandonkachencodebuff-team
authored andcommitted
feat: Add admin UI for managing organization-specific models
This commit introduces a new feature that allows administrators to manage organization-specific finetuned models through a new UI. Key changes include: - A new `ModelConfigSheet` component for selecting models. - Integration of the sheet into the admin organizations page. - Use of `react-query-kit` with `useQuery` and `useMutation` for robust data fetching and state management. - An update to the knowledge file to recommend `useQuery` and `useMutation` for data fetching. - Refactoring of the `admin-auth` file by moving it to a more appropriate location within the `api/admin` directory. Generated with Codebuff 🤖 Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent a8257b8 commit 67e8a2f

File tree

8 files changed

+402
-43
lines changed

8 files changed

+402
-43
lines changed

bun.lock

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/knowledge.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ When creating interactive demos:
118118
- Set z-50 to ensure card appears above other content
119119
- Use transition-opacity for smooth fade effects
120120

121+
### Data Fetching
122+
123+
- **Prefer `useQuery` and `useMutation`**: For all data fetching and mutations, use `@tanstack/react-query`'s `useQuery` and `useMutation` hooks instead of `useEffect` with `fetch`. This provides better caching, state management, and a more declarative API.
124+
121125
### Code Style
122126

123127
- Use ts-pattern's match syntax instead of complex if/else chains
@@ -352,3 +356,7 @@ Important: When modifying or using code from common:
352356
- Always build common package first before running web type checking
353357
- Changes to common won't be reflected in web until common is rebuilt
354358
- This applies to new exports, type changes, and utility functions
359+
360+
## File Naming Conventions
361+
362+
- **Components and Hooks**: Use kebab-case for filenames (e.g., `model-config-sheet.tsx`, `use-model-config.ts`). This ensures consistency across the project.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@stripe/stripe-js": "^4.4.0",
5050
"@t3-oss/env-core": "^0.7.1",
5151
"@t3-oss/env-nextjs": "^0.11.1",
52+
"@tanstack/react-query": "^5.80.6",
5253
"@tanstack/react-virtual": "^3.13.6",
5354
"aceternity-ui": "^0.2.2",
5455
"class-variance-authority": "^0.7.1",

web/src/app/admin/orgs/page.tsx

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEffect, useState } from 'react'
44
import { useSession } from 'next-auth/react'
55
import { useRouter } from 'next/navigation'
6-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
77
import { Button } from '@/components/ui/button'
88
import { Input } from '@/components/ui/input'
99
import { Badge } from '@/components/ui/badge'
@@ -22,6 +22,8 @@ import {
2222
} from 'lucide-react'
2323
import Link from 'next/link'
2424
import { toast } from '@/components/ui/use-toast'
25+
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
26+
import { ModelConfigSheet } from '@/components/organization/model-config-sheet'
2527

2628
interface OrganizationSummary {
2729
id: string
@@ -44,9 +46,11 @@ export default function AdminOrganizationsPage() {
4446
const [organizations, setOrganizations] = useState<OrganizationSummary[]>([])
4547
const [loading, setLoading] = useState(true)
4648
const [searchTerm, setSearchTerm] = useState('')
47-
const [statusFilter, setStatusFilter] = useState<
48-
'all' | 'healthy' | 'warning' | 'critical'
49-
>('all')
49+
const [sortOrder, setSortOrder] = useState('desc')
50+
const [selectedOrg, setSelectedOrg] = useState<OrganizationSummary | null>(
51+
null
52+
)
53+
const [statusFilter, setStatusFilter] = useState('all')
5054

5155
useEffect(() => {
5256
if (status === 'authenticated') {
@@ -309,55 +313,65 @@ export default function AdminOrganizationsPage() {
309313

310314
{/* Organizations List */}
311315
<div className="space-y-4">
312-
{filteredOrganizations.map((org) => (
313-
<Card key={org.id} className="hover:shadow-md transition-shadow">
314-
<CardContent className="p-6">
315-
<div className="flex items-center justify-between">
316-
<div className="flex items-center space-x-4">
316+
<Table>
317+
<TableBody>
318+
{filteredOrganizations.map((org) => (
319+
<TableRow key={org.id}>
320+
<TableCell>
317321
{getHealthStatusIcon(org.health_status)}
318-
<div>
319-
<div className="flex items-center space-x-2">
320-
<h3 className="text-lg font-semibold">{org.name}</h3>
321-
{getHealthStatusBadge(org.health_status)}
322-
</div>
323-
<p className="text-sm text-muted-foreground">
324-
Owner: {org.owner_name} • Created:{' '}
325-
{new Date(org.created_at).toLocaleDateString()}
326-
</p>
327-
<div className="flex items-center space-x-4 mt-2 text-sm text-muted-foreground">
328-
<span className="flex items-center">
329-
<Users className="h-4 w-4 mr-1" />
330-
{org.member_count} members
331-
</span>
332-
<span className="flex items-center">
333-
<GitBranch className="h-4 w-4 mr-1" />
334-
{org.repository_count} repos
335-
</span>
336-
<span className="flex items-center">
337-
<CreditCard className="h-4 w-4 mr-1" />
338-
{org.credit_balance.toLocaleString()} credits
339-
</span>
340-
</div>
322+
</TableCell>
323+
<TableCell>
324+
<div className="flex items-center space-x-2">
325+
<h3 className="text-lg font-semibold">{org.name}</h3>
326+
{getHealthStatusBadge(org.health_status)}
327+
</div>
328+
<p className="text-sm text-muted-foreground">
329+
Owner: {org.owner_name} • Created:{' '}
330+
{new Date(org.created_at).toLocaleDateString()}
331+
</p>
332+
<div className="flex items-center space-x-4 mt-2 text-sm text-muted-foreground">
333+
<span className="flex items-center">
334+
<Users className="h-4 w-4 mr-1" />
335+
{org.member_count} members
336+
</span>
337+
<span className="flex items-center">
338+
<GitBranch className="h-4 w-4 mr-1" />
339+
{org.repository_count} repos
340+
</span>
341+
<span className="flex items-center">
342+
<CreditCard className="h-4 w-4 mr-1" />
343+
{org.credit_balance.toLocaleString()} credits
344+
</span>
341345
</div>
342-
</div>
343-
<div className="flex items-center space-x-2">
344-
<Link href={`/orgs/${org.slug}`}>
346+
</TableCell>
347+
<TableCell>
348+
<Link href={`/admin/orgs/${org.id}`}>
345349
<Button variant="outline" size="sm">
346-
<Eye className="mr-2 h-4 w-4" />
347350
View
348351
</Button>
349352
</Link>
350-
<Button variant="outline" size="sm">
353+
<Button
354+
variant="outline"
355+
size="sm"
356+
onClick={() => setSelectedOrg(org)}
357+
>
351358
<Settings className="mr-2 h-4 w-4" />
352359
Manage
353360
</Button>
354-
</div>
355-
</div>
356-
</CardContent>
357-
</Card>
358-
))}
361+
</TableCell>
362+
</TableRow>
363+
))}
364+
</TableBody>
365+
</Table>
359366
</div>
360367
</div>
368+
{selectedOrg && (
369+
<ModelConfigSheet
370+
organization={selectedOrg}
371+
isOpen={!!selectedOrg}
372+
onClose={() => setSelectedOrg(null)}
373+
/>
374+
)}
361375
</div>
362376
)
363377
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getServerSession } from 'next-auth'
2+
import { NextResponse } from 'next/server'
3+
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
4+
import { utils } from '@codebuff/internal'
5+
import { logger } from '@/util/logger'
6+
7+
/**
8+
* Check if the current user is a Codebuff admin
9+
* Returns the admin user if authorized, or a NextResponse error if not
10+
*/
11+
export async function checkAdminAuth(): Promise<utils.AdminUser | NextResponse> {
12+
const session = await getServerSession(authOptions)
13+
14+
// Use shared admin check utility
15+
const adminUser = await utils.checkSessionIsAdmin(session)
16+
if (!adminUser) {
17+
if (session?.user?.id) {
18+
logger.warn(
19+
{ userId: session.user.id },
20+
'Unauthorized access attempt to admin endpoint'
21+
)
22+
}
23+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
24+
}
25+
26+
return adminUser
27+
}
28+
29+
/**
30+
* Higher-order function to wrap admin API routes with authentication
31+
*/
32+
export function withAdminAuth<T extends any[]>(
33+
handler: (adminUser: utils.AdminUser, ...args: T) => Promise<NextResponse>
34+
) {
35+
return async (...args: T): Promise<NextResponse> => {
36+
const authResult = await checkAdminAuth()
37+
38+
if (authResult instanceof NextResponse) {
39+
return authResult // Return the error response
40+
}
41+
42+
return handler(authResult, ...args)
43+
}
44+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { checkAdminAuth } from '@/app/api/admin/admin-auth'
3+
import db from 'common/db'
4+
import * as schema from 'common/db/schema'
5+
import { eq, and } from 'drizzle-orm'
6+
7+
interface RouteParams {
8+
params: {
9+
orgId: string
10+
feature: string
11+
}
12+
}
13+
14+
// GET handler to fetch feature configuration
15+
export async function GET(
16+
request: NextRequest,
17+
{ params }: RouteParams
18+
): Promise<NextResponse> {
19+
const authResult = await checkAdminAuth()
20+
if (authResult instanceof NextResponse) {
21+
return authResult
22+
}
23+
24+
const { orgId, feature } = params
25+
26+
try {
27+
const featureConfig = await db
28+
.select()
29+
.from(schema.orgFeature)
30+
.where(
31+
and(
32+
eq(schema.orgFeature.org_id, orgId),
33+
eq(schema.orgFeature.feature, feature)
34+
)
35+
)
36+
.limit(1)
37+
38+
if (featureConfig.length === 0) {
39+
return NextResponse.json({ config: null }, { status: 404 })
40+
}
41+
42+
return NextResponse.json(featureConfig[0])
43+
} catch (error) {
44+
console.error('Error fetching feature config:', error)
45+
return NextResponse.json(
46+
{ error: 'Internal server error' },
47+
{ status: 500 }
48+
)
49+
}
50+
}
51+
52+
// POST handler to create or update feature configuration
53+
export async function POST(
54+
request: NextRequest,
55+
{ params }: RouteParams
56+
): Promise<NextResponse> {
57+
const authResult = await checkAdminAuth()
58+
if (authResult instanceof NextResponse) {
59+
return authResult
60+
}
61+
62+
const { orgId, feature } = params
63+
const body = await request.json()
64+
65+
try {
66+
const result = await db
67+
.insert(schema.orgFeature)
68+
.values({
69+
org_id: orgId,
70+
feature: feature,
71+
config: body,
72+
updated_by: authResult.id,
73+
})
74+
.onConflictDoUpdate({
75+
target: [schema.orgFeature.org_id, schema.orgFeature.feature],
76+
set: {
77+
config: body,
78+
updated_by: authResult.id,
79+
updated_at: new Date(),
80+
},
81+
})
82+
.returning()
83+
84+
return NextResponse.json(result[0])
85+
} catch (error) {
86+
console.error('Error saving feature config:', error)
87+
return NextResponse.json(
88+
{ error: 'Internal server error' },
89+
{ status: 500 }
90+
)
91+
}
92+
}

0 commit comments

Comments
 (0)