168 lines
5.8 KiB
TypeScript
168 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { MetricCard } from '@/components/dashboard/metric-card';
|
|
import { SpoonCard } from '@/components/spoons/spoon-card';
|
|
import { MaintenanceQueue } from '@/components/threads/maintenance-queue';
|
|
import { useQuery } from 'convex/react';
|
|
import { GitBranch, MessageSquare, RefreshCw, ShieldCheck } from 'lucide-react';
|
|
|
|
import { api } from '@spoon/backend/convex/_generated/api.js';
|
|
import { Button, Card, CardContent, CardHeader, CardTitle } from '@spoon/ui';
|
|
|
|
const DashboardPage = () => {
|
|
const spoons = useQuery(api.spoons.listMine, {}) ?? [];
|
|
const syncRuns = useQuery(api.syncRuns.listRecent, { limit: 5 }) ?? [];
|
|
const threads = useQuery(api.threads.listMine, { limit: 25 }) ?? [];
|
|
const activeSpoons = spoons.filter(
|
|
(spoon) => spoon.status === 'active',
|
|
).length;
|
|
const behind = spoons.filter((spoon) => spoon.syncStatus === 'behind').length;
|
|
const diverged = spoons.filter(
|
|
(spoon) => spoon.syncStatus === 'diverged',
|
|
).length;
|
|
const openPullRequests = spoons.reduce(
|
|
(total, spoon) => total + (spoon.upstreamAheadBy ?? 0),
|
|
0,
|
|
);
|
|
|
|
return (
|
|
<main className='space-y-6'>
|
|
<div className='flex flex-col justify-between gap-4 md:flex-row md:items-end'>
|
|
<div>
|
|
<h1 className='text-3xl font-semibold tracking-normal'>Dashboard</h1>
|
|
<p className='text-muted-foreground mt-2'>
|
|
Monitor managed forks, upstream activity, and open maintenance
|
|
threads.
|
|
</p>
|
|
</div>
|
|
<Button asChild>
|
|
<Link href='/spoons/new'>Create Spoon</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
|
<MetricCard
|
|
label='Spoons'
|
|
value={spoons.length}
|
|
note={`${activeSpoons} active`}
|
|
icon={GitBranch}
|
|
/>
|
|
<MetricCard
|
|
label='Behind upstream'
|
|
value={behind}
|
|
note={`${diverged} diverged`}
|
|
icon={RefreshCw}
|
|
/>
|
|
<MetricCard
|
|
label='Open threads'
|
|
value={
|
|
threads.filter(
|
|
(thread) =>
|
|
!['resolved', 'ignored', 'failed', 'cancelled'].includes(
|
|
thread.status,
|
|
),
|
|
).length
|
|
}
|
|
note='Across all Spoons'
|
|
icon={MessageSquare}
|
|
/>
|
|
<MetricCard
|
|
label='Upstream commits'
|
|
value={openPullRequests}
|
|
note='Waiting across Spoons'
|
|
icon={ShieldCheck}
|
|
/>
|
|
</div>
|
|
|
|
<section className='space-y-3'>
|
|
<h2 className='text-lg font-semibold'>Maintenance queue</h2>
|
|
<MaintenanceQueue threads={threads} />
|
|
</section>
|
|
|
|
<div className='grid gap-6 xl:grid-cols-2'>
|
|
<section className='space-y-3'>
|
|
<h2 className='text-lg font-semibold'>Recent Spoons</h2>
|
|
{spoons.length ? (
|
|
spoons
|
|
.slice(0, 3)
|
|
.map((spoon) => <SpoonCard key={spoon._id} spoon={spoon} />)
|
|
) : (
|
|
<Card className='shadow-none'>
|
|
<CardContent className='p-6'>
|
|
<p className='font-medium'>No Spoons yet</p>
|
|
<p className='text-muted-foreground mt-2 text-sm'>
|
|
Create a manual Spoon record to start shaping your fork
|
|
maintenance dashboard.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</section>
|
|
<section className='space-y-3'>
|
|
<h2 className='text-lg font-semibold'>Recent activity</h2>
|
|
<Card className='shadow-none'>
|
|
<CardHeader>
|
|
<CardTitle className='text-base'>Upstream checks</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{syncRuns.length ? (
|
|
<div className='space-y-3'>
|
|
{syncRuns.map((run) => (
|
|
<div
|
|
key={run._id}
|
|
className='border-border border p-3 text-sm'
|
|
>
|
|
<p className='font-medium'>
|
|
{run.kind.replaceAll('_', ' ')}
|
|
</p>
|
|
<p className='text-muted-foreground'>
|
|
{run.status.replaceAll('_', ' ')}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className='text-muted-foreground text-sm'>
|
|
Scheduled upstream checks will appear here once provider
|
|
automation is connected.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
<Card className='mt-4 shadow-none'>
|
|
<CardHeader>
|
|
<CardTitle className='text-base'>Recent threads</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{threads.length ? (
|
|
<div className='space-y-3'>
|
|
{threads.slice(0, 5).map((thread) => (
|
|
<div
|
|
key={thread._id}
|
|
className='border-border border p-3 text-sm'
|
|
>
|
|
<p className='font-medium'>{thread.title}</p>
|
|
<p className='text-muted-foreground'>
|
|
{thread.status.replaceAll('_', ' ')} ·{' '}
|
|
{thread.source.replaceAll('_', ' ')}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className='text-muted-foreground text-sm'>
|
|
Threads appear when you request work or upstream changes need
|
|
review.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
);
|
|
};
|
|
|
|
export default DashboardPage;
|