Checkpoint React frontend migration
This commit is contained in:
179
frontend/src/pages/admin/overview/DataOverview.tsx
Normal file
179
frontend/src/pages/admin/overview/DataOverview.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api'
|
||||
import { hasWebsiteScope, scopedQueryKey } from '@/lib/queryKeys'
|
||||
import { useAppStore } from '@/store/useAppStore'
|
||||
import type { Question } from '@/types'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Network, Sparkles } from 'lucide-react'
|
||||
|
||||
type OverviewSnapshot = {
|
||||
id: number
|
||||
tryout_id: string
|
||||
title: string
|
||||
question_count: number
|
||||
created_at: string
|
||||
basis_items: Question[]
|
||||
}
|
||||
|
||||
type OverviewWebsite = {
|
||||
id: number
|
||||
name: string
|
||||
domain: string
|
||||
snapshots: OverviewSnapshot[]
|
||||
}
|
||||
|
||||
type HierarchyOverview = {
|
||||
summary: {
|
||||
websites: number
|
||||
snapshots: number
|
||||
source_questions: number
|
||||
basis_items: number
|
||||
ai_runs: number
|
||||
variants: number
|
||||
snapshots_without_basis: number
|
||||
basis_without_variants: number
|
||||
orphan_variants: number
|
||||
}
|
||||
websites: OverviewWebsite[]
|
||||
}
|
||||
|
||||
function SummaryCard({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DataOverview() {
|
||||
const { websiteId } = useAppStore()
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: scopedQueryKey(websiteId, 'data-overview'),
|
||||
queryFn: async () => {
|
||||
const res = await api.get<HierarchyOverview>('/admin/overview/hierarchy')
|
||||
return res.data
|
||||
},
|
||||
enabled: hasWebsiteScope(websiteId),
|
||||
})
|
||||
|
||||
if (!hasWebsiteScope(websiteId)) {
|
||||
return <div className="text-muted-foreground">Select a website to load the data overview.</div>
|
||||
}
|
||||
|
||||
if (isLoading) return <Skeleton className="h-[520px] w-full" />
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Failed to load data overview</AlertTitle>
|
||||
<AlertDescription>Check the selected website and backend API availability.</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Data Overview</h1>
|
||||
<p className="text-muted-foreground mt-1">Snapshot, basis question, AI run, and variant hierarchy.</p>
|
||||
</div>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to="/admin/import">Open Import</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<SummaryCard label="Snapshots" value={data.summary.snapshots} />
|
||||
<SummaryCard label="Source Questions" value={data.summary.source_questions} />
|
||||
<SummaryCard label="Basis Items" value={data.summary.basis_items} />
|
||||
<SummaryCard label="AI Variants" value={data.summary.variants} />
|
||||
</div>
|
||||
|
||||
{(data.summary.snapshots_without_basis > 0 ||
|
||||
data.summary.basis_without_variants > 0 ||
|
||||
data.summary.orphan_variants > 0) && (
|
||||
<Alert>
|
||||
<Network className="h-4 w-4" />
|
||||
<AlertTitle>Hierarchy gaps detected</AlertTitle>
|
||||
<AlertDescription>
|
||||
{data.summary.snapshots_without_basis} snapshots without promoted basis items,{' '}
|
||||
{data.summary.basis_without_variants} basis items without variants, and{' '}
|
||||
{data.summary.orphan_variants} variants without a visible basis.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{data.websites.map((website) => (
|
||||
<div key={website.id} className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">{website.name}</h2>
|
||||
<p className="text-sm text-muted-foreground">{website.domain}</p>
|
||||
</div>
|
||||
|
||||
{website.snapshots.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-8 text-center text-muted-foreground">
|
||||
No imported snapshots found for this website.
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted text-muted-foreground">
|
||||
<tr>
|
||||
<th className="p-3 text-left font-medium">Snapshot</th>
|
||||
<th className="p-3 text-left font-medium">Tryout</th>
|
||||
<th className="p-3 text-right font-medium">Questions</th>
|
||||
<th className="p-3 text-right font-medium">Basis</th>
|
||||
<th className="p-3 text-right font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{website.snapshots.map((snapshot) => (
|
||||
<tr key={snapshot.id}>
|
||||
<td className="p-3">
|
||||
<div className="font-medium">{snapshot.title}</div>
|
||||
<div className="text-xs text-muted-foreground">Snapshot #{snapshot.id}</div>
|
||||
</td>
|
||||
<td className="p-3">{snapshot.tryout_id}</td>
|
||||
<td className="p-3 text-right">{snapshot.question_count}</td>
|
||||
<td className="p-3 text-right">
|
||||
<Badge variant={snapshot.basis_items.length ? 'default' : 'secondary'}>
|
||||
{snapshot.basis_items.length}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to={`/admin/tryouts/${snapshot.tryout_id}/questions`}>Questions</Link>
|
||||
</Button>
|
||||
{snapshot.basis_items[0] && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to={`/admin/tryouts/${snapshot.tryout_id}/questions/${snapshot.basis_items[0].id}/ai-workspace`}>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
AI
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user