326 lines
12 KiB
TypeScript
326 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useParams } from 'next/navigation';
|
|
import { SpoonActivityTimeline } from '@/components/spoons/spoon-activity-timeline';
|
|
import { SpoonAgentSettingsForm } from '@/components/spoons/spoon-agent-settings-form';
|
|
import { SpoonClonePanel } from '@/components/spoons/spoon-clone-panel';
|
|
import { SpoonCommitList } from '@/components/spoons/spoon-commit-list';
|
|
import { SpoonDetailHeader } from '@/components/spoons/spoon-detail-header';
|
|
import { SpoonMetrics } from '@/components/spoons/spoon-metrics';
|
|
import { SpoonPrList } from '@/components/spoons/spoon-pr-list';
|
|
import { SpoonSecretsForm } from '@/components/spoons/spoon-secrets-form';
|
|
import { SpoonSettingsForm } from '@/components/spoons/spoon-settings-form';
|
|
import { DeleteThreadButton } from '@/components/threads/delete-thread-button';
|
|
import { ThreadWorkspaceForm } from '@/components/threads/thread-workspace-form';
|
|
import { useQuery } from 'convex/react';
|
|
|
|
import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
|
|
import { api } from '@spoon/backend/convex/_generated/api.js';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from '@spoon/ui';
|
|
|
|
const SpoonDetailPage = () => {
|
|
const params = useParams<{ spoonId: string }>();
|
|
const spoonId = params.spoonId as Id<'spoons'>;
|
|
const details = useQuery(api.spoons.getDetails, { spoonId });
|
|
const upstreamCommits =
|
|
useQuery(api.spoonCommits.listForSpoon, {
|
|
spoonId,
|
|
side: 'upstream',
|
|
limit: 100,
|
|
}) ?? [];
|
|
const forkCommits =
|
|
useQuery(api.spoonCommits.listForSpoon, {
|
|
spoonId,
|
|
side: 'fork',
|
|
limit: 100,
|
|
}) ?? [];
|
|
const pullRequests =
|
|
useQuery(api.spoonPullRequests.listForSpoon, { spoonId, limit: 100 }) ?? [];
|
|
const syncRuns =
|
|
useQuery(api.syncRuns.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
|
const threads =
|
|
useQuery(api.threads.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
|
const agentSettings = useQuery(api.spoonAgentSettings.getForSpoon, {
|
|
spoonId,
|
|
});
|
|
const agentJobs =
|
|
useQuery(api.agentJobs.listForSpoon, { spoonId, limit: 25 }) ?? [];
|
|
const canDeleteThread = (thread: (typeof threads)[number]) => {
|
|
const latestJobStatus = thread.latestJobStatus;
|
|
const latestWorkspaceStatus = thread.latestJobWorkspaceStatus;
|
|
if (!latestJobStatus && !latestWorkspaceStatus) return true;
|
|
return (
|
|
['failed', 'cancelled', 'timed_out', 'draft_pr_opened'].includes(
|
|
latestJobStatus ?? '',
|
|
) ||
|
|
['stopped', 'expired', 'failed'].includes(latestWorkspaceStatus ?? '')
|
|
);
|
|
};
|
|
|
|
if (details === undefined) {
|
|
return <main className='text-muted-foreground p-6'>Loading Spoon...</main>;
|
|
}
|
|
|
|
return (
|
|
<main className='space-y-6'>
|
|
<SpoonDetailHeader spoon={details.spoon} state={details.state} />
|
|
<SpoonMetrics
|
|
spoon={details.spoon}
|
|
state={details.state}
|
|
latestThread={threads[0]}
|
|
/>
|
|
{details.spoon.lastError ? (
|
|
<Card className='border-destructive shadow-none'>
|
|
<CardContent className='p-4 text-sm'>
|
|
{details.spoon.lastError}
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
<Tabs defaultValue='overview' className='flex flex-col gap-5'>
|
|
<TabsList
|
|
variant='line'
|
|
className='border-border flex h-auto w-full justify-start overflow-x-auto rounded-none border-b p-0'
|
|
>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='overview'>
|
|
Overview
|
|
</TabsTrigger>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='upstream'>
|
|
Upstream
|
|
</TabsTrigger>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='fork'>
|
|
Fork changes
|
|
</TabsTrigger>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='pulls'>
|
|
Pull requests
|
|
</TabsTrigger>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='threads'>
|
|
Threads
|
|
</TabsTrigger>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='activity'>
|
|
Activity
|
|
</TabsTrigger>
|
|
<TabsTrigger className='h-9 flex-none px-3' value='settings'>
|
|
Settings
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value='overview' className='space-y-4'>
|
|
<div className='grid gap-4 xl:grid-cols-[1.15fr_0.85fr]'>
|
|
<Card className='shadow-none'>
|
|
<CardHeader className='pb-3'>
|
|
<CardTitle className='text-base'>Repository health</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className='grid gap-4 text-sm md:grid-cols-2'>
|
|
<div>
|
|
<p className='text-muted-foreground'>Drift state</p>
|
|
<p className='mt-1 text-xl font-semibold capitalize'>
|
|
{(
|
|
details.state?.status ??
|
|
details.spoon.syncStatus ??
|
|
'unknown'
|
|
).replaceAll('_', ' ')}
|
|
</p>
|
|
{details.effectiveUpstreamAheadBy === 0 &&
|
|
(details.state?.upstreamAheadBy ??
|
|
details.spoon.upstreamAheadBy ??
|
|
0) > 0 ? (
|
|
<p className='text-muted-foreground mt-1 text-xs'>
|
|
Up to date after ignored upstream changes. Raw upstream
|
|
ahead:{' '}
|
|
{details.state?.upstreamAheadBy ??
|
|
details.spoon.upstreamAheadBy}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
<div>
|
|
<p className='text-muted-foreground'>Default branches</p>
|
|
<p className='mt-1 font-medium'>
|
|
{details.state?.upstreamDefaultBranch ??
|
|
details.spoon.upstreamDefaultBranch}{' '}
|
|
→{' '}
|
|
{details.state?.forkDefaultBranch ??
|
|
details.spoon.forkDefaultBranch ??
|
|
details.spoon.upstreamDefaultBranch}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className='text-muted-foreground'>Merge base</p>
|
|
<p className='mt-1 truncate font-mono text-xs'>
|
|
{details.state?.mergeBaseSha ??
|
|
details.spoon.lastMergeBaseCommit ??
|
|
'Unknown'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className='text-muted-foreground'>Cadence</p>
|
|
<p className='mt-1 font-medium capitalize'>
|
|
{details.spoon.syncCadence}
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className='shadow-none'>
|
|
<CardHeader className='pb-3'>
|
|
<CardTitle className='text-base'>Latest thread</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className='space-y-3 text-sm'>
|
|
{threads[0] ? (
|
|
<>
|
|
<div className='grid grid-cols-2 gap-3'>
|
|
<div>
|
|
<p className='text-muted-foreground'>Status</p>
|
|
<p className='mt-1 font-semibold capitalize'>
|
|
{threads[0].status.replaceAll('_', ' ')}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className='text-muted-foreground'>Source</p>
|
|
<p className='mt-1 font-semibold capitalize'>
|
|
{threads[0].source.replaceAll('_', ' ')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<p className='text-muted-foreground'>
|
|
{threads[0].summary ??
|
|
'Open the thread to continue maintenance work.'}
|
|
</p>
|
|
</>
|
|
) : (
|
|
<p className='text-muted-foreground'>
|
|
Refresh GitHub state or create a thread to start maintenance
|
|
work for this Spoon.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
<SpoonClonePanel spoon={details.spoon} />
|
|
</div>
|
|
|
|
<div className='grid gap-4 xl:grid-cols-2'>
|
|
<section className='space-y-3'>
|
|
<div>
|
|
<h2 className='text-base font-semibold'>Upstream waiting</h2>
|
|
<p className='text-muted-foreground text-sm'>
|
|
Commits upstream has that your fork does not.
|
|
</p>
|
|
</div>
|
|
<SpoonCommitList
|
|
commits={upstreamCommits.slice(0, 5)}
|
|
empty='No upstream-only commits are cached. Refresh from GitHub to check drift.'
|
|
/>
|
|
</section>
|
|
<section className='space-y-3'>
|
|
<div>
|
|
<h2 className='text-base font-semibold'>Fork changes</h2>
|
|
<p className='text-muted-foreground text-sm'>
|
|
Custom commits Spoon should preserve during maintenance.
|
|
</p>
|
|
</div>
|
|
<SpoonCommitList
|
|
commits={forkCommits.slice(0, 5)}
|
|
empty='No fork-only commits are cached.'
|
|
/>
|
|
</section>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value='upstream'>
|
|
<SpoonCommitList
|
|
commits={upstreamCommits}
|
|
empty='No upstream changes are waiting, or this Spoon has not been refreshed yet.'
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value='fork'>
|
|
<SpoonCommitList
|
|
commits={forkCommits}
|
|
empty='No fork-only commits are cached. Your customizations will appear here after refresh.'
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value='pulls'>
|
|
<SpoonPrList pullRequests={pullRequests} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value='threads' className='space-y-4'>
|
|
<ThreadWorkspaceForm
|
|
spoon={details.spoon}
|
|
agentSettings={agentSettings}
|
|
/>
|
|
<Card className='shadow-none'>
|
|
<CardHeader>
|
|
<CardTitle className='text-base'>Spoon threads</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className='space-y-3'>
|
|
{threads.length ? (
|
|
threads.map((thread) => (
|
|
<div
|
|
key={thread._id}
|
|
className='border-border hover:border-primary/50 grid gap-3 rounded-md border p-3 transition-colors md:grid-cols-[1fr_auto] md:items-center'
|
|
>
|
|
<Link href={`/threads/${thread._id}`} className='min-w-0'>
|
|
<p className='truncate font-medium'>{thread.title}</p>
|
|
<p className='text-muted-foreground mt-1 text-sm'>
|
|
{thread.status.replaceAll('_', ' ')} ·{' '}
|
|
{thread.source.replaceAll('_', ' ')}
|
|
{thread.latestJobWorkspaceStatus
|
|
? ` · workspace ${thread.latestJobWorkspaceStatus.replaceAll('_', ' ')}`
|
|
: ''}
|
|
</p>
|
|
</Link>
|
|
<div className='flex justify-start md:justify-end'>
|
|
<DeleteThreadButton
|
|
threadId={thread._id}
|
|
disabled={!canDeleteThread(thread)}
|
|
label='Delete'
|
|
variant='outline'
|
|
/>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className='text-muted-foreground text-sm'>
|
|
No threads exist for this Spoon yet.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value='activity'>
|
|
<SpoonActivityTimeline
|
|
syncRuns={syncRuns}
|
|
threads={threads}
|
|
jobs={agentJobs}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value='settings' className='space-y-4'>
|
|
<SpoonSettingsForm
|
|
spoon={details.spoon}
|
|
settings={details.settings}
|
|
/>
|
|
<SpoonAgentSettingsForm
|
|
spoon={details.spoon}
|
|
settings={agentSettings}
|
|
/>
|
|
<SpoonSecretsForm spoonId={spoonId} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</main>
|
|
);
|
|
};
|
|
|
|
export default SpoonDetailPage;
|